diff --git a/.gitignore b/.gitignore index a58d38f39..54bd50397 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,5 @@ tags # YouCompleteMe config stuff. .ycm_extra_conf.* +#OSX Stuff +.DS_Store diff --git a/CMakeLists.txt b/CMakeLists.txt index f2cfc3aef..9ccd513d3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -43,11 +43,20 @@ ENDIF() ######## 3rd Party Libs ######## # Find the required Qt parts +find_package(Qt5Core REQUIRED) find_package(Qt5Widgets REQUIRED) find_package(Qt5Network REQUIRED) +find_package(Qt5Test REQUIRED) +find_package(Qt5Concurrent REQUIRED) find_package(Qt5LinguistTools REQUIRED) -include_directories(${Qt5Widgets_INCLUDE_DIRS}) +include_directories( + ${Qt5Core_INCLUDE_DIRS} + ${Qt5Widgets_INCLUDE_DIRS} + ${Qt5Concurrent_INCLUDE_DIRS} + ${Qt5Network_INCLUDE_DIRS} + ${Qt5Test_INCLUDE_DIRS} + ) # The Qt5 cmake files don't provide its install paths, so ask qmake. get_target_property(QMAKE_EXECUTABLE Qt5::qmake LOCATION) @@ -360,16 +369,22 @@ logic/OpSys.h logic/OpSys.cpp logic/ForgeInstaller.h logic/ForgeInstaller.cpp +logic/LiteLoaderInstaller.h +logic/LiteLoaderInstaller.cpp # Nostalgia logic/NostalgiaInstance.h logic/NostalgiaInstance.cpp +# FTB +logic/OneSixFTBInstance.h +logic/OneSixFTBInstance.cpp +logic/LegacyFTBInstance.h +logic/LegacyFTBInstance.cpp + # Lists logic/lists/InstanceList.h logic/lists/InstanceList.cpp -logic/lists/IconList.h -logic/lists/IconList.cpp logic/lists/BaseVersionList.h logic/lists/BaseVersionList.cpp logic/lists/MinecraftVersionList.h @@ -381,6 +396,13 @@ logic/lists/ForgeVersionList.cpp logic/lists/JavaVersionList.h logic/lists/JavaVersionList.cpp +# Icons +logic/icons/MMCIcon.h +logic/icons/MMCIcon.cpp +logic/icons/IconList.h +logic/icons/IconList.cpp + + # misc model/view logic/EnabledItemFilter.h logic/EnabledItemFilter.cpp @@ -389,6 +411,10 @@ logic/EnabledItemFilter.cpp logic/tasks/ProgressProvider.h logic/tasks/Task.h logic/tasks/Task.cpp +logic/tasks/ThreadTask.h +logic/tasks/ThreadTask.cpp +logic/tasks/SequentialTask.h +logic/tasks/SequentialTask.cpp # Utilities logic/JavaChecker.h @@ -403,6 +429,8 @@ logic/JavaCheckerJob.h logic/JavaCheckerJob.cpp # Assets +logic/assets/AssetsMigrateTask.h +logic/assets/AssetsMigrateTask.cpp logic/assets/AssetsUtils.h logic/assets/AssetsUtils.cpp ) @@ -499,8 +527,8 @@ ADD_EXECUTABLE(MultiMC MACOSX_BUNDLE WIN32 main.cpp ${MULTIMC_RCS}) # Link TARGET_LINK_LIBRARIES(MultiMC MultiMC_common) TARGET_LINK_LIBRARIES(MultiMC_common xz-embedded unpack200 quazip libUtil libSettings libGroupView ${MultiMC_LINK_ADDITIONAL_LIBS}) -QT5_USE_MODULES(MultiMC Core Widgets Network Xml WebKit ${MultiMC_QT_ADDITIONAL_MODULES}) -QT5_USE_MODULES(MultiMC_common Core Widgets Network Xml WebKit ${MultiMC_QT_ADDITIONAL_MODULES}) +QT5_USE_MODULES(MultiMC Core Widgets Network Xml WebKit Concurrent ${MultiMC_QT_ADDITIONAL_MODULES}) +QT5_USE_MODULES(MultiMC_common Core Widgets Network Xml WebKit Concurrent ${MultiMC_QT_ADDITIONAL_MODULES}) ADD_DEPENDENCIES(MultiMC_common MultiMCLauncher JavaCheck) ################################ INSTALLATION AND PACKAGING ################################ @@ -514,9 +542,12 @@ IF(UNIX AND APPLE) SET(MACOSX_BUNDLE_BUNDLE_NAME "MultiMC") SET(MACOSX_BUNDLE_INFO_STRING "MultiMC Minecraft launcher and management utility.") + SET(MACOSX_BUNDLE_GUI_IDENTIFIER "org.multimc.MultiMC5") SET(MACOSX_BUNDLE_BUNDLE_VERSION "${MultiMC_VERSION_MAJOR}.${MultiMC_VERSION_MINOR}.${MultiMC_VERSION_REV}.${MultiMC_VERSION_BUILD}") - #SET(MACOSX_BUNDLE_GUI_IDENTIFIER "") + SET(MACOSX_BUNDLE_SHORT_VERSION_STRING "${MultiMC_VERSION_MAJOR}.${MultiMC_VERSION_MINOR}.${MultiMC_VERSION_REV}.${MultiMC_VERSION_BUILD}") + SET(MACOSX_BUNDLE_LONG_VERSION_STRING "${MultiMC_VERSION_MAJOR}.${MultiMC_VERSION_MINOR}.${MultiMC_VERSION_REV}.${MultiMC_VERSION_BUILD}") SET(MACOSX_BUNDLE_ICON_FILE MultiMC.icns) + SET(MACOSX_BUNDLE_COPYRIGHT "Copyright 2013 MultiMC Contributors") ELSEIF(UNIX) SET(PLUGIN_DEST_DIR plugins) SET(QTCONF_DEST_DIR .) @@ -592,6 +623,18 @@ INSTALL( REGEX "d\\." EXCLUDE REGEX "_debug\\." EXCLUDE ) +IF(APPLE) + # Accessible plugin to make buttons look decent on osx + INSTALL( + DIRECTORY "${QT_PLUGINS_DIR}/accessible" + DESTINATION ${PLUGIN_DEST_DIR} + COMPONENT Runtime + REGEX "quick" EXCLUDE + REGEX "d\\." EXCLUDE + REGEX "_debug\\." EXCLUDE + ) +ENDIF() + endif() # qtconf @@ -658,8 +701,12 @@ ELSE() ENDIF() add_custom_target (translations DEPENDS ${QM_FILES}) +IF(APPLE AND UNIX) ## OSX + install(FILES ${QM_FILES} DESTINATION MultiMC.app/Contents/MacOS/translations) +ELSE() + install(FILES ${QM_FILES} DESTINATION translations) +ENDIF() -install(FILES ${QM_FILES} DESTINATION ${CMAKE_INSTALL_PREFIX}/translations) # Tests add_subdirectory(tests) diff --git a/MultiMC.cpp b/MultiMC.cpp index 12c37d2ab..4e06f558d 100644 --- a/MultiMC.cpp +++ b/MultiMC.cpp @@ -8,11 +8,12 @@ #include #include #include +#include #include "gui/dialogs/VersionSelectDialog.h" #include "logic/lists/InstanceList.h" #include "logic/auth/MojangAccountList.h" -#include "logic/lists/IconList.h" +#include "logic/icons/IconList.h" #include "logic/lists/LwjglVersionList.h" #include "logic/lists/MinecraftVersionList.h" #include "logic/lists/ForgeVersionList.h" @@ -34,16 +35,28 @@ #include #include "config.h" +#ifdef WINDOWS +#define UPDATER_BIN "updater.exe" +#elif LINUX +#define UPDATER_BIN "updater" +#elif OSX +#define UPDATER_BIN "updater" +#else +#error Unsupported operating system. +#endif + using namespace Util::Commandline; -MultiMC::MultiMC(int &argc, char **argv, const QString &root) : QApplication(argc, argv), - m_version{VERSION_MAJOR, VERSION_MINOR, VERSION_BUILD, VERSION_CHANNEL, VERSION_BUILD_TYPE} +MultiMC::MultiMC(int &argc, char **argv, const QString &root) + : QApplication(argc, argv), m_version{VERSION_MAJOR, VERSION_MINOR, VERSION_BUILD, + VERSION_CHANNEL, VERSION_BUILD_TYPE} { setOrganizationName("MultiMC"); setApplicationName("MultiMC5"); initTranslations(); + setAttribute(Qt::AA_UseHighDpiPixmaps); // Don't quit on hiding the last window this->setQuitOnLastWindowClosed(false); @@ -137,9 +150,10 @@ MultiMC::MultiMC(int &argc, char **argv, const QString &root) : QApplication(arg } // change directory - QDir::setCurrent(args["dir"].toString().isEmpty() ? - (root.isEmpty() ? QDir::currentPath() : QDir::current().absoluteFilePath(root)) - : args["dir"].toString()); + QDir::setCurrent( + args["dir"].toString().isEmpty() + ? (root.isEmpty() ? QDir::currentPath() : QDir::current().absoluteFilePath(root)) + : args["dir"].toString()); // init the logger initLogger(); @@ -158,7 +172,7 @@ MultiMC::MultiMC(int &argc, char **argv, const QString &root) : QApplication(arg m_instances.reset(new InstanceList(InstDirSetting->get().toString(), this)); QLOG_INFO() << "Loading Instances..."; m_instances->loadList(); - connect(InstDirSetting, SIGNAL(settingChanged(const Setting &, QVariant)), + connect(InstDirSetting.get(), SIGNAL(settingChanged(const Setting &, QVariant)), m_instances.get(), SLOT(on_InstFolderChanged(const Setting &, QVariant))); // and accounts @@ -179,42 +193,43 @@ MultiMC::MultiMC(int &argc, char **argv, const QString &root) : QApplication(arg { QLOG_INFO() << "No proxy found."; } - else for (auto proxy : proxies) - { - QString proxyDesc; - if (proxy.type() == QNetworkProxy::NoProxy) + else + for (auto proxy : proxies) { - QLOG_INFO() << "Using no proxy is an option!"; - continue; + QString proxyDesc; + if (proxy.type() == QNetworkProxy::NoProxy) + { + QLOG_INFO() << "Using no proxy is an option!"; + continue; + } + 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("%3@%1:%2 pass %4") + .arg(proxy.hostName()) + .arg(proxy.port()) + .arg(proxy.user()) + .arg(proxy.password()); + QLOG_INFO() << proxyDesc; } - 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("%3@%1:%2 pass %4") - .arg(proxy.hostName()) - .arg(proxy.port()) - .arg(proxy.user()) - .arg(proxy.password()); - QLOG_INFO() << proxyDesc; - } // create the global network manager m_qnam.reset(new QNetworkAccessManager(this)); @@ -285,13 +300,26 @@ void MultiMC::initTranslations() } } +void moveFile(const QString &oldName, const QString &newName) +{ + QFile::remove(newName); + QFile::copy(oldName, newName); + QFile::remove(oldName); +} void MultiMC::initLogger() { + static const QString logBase = "MultiMC-%0.log"; + + 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)); + // init the logging mechanism QsLogging::Logger &logger = QsLogging::Logger::instance(); logger.setLoggingLevel(QsLogging::TraceLevel); - m_fileDestination = QsLogging::DestinationFactory::MakeFileDestination("MultiMC.log"); - m_debugDestination = QsLogging::DestinationFactory::MakeDebugOutputDestination(); + m_fileDestination = QsLogging::DestinationFactory::MakeFileDestination(logBase.arg(0)); + m_debugDestination = QsLogging::DestinationFactory::MakeQDebugDestination(); logger.addDestination(m_fileDestination.get()); logger.addDestination(m_debugDestination.get()); // log all the things @@ -302,67 +330,110 @@ void MultiMC::initGlobalSettings() { m_settings.reset(new INISettingsObject("multimc.cfg", this)); // Updates - m_settings->registerSetting(new Setting("UseDevBuilds", false)); - m_settings->registerSetting(new Setting("AutoUpdate", true)); + m_settings->registerSetting("UseDevBuilds", false); + m_settings->registerSetting("AutoUpdate", true); - // Folders - m_settings->registerSetting(new Setting("InstanceDir", "instances")); - m_settings->registerSetting(new Setting("CentralModsDir", "mods")); - m_settings->registerSetting(new Setting("LWJGLDir", "lwjgl")); + // FTB + m_settings->registerSetting("TrackFTBInstances", false); +#ifdef Q_OS_LINUX + QString ftbDefault = QDir::home().absoluteFilePath(".ftblauncher"); +#elif defined(Q_OS_WIN32) + QString ftbDefault = PathCombine(QDir::homePath(), "AppData/Roaming/ftblauncher"); +#elif defined(Q_OS_MAC) + QString ftbDefault = + PathCombine(QDir::homePath(), "Library/Application Support/ftblauncher"); +#endif + m_settings->registerSetting("FTBLauncherRoot", ftbDefault); - // Console - m_settings->registerSetting(new Setting("ShowConsole", true)); - m_settings->registerSetting(new Setting("AutoCloseConsole", true)); - - // Console Colors - // m_settings->registerSetting(new Setting("SysMessageColor", QColor(Qt::blue))); - // m_settings->registerSetting(new Setting("StdOutColor", QColor(Qt::black))); - // m_settings->registerSetting(new Setting("StdErrColor", QColor(Qt::red))); - - // Window Size - m_settings->registerSetting(new Setting("LaunchMaximized", false)); - m_settings->registerSetting(new Setting("MinecraftWinWidth", 854)); - m_settings->registerSetting(new Setting("MinecraftWinHeight", 480)); - - // Auto login - m_settings->registerSetting(new Setting("AutoLogin", false)); - - // Memory - m_settings->registerSetting(new Setting("MinMemAlloc", 512)); - m_settings->registerSetting(new Setting("MaxMemAlloc", 1024)); - m_settings->registerSetting(new Setting("PermGen", 64)); - - // Java Settings - m_settings->registerSetting(new Setting("JavaPath", "")); - m_settings->registerSetting(new Setting("LastHostname", "")); - m_settings->registerSetting(new Setting("JvmArgs", "")); - - // Custom Commands - m_settings->registerSetting(new Setting("PreLaunchCommand", "")); - m_settings->registerSetting(new Setting("PostExitCommand", "")); - - // The cat - m_settings->registerSetting(new Setting("TheCat", false)); - - - m_settings->registerSetting(new Setting("InstSortMode", "Name")); - m_settings->registerSetting(new Setting("SelectedInstance", QString())); - - // Persistent value for the client ID - m_settings->registerSetting(new Setting("YggdrasilClientToken", "")); - QString currentYggID = m_settings->get("YggdrasilClientToken").toString(); - if (currentYggID.isEmpty()) + m_settings->registerSetting("FTBRoot"); + if (m_settings->get("FTBRoot").isNull()) { - QUuid uuid = QUuid::createUuid(); - m_settings->set("YggdrasilClientToken", uuid.toString()); + QString ftbRoot; + QFile f(QDir(m_settings->get("FTBLauncherRoot").toString()) + .absoluteFilePath("ftblaunch.cfg")); + QLOG_INFO() << "Attempting to read" << f.fileName(); + if (f.open(QFile::ReadOnly)) + { + const QString data = QString::fromLatin1(f.readAll()); + QRegularExpression exp("installPath=(.*)"); + ftbRoot = QDir::cleanPath(exp.match(data).captured(1)); +#ifdef Q_OS_WIN32 + if (!ftbRoot.isEmpty()) + { + if (ftbRoot.at(0).isLetter() && ftbRoot.size() > 1 && ftbRoot.at(1) == '/') + { + ftbRoot.remove(1, 1); + } + } +#endif + if (ftbRoot.isEmpty()) + { + QLOG_INFO() << "Failed to get FTB root path"; + } + else + { + QLOG_INFO() << "FTB is installed at" << ftbRoot; + m_settings->set("FTBRoot", ftbRoot); + } + } + else + { + QLOG_WARN() << "Couldn't open" << f.fileName() << ":" << f.errorString(); + QLOG_WARN() << "This is perfectly normal if you don't have FTB installed"; + } } - // Window state and geometry - m_settings->registerSetting(new Setting("MainWindowState", "")); - m_settings->registerSetting(new Setting("MainWindowGeometry", "")); + // Folders + m_settings->registerSetting("InstanceDir", "instances"); + m_settings->registerSetting({"CentralModsDir", "ModsDir"}, "mods"); + m_settings->registerSetting({"LWJGLDir", "LwjglDir"}, "lwjgl"); + m_settings->registerSetting("IconsDir", "icons"); - m_settings->registerSetting(new Setting("ConsoleWindowState", "")); - m_settings->registerSetting(new Setting("ConsoleWindowGeometry", "")); + // Editors + m_settings->registerSetting("JsonEditor", QString()); + + // Console + m_settings->registerSetting("ShowConsole", true); + m_settings->registerSetting("AutoCloseConsole", true); + + // Console Colors + // m_settings->registerSetting("SysMessageColor", QColor(Qt::blue)); + // m_settings->registerSetting("StdOutColor", QColor(Qt::black)); + // m_settings->registerSetting("StdErrColor", QColor(Qt::red)); + + // Window Size + m_settings->registerSetting({"LaunchMaximized", "MCWindowMaximize"}, false); + m_settings->registerSetting({"MinecraftWinWidth", "MCWindowWidth"}, 854); + m_settings->registerSetting({"MinecraftWinHeight", "MCWindowHeight"}, 480); + + // Memory + m_settings->registerSetting({"MinMemAlloc", "MinMemoryAlloc"}, 512); + m_settings->registerSetting({"MaxMemAlloc", "MaxMemoryAlloc"}, 1024); + m_settings->registerSetting("PermGen", 64); + + // Java Settings + m_settings->registerSetting("JavaPath", ""); + m_settings->registerSetting("LastHostname", ""); + m_settings->registerSetting("JvmArgs", ""); + + // 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", ""); } void MultiMC::initHttpMetaCache() @@ -374,6 +445,7 @@ void MultiMC::initHttpMetaCache() m_metacache->addBase("libraries", QDir("libraries").absolutePath()); m_metacache->addBase("minecraftforge", QDir("mods/minecraftforge").absolutePath()); m_metacache->addBase("skins", QDir("accounts/skins").absolutePath()); + m_metacache->addBase("root", QDir(".").absolutePath()); m_metacache->Load(); } @@ -422,27 +494,20 @@ std::shared_ptr MultiMC::javalist() return m_javalist; } -#ifdef WINDOWS -#define UPDATER_BIN "updater.exe" -#elif LINUX -#define UPDATER_BIN "updater" -#elif OSX -#define UPDATER_BIN "updater" -#else -#error Unsupported operating system. -#endif - -void MultiMC::installUpdates(const QString& updateFilesDir, bool restartOnFinish) +void MultiMC::installUpdates(const QString &updateFilesDir, bool restartOnFinish) { QLOG_INFO() << "Installing updates."; #if LINUX - // On Linux, the MultiMC executable file is actually in the bin folder inside the installation directory. + // On Linux, the MultiMC executable file is actually in the bin folder inside the + // installation directory. // This means that MultiMC's *actual* install path is the parent folder. - // We need to tell the updater to run with this directory as the install path, rather than the bin folder where the executable is. + // We need to tell the updater to run with this directory as the install path, rather than + // the bin folder where the executable is. // On other operating systems, we'll just use the path to the executable. QString appDir = QFileInfo(MMC->applicationDirPath()).dir().path(); - // On Linux, we also need to set the finish command to the launch script, rather than the binary. + // On Linux, we also need to set the finish command to the launch script, rather than the + // binary. QString finishCmd = PathCombine(appDir, "MultiMC"); #else QString appDir = MMC->applicationDirPath(); @@ -450,28 +515,35 @@ void MultiMC::installUpdates(const QString& updateFilesDir, bool restartOnFinish #endif // Build the command we'll use to run the updater. - // Note, the above comment about the app dir path on Linux is irrelevant here because the updater binary is always in the + // Note, the above comment about the app dir path on Linux is irrelevant here because the + // updater binary is always in the // same folder as the main binary. QString updaterBinary = PathCombine(MMC->applicationDirPath(), UPDATER_BIN); QStringList args; - // ./updater --install-dir $INSTALL_DIR --package-dir $UPDATEFILES_DIR --script $UPDATEFILES_DIR/file_list.xml --wait $PID --mode main + // ./updater --install-dir $INSTALL_DIR --package-dir $UPDATEFILES_DIR --script + // $UPDATEFILES_DIR/file_list.xml --wait $PID --mode main args << "--install-dir" << appDir; args << "--package-dir" << updateFilesDir; - args << "--script" << PathCombine(updateFilesDir, "file_list.xml"); - args << "--wait" << QString::number(MMC->applicationPid()); + args << "--script" << PathCombine(updateFilesDir, "file_list.xml"); + args << "--wait" << QString::number(MMC->applicationPid()); if (restartOnFinish) - args << "--finish-cmd" << finishCmd; + args << "--finish-cmd" << finishCmd; QLOG_INFO() << "Running updater with command" << updaterBinary << args.join(" "); + QFile::setPermissions(updaterBinary, (QFileDevice::Permission)0x7755); - QProcess::startDetached(updaterBinary, args); + if (!QProcess::startDetached(updaterBinary, args)) + { + QLOG_ERROR() << "Failed to start the updater process!"; + return; + } // Now that we've started the updater, quit MultiMC. MMC->quit(); } -void MultiMC::setUpdateOnExit(const QString& updateFilesDir) +void MultiMC::setUpdateOnExit(const QString &updateFilesDir) { m_updateOnExitPath = updateFilesDir; } @@ -481,5 +553,18 @@ QString MultiMC::getExitUpdatePath() const return m_updateOnExitPath; } +bool MultiMC::openJsonEditor(const QString &filename) +{ + const QString file = QDir::current().absoluteFilePath(filename); + if (m_settings->get("JsonEditor").toString().isEmpty()) + { + return QDesktopServices::openUrl(QUrl::fromLocalFile(file)); + } + else + { + return QProcess::startDetached(m_settings->get("JsonEditor").toString(), QStringList() + << file); + } +} #include "MultiMC.moc" diff --git a/MultiMC.h b/MultiMC.h index 775189b1e..9ad276ff8 100644 --- a/MultiMC.h +++ b/MultiMC.h @@ -6,7 +6,6 @@ #include "logger/QsLog.h" #include "logger/QsLogDest.h" - class MinecraftVersionList; class LWJGLVersionList; class HttpMetaCache; @@ -107,12 +106,12 @@ public: /*! * Installs update from the given update files directory. */ - void installUpdates(const QString& updateFilesDir, bool restartOnFinish=false); + void installUpdates(const QString &updateFilesDir, bool restartOnFinish = false); /*! * Sets MultiMC to install updates from the given directory when it exits. */ - void setUpdateOnExit(const QString& updateFilesDir); + void setUpdateOnExit(const QString &updateFilesDir); /*! * Gets the path to install updates from on exit. @@ -120,6 +119,12 @@ public: */ QString getExitUpdatePath() const; + /*! + * Opens a json file using either a system default editor, or, if note empty, the editor + * specified in the settings + */ + bool openJsonEditor(const QString &filename); + private: void initLogger(); @@ -130,6 +135,9 @@ private: void initTranslations(); private: + friend class UpdateCheckerTest; + friend class DownloadUpdateTaskTest; + std::shared_ptr m_qt_translator; std::shared_ptr m_mmc_translator; std::shared_ptr m_settings; diff --git a/MultiMC.manifest b/MultiMC.manifest new file mode 100644 index 000000000..3acf8f7f3 --- /dev/null +++ b/MultiMC.manifest @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + Custom Minecraft launcher for managing multiple installs. + + + + + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index 456f85e9f..227f0731c 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,16 @@ Check [BUILD.md](BUILD.md) for build instructions. ## Contributing The repository is currently managed by @peterix and @drayshak - we're the ones likely to review pull requests. If you'd like to contribute to the project please talk to us on IRC (Esper/#MultiMC) first! This helps us organise ideas and keep in contact with you, and we're unlikely to accept anything blindly. +We use [Clang Format](http://clang.llvm.org/docs/ClangFormat.html) to format the project. We highly recommend setting it up so the project stays well formatted, but there are issues with it on Windows. If you have trouble setting it up, check [.clang-format](.clang-format) manually. We don't accept pull requests with poor formatting. If you have questions, talk to us on IRC (Esper/#MultiMC) _before_ submitting a pull request. + +## Forking/Redistributing +We keep MultiMC open source because we think it's important to be able to see the source code for a project like this, and we do so using the Apache license. + +Part of the reason for using the Apache license is we don't want people using the "MultiMC" name when redistributing the project. This means people must take the time to go through the source code and remove all references to "MultiMC", including but not limited to the project icon and the title of windows, (no *MultiMC-fork* in the title). + +Apache covers reasonable use for the name - a mention of the project's origins in the About dialog and the license is acceptable. However, it should be abundantly clear that the project is a fork *without* implying that you have our blessing. + + ## License Copyright © 2013 MultiMC Contributors diff --git a/cmake/MacOSXBundleInfo.plist.in b/cmake/MacOSXBundleInfo.plist.in new file mode 100644 index 000000000..809fab00b --- /dev/null +++ b/cmake/MacOSXBundleInfo.plist.in @@ -0,0 +1,40 @@ + + + + + NSPrincipalClass + NSApplication + NSHighResolutionCapable + True + CFBundleDevelopmentRegion + English + CFBundleExecutable + ${MACOSX_BUNDLE_EXECUTABLE_NAME} + CFBundleGetInfoString + ${MACOSX_BUNDLE_INFO_STRING} + CFBundleIconFile + ${MACOSX_BUNDLE_ICON_FILE} + CFBundleIdentifier + ${MACOSX_BUNDLE_GUI_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleLongVersionString + ${MACOSX_BUNDLE_LONG_VERSION_STRING} + CFBundleName + ${MACOSX_BUNDLE_BUNDLE_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + ${MACOSX_BUNDLE_SHORT_VERSION_STRING} + CFBundleSignature + ???? + CFBundleVersion + ${MACOSX_BUNDLE_BUNDLE_VERSION} + CSResourcesFileMapped + + LSRequiresCarbon + + NSHumanReadableCopyright + ${MACOSX_BUNDLE_COPYRIGHT} + + diff --git a/depends/pack200/anti200.cpp b/depends/pack200/anti200.cpp index 3dfdb5dca..1e1ec0c89 100644 --- a/depends/pack200/anti200.cpp +++ b/depends/pack200/anti200.cpp @@ -8,21 +8,36 @@ int main(int argc, char **argv) { - if (argc == 3) + if (argc != 3) { - try - { - unpack_200(argv[1], argv[2]); - } - catch (std::runtime_error &e) - { - std::cerr << "Bad things happened: " << e.what() << std::endl; - return EXIT_FAILURE; - } - return EXIT_SUCCESS; - } - else std::cerr << "Simple pack200 unpacker!" << std::endl << "Run like this:" << std::endl << " " << argv[0] << " input.jar.lzma output.jar" << std::endl; - return EXIT_FAILURE; + return EXIT_FAILURE; + } + + FILE *input = fopen(argv[1], "rb"); + FILE *output = fopen(argv[2], "wb"); + if (!input) + { + std::cerr << "Can't open input file"; + return EXIT_FAILURE; + } + if (!output) + { + fclose(output); + std::cerr << "Can't open output file"; + return EXIT_FAILURE; + } + try + { + unpack_200(input, output); + } + catch (std::runtime_error &e) + { + std::cerr << "Bad things happened: " << e.what() << std::endl; + fclose(input); + fclose(output); + return EXIT_FAILURE; + } + return EXIT_SUCCESS; } diff --git a/depends/pack200/include/unpack200.h b/depends/pack200/include/unpack200.h index bcee80092..f9239488b 100644 --- a/depends/pack200/include/unpack200.h +++ b/depends/pack200/include/unpack200.h @@ -34,4 +34,4 @@ * @return void * @throw std::runtime_error for any error encountered */ -void unpack_200(std::string input_path, std::string output_path); +void unpack_200(FILE * input, FILE * output); diff --git a/depends/pack200/src/unpack200.cpp b/depends/pack200/src/unpack200.cpp index 0a9d27148..22b7f3b09 100644 --- a/depends/pack200/src/unpack200.cpp +++ b/depends/pack200/src/unpack200.cpp @@ -94,20 +94,9 @@ static int read_magic(unpacker *u, char peek[], int peeklen) return magic; } -void unpack_200(std::string input_path, std::string output_path) +void unpack_200(FILE *input, FILE *output) { unpacker u; - FILE *input = fopen(input_path.c_str(), "rb"); - if (!input) - { - throw std::runtime_error("Can't open input file" + input_path); - } - FILE *output = fopen(output_path.c_str(), "wb"); - if (!output) - { - fclose(output); - throw std::runtime_error("Can't open output file" + output_path); - } u.init(read_input_via_stdio); // initialize jar output diff --git a/depends/settings/CMakeLists.txt b/depends/settings/CMakeLists.txt index 154697f63..da853a739 100644 --- a/depends/settings/CMakeLists.txt +++ b/depends/settings/CMakeLists.txt @@ -5,44 +5,27 @@ find_package(Qt5Core REQUIRED) # Include Qt headers. include_directories(${Qt5Base_INCLUDE_DIRS}) -include_directories(${Qt5Network_INCLUDE_DIRS}) -SET(LIBSETTINGS_HEADERS -include/libsettings_config.h - -include/inifile.h - -include/settingsobject.h -include/setting.h -include/overridesetting.h - -include/basicsettingsobject.h -include/inisettingsobject.h - -include/keyring.h -) - -SET(LIBSETTINGS_HEADERS_PRIVATE -src/stubkeyring.h -) SET(LIBSETTINGS_SOURCES -src/inifile.cpp +libsettings_config.h -src/settingsobject.cpp -src/setting.cpp -src/overridesetting.cpp +inifile.h +inifile.cpp -src/basicsettingsobject.cpp -src/inisettingsobject.cpp +settingsobject.h +settingsobject.cpp +inisettingsobject.h +inisettingsobject.cpp -src/keyring.cpp -src/stubkeyring.cpp +setting.h +setting.cpp +overridesetting.h +overridesetting.cpp ) # Set the include dir path. -SET(LIBSETTINGS_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/include" PARENT_SCOPE) -include_directories(${LIBSETTINGS_INCLUDE_DIR}) +SET(LIBSETTINGS_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}" PARENT_SCOPE) # Static link! ADD_DEFINITIONS(-DLIBSETTINGS_STATIC) @@ -59,6 +42,6 @@ IF(MultiMC_CODE_COVERAGE) SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -O0 --coverage") ENDIF(MultiMC_CODE_COVERAGE) -add_library(libSettings STATIC ${LIBSETTINGS_SOURCES} ${LIBSETTINGS_HEADERS} ${LIBSETTINGS_HEADERS_PRIVATE}) +add_library(libSettings STATIC ${LIBSETTINGS_SOURCES}) qt5_use_modules(libSettings Core) target_link_libraries(libSettings) diff --git a/depends/settings/include/keyring.h b/depends/settings/include/keyring.h deleted file mode 100644 index a4da23b1c..000000000 --- a/depends/settings/include/keyring.h +++ /dev/null @@ -1,97 +0,0 @@ -/* Copyright 2013 MultiMC Contributors - * - * Authors: Orochimarufan - * - * 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. - */ - -#pragma once - -#include - -#include "libsettings_config.h" - -/** - * @file libsettings/include/keyring.h - * Access to System Keyrings - */ - -/** - * @brief The Keyring class - * the System Keyring/Keychain/Wallet/Vault/etc - */ -class LIBSETTINGS_EXPORT Keyring -{ -public: - /** - * @brief virtual dtor - */ - virtual ~Keyring() {}; - - /** - * @brief the System Keyring instance - * @return the Keyring instance - */ - static Keyring *instance(); - - /** - * @brief store a password in the Keyring - * @param service the service name - * @param username the account name - * @param password the password to store - * @return success - */ - virtual bool storePassword(QString service, QString username, QString password) = 0; - - /** - * @brief get a password from the Keyring - * @param service the service name - * @param username the account name - * @return the password (success=!isNull()) - */ - virtual QString getPassword(QString service, QString username) = 0; - - /** - * @brief lookup a password - * @param service the service name - * @param username the account name - * @return wether the password is available - */ - virtual bool hasPassword(QString service, QString username) = 0; - - /** - * @brief get a list of all stored accounts. - * @param service the service name - * @return - */ - virtual QStringList getStoredAccounts(QString service) = 0; - - /** - * @brief Remove the specified account from storage - * @param service the service name - * @param username the account name - * @return - */ - virtual void removeStoredAccount(QString service, QString username) = 0; - -protected: - /// fall back to StubKeyring if false - virtual bool isValid() - { - return false; - } - -private: - static Keyring *m_instance; - static void destroy(); -}; diff --git a/depends/settings/src/inifile.cpp b/depends/settings/inifile.cpp similarity index 98% rename from depends/settings/src/inifile.cpp rename to depends/settings/inifile.cpp index 83aec15e8..1170f0b1c 100644 --- a/depends/settings/src/inifile.cpp +++ b/depends/settings/inifile.cpp @@ -13,7 +13,7 @@ * limitations under the License. */ -#include "include/inifile.h" +#include "inifile.h" #include #include diff --git a/depends/settings/include/inifile.h b/depends/settings/inifile.h similarity index 100% rename from depends/settings/include/inifile.h rename to depends/settings/inifile.h diff --git a/depends/settings/src/inisettingsobject.cpp b/depends/settings/inisettingsobject.cpp similarity index 64% rename from depends/settings/src/inisettingsobject.cpp rename to depends/settings/inisettingsobject.cpp index 4a7a7428a..5e52a56f3 100644 --- a/depends/settings/src/inisettingsobject.cpp +++ b/depends/settings/inisettingsobject.cpp @@ -13,8 +13,8 @@ * limitations under the License. */ -#include "include/inisettingsobject.h" -#include "include/setting.h" +#include "inisettingsobject.h" +#include "setting.h" INISettingsObject::INISettingsObject(const QString &path, QObject *parent) : SettingsObject(parent) @@ -32,31 +32,45 @@ void INISettingsObject::changeSetting(const Setting &setting, QVariant value) { if (contains(setting.id())) { + // valid value -> set the main config, remove all the sysnonyms if (value.isValid()) - m_ini.set(setting.configKey(), value); + { + auto list = setting.configKeys(); + m_ini.set(list.takeFirst(), value); + for(auto iter: list) + m_ini.remove(iter); + } + // invalid -> remove all (just like resetSetting) else - m_ini.remove(setting.configKey()); + { + for(auto iter: setting.configKeys()) + m_ini.remove(iter); + } m_ini.saveFile(m_filePath); } } void INISettingsObject::resetSetting(const Setting &setting) { + // if we have the setting, remove all the synonyms. ALL OF THEM if (contains(setting.id())) { - m_ini.remove(setting.configKey()); + for(auto iter: setting.configKeys()) + m_ini.remove(iter); m_ini.saveFile(m_filePath); } } QVariant INISettingsObject::retrieveValue(const Setting &setting) { + // if we have the setting, return value of the first matching synonym if (contains(setting.id())) { - return m_ini.get(setting.configKey(), QVariant()); - } - else - { - return QVariant(); + for(auto iter: setting.configKeys()) + { + if(m_ini.contains(iter)) + return m_ini[iter]; + } } + return QVariant(); } diff --git a/depends/settings/include/inisettingsobject.h b/depends/settings/inisettingsobject.h similarity index 100% rename from depends/settings/include/inisettingsobject.h rename to depends/settings/inisettingsobject.h diff --git a/depends/settings/include/libsettings_config.h b/depends/settings/libsettings_config.h similarity index 99% rename from depends/settings/include/libsettings_config.h rename to depends/settings/libsettings_config.h index ba77f640d..e5beed28e 100644 --- a/depends/settings/include/libsettings_config.h +++ b/depends/settings/libsettings_config.h @@ -26,3 +26,4 @@ #define LIBSETTINGS_EXPORT Q_DECL_IMPORT #endif #endif + diff --git a/depends/settings/src/overridesetting.cpp b/depends/settings/overridesetting.cpp similarity index 82% rename from depends/settings/src/overridesetting.cpp rename to depends/settings/overridesetting.cpp index 5b10920d0..7b5f5418a 100644 --- a/depends/settings/src/overridesetting.cpp +++ b/depends/settings/overridesetting.cpp @@ -13,10 +13,10 @@ * limitations under the License. */ -#include "include/overridesetting.h" +#include "overridesetting.h" -OverrideSetting::OverrideSetting(const QString &name, Setting *other, QObject *parent) - : Setting(name, QVariant(), parent) +OverrideSetting::OverrideSetting(std::shared_ptr other) + : Setting(other->configKeys(), QVariant()) { m_other = other; } diff --git a/depends/settings/include/overridesetting.h b/depends/settings/overridesetting.h similarity index 90% rename from depends/settings/include/overridesetting.h rename to depends/settings/overridesetting.h index 3e60dc7c6..5ef901d09 100644 --- a/depends/settings/include/overridesetting.h +++ b/depends/settings/overridesetting.h @@ -16,6 +16,7 @@ #pragma once #include +#include #include "setting.h" @@ -31,10 +32,10 @@ class LIBSETTINGS_EXPORT OverrideSetting : public Setting { Q_OBJECT public: - explicit OverrideSetting(const QString &name, Setting *other, QObject *parent = 0); + explicit OverrideSetting(std::shared_ptr other); virtual QVariant defValue() const; protected: - Setting *m_other; + std::shared_ptr m_other; }; diff --git a/depends/settings/src/setting.cpp b/depends/settings/setting.cpp similarity index 80% rename from depends/settings/src/setting.cpp rename to depends/settings/setting.cpp index 899463dc6..0d6857718 100644 --- a/depends/settings/src/setting.cpp +++ b/depends/settings/setting.cpp @@ -13,17 +13,17 @@ * limitations under the License. */ -#include "include/setting.h" -#include "include/settingsobject.h" +#include "setting.h" +#include "settingsobject.h" -Setting::Setting(QString id, QVariant defVal, QObject *parent) - : QObject(parent), m_id(id), m_defVal(defVal) +Setting::Setting(QStringList synonyms, QVariant defVal) + : QObject(), m_synonyms(synonyms), m_defVal(defVal) { } QVariant Setting::get() const { - SettingsObject *sbase = qobject_cast(parent()); + SettingsObject *sbase = m_storage; if (!sbase) { return defValue(); diff --git a/depends/settings/include/setting.h b/depends/settings/setting.h similarity index 82% rename from depends/settings/include/setting.h rename to depends/settings/setting.h index 394902078..a73474d27 100644 --- a/depends/settings/include/setting.h +++ b/depends/settings/setting.h @@ -17,6 +17,8 @@ #include #include +#include +#include #include "libsettings_config.h" @@ -29,11 +31,16 @@ class LIBSETTINGS_EXPORT Setting : public QObject { Q_OBJECT public: - /*! - * \brief Constructs a new Setting object with the given parent. - * \param parent The Setting's parent object. + /** + * Construct a Setting + * + * Synonyms are all the possible names used in the settings object, in order of preference. + * First synonym is the ID, which identifies the setting in MultiMC. + * + * defVal is the default value that will be returned when the settings object + * doesn't have any value for this setting. */ - explicit Setting(QString id, QVariant defVal = QVariant(), QObject *parent = 0); + explicit Setting(QStringList synonyms, QVariant defVal = QVariant()); /*! * \brief Gets this setting's ID. @@ -44,7 +51,7 @@ public: */ virtual QString id() const { - return m_id; + return m_synonyms.first(); } /*! @@ -53,9 +60,9 @@ public: * the same as the setting's ID, but it can be different. * \return The setting's config file key. */ - virtual QString configKey() const + virtual QStringList configKeys() const { - return id(); + return m_synonyms; } /*! @@ -67,16 +74,6 @@ public: */ virtual QVariant get() const; - /*! - * \brief Gets this setting's actual value (I.E. not as a QVariant). - * This function is just shorthand for get().value() - * \return The setting's actual value. - */ - template inline T value() const - { - return get().value(); - } - /*! * \brief Gets this setting's default value. * \return The default value of this setting. @@ -111,11 +108,12 @@ slots: * \brief Reset the setting to default * This is done by emitting the settingReset() signal which will then be * handled by the SettingsObject object and cause the setting to change. - * \param value The new value. */ virtual void reset(); protected: - QString m_id; + friend class SettingsObject; + SettingsObject * m_storage; + QStringList m_synonyms; QVariant m_defVal; }; diff --git a/depends/settings/src/settingsobject.cpp b/depends/settings/settingsobject.cpp similarity index 62% rename from depends/settings/src/settingsobject.cpp rename to depends/settings/settingsobject.cpp index 32a63b8db..43fc989a1 100644 --- a/depends/settings/src/settingsobject.cpp +++ b/depends/settings/settingsobject.cpp @@ -13,8 +13,9 @@ * limitations under the License. */ -#include "include/settingsobject.h" -#include "include/setting.h" +#include "settingsobject.h" +#include "setting.h" +#include "overridesetting.h" #include @@ -22,17 +23,49 @@ SettingsObject::SettingsObject(QObject *parent) : QObject(parent) { } +SettingsObject::~SettingsObject() +{ + m_settings.clear(); +} + +std::shared_ptr SettingsObject::registerOverride(std::shared_ptr original) +{ + if (contains(original->id())) + { + qDebug(QString("Failed to register setting %1. ID already exists.") + .arg(original->id()) + .toUtf8()); + return nullptr; // Fail + } + auto override = std::make_shared(original); + override->m_storage = this; + connectSignals(*override); + m_settings.insert(override->id(), override); + return override; +} + +std::shared_ptr SettingsObject::registerSetting(QStringList synonyms, QVariant defVal) +{ + if (synonyms.empty()) + return nullptr; + if (contains(synonyms.first())) + { + qDebug(QString("Failed to register setting %1. ID already exists.") + .arg(synonyms.first()) + .toUtf8()); + return nullptr; // Fail + } + auto setting = std::make_shared(synonyms, defVal); + setting->m_storage = this; + connectSignals(*setting); + m_settings.insert(setting->id(), setting); + return setting; +} + +/* + bool SettingsObject::registerSetting(Setting *setting) { - // Check if setting is null or we already have a setting with the same ID. - if (!setting) - { - qDebug(QString("Failed to register setting. Setting is null.") - .arg(setting->id()) - .toUtf8()); - return false; // Fail - } - if (contains(setting->id())) { qDebug(QString("Failed to register setting %1. ID already exists.") @@ -50,21 +83,8 @@ bool SettingsObject::registerSetting(Setting *setting) // qDebug(QString("Registered setting %1.").arg(setting->id()).toUtf8()); return true; } - -void SettingsObject::unregisterSetting(Setting *setting) -{ - if (!setting || !m_settings.contains(setting->id())) - return; // We can't unregister something that's not registered. - - m_settings.remove(setting->id()); - - // Disconnect signals. - disconnectSignals(*setting); - - setting->setParent(NULL); // Drop ownership. -} - -Setting *SettingsObject::getSetting(const QString &id) const +*/ +std::shared_ptr SettingsObject::getSetting(const QString &id) const { // Make sure there is a setting with the given ID. if (!m_settings.contains(id)) @@ -75,13 +95,13 @@ Setting *SettingsObject::getSetting(const QString &id) const QVariant SettingsObject::get(const QString &id) const { - Setting *setting = getSetting(id); + auto setting = getSetting(id); return (setting ? setting->get() : QVariant()); } bool SettingsObject::set(const QString &id, QVariant value) { - Setting *setting = getSetting(id); + auto setting = getSetting(id); if (!setting) { qDebug(QString("Error changing setting %1. Setting doesn't exist.").arg(id).toUtf8()); @@ -96,16 +116,11 @@ bool SettingsObject::set(const QString &id, QVariant value) void SettingsObject::reset(const QString &id) const { - Setting *setting = getSetting(id); + auto setting = getSetting(id); if (setting) setting->reset(); } -QList SettingsObject::getSettings() -{ - return m_settings.values(); -} - bool SettingsObject::contains(const QString &id) { return m_settings.contains(id); @@ -121,16 +136,3 @@ void SettingsObject::connectSignals(const Setting &setting) connect(&setting, SIGNAL(settingReset(Setting)), SLOT(resetSetting(const Setting &))); connect(&setting, SIGNAL(settingReset(Setting)), SIGNAL(settingReset(const Setting &))); } - -void SettingsObject::disconnectSignals(const Setting &setting) -{ - setting.disconnect(SIGNAL(settingChanged(const Setting &, QVariant)), this, - SLOT(changeSetting(const Setting &, QVariant))); - setting.disconnect(SIGNAL(settingChanged(const Setting &, QVariant)), this, - SIGNAL(settingChanged(const Setting &, QVariant))); - - setting.disconnect(SIGNAL(settingReset(const Setting &, QVariant)), this, - SLOT(resetSetting(const Setting &, QVariant))); - setting.disconnect(SIGNAL(settingReset(const Setting &, QVariant)), this, - SIGNAL(settingReset(const Setting &, QVariant))); -} diff --git a/depends/settings/include/settingsobject.h b/depends/settings/settingsobject.h similarity index 66% rename from depends/settings/include/settingsobject.h rename to depends/settings/settingsobject.h index 7a6b3cb60..27746f2dc 100644 --- a/depends/settings/include/settingsobject.h +++ b/depends/settings/settingsobject.h @@ -17,6 +17,9 @@ #include #include +#include +#include +#include #include "libsettings_config.h" @@ -39,32 +42,37 @@ class LIBSETTINGS_EXPORT SettingsObject : public QObject Q_OBJECT public: explicit SettingsObject(QObject *parent = 0); - + virtual ~SettingsObject(); /*! - * \brief Registers the given setting with this SettingsObject and connects the necessary - * signals. + * Registers an override setting for the given original setting in this settings object + * * This will fail if there is already a setting with the same ID as * the one that is being registered. - * \note Registering a setting object causes the SettingsObject to take ownership - * of the object. This means that setting's parent will be set to the object - * it was registered with. Because the object it was registered with has taken - * ownership, it becomes responsible for managing that setting object's memory. - * \warning Do \b not delete the setting after registering it. - * \param setting A pointer to the setting that will be registered. - * \return True if successful. False if registry failed. + * \return A valid Setting shared pointer if successful. */ - virtual bool registerSetting(Setting *setting); + std::shared_ptr registerOverride(std::shared_ptr original); /*! - * \brief Unregisters the given setting from this SettingsObject and disconnects its - * signals. - * \note This does not delete the setting. Furthermore, when the setting is - * unregistered, the SettingsObject drops ownership of the setting. This means - * that if you unregister a setting, its parent is set to null and you become - * responsible for freeing its memory. - * \param setting The setting to unregister. + * Registers the given setting with this SettingsObject and connects the necessary signals. + * + * This will fail if there is already a setting with the same ID as + * the one that is being registered. + * \return A valid Setting shared pointer if successful. */ - virtual void unregisterSetting(Setting *setting); + std::shared_ptr registerSetting(QStringList synonyms, + QVariant defVal = QVariant()); + + /*! + * Registers the given setting with this SettingsObject and connects the necessary signals. + * + * This will fail if there is already a setting with the same ID as + * the one that is being registered. + * \return A valid Setting shared pointer if successful. + */ + std::shared_ptr registerSetting(QString id, QVariant defVal = QVariant()) + { + return registerSetting(QStringList(id), defVal); + } /*! * \brief Gets the setting with the given ID. @@ -73,18 +81,7 @@ public: * Returns null if there is no setting with the given ID. * \sa operator []() */ - virtual Setting *getSetting(const QString &id) const; - - /*! - * \brief Same as getSetting() - * \param id The ID of the setting to get. - * \return A pointer to the setting with the given ID. - * \sa getSetting() - */ - inline Setting *operator[](const QString &id) - { - return getSetting(id); - } + std::shared_ptr getSetting(const QString &id) const; /*! * \brief Gets the value of the setting with the given ID. @@ -92,7 +89,7 @@ public: * \return The setting's value as a QVariant. * If no setting with the given ID exists, returns an invalid QVariant. */ - virtual QVariant get(const QString &id) const; + QVariant get(const QString &id) const; /*! * \brief Sets the value of the setting with the given ID. @@ -101,27 +98,20 @@ public: * \param value The new value of the setting. * \return True if successful, false if it failed. */ - virtual bool set(const QString &id, QVariant value); + bool set(const QString &id, QVariant value); /*! * \brief Reverts the setting with the given ID to default. * \param id The ID of the setting to reset. */ - virtual void reset(const QString &id) const; - - /*! - * \brief Gets a QList with pointers to all of the registered settings. - * The order of the entries in the list is undefined. - * \return A QList with pointers to all registered settings. - */ - virtual QList getSettings(); + void reset(const QString &id) const; /*! * \brief Checks if this SettingsObject contains a setting with the given ID. * \param id The ID to check for. * \return True if the SettingsObject has a setting with the given ID. */ - virtual bool contains(const QString &id); + bool contains(const QString &id); signals: /*! @@ -167,13 +157,7 @@ protected: * \brief Connects the necessary signals to the given Setting. * \param setting The setting to connect. */ - virtual void connectSignals(const Setting &setting); - - /*! - * \brief Disconnects signals from the given Setting. - * \param setting The setting to disconnect. - */ - virtual void disconnectSignals(const Setting &setting); + void connectSignals(const Setting &setting); /*! * \brief Function used by Setting objects to get their values from the SettingsObject. @@ -185,5 +169,5 @@ protected: friend class Setting; private: - QMap m_settings; + QMap> m_settings; }; diff --git a/depends/settings/src/keyring.cpp b/depends/settings/src/keyring.cpp deleted file mode 100644 index 9eaba6841..000000000 --- a/depends/settings/src/keyring.cpp +++ /dev/null @@ -1,63 +0,0 @@ -/* Copyright 2013 MultiMC Contributors - * - * Authors: Orochimarufan - * - * 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 "include/keyring.h" - -#include "osutils.h" - -#include "stubkeyring.h" - -// system specific keyrings -/*#if defined(OSX) -class OSXKeychain; -#define KEYRING OSXKeychain -#elif defined(LINUX) -class XDGKeyring; -#define KEYRING XDGKeyring -#elif defined(WINDOWS) -class Win32Keystore; -#define KEYRING Win32Keystore -#else -#pragma message Keyrings are not supported on your os. Falling back to the insecure StubKeyring -#endif*/ - -Keyring *Keyring::instance() -{ - if (m_instance == nullptr) - { -#ifdef KEYRING - m_instance = new KEYRING(); - if (!m_instance->isValid()) - { - qWarning("Could not create SystemKeyring! falling back to StubKeyring."); - m_instance = new StubKeyring(); - } -#else - qWarning("Keyrings are not supported on your OS. Fallback StubKeyring is insecure!"); - m_instance = new StubKeyring(); -#endif - atexit(Keyring::destroy); - } - return m_instance; -} - -void Keyring::destroy() -{ - delete m_instance; -} - -Keyring *Keyring::m_instance; diff --git a/depends/settings/src/stubkeyring.cpp b/depends/settings/src/stubkeyring.cpp deleted file mode 100644 index 53fca0259..000000000 --- a/depends/settings/src/stubkeyring.cpp +++ /dev/null @@ -1,105 +0,0 @@ -/* Copyright 2013 MultiMC Contributors - * - * Authors: Orochimarufan - * - * 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 "stubkeyring.h" - -#include - -// Scrambling -// this is NOT SAFE, but it's not plain either. -int scrambler = 0x9586309; - -QString scramble(QString in_) -{ - QByteArray in = in_.toUtf8(); - QByteArray out; - for (int i = 0; i < in.length(); i++) - out.append(in.at(i) ^ scrambler); - return QString::fromUtf8(out); -} - -inline QString base64(QString in) -{ - return QString(in.toUtf8().toBase64()); -} -inline QString unbase64(QString in) -{ - return QString::fromUtf8(QByteArray::fromBase64(in.toLatin1())); -} - -inline QString scramble64(QString in) -{ - return base64(scramble(in)); -} -inline QString unscramble64(QString in) -{ - return scramble(unbase64(in)); -} - -// StubKeyring implementation -inline QString generateKey(QString service, QString username) -{ - return QString("%1/%2").arg(base64(service)).arg(scramble64(username)); -} - -bool StubKeyring::storePassword(QString service, QString username, QString password) -{ - m_settings.setValue(generateKey(service, username), scramble64(password)); - return true; -} - -QString StubKeyring::getPassword(QString service, QString username) -{ - QString key = generateKey(service, username); - if (!m_settings.contains(key)) - return QString(); - return unscramble64(m_settings.value(key).toString()); -} - -bool StubKeyring::hasPassword(QString service, QString username) -{ - return m_settings.contains(generateKey(service, username)); -} - -QStringList StubKeyring::getStoredAccounts(QString service) -{ - service = base64(service).append('/'); - QStringList out; - QStringList in(m_settings.allKeys()); - QStringListIterator it(in); - while (it.hasNext()) - { - QString c = it.next(); - if (c.startsWith(service)) - out << unscramble64(c.mid(service.length())); - } - return out; -} - -void StubKeyring::removeStoredAccount(QString service, QString username) -{ - QString key = generateKey(service, username); - m_settings.remove(key); -} - -// FIXME: this needs tweaking/changes for user account level storage -StubKeyring::StubKeyring() - : - // m_settings(QSettings::UserScope, "Orochimarufan", "Keyring") - m_settings("keyring.cfg", QSettings::IniFormat) -{ -} diff --git a/depends/settings/src/stubkeyring.h b/depends/settings/src/stubkeyring.h deleted file mode 100644 index 1f4b1be07..000000000 --- a/depends/settings/src/stubkeyring.h +++ /dev/null @@ -1,47 +0,0 @@ -/* Copyright 2013 MultiMC Contributors - * - * Authors: Orochimarufan - * - * 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. - */ - -#pragma once - -#include "include/keyring.h" - -#include - -class StubKeyring : public Keyring -{ -public: - /** - * @brief virtual dtor - */ - virtual ~StubKeyring() {}; - - virtual bool storePassword(QString service, QString username, QString password); - virtual QString getPassword(QString service, QString username); - virtual bool hasPassword(QString service, QString username); - virtual QStringList getStoredAccounts(QString service); - virtual void removeStoredAccount(QString service, QString username); - -private: - friend class Keyring; - explicit StubKeyring(); - virtual bool isValid() - { - return true; - } - - QSettings m_settings; -}; diff --git a/depends/util/src/pathutils.cpp b/depends/util/src/pathutils.cpp index 485d03e8c..208887549 100644 --- a/depends/util/src/pathutils.cpp +++ b/depends/util/src/pathutils.cpp @@ -23,10 +23,7 @@ QString PathCombine(QString path1, QString path2) { - if (!path1.endsWith('/')) - return path1.append('/').append(path2); - else - return path1.append(path2); + return QDir::cleanPath(path1 + QDir::separator() + path2); } QString PathCombine(QString path1, QString path2, QString path3) diff --git a/gui/ConsoleWindow.cpp b/gui/ConsoleWindow.cpp index 24afbc0ad..e640d2619 100644 --- a/gui/ConsoleWindow.cpp +++ b/gui/ConsoleWindow.cpp @@ -58,10 +58,17 @@ ConsoleWindow::~ConsoleWindow() void ConsoleWindow::writeColor(QString text, const char *color) { // append a paragraph - if (color != nullptr) - ui->text->appendHtml(QString("%2").arg(color).arg(text)); - else - ui->text->appendPlainText(text); + QString newtext; + newtext += ""; + newtext += text.toHtmlEscaped(); + newtext += ""; + ui->text->appendHtml(newtext); } void ConsoleWindow::write(QString data, MessageLevel::Enum mode) diff --git a/gui/ConsoleWindow.ui b/gui/ConsoleWindow.ui index 62cc89ac2..c2307ecc5 100644 --- a/gui/ConsoleWindow.ui +++ b/gui/ConsoleWindow.ui @@ -17,11 +17,6 @@ - - - 10 - - false diff --git a/gui/MainWindow.cpp b/gui/MainWindow.cpp index 85ad319bd..42823fa58 100644 --- a/gui/MainWindow.cpp +++ b/gui/MainWindow.cpp @@ -66,7 +66,7 @@ #include "logic/lists/InstanceList.h" #include "logic/lists/MinecraftVersionList.h" #include "logic/lists/LwjglVersionList.h" -#include "logic/lists/IconList.h" +#include "logic/icons/IconList.h" #include "logic/lists/JavaVersionList.h" #include "logic/auth/flows/AuthenticateTask.h" @@ -90,7 +90,9 @@ #include "logic/LegacyInstance.h" #include "logic/assets/AssetsUtils.h" +#include "logic/assets/AssetsMigrateTask.h" #include +#include MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { @@ -99,7 +101,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi setWindowTitle(QString("MultiMC %1").arg(MMC->version().toString())); // OSX magic. - setUnifiedTitleAndToolBarOnMac(true); + // setUnifiedTitleAndToolBarOnMac(true); // The instance action toolbar customizations { @@ -178,6 +180,10 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi connect(view->selectionModel(), SIGNAL(currentChanged(const QModelIndex &, const QModelIndex &)), this, SLOT(instanceChanged(const QModelIndex &, const QModelIndex &))); + + // track icon changes and update the toolbar! + connect(MMC->icons().get(), SIGNAL(iconUpdated(QString)), SLOT(iconUpdated(QString))); + // model reset -> selection is invalid. All the instance pointers are wrong. // FIXME: stop using POINTERS everywhere connect(MMC->instances().get(), SIGNAL(dataIsInvalid()), SLOT(selectionBad())); @@ -264,8 +270,14 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi // set up the updater object. auto updater = MMC->updateChecker(); - QObject::connect(updater.get(), &UpdateChecker::updateAvailable, this, - &MainWindow::updateAvailable); + connect(updater.get(), &UpdateChecker::updateAvailable, this, + &MainWindow::updateAvailable); + connect(updater.get(), &UpdateChecker::noUpdateFound, [this]() + { + CustomMessageBox::selectable( + this, tr("No update found."), + tr("No MultiMC update was found!\nYou are using the latest version."))->exec(); + }); // if automatic update checks are allowed, start one. if (MMC->settings()->get("AutoUpdate").toBool()) on_actionCheckUpdate_triggered(); @@ -292,8 +304,6 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi // removing this looks stupid view->setFocus(); - - AssetsUtils::migrateOldAssets(); } MainWindow::~MainWindow() @@ -502,7 +512,7 @@ void MainWindow::downloadUpdates(QString repo, int versionId, bool installOnExit if (installOnExit) MMC->setUpdateOnExit(updateTask.updateFilesDir()); else - MMC->installUpdates(updateTask.updateFilesDir()); + MMC->installUpdates(updateTask.updateFilesDir(), true); } } @@ -674,6 +684,20 @@ void MainWindow::on_actionChangeInstIcon_triggered() } } +void MainWindow::iconUpdated(QString icon) +{ + if(icon == m_currentInstIcon) + { + ui->actionChangeInstIcon->setIcon(MMC->icons()->getIcon(m_currentInstIcon)); + } +} + +void MainWindow::updateInstanceToolIcon(QString new_icon) +{ + m_currentInstIcon = new_icon; + ui->actionChangeInstIcon->setIcon(MMC->icons()->getIcon(m_currentInstIcon)); +} + void MainWindow::on_actionChangeInstGroup_triggered() { if (!m_selectedInstance) @@ -721,7 +745,8 @@ void MainWindow::on_actionConfig_Folder_triggered() void MainWindow::on_actionCheckUpdate_triggered() { auto updater = MMC->updateChecker(); - updater->checkForUpdate(); + + updater->checkForUpdate(true); } void MainWindow::on_actionSettings_triggered() @@ -905,6 +930,8 @@ void MainWindow::doLaunch() if (!account.get()) return; + QString failReason = tr("Your account is currently not logged in. Please enter " + "your password to log in again."); // do the login. if the account has an access token, try to refresh it first. if (account->accountStatus() != NotVerified) { @@ -919,13 +946,28 @@ void MainWindow::doLaunch() { updateInstance(m_selectedInstance, account); } - // revert from online to verified. + else + { + if (!task->successful()) + { + failReason = task->failReason(); + } + if (loginWithPassword(account, failReason)) + updateInstance(m_selectedInstance, account); + } + // in any case, revert from online to verified. + account->downgrade(); + } + else + { + if (loginWithPassword(account, failReason)) + { + updateInstance(m_selectedInstance, account); + account->downgrade(); + } + // in any case, revert from online to verified. account->downgrade(); - return; } - if (loginWithPassword(account, tr("Your account is currently not logged in. Please enter " - "your password to log in again."))) - updateInstance(m_selectedInstance, account); } bool MainWindow::loginWithPassword(MojangAccountPtr account, const QString &errorMsg) @@ -1042,22 +1084,9 @@ void MainWindow::on_actionChangeInstMCVersion_triggered() VersionSelectDialog vselect(m_selectedInstance->versionList().get(), tr("Change Minecraft version"), this); vselect.setFilter(1, "OneSix"); - if (vselect.exec() && vselect.selectedVersion()) - { - if (m_selectedInstance->versionIsCustom()) - { - auto result = CustomMessageBox::selectable( - this, tr("Are you sure?"), - tr("This will remove any library/version customization you did previously. " - "This includes things like Forge install and similar."), - QMessageBox::Warning, QMessageBox::Ok | QMessageBox::Abort, - QMessageBox::Abort)->exec(); + if(!vselect.exec() || !vselect.selectedVersion()) + return; - if (result != QMessageBox::Ok) - return; - } - m_selectedInstance->setIntendedVersionId(vselect.selectedVersion()->descriptor()); - } if (!MMC->accounts()->anyAccountIsValid()) { CustomMessageBox::selectable( @@ -1067,7 +1096,22 @@ void MainWindow::on_actionChangeInstMCVersion_triggered() QMessageBox::Warning)->show(); return; } - auto updateTask = m_selectedInstance->doUpdate(false /*only_prepare*/); + + if (m_selectedInstance->versionIsCustom()) + { + auto result = CustomMessageBox::selectable( + this, tr("Are you sure?"), + tr("This will remove any library/version customization you did previously. " + "This includes things like Forge install and similar."), + QMessageBox::Warning, QMessageBox::Ok | QMessageBox::Abort, + QMessageBox::Abort)->exec(); + + if (result != QMessageBox::Ok) + return; + } + m_selectedInstance->setIntendedVersionId(vselect.selectedVersion()->descriptor()); + + auto updateTask = m_selectedInstance->doUpdate(false); if (!updateTask) { return; @@ -1109,7 +1153,6 @@ void MainWindow::instanceChanged(const QModelIndex ¤t, const QModelIndex & .value())) { ui->instanceToolBar->setEnabled(true); - QString iconKey = m_selectedInstance->iconKey(); renameButton->setText(m_selectedInstance->name()); ui->actionChangeInstLWJGLVersion->setEnabled( m_selectedInstance->menuActionEnabled("actionChangeInstLWJGLVersion")); @@ -1118,8 +1161,7 @@ void MainWindow::instanceChanged(const QModelIndex ¤t, const QModelIndex & ui->actionChangeInstMCVersion->setEnabled( m_selectedInstance->menuActionEnabled("actionChangeInstMCVersion")); m_statusLeft->setText(m_selectedInstance->getStatusbarDescription()); - auto ico = MMC->icons()->getIcon(iconKey); - ui->actionChangeInstIcon->setIcon(ico); + updateInstanceToolIcon(m_selectedInstance->iconKey()); MMC->settings()->set("SelectedInstance", m_selectedInstance->id()); } @@ -1134,12 +1176,11 @@ void MainWindow::instanceChanged(const QModelIndex ¤t, const QModelIndex & void MainWindow::selectionBad() { m_selectedInstance = nullptr; - QString iconKey = "infinity"; + statusBar()->clearMessage(); ui->instanceToolBar->setEnabled(false); renameButton->setText(tr("Rename Instance")); - auto ico = MMC->icons()->getIcon(iconKey); - ui->actionChangeInstIcon->setIcon(ico); + updateInstanceToolIcon("infinity"); } void MainWindow::on_actionEditInstNotes_triggered() @@ -1162,6 +1203,32 @@ void MainWindow::instanceEnded() this->show(); } +void MainWindow::checkMigrateLegacyAssets() +{ + int legacyAssets = AssetsUtils::findLegacyAssets(); + if(legacyAssets > 0) + { + ProgressDialog migrateDlg(this); + AssetsMigrateTask migrateTask(legacyAssets, &migrateDlg); + { + ThreadTask threadTask(&migrateTask); + + if (migrateDlg.exec(&threadTask)) + { + QLOG_INFO() << "Assets migration task completed successfully"; + } + else + { + QLOG_INFO() << "Assets migration task reported failure"; + } + } + } + else + { + QLOG_INFO() << "Didn't find any legacy assets to migrate"; + } +} + void MainWindow::checkSetDefaultJava() { bool askForJava = false; diff --git a/gui/MainWindow.h b/gui/MainWindow.h index 60fde281c..f5b250063 100644 --- a/gui/MainWindow.h +++ b/gui/MainWindow.h @@ -51,6 +51,7 @@ public: void openWebPage(QUrl url); void checkSetDefaultJava(); + void checkMigrateLegacyAssets(); private slots: @@ -145,6 +146,9 @@ slots: void assetsFailed(); void assetsFinished(); + // called when an icon is changed in the icon model. + void iconUpdated(QString); + public slots: void instanceActivated(QModelIndex); @@ -173,6 +177,7 @@ slots: protected: bool eventFilter(QObject *obj, QEvent *ev); void setCatBackground(bool enabled); + void updateInstanceToolIcon(QString new_icon); private: Ui::MainWindow *ui; @@ -186,6 +191,7 @@ private: QToolButton* newsLabel; BaseInstance *m_selectedInstance; + QString m_currentInstIcon; Task *m_versionLoadTask; diff --git a/gui/dialogs/AboutDialog.ui b/gui/dialogs/AboutDialog.ui index 7b91ebc80..df9b1a53d 100644 --- a/gui/dialogs/AboutDialog.ui +++ b/gui/dialogs/AboutDialog.ui @@ -6,8 +6,8 @@ 0 0 - 706 - 579 + 707 + 593 @@ -103,8 +103,8 @@ 0 0 - 688 - 313 + 685 + 304 @@ -113,6 +113,9 @@ + + true + <html><head/><body><p>MultiMC is a custom launcher that makes managing Minecraft easier by allowing you to have multiple instances of Minecraft at once.</p></body></html> @@ -162,8 +165,8 @@ 0 0 - 688 - 313 + 685 + 304 @@ -179,13 +182,22 @@ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } -</style></head><body style=" font-family:'Cantarell'; font-size:11pt; font-weight:400; font-style:normal;"> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Ubuntu';">Andrew Okin &lt;</span><a href="mailto:forkk@forkk.net"><span style=" font-family:'Ubuntu'; text-decoration: underline; color:#0000ff;">forkk@forkk.net</span></a><span style=" font-family:'Ubuntu';">&gt;</span></p> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Ubuntu';">Petr Mrázek &lt;</span><a href="mailto:peterix@gmail.com"><span style=" font-family:'Ubuntu'; text-decoration: underline; color:#0000ff;">peterix@gmail.com</span></a><span style=" font-family:'Ubuntu';">&gt;</span></p> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Ubuntu';">Orochimarufan &lt;</span><a href="mailto:orochimarufan.x3@gmail.com"><span style=" font-family:'Ubuntu'; text-decoration: underline; color:#0000ff;">orochimarufan.x3@gmail.com</span></a><span style=" font-family:'Ubuntu';">&gt;</span></p> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Ubuntu';">TakSuyu &lt;</span><a href="mailto:taksuyu@gmail.com"><span style=" font-family:'Ubuntu'; text-decoration: underline; color:#0000ff;">taksuyu@gmail.com</span></a><span style=" font-family:'Ubuntu';">&gt;</span></p> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Ubuntu';">Sky (Drayshak) &lt;</span><span style=" font-family:'Ubuntu'; text-decoration: underline; color:#0000ff;">multimc@bunnies.cc</span><span style=" font-family:'Ubuntu';">&gt;</span></p> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Ubuntu';">Kilobyte &lt;</span><a href="mailto:stiepen22@gmx.de"><span style=" font-family:'Ubuntu'; text-decoration: underline; color:#0000ff;">stiepen22@gmx.de</span></a><span style=" font-family:'Ubuntu';">&gt;</span></p></body></html> +</style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:7.8pt; font-weight:400; font-style:normal;"> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:10pt; font-weight:600;">MultiMC</span></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:10pt;">Andrew Okin &lt;</span><a href="mailto:forkk@forkk.net"><span style=" font-size:10pt; text-decoration: underline; color:#0000ff;">forkk@forkk.net</span></a><span style=" font-size:10pt;">&gt;</span></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:10pt;">Petr Mrázek &lt;</span><a href="mailto:peterix@gmail.com"><span style=" font-size:10pt; text-decoration: underline; color:#0000ff;">peterix@gmail.com</span></a><span style=" font-size:10pt;">&gt;</span></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:10pt;">Sky &lt;</span><a href="https://www.twitter.com/drayshak"><span style=" font-size:10pt; text-decoration: underline; color:#0000ff;">@drayshak</span></a><span style=" font-size:10pt;">&gt;</span></p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-size:10pt; font-weight:600;"><br /></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Ubuntu'; font-size:10pt; font-weight:600;">With thanks to</span></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:10pt;">Orochimarufan &lt;</span><a href="mailto:orochimarufan.x3@gmail.com"><span style=" font-size:10pt; text-decoration: underline; color:#0000ff;">orochimarufan.x3@gmail.com</span></a><span style=" font-size:10pt;">&gt;</span></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:10pt;">TakSuyu &lt;</span><a href="mailto:taksuyu@gmail.com"><span style=" font-size:10pt; text-decoration: underline; color:#0000ff;">taksuyu@gmail.com</span></a><span style=" font-size:10pt;">&gt;</span></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:10pt;">Kilobyte &lt;</span><a href="mailto:stiepen22@gmx.de"><span style=" font-size:10pt; text-decoration: underline; color:#0000ff;">stiepen22@gmx.de</span></a><span style=" font-size:10pt;">&gt;</span></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:10pt;">Jan (02JanDal) &lt;</span><a href="mailto:02jandal@gmail.com"><span style=" font-size:10pt; text-decoration: underline; color:#0000ff;">02jandal@gmail.com</span></a><span style=" font-size:10pt;">&gt;</span></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:10pt;">Robotbrain &lt;</span><a href="https://twitter.com/skylordelros"><span style=" font-size:10pt; text-decoration: underline; color:#0000ff;">@skylordelros</span></a><span style=" font-size:10pt;">&gt;</span></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:10pt;">Rootbear75 &lt;</span><a href="https://twitter.com/rootbear75"><span style=" font-size:10pt; text-decoration: underline; color:#0000ff;">@rootbear75</span></a><span style=" font-size:10pt;">&gt; (build server)</span></p></body></html> + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse @@ -206,8 +218,8 @@ p, li { white-space: pre-wrap; } 0 0 - 688 - 313 + 684 + 290 @@ -234,9 +246,9 @@ p, li { white-space: pre-wrap; } <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } -</style></head><body style=" font-family:'DejaVu Sans Mono'; font-size:11pt; font-weight:400; font-style:normal;"> +</style></head><body style=" font-family:'DejaVu Sans Mono'; font-size:7.8pt; font-weight:400; font-style:normal;"> <p align="center" style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Bitstream Vera Sans'; font-size:18pt; font-weight:600;">MultiMC</span></p> -<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:10pt;">Copyright 2012 MultiMC Contributors</span></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:10pt;">Copyright 2012-2014 MultiMC Contributors</span></p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:10pt;">Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);</span></p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:10pt;">you may not use this file except in compliance with the License.</span></p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:10pt;">You may obtain a copy of the License at</span></p> @@ -361,6 +373,39 @@ p, li { white-space: pre-wrap; } + + + + 0 + 0 + 684 + 290 + + + + Forking/Redistribution + + + + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:7.8pt; font-weight:400; font-style:normal;"> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Bitstream Vera Sans'; font-size:11pt;">We keep MultiMC open source because we think it's important to be able to see the source code for a project like this, and we do so using the Apache license.</span></p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'Bitstream Vera Sans'; font-size:11pt;"><br /></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Bitstream Vera Sans'; font-size:11pt;">Part of the reason for using the Apache license is we don't want people using the &quot;MultiMC&quot; name when redistributing the project. This means people must take the time to go through the source code and remove all references to &quot;MultiMC&quot;, including but not limited to the project icon and the title of windows, (no *MultiMC-fork* in the title).</span></p> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'Bitstream Vera Sans'; font-size:11pt;"><br /></p> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Bitstream Vera Sans'; font-size:11pt;">The Apache license covers reasonable use for the name - a mention of the project's origins in the About dialog and the license is acceptable. However, it should be abundantly clear that the project is a fork </span><span style=" font-family:'Bitstream Vera Sans'; font-size:11pt; font-weight:600;">without</span><span style=" font-family:'Bitstream Vera Sans'; font-size:11pt;"> implying that you have our blessing.</span></p></body></html> + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + diff --git a/gui/dialogs/AccountListDialog.cpp b/gui/dialogs/AccountListDialog.cpp index 242590fb5..1712e3526 100644 --- a/gui/dialogs/AccountListDialog.cpp +++ b/gui/dialogs/AccountListDialog.cpp @@ -26,7 +26,9 @@ #include #include #include +#include "CustomMessageBox.h" #include +#include #include @@ -147,5 +149,12 @@ void AccountListDialog::addAccount(const QString& errMsg) job->start(); } + else + { + auto reason = task->failReason(); + auto dlg = CustomMessageBox::selectable(this, tr("Login error."), reason, QMessageBox::Critical); + dlg->exec(); + delete dlg; + } } } diff --git a/gui/dialogs/CopyInstanceDialog.cpp b/gui/dialogs/CopyInstanceDialog.cpp index 9d7ac30c7..4095408b9 100644 --- a/gui/dialogs/CopyInstanceDialog.cpp +++ b/gui/dialogs/CopyInstanceDialog.cpp @@ -27,7 +27,7 @@ #include "logic/InstanceFactory.h" #include "logic/BaseVersion.h" -#include "logic/lists/IconList.h" +#include "logic/icons/IconList.h" #include "logic/lists/MinecraftVersionList.h" #include "logic/tasks/Task.h" #include "logic/BaseInstance.h" diff --git a/gui/dialogs/EditAccountDialog.cpp b/gui/dialogs/EditAccountDialog.cpp index dd3f0523a..a1bd55916 100644 --- a/gui/dialogs/EditAccountDialog.cpp +++ b/gui/dialogs/EditAccountDialog.cpp @@ -15,6 +15,8 @@ #include "EditAccountDialog.h" #include "ui_EditAccountDialog.h" +#include +#include EditAccountDialog::EditAccountDialog(const QString &text, QWidget *parent, int flags) : QDialog(parent), ui(new Ui::EditAccountDialog) @@ -33,6 +35,11 @@ EditAccountDialog::~EditAccountDialog() delete ui; } +void EditAccountDialog::on_label_linkActivated(const QString &link) +{ + QDesktopServices::openUrl(QUrl(link)); +} + QString EditAccountDialog::username() const { return ui->userTextBox->text(); diff --git a/gui/dialogs/EditAccountDialog.h b/gui/dialogs/EditAccountDialog.h index be3a88d82..83f251241 100644 --- a/gui/dialogs/EditAccountDialog.h +++ b/gui/dialogs/EditAccountDialog.h @@ -52,6 +52,9 @@ public: PasswordField, }; +private slots: + void on_label_linkActivated(const QString &link); + private: Ui::EditAccountDialog *ui; }; diff --git a/gui/dialogs/EditAccountDialog.ui b/gui/dialogs/EditAccountDialog.ui index 1a8f9dba0..5f727bd4c 100644 --- a/gui/dialogs/EditAccountDialog.ui +++ b/gui/dialogs/EditAccountDialog.ui @@ -19,6 +19,12 @@ Message label placeholder. + + Qt::RichText + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + diff --git a/gui/dialogs/IconPickerDialog.cpp b/gui/dialogs/IconPickerDialog.cpp index 99d6dc9a4..f7970b377 100644 --- a/gui/dialogs/IconPickerDialog.cpp +++ b/gui/dialogs/IconPickerDialog.cpp @@ -25,7 +25,7 @@ #include "gui/Platform.h" #include "gui/widgets/InstanceDelegate.h" -#include "logic/lists/IconList.h" +#include "logic/icons/IconList.h" IconPickerDialog::IconPickerDialog(QWidget *parent) : QDialog(parent), ui(new Ui::IconPickerDialog) @@ -103,7 +103,7 @@ void IconPickerDialog::addNewIcon() QString selectIcons = tr("Select Icons"); //: The type of icon files QStringList fileNames = QFileDialog::getOpenFileNames(this, selectIcons, QString(), - tr("Icons") + "(*.png *.jpg *.jpeg)"); + tr("Icons") + "(*.png *.jpg *.jpeg *.ico)"); MMC->icons()->installIcons(fileNames); } diff --git a/gui/dialogs/InstanceSettings.cpp b/gui/dialogs/InstanceSettings.cpp index 641c7fab1..edb4a9219 100644 --- a/gui/dialogs/InstanceSettings.cpp +++ b/gui/dialogs/InstanceSettings.cpp @@ -36,6 +36,9 @@ InstanceSettings::InstanceSettings(SettingsObject *obj, QWidget *parent) { MultiMCPlatform::fixWM_CLASS(this); ui->setupUi(this); + + restoreGeometry(QByteArray::fromBase64(MMC->settings()->get("SettingsGeometry").toByteArray())); + loadSettings(); } @@ -47,7 +50,13 @@ InstanceSettings::~InstanceSettings() void InstanceSettings::showEvent(QShowEvent *ev) { QDialog::showEvent(ev); - adjustSize(); +} + +void InstanceSettings::closeEvent(QCloseEvent *ev) +{ + MMC->settings()->set("SettingsGeometry", saveGeometry().toBase64()); + + QDialog::closeEvent(ev); } void InstanceSettings::on_customCommandsGroupBox_toggled(bool state) @@ -57,12 +66,16 @@ void InstanceSettings::on_customCommandsGroupBox_toggled(bool state) void InstanceSettings::on_buttonBox_accepted() { + MMC->settings()->set("SettingsGeometry", saveGeometry().toBase64()); + applySettings(); accept(); } void InstanceSettings::on_buttonBox_rejected() { + MMC->settings()->set("SettingsGeometry", saveGeometry().toBase64()); + reject(); } @@ -98,18 +111,6 @@ void InstanceSettings::applySettings() m_obj->reset("MinecraftWinHeight"); } - // Auto Login - bool login = ui->accountSettingsBox->isChecked(); - m_obj->set("OverrideLogin", login); - if (login) - { - m_obj->set("AutoLogin", ui->autoLoginCheckBox->isChecked()); - } - else - { - m_obj->reset("AutoLogin"); - } - // Memory bool memory = ui->memoryGroupBox->isChecked(); m_obj->set("OverrideMemory", memory); @@ -170,10 +171,6 @@ void InstanceSettings::loadSettings() ui->windowWidthSpinBox->setValue(m_obj->get("MinecraftWinWidth").toInt()); ui->windowHeightSpinBox->setValue(m_obj->get("MinecraftWinHeight").toInt()); - // Auto Login - ui->accountSettingsBox->setChecked(m_obj->get("OverrideLogin").toBool()); - ui->autoLoginCheckBox->setChecked(m_obj->get("AutoLogin").toBool()); - // Memory ui->memoryGroupBox->setChecked(m_obj->get("OverrideMemory").toBool()); ui->minMemSpinBox->setValue(m_obj->get("MinMemAlloc").toInt()); @@ -243,4 +240,4 @@ void InstanceSettings::checkFinished(JavaCheckResult result) tr("The specified java binary didn't work. You should use the auto-detect feature, " "or set the path to the java executable.")); } -} \ No newline at end of file +} diff --git a/gui/dialogs/InstanceSettings.h b/gui/dialogs/InstanceSettings.h index 1e7b9f6e2..e296db4c6 100644 --- a/gui/dialogs/InstanceSettings.h +++ b/gui/dialogs/InstanceSettings.h @@ -39,6 +39,7 @@ public: protected: virtual void showEvent(QShowEvent *); + virtual void closeEvent(QCloseEvent *); private slots: void on_customCommandsGroupBox_toggled(bool arg1); diff --git a/gui/dialogs/InstanceSettings.ui b/gui/dialogs/InstanceSettings.ui index c4a7d6ed1..9260caea1 100644 --- a/gui/dialogs/InstanceSettings.ui +++ b/gui/dialogs/InstanceSettings.ui @@ -131,31 +131,6 @@ - - - - true - - - Account Settings - - - true - - - false - - - - - - Login automatically when an instance icon is double clicked? - - - - - - @@ -411,7 +386,6 @@ consoleSettingsBox showConsoleCheck autoCloseConsoleCheck - accountSettingsBox memoryGroupBox minMemSpinBox maxMemSpinBox diff --git a/gui/dialogs/NewInstanceDialog.cpp b/gui/dialogs/NewInstanceDialog.cpp index 5b2cd0863..c7b273af6 100644 --- a/gui/dialogs/NewInstanceDialog.cpp +++ b/gui/dialogs/NewInstanceDialog.cpp @@ -19,7 +19,7 @@ #include "logic/InstanceFactory.h" #include "logic/BaseVersion.h" -#include "logic/lists/IconList.h" +#include "logic/icons/IconList.h" #include "logic/lists/MinecraftVersionList.h" #include "logic/tasks/Task.h" diff --git a/gui/dialogs/OneSixModEditDialog.cpp b/gui/dialogs/OneSixModEditDialog.cpp index 51ea2d191..3982f17d8 100644 --- a/gui/dialogs/OneSixModEditDialog.cpp +++ b/gui/dialogs/OneSixModEditDialog.cpp @@ -38,6 +38,7 @@ #include "logic/EnabledItemFilter.h" #include "logic/lists/ForgeVersionList.h" #include "logic/ForgeInstaller.h" +#include "logic/LiteLoaderInstaller.h" OneSixModEditDialog::OneSixModEditDialog(OneSixInstance *inst, QWidget *parent) : QDialog(parent), ui(new Ui::OneSixModEditDialog), m_inst(inst) @@ -95,6 +96,8 @@ void OneSixModEditDialog::updateVersionControls() ui->customizeBtn->setEnabled(!customVersion); ui->revertBtn->setEnabled(customVersion); ui->forgeBtn->setEnabled(true); + ui->liteloaderBtn->setEnabled(LiteLoaderInstaller(m_inst->intendedVersionId()).canApply()); + ui->customEditorBtn->setEnabled(customVersion); } void OneSixModEditDialog::disableVersionControls() @@ -102,6 +105,8 @@ void OneSixModEditDialog::disableVersionControls() ui->customizeBtn->setEnabled(false); ui->revertBtn->setEnabled(false); ui->forgeBtn->setEnabled(false); + ui->liteloaderBtn->setEnabled(false); + ui->customEditorBtn->setEnabled(false); } void OneSixModEditDialog::on_customizeBtn_clicked() @@ -131,6 +136,17 @@ void OneSixModEditDialog::on_revertBtn_clicked() } } +void OneSixModEditDialog::on_customEditorBtn_clicked() +{ + if (m_inst->versionIsCustom()) + { + if (!MMC->openJsonEditor(m_inst->instanceRoot() + "/custom.json")) + { + QMessageBox::warning(this, tr("Error"), tr("Unable to open custom.json, check the settings")); + } + } +} + void OneSixModEditDialog::on_forgeBtn_clicked() { VersionSelectDialog vselect(MMC->forgelist().get(), tr("Select Forge version"), this); @@ -204,6 +220,32 @@ void OneSixModEditDialog::on_forgeBtn_clicked() } } +void OneSixModEditDialog::on_liteloaderBtn_clicked() +{ + LiteLoaderInstaller liteloader(m_inst->intendedVersionId()); + if (!liteloader.canApply()) + { + QMessageBox::critical( + this, tr("LiteLoader"), + tr("There is no information available on how to install LiteLoader " + "into this version of Minecraft")); + return; + } + if (!m_inst->versionIsCustom()) + { + m_inst->customizeVersion(); + m_version = m_inst->getFullVersion(); + main_model->setSourceModel(m_version.get()); + updateVersionControls(); + } + if (!liteloader.apply(m_version)) + { + QMessageBox::critical( + this, tr("LiteLoader"), + tr("For reasons unknown, the LiteLoader installation failed. Check your MultiMC log files for details.")); + } +} + bool OneSixModEditDialog::loaderListFilter(QKeyEvent *keyEvent) { switch (keyEvent->key()) diff --git a/gui/dialogs/OneSixModEditDialog.h b/gui/dialogs/OneSixModEditDialog.h index 5376e5263..2510c59cd 100644 --- a/gui/dialogs/OneSixModEditDialog.h +++ b/gui/dialogs/OneSixModEditDialog.h @@ -44,8 +44,10 @@ slots: // Questionable: SettingsDialog doesn't need this for some reason? void on_buttonBox_rejected(); void on_forgeBtn_clicked(); + void on_liteloaderBtn_clicked(); void on_customizeBtn_clicked(); void on_revertBtn_clicked(); + void on_customEditorBtn_clicked(); void updateVersionControls(); void disableVersionControls(); diff --git a/gui/dialogs/OneSixModEditDialog.ui b/gui/dialogs/OneSixModEditDialog.ui index 48aa87ee5..899e0cbfa 100644 --- a/gui/dialogs/OneSixModEditDialog.ui +++ b/gui/dialogs/OneSixModEditDialog.ui @@ -77,6 +77,13 @@ + + + + Install LiteLoader + + + @@ -136,6 +143,20 @@ + + + + Qt::Horizontal + + + + + + + Open custom.json + + + diff --git a/gui/dialogs/SettingsDialog.cpp b/gui/dialogs/SettingsDialog.cpp index e7f537e39..9362075ed 100644 --- a/gui/dialogs/SettingsDialog.cpp +++ b/gui/dialogs/SettingsDialog.cpp @@ -40,6 +40,12 @@ SettingsDialog::SettingsDialog(QWidget *parent) : QDialog(parent), ui(new Ui::Se ui->sortingModeGroup->setId(ui->sortByNameBtn, Sort_Name); ui->sortingModeGroup->setId(ui->sortLastLaunchedBtn, Sort_LastLaunch); +#if QT_VERSION >= QT_VERSION_CHECK(5, 2, 0) + ui->jsonEditorTextBox->setClearButtonEnabled(true); +#endif + + restoreGeometry(QByteArray::fromBase64(MMC->settings()->get("SettingsGeometry").toByteArray())); + loadSettings(MMC->settings().get()); updateCheckboxStuff(); } @@ -51,7 +57,13 @@ SettingsDialog::~SettingsDialog() void SettingsDialog::showEvent(QShowEvent *ev) { QDialog::showEvent(ev); - adjustSize(); +} + +void SettingsDialog::closeEvent(QCloseEvent *ev) +{ + MMC->settings()->set("SettingsGeometry", saveGeometry().toBase64()); + + QDialog::closeEvent(ev); } void SettingsDialog::updateCheckboxStuff() @@ -60,6 +72,32 @@ void SettingsDialog::updateCheckboxStuff() ui->windowHeightSpinBox->setEnabled(!ui->maximizedCheckBox->isChecked()); } +void SettingsDialog::on_ftbLauncherBrowseBtn_clicked() +{ + QString raw_dir = QFileDialog::getExistingDirectory(this, tr("FTB Launcher Directory"), + ui->ftbLauncherBox->text()); + QString cooked_dir = NormalizePath(raw_dir); + + // do not allow current dir - it's dirty. Do not allow dirs that don't exist + if (!cooked_dir.isEmpty() && QDir(cooked_dir).exists()) + { + ui->ftbLauncherBox->setText(cooked_dir); + } +} + +void SettingsDialog::on_ftbBrowseBtn_clicked() +{ + QString raw_dir = QFileDialog::getExistingDirectory(this, tr("FTB Directory"), + ui->ftbBox->text()); + QString cooked_dir = NormalizePath(raw_dir); + + // do not allow current dir - it's dirty. Do not allow dirs that don't exist + if (!cooked_dir.isEmpty() && QDir(cooked_dir).exists()) + { + ui->ftbBox->setText(cooked_dir); + } +} + void SettingsDialog::on_instDirBrowseBtn_clicked() { QString raw_dir = QFileDialog::getExistingDirectory(this, tr("Instance Directory"), @@ -72,6 +110,18 @@ void SettingsDialog::on_instDirBrowseBtn_clicked() ui->instDirTextBox->setText(cooked_dir); } } +void SettingsDialog::on_iconsDirBrowseBtn_clicked() +{ + QString raw_dir = QFileDialog::getExistingDirectory(this, tr("Icons Directory"), + ui->iconsDirTextBox->text()); + QString cooked_dir = NormalizePath(raw_dir); + + // do not allow current dir - it's dirty. Do not allow dirs that don't exist + if (!cooked_dir.isEmpty() && QDir(cooked_dir).exists()) + { + ui->iconsDirTextBox->setText(cooked_dir); + } +} void SettingsDialog::on_modsDirBrowseBtn_clicked() { @@ -99,6 +149,36 @@ void SettingsDialog::on_lwjglDirBrowseBtn_clicked() } } +void SettingsDialog::on_jsonEditorBrowseBtn_clicked() +{ + QString raw_file = QFileDialog::getOpenFileName( + this, tr("JSON Editor"), + ui->jsonEditorTextBox->text().isEmpty() + #if defined(Q_OS_LINUX) + ? QString("/usr/bin") + #else + ? QStandardPaths::standardLocations(QStandardPaths::ApplicationsLocation).first() + #endif + : ui->jsonEditorTextBox->text()); + QString cooked_file = NormalizePath(raw_file); + + if (cooked_file.isEmpty()) + { + return; + } + + // it has to exist and be an executable + if (QFileInfo(cooked_file).exists() && + QFileInfo(cooked_file).isExecutable()) + { + ui->jsonEditorTextBox->setText(cooked_file); + } + else + { + QMessageBox::warning(this, tr("Invalid"), tr("The file chosen does not seem to be an executable")); + } +} + void SettingsDialog::on_maximizedCheckBox_clicked(bool checked) { Q_UNUSED(checked); @@ -108,6 +188,13 @@ void SettingsDialog::on_maximizedCheckBox_clicked(bool checked) void SettingsDialog::on_buttonBox_accepted() { applySettings(MMC->settings().get()); + + MMC->settings()->set("SettingsGeometry", saveGeometry().toBase64()); +} + +void SettingsDialog::on_buttonBox_rejected() +{ + MMC->settings()->set("SettingsGeometry", saveGeometry().toBase64()); } void SettingsDialog::applySettings(SettingsObject *s) @@ -135,11 +222,29 @@ void SettingsDialog::applySettings(SettingsObject *s) // Updates s->set("AutoUpdate", ui->autoUpdateCheckBox->isChecked()); + // FTB + s->set("TrackFTBInstances", ui->trackFtbBox->isChecked()); + s->set("FTBLauncherRoot", ui->ftbLauncherBox->text()); + s->set("FTBRoot", ui->ftbBox->text()); + // Folders // TODO: Offer to move instances to new instance folder. s->set("InstanceDir", ui->instDirTextBox->text()); s->set("CentralModsDir", ui->modsDirTextBox->text()); s->set("LWJGLDir", ui->lwjglDirTextBox->text()); + s->set("IconsDir", ui->iconsDirTextBox->text()); + + // Editors + QString jsonEditor = ui->jsonEditorTextBox->text(); + if (!jsonEditor.isEmpty() && (!QFileInfo(jsonEditor).exists() || !QFileInfo(jsonEditor).isExecutable())) + { + QString found = QStandardPaths::findExecutable(jsonEditor); + if (!found.isEmpty()) + { + jsonEditor = found; + } + } + s->set("JsonEditor", jsonEditor); // Console s->set("ShowConsole", ui->showConsoleCheck->isChecked()); @@ -150,9 +255,6 @@ void SettingsDialog::applySettings(SettingsObject *s) s->set("MinecraftWinWidth", ui->windowWidthSpinBox->value()); s->set("MinecraftWinHeight", ui->windowHeightSpinBox->value()); - // Auto Login - s->set("AutoLogin", ui->autoLoginCheckBox->isChecked()); - // Memory s->set("MinMemAlloc", ui->minMemSpinBox->value()); s->set("MaxMemAlloc", ui->maxMemSpinBox->value()); @@ -188,10 +290,19 @@ void SettingsDialog::loadSettings(SettingsObject *s) ui->autoUpdateCheckBox->setChecked(s->get("AutoUpdate").toBool()); ui->devBuildsCheckBox->setChecked(s->get("UseDevBuilds").toBool()); + // FTB + ui->trackFtbBox->setChecked(s->get("TrackFTBInstances").toBool()); + ui->ftbLauncherBox->setText(s->get("FTBLauncherRoot").toString()); + ui->ftbBox->setText(s->get("FTBRoot").toString()); + // Folders ui->instDirTextBox->setText(s->get("InstanceDir").toString()); ui->modsDirTextBox->setText(s->get("CentralModsDir").toString()); ui->lwjglDirTextBox->setText(s->get("LWJGLDir").toString()); + ui->iconsDirTextBox->setText(s->get("IconsDir").toString()); + + // Editors + ui->jsonEditorTextBox->setText(s->get("JsonEditor").toString()); // Console ui->showConsoleCheck->setChecked(s->get("ShowConsole").toBool()); @@ -202,9 +313,6 @@ void SettingsDialog::loadSettings(SettingsObject *s) ui->windowWidthSpinBox->setValue(s->get("MinecraftWinWidth").toInt()); ui->windowHeightSpinBox->setValue(s->get("MinecraftWinHeight").toInt()); - // Auto Login - ui->autoLoginCheckBox->setChecked(s->get("AutoLogin").toBool()); - // Memory ui->minMemSpinBox->setValue(s->get("MinMemAlloc").toInt()); ui->maxMemSpinBox->setValue(s->get("MaxMemAlloc").toInt()); diff --git a/gui/dialogs/SettingsDialog.h b/gui/dialogs/SettingsDialog.h index 0cb8fa382..11fdb6965 100644 --- a/gui/dialogs/SettingsDialog.h +++ b/gui/dialogs/SettingsDialog.h @@ -41,20 +41,32 @@ public: void loadSettings(SettingsObject *s); protected: - virtual void showEvent(QShowEvent *); + virtual void showEvent(QShowEvent *ev); + virtual void closeEvent(QCloseEvent *ev); private slots: + void on_ftbLauncherBrowseBtn_clicked(); + + void on_ftbBrowseBtn_clicked(); + void on_instDirBrowseBtn_clicked(); void on_modsDirBrowseBtn_clicked(); void on_lwjglDirBrowseBtn_clicked(); + + void on_jsonEditorBrowseBtn_clicked(); + + void on_iconsDirBrowseBtn_clicked(); + void on_maximizedCheckBox_clicked(bool checked); void on_buttonBox_accepted(); + void on_buttonBox_rejected(); + void on_javaDetectBtn_clicked(); void on_javaTestBtn_clicked(); diff --git a/gui/dialogs/SettingsDialog.ui b/gui/dialogs/SettingsDialog.ui index 0dbc8def8..dbc8ca888 100644 --- a/gui/dialogs/SettingsDialog.ui +++ b/gui/dialogs/SettingsDialog.ui @@ -7,7 +7,7 @@ 0 0 526 - 599 + 628 @@ -39,7 +39,7 @@ General - + @@ -95,6 +95,93 @@ + + + + FTB + + + + + + false + + + + 0 + 0 + + + + + 28 + 16777215 + + + + Qt::TabFocus + + + ... + + + + + + + Launcher: + + + + + + + false + + + + + + + Track FTB instances + + + + + + + + + + true + + + + 0 + 0 + + + + + 28 + 16777215 + + + + ... + + + + + + + Files: + + + + + + @@ -128,6 +215,9 @@ + + + @@ -142,9 +232,6 @@ - - - @@ -152,6 +239,49 @@ + + + + + + + Icons: + + + + + + + ... + + + + + + + + + + External Editors (leave empty for system default) + + + + + + + + + JSON Editor: + + + + + + + ... + + + @@ -261,22 +391,6 @@ - - - - Account Settings - - - - - - Login automatically when an instance icon is double clicked? - - - - - - @@ -394,25 +508,6 @@ - - - - - 0 - 0 - - - - JVM arguments: - - - - - - - - - @@ -426,7 +521,7 @@ - + @@ -439,19 +534,48 @@ - - + + - + 0 0 - Browse... + JVM arguments: + + + + + + + + + + 0 + 0 + + + + + 28 + 16777215 + + + + ... + + + + + + + + @@ -520,7 +644,7 @@ - settingsTabs + settingsTab buttonBox sortLastLaunchedBtn sortByNameBtn diff --git a/gui/widgets/MCModInfoFrame.cpp b/gui/widgets/MCModInfoFrame.cpp index ad167bc91..abcea6c68 100644 --- a/gui/widgets/MCModInfoFrame.cpp +++ b/gui/widgets/MCModInfoFrame.cpp @@ -30,7 +30,7 @@ void MCModInfoFrame::updateWithMod(Mod &m) QString text = ""; QString name = ""; - if(m.name().isEmpty()) name = m.id(); + if(m.name().isEmpty()) name = m.mmc_id(); else name = m.name(); if(m.homeurl().isEmpty()) text = name; diff --git a/gui/widgets/ModListView.cpp b/gui/widgets/ModListView.cpp index 838af75e2..9d5950c30 100644 --- a/gui/widgets/ModListView.cpp +++ b/gui/widgets/ModListView.cpp @@ -44,8 +44,9 @@ void ModListView::setModel ( QAbstractItemModel* model ) QTreeView::setModel ( model ); auto head = header(); head->setStretchLastSection(false); - head->setSectionResizeMode(0, QHeaderView::Stretch); - for(int i = 1; i < head->count(); i++) + head->setSectionResizeMode(0, QHeaderView::ResizeToContents); + head->setSectionResizeMode(1, QHeaderView::Stretch); + for(int i = 2; i < head->count(); i++) head->setSectionResizeMode(i, QHeaderView::ResizeToContents); dropIndicatorPosition(); } diff --git a/logger/QsLogDest.cpp b/logger/QsLogDest.cpp index 2fd29b232..4a47060eb 100644 --- a/logger/QsLogDest.cpp +++ b/logger/QsLogDest.cpp @@ -77,6 +77,15 @@ void DebugOutputDestination::write(const QString &message) QsDebugOutput::output(message); } +class QDebugDestination : public Destination +{ +public: + virtual void write(const QString &message) + { + qDebug() << message; + }; +}; + DestinationPtr DestinationFactory::MakeFileDestination(const QString &filePath) { return DestinationPtr(new FileDestination(filePath)); @@ -87,4 +96,9 @@ DestinationPtr DestinationFactory::MakeDebugOutputDestination() return DestinationPtr(new DebugOutputDestination); } +DestinationPtr DestinationFactory::MakeQDebugDestination() +{ + return DestinationPtr(new QDebugDestination); +} + } // end namespace diff --git a/logger/QsLogDest.h b/logger/QsLogDest.h index e7fcc0456..a8000022b 100644 --- a/logger/QsLogDest.h +++ b/logger/QsLogDest.h @@ -47,6 +47,7 @@ class DestinationFactory public: static DestinationPtr MakeFileDestination(const QString &filePath); static DestinationPtr MakeDebugOutputDestination(); + static DestinationPtr MakeQDebugDestination(); }; } // end namespace diff --git a/logic/BaseInstance.cpp b/logic/BaseInstance.cpp index 6f8222b78..ac66a8d59 100644 --- a/logic/BaseInstance.cpp +++ b/logic/BaseInstance.cpp @@ -27,6 +27,7 @@ #include "pathutils.h" #include "lists/MinecraftVersionList.h" +#include "logic/icons/IconList.h" BaseInstance::BaseInstance(BaseInstancePrivate *d_in, const QString &rootDir, SettingsObject *settings_obj, QObject *parent) @@ -36,10 +37,11 @@ BaseInstance::BaseInstance(BaseInstancePrivate *d_in, const QString &rootDir, d->m_settings = settings_obj; d->m_rootDir = rootDir; - settings().registerSetting(new Setting("name", "Unnamed Instance")); - settings().registerSetting(new Setting("iconKey", "default")); - settings().registerSetting(new Setting("notes", "")); - settings().registerSetting(new Setting("lastLaunchTime", 0)); + settings().registerSetting("name", "Unnamed Instance"); + settings().registerSetting("iconKey", "default"); + connect(MMC->icons().get(), SIGNAL(iconUpdated(QString)), SLOT(iconUpdated(QString))); + settings().registerSetting("notes", ""); + settings().registerSetting("lastLaunchTime", 0); /* * custom base jar has no default. it is determined in code... see the accessor methods for @@ -48,54 +50,45 @@ BaseInstance::BaseInstance(BaseInstancePrivate *d_in, const QString &rootDir, * for instances that DO NOT have the CustomBaseJar setting (legacy instances), * [.]minecraft/bin/mcbackup.jar is the default base jar */ - settings().registerSetting(new Setting("UseCustomBaseJar", true)); - settings().registerSetting(new Setting("CustomBaseJar", "")); + settings().registerSetting("UseCustomBaseJar", true); + settings().registerSetting("CustomBaseJar", ""); auto globalSettings = MMC->settings(); // Java Settings - settings().registerSetting(new Setting("OverrideJava", false)); - settings().registerSetting( - new OverrideSetting("JavaPath", globalSettings->getSetting("JavaPath"))); - settings().registerSetting( - new OverrideSetting("JvmArgs", globalSettings->getSetting("JvmArgs"))); + settings().registerSetting("OverrideJava", false); + settings().registerOverride(globalSettings->getSetting("JavaPath")); + settings().registerOverride(globalSettings->getSetting("JvmArgs")); // Custom Commands - settings().registerSetting(new Setting("OverrideCommands", false)); - settings().registerSetting(new OverrideSetting( - "PreLaunchCommand", globalSettings->getSetting("PreLaunchCommand"))); - settings().registerSetting( - new OverrideSetting("PostExitCommand", globalSettings->getSetting("PostExitCommand"))); + settings().registerSetting({"OverrideCommands","OverrideLaunchCmd"}, false); + settings().registerOverride(globalSettings->getSetting("PreLaunchCommand")); + settings().registerOverride(globalSettings->getSetting("PostExitCommand")); // Window Size - settings().registerSetting(new Setting("OverrideWindow", false)); - settings().registerSetting( - new OverrideSetting("LaunchMaximized", globalSettings->getSetting("LaunchMaximized"))); - settings().registerSetting(new OverrideSetting( - "MinecraftWinWidth", globalSettings->getSetting("MinecraftWinWidth"))); - settings().registerSetting(new OverrideSetting( - "MinecraftWinHeight", globalSettings->getSetting("MinecraftWinHeight"))); + settings().registerSetting("OverrideWindow", false); + settings().registerOverride(globalSettings->getSetting("LaunchMaximized")); + settings().registerOverride(globalSettings->getSetting("MinecraftWinWidth")); + settings().registerOverride(globalSettings->getSetting("MinecraftWinHeight")); // Memory - settings().registerSetting(new Setting("OverrideMemory", false)); - settings().registerSetting( - new OverrideSetting("MinMemAlloc", globalSettings->getSetting("MinMemAlloc"))); - settings().registerSetting( - new OverrideSetting("MaxMemAlloc", globalSettings->getSetting("MaxMemAlloc"))); - settings().registerSetting( - new OverrideSetting("PermGen", globalSettings->getSetting("PermGen"))); - - // Auto login - settings().registerSetting(new Setting("OverrideLogin", false)); - settings().registerSetting( - new OverrideSetting("AutoLogin", globalSettings->getSetting("AutoLogin"))); + settings().registerSetting("OverrideMemory", false); + settings().registerOverride(globalSettings->getSetting("MinMemAlloc")); + settings().registerOverride(globalSettings->getSetting("MaxMemAlloc")); + settings().registerOverride(globalSettings->getSetting("PermGen")); // Console - settings().registerSetting(new Setting("OverrideConsole", false)); - settings().registerSetting( - new OverrideSetting("ShowConsole", globalSettings->getSetting("ShowConsole"))); - settings().registerSetting(new OverrideSetting( - "AutoCloseConsole", globalSettings->getSetting("AutoCloseConsole"))); + settings().registerSetting("OverrideConsole", false); + settings().registerOverride(globalSettings->getSetting("ShowConsole")); + settings().registerOverride(globalSettings->getSetting("AutoCloseConsole")); +} + +void BaseInstance::iconUpdated(QString key) +{ + if(iconKey() == key) + { + emit propertiesChanged(this); + } } void BaseInstance::nuke() diff --git a/logic/BaseInstance.h b/logic/BaseInstance.h index 5f4266767..01d6dc7d2 100644 --- a/logic/BaseInstance.h +++ b/logic/BaseInstance.h @@ -184,6 +184,9 @@ signals: */ void nuked(BaseInstance *inst); +protected slots: + void iconUpdated(QString key); + protected: std::shared_ptr inst_d; }; diff --git a/logic/InstanceFactory.cpp b/logic/InstanceFactory.cpp index 66b271d03..1f1a58796 100644 --- a/logic/InstanceFactory.cpp +++ b/logic/InstanceFactory.cpp @@ -20,7 +20,9 @@ #include "BaseInstance.h" #include "LegacyInstance.h" +#include "LegacyFTBInstance.h" #include "OneSixInstance.h" +#include "OneSixFTBInstance.h" #include "NostalgiaInstance.h" #include "BaseVersion.h" #include "MinecraftVersion.h" @@ -43,7 +45,7 @@ InstanceFactory::InstLoadError InstanceFactory::loadInstance(BaseInstance *&inst { auto m_settings = new INISettingsObject(PathCombine(instDir, "instance.cfg")); - m_settings->registerSetting(new Setting("InstanceType", "Legacy")); + m_settings->registerSetting("InstanceType", "Legacy"); QString inst_type = m_settings->get("InstanceType").toString(); @@ -60,6 +62,14 @@ InstanceFactory::InstLoadError InstanceFactory::loadInstance(BaseInstance *&inst { inst = new NostalgiaInstance(instDir, m_settings, this); } + else if (inst_type == "LegacyFTB") + { + inst = new LegacyFTBInstance(instDir, m_settings, this); + } + else if (inst_type == "OneSixFTB") + { + inst = new OneSixFTBInstance(instDir, m_settings, this); + } else { return InstanceFactory::UnknownLoadError; @@ -69,7 +79,8 @@ InstanceFactory::InstLoadError InstanceFactory::loadInstance(BaseInstance *&inst InstanceFactory::InstCreateError InstanceFactory::createInstance(BaseInstance *&inst, BaseVersionPtr version, - const QString &instDir) + const QString &instDir, + const InstType type) { QDir rootDir(instDir); @@ -83,34 +94,65 @@ InstanceFactory::InstCreateError InstanceFactory::createInstance(BaseInstance *& return InstanceFactory::NoSuchVersion; auto m_settings = new INISettingsObject(PathCombine(instDir, "instance.cfg")); - m_settings->registerSetting(new Setting("InstanceType", "Legacy")); + m_settings->registerSetting("InstanceType", "Legacy"); - switch (mcVer->type) + if (type == NormalInst) { - case MinecraftVersion::Legacy: - m_settings->set("InstanceType", "Legacy"); - inst = new LegacyInstance(instDir, m_settings, this); - inst->setIntendedVersionId(version->descriptor()); - inst->setShouldUseCustomBaseJar(false); - break; - case MinecraftVersion::OneSix: - m_settings->set("InstanceType", "OneSix"); - inst = new OneSixInstance(instDir, m_settings, this); - inst->setIntendedVersionId(version->descriptor()); - inst->setShouldUseCustomBaseJar(false); - break; - case MinecraftVersion::Nostalgia: - m_settings->set("InstanceType", "Nostalgia"); - inst = new NostalgiaInstance(instDir, m_settings, this); - inst->setIntendedVersionId(version->descriptor()); - inst->setShouldUseCustomBaseJar(false); - break; - default: + switch (mcVer->type) + { + case MinecraftVersion::Legacy: + m_settings->set("InstanceType", "Legacy"); + inst = new LegacyInstance(instDir, m_settings, this); + inst->setIntendedVersionId(version->descriptor()); + inst->setShouldUseCustomBaseJar(false); + break; + case MinecraftVersion::OneSix: + m_settings->set("InstanceType", "OneSix"); + inst = new OneSixInstance(instDir, m_settings, this); + inst->setIntendedVersionId(version->descriptor()); + inst->setShouldUseCustomBaseJar(false); + break; + case MinecraftVersion::Nostalgia: + m_settings->set("InstanceType", "Nostalgia"); + inst = new NostalgiaInstance(instDir, m_settings, this); + inst->setIntendedVersionId(version->descriptor()); + inst->setShouldUseCustomBaseJar(false); + break; + default: + { + delete m_settings; + return InstanceFactory::NoSuchVersion; + } + } + } + else if (type == FTBInstance) + { + switch (mcVer->type) + { + case MinecraftVersion::Legacy: + m_settings->set("InstanceType", "LegacyFTB"); + inst = new LegacyFTBInstance(instDir, m_settings, this); + inst->setIntendedVersionId(version->descriptor()); + inst->setShouldUseCustomBaseJar(false); + break; + case MinecraftVersion::OneSix: + m_settings->set("InstanceType", "OneSixFTB"); + inst = new OneSixFTBInstance(instDir, m_settings, this); + inst->setIntendedVersionId(version->descriptor()); + inst->setShouldUseCustomBaseJar(false); + break; + default: + { + delete m_settings; + return InstanceFactory::NoSuchVersion; + } + } + } + else { delete m_settings; return InstanceFactory::NoSuchVersion; } - } // FIXME: really, how do you even know? return InstanceFactory::NoCreateError; @@ -128,7 +170,17 @@ InstanceFactory::InstCreateError InstanceFactory::copyInstance(BaseInstance *&ne rootDir.removeRecursively(); return InstanceFactory::CantCreateDir; } + auto m_settings = new INISettingsObject(PathCombine(instDir, "instance.cfg")); + m_settings->registerSetting("InstanceType", "Legacy"); + QString inst_type = m_settings->get("InstanceType").toString(); + + if(inst_type == "OneSixFTB") + m_settings->set("InstanceType", "OneSix"); + if(inst_type == "LegacyFTB") + m_settings->set("InstanceType", "Legacy"); + auto error = loadInstance(newInstance, instDir); + switch (error) { case NoLoadError: diff --git a/logic/InstanceFactory.h b/logic/InstanceFactory.h index 01e5af7ea..5ff4c7ec6 100644 --- a/logic/InstanceFactory.h +++ b/logic/InstanceFactory.h @@ -55,18 +55,25 @@ public: CantCreateDir }; + enum InstType + { + NormalInst, + FTBInstance + }; + /*! * \brief Creates a stub instance * * \param inst Pointer to store the created instance in. - * \param inst Game version to use for the instance + * \param version Game version to use for the instance * \param instDir The new instance's directory. + * \param type The type of instance to create * \return An InstCreateError error code. * - InstExists if the given instance directory is already an instance. * - CantCreateDir if the given instance directory cannot be created. */ InstCreateError createInstance(BaseInstance *&inst, BaseVersionPtr version, - const QString &instDir); + const QString &instDir, const InstType type = NormalInst); /*! * \brief Creates a copy of an existing instance with a new name diff --git a/logic/JavaChecker.cpp b/logic/JavaChecker.cpp index 2b94fbb60..113974ffd 100644 --- a/logic/JavaChecker.cpp +++ b/logic/JavaChecker.cpp @@ -99,6 +99,7 @@ void JavaChecker::error(QProcess::ProcessError err) if(err == QProcess::FailedToStart) { killTimer.stop(); + checkerJar.remove(); JavaCheckResult result; { @@ -116,6 +117,5 @@ void JavaChecker::timeout() if(process) { process->kill(); - process.reset(); } } diff --git a/logic/JavaUtils.cpp b/logic/JavaUtils.cpp index e1b3bc640..cf47df6f6 100644 --- a/logic/JavaUtils.cpp +++ b/logic/JavaUtils.cpp @@ -177,10 +177,10 @@ QList JavaUtils::FindJavaPaths() #elif OSX QList JavaUtils::FindJavaPaths() { - QLOG_INFO() << "OS X Java detection incomplete - defaulting to \"java\""; - QList javas; javas.append(this->GetDefaultJava()->path); + javas.append("/Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/bin/java"); + javas.append("/System/Library/Frameworks/JavaVM.framework/Versions/Current/Commands/java"); return javas; } diff --git a/logic/LegacyFTBInstance.cpp b/logic/LegacyFTBInstance.cpp new file mode 100644 index 000000000..84d5a9005 --- /dev/null +++ b/logic/LegacyFTBInstance.cpp @@ -0,0 +1,16 @@ +#include "LegacyFTBInstance.h" + +LegacyFTBInstance::LegacyFTBInstance(const QString &rootDir, SettingsObject *settings, QObject *parent) : + LegacyInstance(rootDir, settings, parent) +{ +} + +QString LegacyFTBInstance::getStatusbarDescription() +{ + return "Legacy FTB: " + intendedVersionId(); +} + +bool LegacyFTBInstance::menuActionEnabled(QString action_name) const +{ + return false; +} diff --git a/logic/LegacyFTBInstance.h b/logic/LegacyFTBInstance.h new file mode 100644 index 000000000..2ae727978 --- /dev/null +++ b/logic/LegacyFTBInstance.h @@ -0,0 +1,13 @@ +#pragma once + +#include "LegacyInstance.h" + +class LegacyFTBInstance : public LegacyInstance +{ + Q_OBJECT +public: + explicit LegacyFTBInstance(const QString &rootDir, SettingsObject *settings, + QObject *parent = 0); + virtual QString getStatusbarDescription(); + virtual bool menuActionEnabled(QString action_name) const; +}; diff --git a/logic/LegacyInstance.cpp b/logic/LegacyInstance.cpp index fef27bcde..0bc0961e5 100644 --- a/logic/LegacyInstance.cpp +++ b/logic/LegacyInstance.cpp @@ -27,7 +27,7 @@ #include "logic/MinecraftProcess.h" #include "logic/LegacyUpdate.h" -#include "logic/lists/IconList.h" +#include "logic/icons/IconList.h" #include "gui/dialogs/LegacyModEditDialog.h" @@ -37,11 +37,11 @@ LegacyInstance::LegacyInstance(const QString &rootDir, SettingsObject *settings, QObject *parent) : BaseInstance(new LegacyInstancePrivate(), rootDir, settings, parent) { - settings->registerSetting(new Setting("NeedsRebuild", true)); - settings->registerSetting(new Setting("ShouldUpdate", false)); - settings->registerSetting(new Setting("JarVersion", "Unknown")); - settings->registerSetting(new Setting("LwjglVersion", "2.9.0")); - settings->registerSetting(new Setting("IntendedJarVersion", "")); + settings->registerSetting("NeedsRebuild", true); + settings->registerSetting("ShouldUpdate", false); + settings->registerSetting("JarVersion", "Unknown"); + settings->registerSetting("LwjglVersion", "2.9.0"); + settings->registerSetting("IntendedJarVersion", ""); } std::shared_ptr LegacyInstance::doUpdate(bool only_prepare) @@ -150,6 +150,7 @@ std::shared_ptr LegacyInstance::jarModList() void LegacyInstance::jarModsChanged() { + QLOG_INFO() << "Jar mods of instance " << name() << " have changed. Jar will be rebuilt."; setShouldRebuild(true); } diff --git a/logic/LegacyUpdate.cpp b/logic/LegacyUpdate.cpp index e71b270e0..cb3598a70 100644 --- a/logic/LegacyUpdate.cpp +++ b/logic/LegacyUpdate.cpp @@ -76,7 +76,7 @@ void LegacyUpdate::lwjglStart() return; } - setStatus("Downloading new LWJGL."); + setStatus(tr("Downloading new LWJGL...")); auto version = list->getVersion(lwjglVersion); if (!version) { @@ -144,7 +144,7 @@ void LegacyUpdate::lwjglFinished(QNetworkReply *reply) saveMe.open(QIODevice::WriteOnly); saveMe.write(m_reply->readAll()); saveMe.close(); - setStatus("Installing new LWJGL..."); + setStatus(tr("Installing new LWJGL...")); extractLwjgl(); jarStart(); } @@ -220,7 +220,7 @@ void LegacyUpdate::extractLwjgl() // Now if destFileName is still empty, go to the next file. if (!destFileName.isEmpty()) { - setStatus("Installing new LWJGL - Extracting " + name); + setStatus(tr("Installing new LWJGL - extracting ") + name + "..."); QFile output(destFileName); output.open(QIODevice::WriteOnly); output.write(file.readAll()); // FIXME: wste of memory!? @@ -250,7 +250,7 @@ void LegacyUpdate::jarStart() return; } - setStatus("Checking for jar updates..."); + setStatus(tr("Checking for jar updates...")); // Make directories QDir binDir(inst->binDir()); if (!binDir.exists() && !binDir.mkpath(".")) @@ -260,7 +260,7 @@ void LegacyUpdate::jarStart() } // Build a list of URLs that will need to be downloaded. - setStatus("Downloading new minecraft.jar"); + setStatus(tr("Downloading new minecraft.jar ...")); QString version_id = inst->intendedVersionId(); QString localPath = version_id + "/" + version_id + ".jar"; @@ -294,7 +294,7 @@ void LegacyUpdate::jarFailed() bool LegacyUpdate::MergeZipFiles(QuaZip *into, QFileInfo from, QSet &contained, MetainfAction metainf) { - setStatus("Installing mods - Adding " + from.fileName()); + setStatus(tr("Installing mods: Adding ") + from.fileName() + " ..."); QuaZip modZip(from.filePath()); modZip.open(QuaZip::mdUnzip); @@ -380,7 +380,7 @@ void LegacyUpdate::ModTheJar() return; } - setStatus("Installing mods - backing up minecraft.jar..."); + setStatus(tr("Installing mods: Backing up minecraft.jar ...")); if (!baseJar.exists() && !QFile::copy(runnableJar.filePath(), baseJar.filePath())) { emitFailed("It seems both the active and base jar are gone. A fresh base jar will " @@ -405,7 +405,7 @@ void LegacyUpdate::ModTheJar() } // TaskStep(); // STEP 1 - setStatus("Installing mods - Opening minecraft.jar"); + setStatus(tr("Installing mods: Opening minecraft.jar ...")); QuaZip zipOut(runnableJar.filePath()); if (!zipOut.open(QuaZip::mdCreate)) @@ -419,10 +419,15 @@ void LegacyUpdate::ModTheJar() QSet addedFiles; // Modify the jar - setStatus("Installing mods - Adding mod files..."); + setStatus(tr("Installing mods: Adding mod files...")); for (int i = modList->size() - 1; i >= 0; i--) { auto &mod = modList->operator[](i); + + // do not merge disabled mods. + if(!mod.enabled()) + continue; + if (mod.type() == Mod::MOD_ZIPFILE) { if (!MergeZipFiles(&zipOut, mod.filename(), addedFiles, LegacyUpdate::KeepMetainf)) diff --git a/logic/LiteLoaderInstaller.cpp b/logic/LiteLoaderInstaller.cpp new file mode 100644 index 000000000..07fffff3b --- /dev/null +++ b/logic/LiteLoaderInstaller.cpp @@ -0,0 +1,102 @@ +/* Copyright 2013 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 "LiteLoaderInstaller.h" + +#include "OneSixVersion.h" +#include "OneSixLibrary.h" + +QMap LiteLoaderInstaller::m_launcherWrapperVersionMapping; + +LiteLoaderInstaller::LiteLoaderInstaller(const QString &mcVersion) : m_mcVersion(mcVersion) +{ + if (m_launcherWrapperVersionMapping.isEmpty()) + { + m_launcherWrapperVersionMapping["1.6.2"] = "1.3"; + m_launcherWrapperVersionMapping["1.6.4"] = "1.8"; + //m_launcherWrapperVersionMapping["1.7.2"] = "1.8"; + //m_launcherWrapperVersionMapping["1.7.4"] = "1.8"; + } +} + +bool LiteLoaderInstaller::canApply() const +{ + return m_launcherWrapperVersionMapping.contains(m_mcVersion); +} + +bool LiteLoaderInstaller::apply(std::shared_ptr to) +{ + to->externalUpdateStart(); + + applyLaunchwrapper(to); + applyLiteLoader(to); + + to->mainClass = "net.minecraft.launchwrapper.Launch"; + if (!to->minecraftArguments.contains( + " --tweakClass com.mumfrey.liteloader.launch.LiteLoaderTweaker")) + { + to->minecraftArguments.append( + " --tweakClass com.mumfrey.liteloader.launch.LiteLoaderTweaker"); + } + + to->externalUpdateFinish(); + return to->toOriginalFile(); +} + +void LiteLoaderInstaller::applyLaunchwrapper(std::shared_ptr to) +{ + const QString intendedVersion = m_launcherWrapperVersionMapping[m_mcVersion]; + + QMutableListIterator> it(to->libraries); + while (it.hasNext()) + { + it.next(); + if (it.value()->rawName().startsWith("net.minecraft:launchwrapper:")) + { + if (it.value()->version() >= intendedVersion) + { + return; + } + else + { + it.remove(); + } + } + } + + std::shared_ptr lib(new OneSixLibrary( + "net.minecraft:launchwrapper:" + m_launcherWrapperVersionMapping[m_mcVersion])); + lib->finalize(); + to->libraries.prepend(lib); +} + +void LiteLoaderInstaller::applyLiteLoader(std::shared_ptr to) +{ + QMutableListIterator> it(to->libraries); + while (it.hasNext()) + { + it.next(); + if (it.value()->rawName().startsWith("com.mumfrey:liteloader:")) + { + it.remove(); + } + } + + std::shared_ptr lib( + new OneSixLibrary("com.mumfrey:liteloader:" + m_mcVersion)); + lib->setBaseUrl("http://dl.liteloader.com/versions/"); + lib->finalize(); + to->libraries.prepend(lib); +} diff --git a/depends/settings/include/basicsettingsobject.h b/logic/LiteLoaderInstaller.h similarity index 56% rename from depends/settings/include/basicsettingsobject.h rename to logic/LiteLoaderInstaller.h index 387a36461..44b306d69 100644 --- a/depends/settings/include/basicsettingsobject.h +++ b/logic/LiteLoaderInstaller.h @@ -14,29 +14,26 @@ */ #pragma once +#include +#include +#include -#include -#include +class OneSixVersion; -#include "settingsobject.h" - -#include "libsettings_config.h" - -/*! - * \brief A settings object that stores its settings in a QSettings object. - */ -class LIBSETTINGS_EXPORT BasicSettingsObject : public SettingsObject +class LiteLoaderInstaller { - Q_OBJECT public: - explicit BasicSettingsObject(QObject *parent = 0); + LiteLoaderInstaller(const QString &mcVersion); -protected -slots: - virtual void changeSetting(const Setting &setting, QVariant value); + bool canApply() const; -protected: - virtual QVariant retrieveValue(const Setting &setting); + bool apply(std::shared_ptr to); - QSettings config; +private: + QString m_mcVersion; + + void applyLaunchwrapper(std::shared_ptr to); + void applyLiteLoader(std::shared_ptr to); + + static QMap m_launcherWrapperVersionMapping; }; diff --git a/logic/Mod.cpp b/logic/Mod.cpp index cff9467eb..6732446d0 100644 --- a/logic/Mod.cpp +++ b/logic/Mod.cpp @@ -35,20 +35,45 @@ Mod::Mod(const QFileInfo &file) void Mod::repath(const QFileInfo &file) { m_file = file; - m_name = file.completeBaseName(); - m_id = file.fileName(); + QString name_base = file.fileName(); m_type = Mod::MOD_UNKNOWN; + if (m_file.isDir()) + { m_type = MOD_FOLDER; + m_name = name_base; + m_mmc_id = name_base; + } else if (m_file.isFile()) { - QString ext = m_file.suffix().toLower(); - if (ext == "zip" || ext == "jar") - m_type = MOD_ZIPFILE; + if (name_base.endsWith(".disabled")) + { + m_enabled = false; + name_base.chop(9); + } else + { + m_enabled = true; + } + m_mmc_id = name_base; + if (name_base.endsWith(".zip") || name_base.endsWith(".jar")) + { + m_type = MOD_ZIPFILE; + name_base.chop(4); + } + else if (name_base.endsWith(".litemod")) + { + m_type = MOD_LITEMOD; + name_base.chop(8); + } + else + { m_type = MOD_SINGLEFILE; + } + m_name = name_base; } + if (m_type == MOD_ZIPFILE) { QuaZip zip(m_file.filePath()); @@ -59,7 +84,7 @@ void Mod::repath(const QFileInfo &file) if (zip.setCurrentFile("mcmod.info")) { - if(!file.open(QIODevice::ReadOnly)) + if (!file.open(QIODevice::ReadOnly)) { zip.close(); return; @@ -100,6 +125,27 @@ void Mod::repath(const QFileInfo &file) ReadMCModInfo(data); } } + else if (m_type == MOD_LITEMOD) + { + QuaZip zip(m_file.filePath()); + if (!zip.open(QuaZip::mdUnzip)) + return; + + QuaZipFile file(&zip); + + if (zip.setCurrentFile("litemod.json")) + { + if (!file.open(QIODevice::ReadOnly)) + { + zip.close(); + return; + } + + ReadLiteModInfo(file.readAll()); + file.close(); + } + zip.close(); + } } // NEW format @@ -114,7 +160,7 @@ void Mod::ReadMCModInfo(QByteArray contents) if (!arr.at(0).isObject()) return; auto firstObj = arr.at(0).toObject(); - m_id = firstObj.value("modid").toString(); + m_mod_id = firstObj.value("modid").toString(); m_name = firstObj.value("name").toString(); m_version = firstObj.value("version").toString(); m_homeurl = firstObj.value("url").toString(); @@ -132,8 +178,7 @@ void Mod::ReadMCModInfo(QByteArray contents) } m_credits = firstObj.value("credits").toString(); return; - } - ; + }; QJsonParseError jsonError; QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); // this is the very old format that had just the array @@ -163,7 +208,7 @@ void Mod::ReadForgeInfo(QByteArray contents) { // Read the data m_name = "Minecraft Forge"; - m_id = "Forge"; + m_mod_id = "Forge"; m_homeurl = "http://www.minecraftforge.net/forum/"; INIFile ini; if (!ini.loadFile(contents)) @@ -177,15 +222,40 @@ void Mod::ReadForgeInfo(QByteArray contents) m_version = major + "." + minor + "." + revision + "." + build; } +void Mod::ReadLiteModInfo(QByteArray contents) +{ + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); + auto object = jsonDoc.object(); + if(object.contains("name")) + { + m_mod_id = m_name = object.value("name").toString(); + } + if(object.contains("version")) + { + m_version=object.value("version").toString(""); + } + else + { + m_version=object.value("revision").toString(""); + } + m_mcversion = object.value("mcversion").toString(); + m_authors = object.value("author").toString(); + m_description = object.value("description").toString(); + m_homeurl = object.value("url").toString(); +} + bool Mod::replace(Mod &with) { if (!destroy()) return false; bool success = false; auto t = with.type(); + if (t == MOD_ZIPFILE || t == MOD_SINGLEFILE) { - success = QFile::copy(with.m_file.filePath(), m_file.path()); + QLOG_DEBUG() << "Copy: " << with.m_file.filePath() << " to " << m_file.filePath(); + success = QFile::copy(with.m_file.filePath(), m_file.filePath()); } if (t == MOD_FOLDER) { @@ -193,11 +263,17 @@ bool Mod::replace(Mod &with) } if (success) { - m_id = with.m_id; - m_mcversion = with.m_mcversion; - m_type = with.m_type; m_name = with.m_name; + m_mmc_id = with.m_mmc_id; + m_mod_id = with.m_mod_id; m_version = with.m_version; + m_mcversion = with.m_mcversion; + m_description = with.m_description; + m_authors = with.m_authors; + m_credits = with.m_credits; + m_homeurl = with.m_homeurl; + m_type = with.m_type; + m_file.refresh(); } return success; } @@ -232,6 +308,7 @@ QString Mod::version() const switch (type()) { case MOD_ZIPFILE: + case MOD_LITEMOD: return m_version; case MOD_FOLDER: return "Folder"; @@ -241,3 +318,41 @@ QString Mod::version() const return "VOID"; } } + +bool Mod::enable(bool value) +{ + if (m_type == Mod::MOD_UNKNOWN || m_type == Mod::MOD_FOLDER) + return false; + + if (m_enabled == value) + return false; + + QString path = m_file.absoluteFilePath(); + if (value) + { + QFile foo(path); + if (!path.endsWith(".disabled")) + return false; + path.chop(9); + if (!foo.rename(path)) + return false; + } + else + { + QFile foo(path); + path += ".disabled"; + if (!foo.rename(path)) + return false; + } + m_file = QFileInfo(path); + m_enabled = value; + return true; +} +bool Mod::operator==(const Mod &other) const +{ + return mmc_id() == other.mmc_id(); +} +bool Mod::strongCompare(const Mod &other) const +{ + return mmc_id() == other.mmc_id() && version() == other.version() && type() == other.type(); +} diff --git a/logic/Mod.h b/logic/Mod.h index ca362a9d1..2eb2b97a5 100644 --- a/logic/Mod.h +++ b/logic/Mod.h @@ -25,6 +25,7 @@ public: MOD_ZIPFILE, //!< The mod is a zip file containing the mod's class files. MOD_SINGLEFILE, //!< The mod is a single file (not a zip file). MOD_FOLDER, //!< The mod is in a folder on the filesystem. + MOD_LITEMOD, //!< The mod is a litemod }; Mod(const QFileInfo &file); @@ -33,9 +34,13 @@ public: { return m_file; } - QString id() const + QString mmc_id() const { - return m_id; + return m_mmc_id; + } + QString mod_id() const + { + return m_mod_id; } ModType type() const { @@ -77,6 +82,13 @@ public: return m_credits; } + bool enabled() const + { + return m_enabled; + } + + bool enable(bool value); + // delete all the files of this mod bool destroy(); // replace this mod with a copy of the other @@ -85,19 +97,13 @@ public: void repath(const QFileInfo &file); // WEAK compare operator - used for replacing mods - bool operator==(const Mod &other) const - { - return filename() == other.filename(); - } - bool strongCompare(const Mod &other) const - { - return filename() == other.filename() && id() == other.id() && - version() == other.version() && type() == other.type(); - } + bool operator==(const Mod &other) const; + bool strongCompare(const Mod &other) const; private: void ReadMCModInfo(QByteArray contents); void ReadForgeInfo(QByteArray contents); + void ReadLiteModInfo(QByteArray contents); protected: @@ -108,7 +114,9 @@ protected: */ QFileInfo m_file; - QString m_id; + QString m_mmc_id; + QString m_mod_id; + bool m_enabled = true; QString m_name; QString m_version; QString m_mcversion; diff --git a/logic/ModList.cpp b/logic/ModList.cpp index d5235fe9e..fd41bcf76 100644 --- a/logic/ModList.cpp +++ b/logic/ModList.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include "logger/QsLog.h" @@ -27,7 +28,7 @@ ModList::ModList(const QString &dir, const QString &list_file) { m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs | QDir::NoSymLinks); - m_dir.setSorting(QDir::Name); + m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); m_list_id = QUuid::createUuid().toString(); m_watcher = new QFileSystemWatcher(this); is_watching = false; @@ -66,52 +67,89 @@ bool ModList::update() if (!isValid()) return false; + QList orderedMods; QList newMods; m_dir.refresh(); auto folderContents = m_dir.entryInfoList(); - bool orderWasInvalid = false; + bool orderOrStateChanged = false; // first, process the ordered items (if any) - QStringList listOrder = readListFile(); + OrderList listOrder = readListFile(); for (auto item : listOrder) { - QFileInfo info(m_dir.filePath(item)); - int idx = folderContents.indexOf(info); + QFileInfo infoEnabled(m_dir.filePath(item.id)); + QFileInfo infoDisabled(m_dir.filePath(item.id + ".disabled")); + int idxEnabled = folderContents.indexOf(infoEnabled); + int idxDisabled = folderContents.indexOf(infoDisabled); + bool isEnabled; + // if both enabled and disabled versions are present, it's a special case... + if (idxEnabled >= 0 && idxDisabled >= 0) + { + // we only process the one we actually have in the order file. + // and exactly as we have it. + // THIS IS A CORNER CASE + isEnabled = item.enabled; + } + else + { + // only one is present. + // we pick the one that we found. + // we assume the mod was enabled/disabled by external means + isEnabled = idxEnabled >= 0; + } + int idx = isEnabled ? idxEnabled : idxDisabled; + QFileInfo & info = isEnabled ? infoEnabled : infoDisabled; // if the file from the index file exists if (idx != -1) { // remove from the actual folder contents list folderContents.takeAt(idx); // append the new mod - newMods.append(Mod(info)); + orderedMods.append(Mod(info)); + if (isEnabled != item.enabled) + orderOrStateChanged = true; } else { - orderWasInvalid = true; + orderOrStateChanged = true; } } - for (auto entry : folderContents) + // if there are any untracked files... + if (folderContents.size()) { - newMods.append(Mod(entry)); - } - if (mods.size() != newMods.size()) - { - orderWasInvalid = true; - } - else - for (int i = 0; i < mods.size(); i++) + // the order surely changed! + for (auto entry : folderContents) { - if (!mods[i].strongCompare(newMods[i])) - { - orderWasInvalid = true; - break; - } + newMods.append(Mod(entry)); } - beginResetModel(); - mods.swap(newMods); - endResetModel(); - if (orderWasInvalid) + std::sort(newMods.begin(), newMods.end(), [](const Mod & left, const Mod & right) + { return left.name().localeAwareCompare(right.name()) <= 0; }); + orderedMods.append(newMods); + orderOrStateChanged = true; + } + // otherwise, if we were already tracking some mods + else if (mods.size()) { + // if the number doesn't match, order changed. + if (mods.size() != orderedMods.size()) + orderOrStateChanged = true; + // if it does match, compare the mods themselves + else + for (int i = 0; i < mods.size(); i++) + { + if (!mods[i].strongCompare(orderedMods[i])) + { + orderOrStateChanged = true; + break; + } + } + } + beginResetModel(); + mods.swap(orderedMods); + endResetModel(); + if (orderOrStateChanged && !m_list_file.isEmpty()) + { + QLOG_INFO() << "Mod list " << m_list_file << " changed!"; saveListFile(); emit changed(); } @@ -123,17 +161,19 @@ void ModList::directoryChanged(QString path) update(); } -QStringList ModList::readListFile() +ModList::OrderList ModList::readListFile() { - QStringList stringList; + OrderList itemList; if (m_list_file.isNull() || m_list_file.isEmpty()) - return stringList; + return itemList; QFile textFile(m_list_file); if (!textFile.open(QIODevice::ReadOnly | QIODevice::Text)) - return QStringList(); + return OrderList(); - QTextStream textStream(&textFile); + QTextStream textStream; + textStream.setAutoDetectUnicode(true); + textStream.setDevice(&textFile); while (true) { QString line = textStream.readLine(); @@ -141,11 +181,18 @@ QStringList ModList::readListFile() break; else { - stringList.append(line); + OrderItem it; + it.enabled = !line.endsWith(".disabled"); + if (!it.enabled) + { + line.chop(9); + } + it.id = line; + itemList.append(it); } } textFile.close(); - return stringList; + return itemList; } bool ModList::saveListFile() @@ -155,12 +202,16 @@ bool ModList::saveListFile() QFile textFile(m_list_file); if (!textFile.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) return false; - QTextStream textStream(&textFile); + QTextStream textStream; + textStream.setGenerateByteOrderMark(true); + textStream.setCodec("UTF-8"); + textStream.setDevice(&textFile); for (auto mod : mods) { - auto pathname = mod.filename(); - QString filename = pathname.fileName(); - textStream << filename << endl; + textStream << mod.mmc_id(); + if (!mod.enabled()) + textStream << ".disabled"; + textStream << endl; } textFile.close(); return false; @@ -185,6 +236,9 @@ bool ModList::installMod(const QFileInfo &filename, int index) int idx = mods.indexOf(m); if (idx != -1) { + int idx2 = mods.indexOf(m,idx+1); + if(idx2 != -1) + return false; if (mods[idx].replace(m)) { @@ -201,7 +255,7 @@ bool ModList::installMod(const QFileInfo &filename, int index) auto type = m.type(); if (type == Mod::MOD_UNKNOWN) return false; - if (type == Mod::MOD_SINGLEFILE || type == Mod::MOD_ZIPFILE) + if (type == Mod::MOD_SINGLEFILE || type == Mod::MOD_ZIPFILE || type == Mod::MOD_LITEMOD) { QString newpath = PathCombine(m_dir.path(), filename.fileName()); if (!QFile::copy(filename.filePath(), newpath)) @@ -327,7 +381,7 @@ bool ModList::moveModsDown(int first, int last) int ModList::columnCount(const QModelIndex &parent) const { - return 2; + return 3; } QVariant ModList::data(const QModelIndex &index, int role) const @@ -341,43 +395,96 @@ QVariant ModList::data(const QModelIndex &index, int role) const if (row < 0 || row >= mods.size()) return QVariant(); - if (role != Qt::DisplayRole) - return QVariant(); - - switch (column) + switch (role) { - case 0: - return mods[row].name(); - case 1: - return mods[row].version(); - case 2: - return mods[row].mcversion(); + case Qt::DisplayRole: + switch (index.column()) + { + case NameColumn: + return mods[row].name(); + case VersionColumn: + return mods[row].version(); + + default: + return QVariant(); + } + + case Qt::ToolTipRole: + return mods[row].mmc_id(); + + case Qt::CheckStateRole: + switch (index.column()) + { + case ActiveColumn: + return mods[row].enabled(); + default: + return QVariant(); + } default: return QVariant(); } } +bool ModList::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (index.row() < 0 || index.row() >= rowCount(index) || !index.isValid()) + { + return false; + } + + if (role == Qt::CheckStateRole) + { + auto &mod = mods[index.row()]; + if (mod.enable(!mod.enabled())) + { + emit dataChanged(index, index); + return true; + } + } + return false; +} + QVariant ModList::headerData(int section, Qt::Orientation orientation, int role) const { - if (role != Qt::DisplayRole || orientation != Qt::Horizontal) - return QVariant(); - switch (section) + switch (role) { - case 0: - return QString("Name"); - case 1: - return QString("Version"); - case 2: - return QString("Minecraft"); + case Qt::DisplayRole: + switch (section) + { + case ActiveColumn: + return QString(); + case NameColumn: + return QString("Name"); + case VersionColumn: + return QString("Version"); + default: + return QVariant(); + } + + case Qt::ToolTipRole: + switch (section) + { + case ActiveColumn: + return "Is the mod enabled?"; + case NameColumn: + return "The name of the mod."; + case VersionColumn: + return "The version of the mod."; + default: + return QVariant(); + } + default: + return QVariant(); } - return QString(); + return QVariant(); } Qt::ItemFlags ModList::flags(const QModelIndex &index) const { Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index); if (index.isValid()) - return Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | defaultFlags; + return Qt::ItemIsUserCheckable | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | + defaultFlags; else return Qt::ItemIsDropEnabled | defaultFlags; } @@ -456,6 +563,14 @@ bool ModList::dropMimeData(const QMimeData *data, Qt::DropAction action, int row QString filename = url.toLocalFile(); installMod(filename, row); QLOG_INFO() << "installing: " << filename; + // if there is no ordering, re-sort the list + if (m_list_file.isEmpty()) + { + beginResetModel(); + std::sort(mods.begin(), mods.end(), [](const Mod & left, const Mod & right) + { return left.name().localeAwareCompare(right.name()) <= 0; }); + endResetModel(); + } } if (was_watching) startWatching(); diff --git a/logic/ModList.h b/logic/ModList.h index 803a54293..0d6507fb6 100644 --- a/logic/ModList.h +++ b/logic/ModList.h @@ -34,9 +34,18 @@ class ModList : public QAbstractListModel { Q_OBJECT public: + enum Columns + { + ActiveColumn = 0, + NameColumn, + VersionColumn + }; ModList(const QString &dir, const QString &list_file = QString()); virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; + virtual bool setData(const QModelIndex &index, const QVariant &value, + int role = Qt::EditRole); + virtual int rowCount(const QModelIndex &parent = QModelIndex()) const { return size(); @@ -59,7 +68,6 @@ public: { return mods[index]; } - ; /// Reloads the mod list and returns true if the list changed. virtual bool update(); @@ -119,7 +127,13 @@ public: } private: - QStringList readListFile(); + struct OrderItem + { + QString id; + bool enabled = false; + }; + typedef QList OrderList; + OrderList readListFile(); bool saveListFile(); private slots: diff --git a/logic/OneSixFTBInstance.cpp b/logic/OneSixFTBInstance.cpp new file mode 100644 index 000000000..4bb5cf429 --- /dev/null +++ b/logic/OneSixFTBInstance.cpp @@ -0,0 +1,120 @@ +#include "OneSixFTBInstance.h" + +#include "OneSixVersion.h" +#include "OneSixLibrary.h" +#include "tasks/SequentialTask.h" +#include "ForgeInstaller.h" +#include "lists/ForgeVersionList.h" +#include "MultiMC.h" + +class OneSixFTBInstanceForge : public Task +{ + Q_OBJECT +public: + explicit OneSixFTBInstanceForge(const QString &version, OneSixFTBInstance *inst, QObject *parent = 0) : + Task(parent), instance(inst), version("Forge " + version) + { + } + + void executeTask() + { + for (int i = 0; i < MMC->forgelist()->count(); ++i) + { + if (MMC->forgelist()->at(i)->name() == version) + { + forgeVersion = std::dynamic_pointer_cast(MMC->forgelist()->at(i)); + break; + } + } + if (!forgeVersion) + { + emitFailed(QString("Couldn't find forge version ") + version ); + return; + } + entry = MMC->metacache()->resolveEntry("minecraftforge", forgeVersion->filename); + if (entry->stale) + { + setStatus(tr("Downloading Forge...")); + fjob = new NetJob("Forge download"); + fjob->addNetAction(CacheDownload::make(forgeVersion->installer_url, entry)); + connect(fjob, &NetJob::failed, [this](){emitFailed(m_failReason);}); + connect(fjob, &NetJob::succeeded, this, &OneSixFTBInstanceForge::installForge); + connect(fjob, &NetJob::progress, [this](qint64 c, qint64 total){ setProgress(100 * c / total); }); + fjob->start(); + } + else + { + installForge(); + } + } + +private +slots: + void installForge() + { + setStatus(tr("Installing Forge...")); + QString forgePath = entry->getFullPath(); + ForgeInstaller forge(forgePath, forgeVersion->universal_url); + if (!instance->reloadFullVersion()) + { + emitFailed(tr("Couldn't load the version config")); + return; + } + instance->revertCustomVersion(); + instance->customizeVersion(); + auto version = instance->getFullVersion(); + if (!forge.apply(version)) + { + emitFailed(tr("Couldn't install Forge")); + return; + } + emitSucceeded(); + } + +private: + OneSixFTBInstance *instance; + QString version; + ForgeVersionPtr forgeVersion; + MetaEntryPtr entry; + NetJob *fjob; +}; + +OneSixFTBInstance::OneSixFTBInstance(const QString &rootDir, SettingsObject *settings, QObject *parent) : + OneSixInstance(rootDir, settings, parent) +{ + QFile f(QDir(minecraftRoot()).absoluteFilePath("pack.json")); + if (f.open(QFile::ReadOnly)) + { + QString data = QString::fromUtf8(f.readAll()); + QRegularExpressionMatch match = QRegularExpression("net.minecraftforge:minecraftforge:[\\.\\d]*").match(data); + m_forge.reset(new OneSixLibrary(match.captured())); + m_forge->finalize(); + } +} + +QString OneSixFTBInstance::getStatusbarDescription() +{ + return "OneSix FTB: " + intendedVersionId(); +} +bool OneSixFTBInstance::menuActionEnabled(QString action_name) const +{ + return false; +} + +std::shared_ptr OneSixFTBInstance::doUpdate(bool only_prepare) +{ + std::shared_ptr task; + task.reset(new SequentialTask(this)); + if (!MMC->forgelist()->isLoaded()) + { + task->addTask(std::shared_ptr(MMC->forgelist()->getLoadTask())); + } + task->addTask(OneSixInstance::doUpdate(only_prepare)); + task->addTask(std::shared_ptr(new OneSixFTBInstanceForge(m_forge->version(), this, this))); + //FIXME: yes. this may appear dumb. but the previous step can change the list, so we do it all again. + //TODO: Add a graph task. Construct graphs of tasks so we may capture the logic properly. + task->addTask(OneSixInstance::doUpdate(only_prepare)); + return task; +} + +#include "OneSixFTBInstance.moc" diff --git a/logic/OneSixFTBInstance.h b/logic/OneSixFTBInstance.h new file mode 100644 index 000000000..7600090c8 --- /dev/null +++ b/logic/OneSixFTBInstance.h @@ -0,0 +1,20 @@ +#pragma once + +#include "OneSixInstance.h" + +class OneSixLibrary; + +class OneSixFTBInstance : public OneSixInstance +{ + Q_OBJECT +public: + explicit OneSixFTBInstance(const QString &rootDir, SettingsObject *settings, + QObject *parent = 0); + virtual QString getStatusbarDescription(); + virtual bool menuActionEnabled(QString action_name) const; + + virtual std::shared_ptr doUpdate(bool only_prepare) override; + +private: + std::shared_ptr m_forge; +}; diff --git a/logic/OneSixInstance.cpp b/logic/OneSixInstance.cpp index fd41b9e5d..2392c683a 100644 --- a/logic/OneSixInstance.cpp +++ b/logic/OneSixInstance.cpp @@ -33,8 +33,8 @@ OneSixInstance::OneSixInstance(const QString &rootDir, SettingsObject *setting_o : BaseInstance(new OneSixInstancePrivate(), rootDir, setting_obj, parent) { I_D(OneSixInstance); - d->m_settings->registerSetting(new Setting("IntendedVersion", "")); - d->m_settings->registerSetting(new Setting("ShouldUpdate", false)); + d->m_settings->registerSetting("IntendedVersion", ""); + d->m_settings->registerSetting("ShouldUpdate", false); reloadFullVersion(); } diff --git a/logic/OneSixLibrary.h b/logic/OneSixLibrary.h index 5cb867c22..3f0bc83dd 100644 --- a/logic/OneSixLibrary.h +++ b/logic/OneSixLibrary.h @@ -68,6 +68,12 @@ public: m_name = name; } + /// Returns the raw name field + QString rawName() const + { + return m_name; + } + QJsonObject toJson(); /** diff --git a/logic/OneSixUpdate.cpp b/logic/OneSixUpdate.cpp index 696eeff0b..4d93477a6 100644 --- a/logic/OneSixUpdate.cpp +++ b/logic/OneSixUpdate.cpp @@ -57,7 +57,7 @@ void OneSixUpdate::executeTask() /* * FIXME: in offline mode, do not proceed! */ - setStatus("Testing the Java installation."); + setStatus(tr("Testing the Java installation...")); QString java_path = m_inst->settings().get("JavaPath").toString(); checker.reset(new JavaChecker()); @@ -89,7 +89,7 @@ void OneSixUpdate::executeTask() void OneSixUpdate::checkJavaOnline() { - setStatus("Testing the Java installation."); + setStatus(tr("Testing the Java installation...")); QString java_path = m_inst->settings().get("JavaPath").toString(); checker.reset(new JavaChecker()); @@ -128,7 +128,7 @@ void OneSixUpdate::checkFinishedOffline(JavaCheckResult result) void OneSixUpdate::versionFileStart() { QLOG_INFO() << m_inst->name() << ": getting version file."; - setStatus("Getting the version files from Mojang."); + setStatus(tr("Getting the version files from Mojang...")); QString urlstr = "http://" + URLConstants::AWS_DOWNLOAD_VERSIONS + targetVersion->descriptor() + "/" + targetVersion->descriptor() + ".json"; auto job = new NetJob("Version index"); @@ -196,7 +196,7 @@ void OneSixUpdate::versionFileFailed() void OneSixUpdate::assetIndexStart() { - setStatus("Updating asset index."); + setStatus(tr("Updating assets index...")); OneSixInstance *inst = (OneSixInstance *)m_inst; std::shared_ptr version = inst->getFullVersion(); QString assetName = version->assets; @@ -247,7 +247,7 @@ void OneSixUpdate::assetIndexFinished() } if(dls.size()) { - setStatus("Getting the assets files from Mojang..."); + setStatus(tr("Getting the assets files from Mojang...")); auto job = new NetJob("Assets for " + inst->name()); for(auto dl: dls) job->addNetAction(dl); @@ -281,7 +281,7 @@ void OneSixUpdate::assetsFailed() void OneSixUpdate::jarlibStart() { - setStatus("Getting the library files from Mojang."); + setStatus(tr("Getting the library files from Mojang...")); QLOG_INFO() << m_inst->name() << ": downloading libraries"; OneSixInstance *inst = (OneSixInstance *)m_inst; bool successful = inst->reloadFullVersion(); @@ -369,7 +369,7 @@ void OneSixUpdate::jarlibFailed() void OneSixUpdate::prepareForLaunch() { - setStatus("Preparing for launch."); + setStatus(tr("Preparing for launch...")); QLOG_INFO() << m_inst->name() << ": preparing for launch"; auto onesix_inst = (OneSixInstance *)m_inst; diff --git a/logic/assets/AssetsMigrateTask.cpp b/logic/assets/AssetsMigrateTask.cpp new file mode 100644 index 000000000..7c1f52040 --- /dev/null +++ b/logic/assets/AssetsMigrateTask.cpp @@ -0,0 +1,143 @@ +#include "AssetsMigrateTask.h" +#include "MultiMC.h" +#include "logger/QsLog.h" +#include +#include +#include +#include +#include "gui/dialogs/CustomMessageBox.h" +#include + +AssetsMigrateTask::AssetsMigrateTask(int expected, QObject *parent) + : Task(parent) +{ + this->m_expected = expected; +} + +void AssetsMigrateTask::executeTask() +{ + this->setStatus(tr("Migrating legacy assets...")); + this->setProgress(0); + + QDir assets_dir("assets"); + if (!assets_dir.exists()) + { + emitFailed("Assets directory didn't exist"); + return; + } + assets_dir.setFilter(QDir::AllEntries | QDir::NoDotAndDotDot); + int base_length = assets_dir.path().length(); + + QList blacklist = {"indexes", "objects", "virtual"}; + + if (!assets_dir.exists("objects")) + assets_dir.mkdir("objects"); + QDir objects_dir("assets/objects"); + + QDirIterator iterator(assets_dir, QDirIterator::Subdirectories); + int successes = 0; + int failures = 0; + while (iterator.hasNext()) + { + QString currentDir = iterator.next(); + currentDir = currentDir.remove(0, base_length + 1); + + bool ignore = false; + for (QString blacklisted : blacklist) + { + if (currentDir.startsWith(blacklisted)) + ignore = true; + } + + if (!iterator.fileInfo().isDir() && !ignore) + { + QString filename = iterator.filePath(); + + QFile input(filename); + input.open(QIODevice::ReadOnly | QIODevice::WriteOnly); + QString sha1sum = + QCryptographicHash::hash(input.readAll(), QCryptographicHash::Sha1) + .toHex() + .constData(); + + QString object_name = filename.remove(0, base_length + 1); + QLOG_DEBUG() << "Processing" << object_name << ":" << sha1sum << input.size(); + + QString object_tlk = sha1sum.left(2); + QString object_tlk_dir = objects_dir.path() + "/" + object_tlk; + + QDir tlk_dir(object_tlk_dir); + if (!tlk_dir.exists()) + objects_dir.mkdir(object_tlk); + + QString new_filename = tlk_dir.path() + "/" + sha1sum; + QFile new_object(new_filename); + if (!new_object.exists()) + { + bool rename_success = input.rename(new_filename); + QLOG_DEBUG() << " Doesn't exist, copying to" << new_filename << ":" + << QString::number(rename_success); + if (rename_success) + successes++; + else + failures++; + } + else + { + input.remove(); + QLOG_DEBUG() << " Already exists, deleting original and not copying."; + } + + this->setProgress(100 * ((successes + failures) / (float) this->m_expected)); + } + } + + if (successes + failures == 0) + { + this->setProgress(100); + QLOG_DEBUG() << "No legacy assets needed importing."; + } + else + { + QLOG_DEBUG() << "Finished copying legacy assets:" << successes << "successes and" + << failures << "failures."; + + this->setStatus("Cleaning up legacy assets..."); + this->setProgress(100); + + QDirIterator cleanup_iterator(assets_dir); + + while (cleanup_iterator.hasNext()) + { + QString currentDir = cleanup_iterator.next(); + currentDir = currentDir.remove(0, base_length + 1); + + bool ignore = false; + for (QString blacklisted : blacklist) + { + if (currentDir.startsWith(blacklisted)) + ignore = true; + } + + if (cleanup_iterator.fileInfo().isDir() && !ignore) + { + QString path = cleanup_iterator.filePath(); + QDir folder(path); + + QLOG_DEBUG() << "Cleaning up legacy assets folder:" << path; + + folder.removeRecursively(); + } + } + } + + if(failures > 0) + { + emitFailed(QString("Failed to migrate %1 legacy assets").arg(failures)); + } + else + { + emitSucceeded(); + } +} + diff --git a/logic/assets/AssetsMigrateTask.h b/logic/assets/AssetsMigrateTask.h new file mode 100644 index 000000000..d8d58c974 --- /dev/null +++ b/logic/assets/AssetsMigrateTask.h @@ -0,0 +1,18 @@ +#pragma once +#include "logic/tasks/Task.h" +#include +#include +#include + +class AssetsMigrateTask : public Task +{ + Q_OBJECT +public: + explicit AssetsMigrateTask(int expected, QObject* parent=0); + +protected: + virtual void executeTask(); + +private: + int m_expected; +}; diff --git a/logic/assets/AssetsUtils.cpp b/logic/assets/AssetsUtils.cpp index 11d928cf3..bca7773d2 100644 --- a/logic/assets/AssetsUtils.cpp +++ b/logic/assets/AssetsUtils.cpp @@ -25,23 +25,18 @@ namespace AssetsUtils { -void migrateOldAssets() +int findLegacyAssets() { QDir assets_dir("assets"); if (!assets_dir.exists()) - return; + return 0; assets_dir.setFilter(QDir::AllEntries | QDir::NoDotAndDotDot); int base_length = assets_dir.path().length(); QList blacklist = {"indexes", "objects", "virtual"}; - if (!assets_dir.exists("objects")) - assets_dir.mkdir("objects"); - QDir objects_dir("assets/objects"); - QDirIterator iterator(assets_dir, QDirIterator::Subdirectories); - int successes = 0; - int failures = 0; + int found = 0; while (iterator.hasNext()) { QString currentDir = iterator.next(); @@ -56,79 +51,11 @@ void migrateOldAssets() if (!iterator.fileInfo().isDir() && !ignore) { - QString filename = iterator.filePath(); - - QFile input(filename); - input.open(QIODevice::ReadOnly | QIODevice::WriteOnly); - QString sha1sum = - QCryptographicHash::hash(input.readAll(), QCryptographicHash::Sha1) - .toHex() - .constData(); - - QString object_name = filename.remove(0, base_length + 1); - QLOG_DEBUG() << "Processing" << object_name << ":" << sha1sum << input.size(); - - QString object_tlk = sha1sum.left(2); - QString object_tlk_dir = objects_dir.path() + "/" + object_tlk; - - QDir tlk_dir(object_tlk_dir); - if (!tlk_dir.exists()) - objects_dir.mkdir(object_tlk); - - QString new_filename = tlk_dir.path() + "/" + sha1sum; - QFile new_object(new_filename); - if (!new_object.exists()) - { - bool rename_success = input.rename(new_filename); - QLOG_DEBUG() << " Doesn't exist, copying to" << new_filename << ":" - << QString::number(rename_success); - if (rename_success) - successes++; - else - failures++; - } - else - { - input.remove(); - QLOG_DEBUG() << " Already exists, deleting original and not copying."; - } + found++; } } - if (successes + failures == 0) - { - QLOG_DEBUG() << "No legacy assets needed importing."; - } - else - { - QLOG_DEBUG() << "Finished copying legacy assets:" << successes << "successes and" - << failures << "failures."; - - QDirIterator cleanup_iterator(assets_dir); - - while (cleanup_iterator.hasNext()) - { - QString currentDir = cleanup_iterator.next(); - currentDir = currentDir.remove(0, base_length + 1); - - bool ignore = false; - for (QString blacklisted : blacklist) - { - if (currentDir.startsWith(blacklisted)) - ignore = true; - } - - if (cleanup_iterator.fileInfo().isDir() && !ignore) - { - QString path = cleanup_iterator.filePath(); - QDir folder(path); - - QLOG_DEBUG() << "Cleaning up legacy assets folder:" << path; - - folder.removeRecursively(); - } - } - } + return found; } /* diff --git a/logic/assets/AssetsUtils.h b/logic/assets/AssetsUtils.h index 5276d5a5d..aaacc2dbf 100644 --- a/logic/assets/AssetsUtils.h +++ b/logic/assets/AssetsUtils.h @@ -34,6 +34,6 @@ struct AssetsIndex namespace AssetsUtils { -void migrateOldAssets(); bool loadAssetsIndexJson(QString file, AssetsIndex* index); +int findLegacyAssets(); } diff --git a/logic/auth/MojangAccount.cpp b/logic/auth/MojangAccount.cpp index bc6af98f7..a462eda5a 100644 --- a/logic/auth/MojangAccount.cpp +++ b/logic/auth/MojangAccount.cpp @@ -32,7 +32,8 @@ MojangAccountPtr MojangAccount::loadFromJson(const QJsonObject &object) // The JSON object must at least have a username for it to be valid. if (!object.value("username").isString()) { - QLOG_ERROR() << "Can't load Mojang account info from JSON object. Username field is missing or of the wrong type."; + QLOG_ERROR() << "Can't load Mojang account info from JSON object. Username field is " + "missing or of the wrong type."; return nullptr; } @@ -43,7 +44,8 @@ MojangAccountPtr MojangAccount::loadFromJson(const QJsonObject &object) QJsonArray profileArray = object.value("profiles").toArray(); if (profileArray.size() < 1) { - QLOG_ERROR() << "Can't load Mojang account with username \"" << username << "\". No profiles found."; + QLOG_ERROR() << "Can't load Mojang account with username \"" << username + << "\". No profiles found."; return nullptr; } @@ -63,7 +65,7 @@ MojangAccountPtr MojangAccount::loadFromJson(const QJsonObject &object) } MojangAccountPtr account(new MojangAccount()); - if(object.value("user").isObject()) + if (object.value("user").isObject()) { User u; QJsonObject userStructure = object.value("user").toObject(); @@ -92,7 +94,7 @@ MojangAccountPtr MojangAccount::loadFromJson(const QJsonObject &object) return account; } -MojangAccountPtr MojangAccount::createFromUsername(const QString& username) +MojangAccountPtr MojangAccount::createFromUsername(const QString &username) { MojangAccountPtr account(new MojangAccount()); account->m_clientToken = QUuid::createUuid().toString().remove(QRegExp("[{}-]")); @@ -152,27 +154,27 @@ bool MojangAccount::setCurrentProfile(const QString &profileId) return false; } -const AccountProfile* MojangAccount::currentProfile() const +const AccountProfile *MojangAccount::currentProfile() const { - if(m_currentProfile == -1) + if (m_currentProfile == -1) return nullptr; return &m_profiles[m_currentProfile]; } AccountStatus MojangAccount::accountStatus() const { - if(m_accessToken.isEmpty()) + if (m_accessToken.isEmpty()) return NotVerified; - if(!m_online) + if (!m_online) return Verified; return Online; } -std::shared_ptr MojangAccount::login(QString password) +std::shared_ptr MojangAccount::login(QString password) { - if(m_currentTask) + if (m_currentTask) return m_currentTask; - if(password.isEmpty()) + if (password.isEmpty()) { m_currentTask.reset(new RefreshTask(this)); } @@ -196,7 +198,7 @@ void MojangAccount::authFailed(QString reason) { // This is emitted when the yggdrasil tasks time out or are cancelled. // -> we treat the error as no-op - if(reason != "Yggdrasil task cancelled.") + if (reason != "Yggdrasil task cancelled.") { m_online = false; m_accessToken = QString(); diff --git a/logic/auth/MojangAccount.h b/logic/auth/MojangAccount.h index 325aa8261..dd5d54ae6 100644 --- a/logic/auth/MojangAccount.h +++ b/logic/auth/MojangAccount.h @@ -95,7 +95,7 @@ public: /* manipulation */ * Attempt to login. Empty password means we use the token. * If the attempt fails because we already are performing some task, it returns false. */ - std::shared_ptr login(QString password = QString()); + std::shared_ptr login(QString password = QString()); void downgrade() { diff --git a/logic/auth/YggdrasilTask.cpp b/logic/auth/YggdrasilTask.cpp index 088e1fc06..277d7bfdc 100644 --- a/logic/auth/YggdrasilTask.cpp +++ b/logic/auth/YggdrasilTask.cpp @@ -48,6 +48,7 @@ void YggdrasilTask::executeTask() connect(m_netReply, &QNetworkReply::finished, this, &YggdrasilTask::processReply); connect(m_netReply, &QNetworkReply::uploadProgress, this, &YggdrasilTask::refreshTimers); connect(m_netReply, &QNetworkReply::downloadProgress, this, &YggdrasilTask::refreshTimers); + connect(m_netReply, &QNetworkReply::sslErrors, this, &YggdrasilTask::sslErrors); timeout_keeper.setSingleShot(true); timeout_keeper.start(timeout_max); counter.setSingleShot(false); @@ -75,16 +76,45 @@ void YggdrasilTask::abort() m_netReply->abort(); } +void YggdrasilTask::sslErrors(QList errors) +{ + int i = 1; + for (auto error : errors) + { + QLOG_ERROR() << "LOGIN SSL Error #" << i << " : " << error.errorString(); + auto cert = error.certificate(); + QLOG_ERROR() << "Certificate in question:\n" << cert.toText(); + i++; + } +} + void YggdrasilTask::processReply() { setStatus(getStateMessage(STATE_PROCESSING_RESPONSE)); + if (m_netReply->error() == QNetworkReply::SslHandshakeFailedError) + { + emitFailed( + tr("SSL Handshake failed.
There might be a few causes for it:
" + "
    " + "
  • You use Windows XP and need to update " + "your root certificates
  • " + "
  • Some device on your network is interfering with SSL traffic. In that case, " + "you have bigger worries than Minecraft not starting.
  • " + "
  • Possibly something else. Check the MultiMC log file for details
  • " + "
")); + return; + } + // any network errors lead to offline mode right now if (m_netReply->error() >= QNetworkReply::ConnectionRefusedError && m_netReply->error() <= QNetworkReply::UnknownNetworkError) { // WARNING/FIXME: the value here is used in MojangAccount to detect the cancel/timeout emitFailed("Yggdrasil task cancelled."); + QLOG_ERROR() << "Yggdrasil task cancelled because of: " << m_netReply->error() << " : " + << m_netReply->errorString(); return; } @@ -172,10 +202,10 @@ QString YggdrasilTask::getStateMessage(const YggdrasilTask::State state) const switch (state) { case STATE_SENDING_REQUEST: - return tr("Sending request to auth servers."); + return tr("Sending request to auth servers..."); case STATE_PROCESSING_RESPONSE: - return tr("Processing response from servers."); + return tr("Processing response from servers..."); default: - return tr("Processing. Please wait."); + return tr("Processing. Please wait..."); } } diff --git a/logic/auth/YggdrasilTask.h b/logic/auth/YggdrasilTask.h index 1f81a2d04..85f5a1e1f 100644 --- a/logic/auth/YggdrasilTask.h +++ b/logic/auth/YggdrasilTask.h @@ -20,6 +20,7 @@ #include #include #include +#include #include "logic/auth/MojangAccount.h" @@ -99,6 +100,7 @@ slots: void processReply(); void refreshTimers(qint64, qint64); void heartbeat(); + void sslErrors(QList); public slots: diff --git a/logic/auth/flows/AuthenticateTask.cpp b/logic/auth/flows/AuthenticateTask.cpp index f60be35d3..6548c4e9c 100644 --- a/logic/auth/flows/AuthenticateTask.cpp +++ b/logic/auth/flows/AuthenticateTask.cpp @@ -194,9 +194,9 @@ QString AuthenticateTask::getStateMessage(const YggdrasilTask::State state) cons switch (state) { case STATE_SENDING_REQUEST: - return tr("Authenticating: Sending request."); + return tr("Authenticating: Sending request..."); case STATE_PROCESSING_RESPONSE: - return tr("Authenticating: Processing response."); + return tr("Authenticating: Processing response..."); default: return YggdrasilTask::getStateMessage(state); } diff --git a/logic/auth/flows/RefreshTask.cpp b/logic/auth/flows/RefreshTask.cpp index 5f68ccc79..f63c736ec 100644 --- a/logic/auth/flows/RefreshTask.cpp +++ b/logic/auth/flows/RefreshTask.cpp @@ -145,9 +145,9 @@ QString RefreshTask::getStateMessage(const YggdrasilTask::State state) const switch (state) { case STATE_SENDING_REQUEST: - return tr("Refreshing login token."); + return tr("Refreshing login token..."); case STATE_PROCESSING_RESPONSE: - return tr("Refreshing login token: Processing response."); + return tr("Refreshing login token: Processing response..."); default: return YggdrasilTask::getStateMessage(state); } diff --git a/logic/auth/flows/ValidateTask.cpp b/logic/auth/flows/ValidateTask.cpp index 84d5e703c..4f7323fd2 100644 --- a/logic/auth/flows/ValidateTask.cpp +++ b/logic/auth/flows/ValidateTask.cpp @@ -55,9 +55,9 @@ QString ValidateTask::getStateMessage(const YggdrasilTask::State state) const switch (state) { case STATE_SENDING_REQUEST: - return tr("Validating Access Token: Sending request."); + return tr("Validating access token: Sending request..."); case STATE_PROCESSING_RESPONSE: - return tr("Validating Access Token: Processing response."); + return tr("Validating access token: Processing response..."); default: return YggdrasilTask::getStateMessage(state); } diff --git a/logic/icons/IconList.cpp b/logic/icons/IconList.cpp new file mode 100644 index 000000000..cda2db7bf --- /dev/null +++ b/logic/icons/IconList.cpp @@ -0,0 +1,351 @@ +/* Copyright 2013 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 "IconList.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define MAX_SIZE 1024 + +IconList::IconList(QObject *parent) : QAbstractListModel(parent) +{ + // add builtin icons + QDir instance_icons(":/icons/instances/"); + auto file_info_list = instance_icons.entryInfoList(QDir::Files, QDir::Name); + for (auto file_info : file_info_list) + { + QString key = file_info.baseName(); + addIcon(key, key, file_info.absoluteFilePath(), MMCIcon::Builtin); + } + + m_watcher.reset(new QFileSystemWatcher()); + is_watching = false; + connect(m_watcher.get(), SIGNAL(directoryChanged(QString)), + SLOT(directoryChanged(QString))); + connect(m_watcher.get(), SIGNAL(fileChanged(QString)), SLOT(fileChanged(QString))); + + auto setting = MMC->settings()->getSetting("IconsDir"); + QString path = setting->get().toString(); + connect(setting.get(), SIGNAL(settingChanged(const Setting &, QVariant)), + SLOT(settingChanged(const Setting &, QVariant))); + directoryChanged(path); +} + +void IconList::directoryChanged(const QString &path) +{ + QDir new_dir (path); + if(m_dir.absolutePath() != new_dir.absolutePath()) + { + m_dir.setPath(path); + m_dir.refresh(); + if(is_watching) + stopWatching(); + startWatching(); + } + if(!m_dir.exists()) + if(!ensureFolderPathExists(m_dir.absolutePath())) + return; + m_dir.refresh(); + auto new_list = m_dir.entryList(QDir::Files, QDir::Name); + for (auto it = new_list.begin(); it != new_list.end(); it++) + { + QString &foo = (*it); + foo = m_dir.filePath(foo); + } + auto new_set = new_list.toSet(); + QList current_list; + for (auto &it : icons) + { + if (!it.has(MMCIcon::FileBased)) + continue; + current_list.push_back(it.m_images[MMCIcon::FileBased].filename); + } + QSet current_set = current_list.toSet(); + + QSet to_remove = current_set; + to_remove -= new_set; + + QSet to_add = new_set; + to_add -= current_set; + + for (auto remove : to_remove) + { + QLOG_INFO() << "Removing " << remove; + QFileInfo rmfile(remove); + QString key = rmfile.baseName(); + int idx = getIconIndex(key); + if (idx == -1) + continue; + icons[idx].remove(MMCIcon::FileBased); + if (icons[idx].type() == MMCIcon::ToBeDeleted) + { + beginRemoveRows(QModelIndex(), idx, idx); + icons.remove(idx); + reindex(); + endRemoveRows(); + } + else + { + dataChanged(index(idx), index(idx)); + } + m_watcher->removePath(remove); + emit iconUpdated(key); + } + + for (auto add : to_add) + { + QLOG_INFO() << "Adding " << add; + QFileInfo addfile(add); + QString key = addfile.baseName(); + if (addIcon(key, QString(), addfile.filePath(), MMCIcon::FileBased)) + { + m_watcher->addPath(add); + emit iconUpdated(key); + } + } +} + +void IconList::fileChanged(const QString &path) +{ + QLOG_INFO() << "Checking " << path; + QFileInfo checkfile(path); + if (!checkfile.exists()) + return; + QString key = checkfile.baseName(); + int idx = getIconIndex(key); + if (idx == -1) + return; + QIcon icon(path); + if (!icon.availableSizes().size()) + return; + + icons[idx].m_images[MMCIcon::FileBased].icon = icon; + dataChanged(index(idx), index(idx)); + emit iconUpdated(key); +} + +void IconList::settingChanged(const Setting &setting, QVariant value) +{ + if(setting.id() != "IconsDir") + return; + + directoryChanged(value.toString()); +} + +void IconList::startWatching() +{ + auto abs_path = m_dir.absolutePath(); + ensureFolderPathExists(abs_path); + is_watching = m_watcher->addPath(abs_path); + if (is_watching) + { + QLOG_INFO() << "Started watching " << abs_path; + } + else + { + QLOG_INFO() << "Failed to start watching " << abs_path; + } +} + +void IconList::stopWatching() +{ + m_watcher->removePaths(m_watcher->files()); + m_watcher->removePaths(m_watcher->directories()); + is_watching = false; +} + +QStringList IconList::mimeTypes() const +{ + QStringList types; + types << "text/uri-list"; + return types; +} +Qt::DropActions IconList::supportedDropActions() const +{ + return Qt::CopyAction; +} + +bool IconList::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, + const QModelIndex &parent) +{ + if (action == Qt::IgnoreAction) + return true; + // check if the action is supported + if (!data || !(action & supportedDropActions())) + return false; + + // files dropped from outside? + if (data->hasUrls()) + { + auto urls = data->urls(); + QStringList iconFiles; + for (auto url : urls) + { + // only local files may be dropped... + if (!url.isLocalFile()) + continue; + iconFiles += url.toLocalFile(); + } + installIcons(iconFiles); + return true; + } + return false; +} + +Qt::ItemFlags IconList::flags(const QModelIndex &index) const +{ + Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index); + if (index.isValid()) + return Qt::ItemIsDropEnabled | defaultFlags; + else + return Qt::ItemIsDropEnabled | defaultFlags; +} + +QVariant IconList::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + int row = index.row(); + + if (row < 0 || row >= icons.size()) + return QVariant(); + + switch (role) + { + case Qt::DecorationRole: + return icons[row].icon(); + case Qt::DisplayRole: + return icons[row].name(); + case Qt::UserRole: + return icons[row].m_key; + default: + return QVariant(); + } +} + +int IconList::rowCount(const QModelIndex &parent) const +{ + return icons.size(); +} + +void IconList::installIcons(QStringList iconFiles) +{ + for (QString file : iconFiles) + { + QFileInfo fileinfo(file); + if (!fileinfo.isReadable() || !fileinfo.isFile()) + continue; + QString target = PathCombine("icons", fileinfo.fileName()); + + QString suffix = fileinfo.suffix(); + if (suffix != "jpeg" && suffix != "png" && suffix != "jpg" && suffix != "ico") + continue; + + if (!QFile::copy(file, target)) + continue; + } +} + +bool IconList::deleteIcon(QString key) +{ + int iconIdx = getIconIndex(key); + if (iconIdx == -1) + return false; + auto &iconEntry = icons[iconIdx]; + if (iconEntry.has(MMCIcon::FileBased)) + { + return QFile::remove(iconEntry.m_images[MMCIcon::FileBased].filename); + } + return false; +} + +bool IconList::addIcon(QString key, QString name, QString path, MMCIcon::Type type) +{ + // replace the icon even? is the input valid? + QIcon icon(path); + if (!icon.availableSizes().size()) + return false; + auto iter = name_index.find(key); + if (iter != name_index.end()) + { + auto &oldOne = icons[*iter]; + oldOne.replace(type, icon, path); + dataChanged(index(*iter), index(*iter)); + return true; + } + else + { + // add a new icon + beginInsertRows(QModelIndex(), icons.size(), icons.size()); + { + MMCIcon mmc_icon; + mmc_icon.m_name = name; + mmc_icon.m_key = key; + mmc_icon.replace(type, icon, path); + icons.push_back(mmc_icon); + name_index[key] = icons.size() - 1; + } + endInsertRows(); + return true; + } +} + +void IconList::reindex() +{ + name_index.clear(); + int i = 0; + for (auto &iter : icons) + { + name_index[iter.m_key] = i; + i++; + } +} + +QIcon IconList::getIcon(QString key) +{ + int icon_index = getIconIndex(key); + + if (icon_index != -1) + return icons[icon_index].icon(); + + // Fallback for icons that don't exist. + icon_index = getIconIndex("infinity"); + + if (icon_index != -1) + return icons[icon_index].icon(); + return QIcon(); +} + +int IconList::getIconIndex(QString key) +{ + if (key == "default") + key = "infinity"; + + auto iter = name_index.find(key); + if (iter != name_index.end()) + return *iter; + + return -1; +} + +//#include "IconList.moc" diff --git a/logic/lists/IconList.h b/logic/icons/IconList.h similarity index 68% rename from logic/lists/IconList.h rename to logic/icons/IconList.h index 40ad043b3..322411d13 100644 --- a/logic/lists/IconList.h +++ b/logic/icons/IconList.h @@ -17,15 +17,21 @@ #include #include +#include +#include #include +#include +#include "MMCIcon.h" +#include "setting.h" -class Private; +class QFileSystemWatcher; class IconList : public QAbstractListModel { + Q_OBJECT public: - IconList(); - virtual ~IconList(); + explicit IconList(QObject *parent = 0); + virtual ~IconList() {}; QIcon getIcon(QString key); int getIconIndex(QString key); @@ -33,7 +39,7 @@ public: virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; virtual int rowCount(const QModelIndex &parent = QModelIndex()) const; - bool addIcon(QString key, QString name, QString path, bool is_builtin = false); + bool addIcon(QString key, QString name, QString path, MMCIcon::Type type); bool deleteIcon(QString key); virtual QStringList mimeTypes() const; @@ -44,11 +50,28 @@ public: void installIcons(QStringList iconFiles); + void startWatching(); + void stopWatching(); + +signals: + void iconUpdated(QString key); + private: // hide copy constructor IconList(const IconList &) = delete; // hide assign op IconList &operator=(const IconList &) = delete; void reindex(); - Private *d; + +protected +slots: + void directoryChanged(const QString &path); + void fileChanged(const QString &path); + void settingChanged(const Setting & setting, QVariant value); +private: + std::shared_ptr m_watcher; + bool is_watching; + QMap name_index; + QVector icons; + QDir m_dir; }; diff --git a/logic/icons/MMCIcon.cpp b/logic/icons/MMCIcon.cpp new file mode 100644 index 000000000..d721513d6 --- /dev/null +++ b/logic/icons/MMCIcon.cpp @@ -0,0 +1,89 @@ +/* Copyright 2013 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 "MMCIcon.h" +#include + +MMCIcon::Type operator--(MMCIcon::Type &t, int) +{ + MMCIcon::Type temp = t; + switch (t) + { + case MMCIcon::Type::Builtin: + t = MMCIcon::Type::ToBeDeleted; + break; + case MMCIcon::Type::Transient: + t = MMCIcon::Type::Builtin; + break; + case MMCIcon::Type::FileBased: + t = MMCIcon::Type::Transient; + break; + default: + { + } + } + return temp; +} + +MMCIcon::Type MMCIcon::type() const +{ + return m_current_type; +} + +QString MMCIcon::name() const +{ + if (m_name.size()) + return m_name; + return m_key; +} + +bool MMCIcon::has(MMCIcon::Type _type) const +{ + return m_images[_type].present(); +} + +QIcon MMCIcon::icon() const +{ + if (m_current_type == Type::ToBeDeleted) + return QIcon(); + return m_images[m_current_type].icon; +} + +void MMCIcon::remove(Type rm_type) +{ + m_images[rm_type].filename = QString(); + m_images[rm_type].icon = QIcon(); + for (auto iter = rm_type; iter != Type::ToBeDeleted; iter--) + { + if (m_images[iter].present()) + { + m_current_type = iter; + return; + } + } + m_current_type = Type::ToBeDeleted; +} + +void MMCIcon::replace(MMCIcon::Type new_type, QIcon icon, QString path) +{ + QFileInfo foo(path); + if (new_type > m_current_type || m_current_type == MMCIcon::ToBeDeleted) + { + m_current_type = new_type; + } + m_images[new_type].icon = icon; + m_images[new_type].changed = foo.lastModified(); + m_images[new_type].filename = path; +} diff --git a/depends/settings/src/basicsettingsobject.cpp b/logic/icons/MMCIcon.h similarity index 51% rename from depends/settings/src/basicsettingsobject.cpp rename to logic/icons/MMCIcon.h index 96b388513..5e4b3bb61 100644 --- a/depends/settings/src/basicsettingsobject.cpp +++ b/logic/icons/MMCIcon.h @@ -13,32 +13,40 @@ * limitations under the License. */ -#include "include/basicsettingsobject.h" -#include "include/setting.h" - -BasicSettingsObject::BasicSettingsObject(QObject *parent) : SettingsObject(parent) +#pragma once +#include +#include +#include +struct MMCImage { -} + QIcon icon; + QString filename; + QDateTime changed; + bool present() const + { + return !icon.isNull(); + } +}; -void BasicSettingsObject::changeSetting(const Setting &setting, QVariant value) +struct MMCIcon { - if (contains(setting.id())) + enum Type : unsigned { - if (value.isValid()) - config.setValue(setting.configKey(), value); - else - config.remove(setting.configKey()); - } -} + Builtin, + Transient, + FileBased, + ICONS_TOTAL, + ToBeDeleted + }; + QString m_key; + QString m_name; + MMCImage m_images[ICONS_TOTAL]; + Type m_current_type = ToBeDeleted; -QVariant BasicSettingsObject::retrieveValue(const Setting &setting) -{ - if (contains(setting.id())) - { - return config.value(setting.configKey()); - } - else - { - return QVariant(); - } -} + Type type() const; + QString name() const; + bool has(Type _type) const; + QIcon icon() const; + void remove(Type rm_type); + void replace(Type new_type, QIcon icon, QString path = QString()); +}; diff --git a/logic/lists/ForgeVersionList.cpp b/logic/lists/ForgeVersionList.cpp index b5e421af4..56eca7443 100644 --- a/logic/lists/ForgeVersionList.cpp +++ b/logic/lists/ForgeVersionList.cpp @@ -15,6 +15,7 @@ #include "ForgeVersionList.h" #include +#include #include "MultiMC.h" #include @@ -23,8 +24,6 @@ #include "logger/QsLog.h" -#define JSON_URL "http://files.minecraftforge.net/minecraftforge/json" - ForgeVersionList::ForgeVersionList(QObject *parent) : BaseVersionList(parent) { } @@ -159,44 +158,43 @@ ForgeListLoadTask::ForgeListLoadTask(ForgeVersionList *vlist) : Task() void ForgeListLoadTask::executeTask() { + setStatus(tr("Fetching Forge version lists...")); auto job = new NetJob("Version index"); // we do not care if the version is stale or not. auto forgeListEntry = MMC->metacache()->resolveEntry("minecraftforge", "list.json"); + auto gradleForgeListEntry = MMC->metacache()->resolveEntry("minecraftforge", "json"); // verify by poking the server. forgeListEntry->stale = true; + gradleForgeListEntry->stale = true; + + job->addNetAction(listDownload = CacheDownload::make(QUrl(URLConstants::FORGE_LEGACY_URL), + forgeListEntry)); + job->addNetAction(gradleListDownload = CacheDownload::make( + QUrl(URLConstants::FORGE_GRADLE_URL), gradleForgeListEntry)); + + connect(listDownload.get(), SIGNAL(failed(int)), SLOT(listFailed())); + connect(gradleListDownload.get(), SIGNAL(failed(int)), SLOT(gradleListFailed())); - job->addNetAction(CacheDownload::make(QUrl(JSON_URL), forgeListEntry)); listJob.reset(job); - connect(listJob.get(), SIGNAL(succeeded()), SLOT(list_downloaded())); - connect(listJob.get(), SIGNAL(failed()), SLOT(list_failed())); + connect(listJob.get(), SIGNAL(succeeded()), SLOT(listDownloaded())); connect(listJob.get(), SIGNAL(progress(qint64, qint64)), SIGNAL(progress(qint64, qint64))); listJob->start(); } -void ForgeListLoadTask::list_failed() -{ - auto DlJob = listJob->first(); - auto reply = DlJob->m_reply; - if (reply) - { - QLOG_ERROR() << "Getting forge version list failed: " << reply->errorString(); - } - else - QLOG_ERROR() << "Getting forge version list failed for reasons unknown."; -} - -void ForgeListLoadTask::list_downloaded() +bool ForgeListLoadTask::parseForgeList(QList &out) { QByteArray data; { - auto DlJob = listJob->first(); - auto filename = std::dynamic_pointer_cast(DlJob)->m_target_path; + auto dlJob = listDownload; + auto filename = std::dynamic_pointer_cast(dlJob)->m_target_path; QFile listFile(filename); if (!listFile.open(QIODevice::ReadOnly)) - return; + { + return false; + } data = listFile.readAll(); - DlJob.reset(); + dlJob.reset(); } QJsonParseError jsonError; @@ -205,13 +203,13 @@ void ForgeListLoadTask::list_downloaded() if (jsonError.error != QJsonParseError::NoError) { emitFailed("Error parsing version list JSON:" + jsonError.errorString()); - return; + return false; } if (!jsonDoc.isObject()) { - emitFailed("Error parsing version list JSON: jsonDoc is not an object"); - return; + emitFailed("Error parsing version list JSON: JSON root is not an object"); + return false; } QJsonObject root = jsonDoc.object(); @@ -221,11 +219,10 @@ void ForgeListLoadTask::list_downloaded() { emitFailed( "Error parsing version list JSON: version list object is missing 'builds' array"); - return; + return false; } QJsonArray builds = root.value("builds").toArray(); - QList tempList; for (int i = 0; i < builds.count(); i++) { // Load the version info. @@ -246,7 +243,9 @@ void ForgeListLoadTask::list_downloaded() for (int j = 0; j < files.count(); j++) { if (!files[j].isObject()) + { continue; + } QJsonObject file = files[j].toObject(); buildtype = file.value("buildtype").toString(); if ((buildtype == "client" || buildtype == "universal") && !valid) @@ -262,7 +261,9 @@ void ForgeListLoadTask::list_downloaded() { QString ext = file.value("ext").toString(); if (ext.isEmpty()) + { continue; + } changelog_url = file.value("url").toString(); } else if (buildtype == "installer") @@ -282,15 +283,161 @@ void ForgeListLoadTask::list_downloaded() fVersion->jobbuildver = jobbuildver; fVersion->mcver = mcver; if (installer_filename.isEmpty()) + { fVersion->filename = filename; + } else + { fVersion->filename = installer_filename; + } fVersion->m_buildnr = build_nr; - tempList.append(fVersion); + out.append(fVersion); } } - m_list->updateListData(tempList); + + return true; +} + +bool ForgeListLoadTask::parseForgeGradleList(QList &out) +{ + QByteArray data; + { + auto dlJob = gradleListDownload; + auto filename = std::dynamic_pointer_cast(dlJob)->m_target_path; + QFile listFile(filename); + if (!listFile.open(QIODevice::ReadOnly)) + { + return false; + } + data = listFile.readAll(); + dlJob.reset(); + } + + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError); + + if (jsonError.error != QJsonParseError::NoError) + { + emitFailed("Error parsing gradle version list JSON:" + jsonError.errorString()); + return false; + } + + if (!jsonDoc.isObject()) + { + emitFailed("Error parsing gradle version list JSON: JSON root is not an object"); + return false; + } + + QJsonObject root = jsonDoc.object(); + + // we probably could hard code these, but it might still be worth doing it this way + const QString webpath = root.value("webpath").toString(); + const QString artifact = root.value("artifact").toString(); + + QJsonObject numbers = root.value("number").toObject(); + for (auto it = numbers.begin(); it != numbers.end(); ++it) + { + QJsonObject number = it.value().toObject(); + std::shared_ptr fVersion(new ForgeVersion()); + fVersion->m_buildnr = number.value("build").toDouble(); + fVersion->jobbuildver = number.value("version").toString(); + fVersion->mcver = number.value("mcversion").toString(); + fVersion->filename = ""; + QString filename, installer_filename; + QJsonArray files = number.value("files").toArray(); + for (auto fIt = files.begin(); fIt != files.end(); ++fIt) + { + // TODO with gradle we also get checksums, use them + QJsonArray file = (*fIt).toArray(); + if (file.size() < 3) + { + continue; + } + if (file.at(1).toString() == "installer") + { + fVersion->installer_url = QString("%1/%2-%3/%4-%2-%3-installer.%5").arg( + webpath, fVersion->mcver, fVersion->jobbuildver, artifact, + file.at(0).toString()); + installer_filename = QString("%1-%2-%3-installer.%4").arg( + artifact, fVersion->mcver, fVersion->jobbuildver, file.at(0).toString()); + } + else if (file.at(1).toString() == "universal") + { + fVersion->universal_url = QString("%1/%2-%3/%4-%2-%3-universal.%5").arg( + webpath, fVersion->mcver, fVersion->jobbuildver, artifact, + file.at(0).toString()); + filename = QString("%1-%2-%3-universal.%4").arg( + artifact, fVersion->mcver, fVersion->jobbuildver, file.at(0).toString()); + } + else if (file.at(1).toString() == "changelog") + { + fVersion->changelog_url = QString("%1/%2-%3/%4-%2-%3-changelog.%5").arg( + webpath, fVersion->mcver, fVersion->jobbuildver, artifact, + file.at(0).toString()); + } + } + if (fVersion->installer_url.isEmpty() && fVersion->universal_url.isEmpty()) + { + continue; + } + fVersion->filename = fVersion->installer_url.isEmpty() ? filename : installer_filename; + out.append(fVersion); + } + + return true; +} + +void ForgeListLoadTask::listDownloaded() +{ + QList list; + bool ret = true; + if (!parseForgeList(list)) + { + ret = false; + } + if (!parseForgeGradleList(list)) + { + ret = false; + } + + if (!ret) + { + return; + } + + qSort(list.begin(), list.end(), [](const BaseVersionPtr & p1, const BaseVersionPtr & p2) + { + // TODO better comparison (takes major/minor/build number into account) + return p1->name() > p2->name(); + }); + + m_list->updateListData(list); emitSucceeded(); return; } + +void ForgeListLoadTask::listFailed() +{ + auto reply = listDownload->m_reply; + if (reply) + { + QLOG_ERROR() << "Getting forge version list failed: " << reply->errorString(); + } + else + { + QLOG_ERROR() << "Getting forge version list failed for reasons unknown."; + } +} +void ForgeListLoadTask::gradleListFailed() +{ + auto reply = gradleListDownload->m_reply; + if (reply) + { + QLOG_ERROR() << "Getting forge version list failed: " << reply->errorString(); + } + else + { + QLOG_ERROR() << "Getting forge version list failed for reasons unknown."; + } +} diff --git a/logic/lists/ForgeVersionList.h b/logic/lists/ForgeVersionList.h index bf9e87b2e..924084aed 100644 --- a/logic/lists/ForgeVersionList.h +++ b/logic/lists/ForgeVersionList.h @@ -80,7 +80,7 @@ public: protected: QList m_vlist; - bool m_loaded; + bool m_loaded = false; protected slots: @@ -98,10 +98,18 @@ public: protected slots: - void list_downloaded(); - void list_failed(); + void listDownloaded(); + void listFailed(); + void gradleListFailed(); protected: NetJobPtr listJob; ForgeVersionList *m_list; + + CacheDownloadPtr listDownload; + CacheDownloadPtr gradleListDownload; + +private: + bool parseForgeList(QList &out); + bool parseForgeGradleList(QList &out); }; diff --git a/logic/lists/IconList.cpp b/logic/lists/IconList.cpp deleted file mode 100644 index ecfb8c3cb..000000000 --- a/logic/lists/IconList.cpp +++ /dev/null @@ -1,271 +0,0 @@ -/* Copyright 2013 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 "IconList.h" -#include -#include -#include -#include -#include -#include -#define MAX_SIZE 1024 - -struct entry -{ - QString key; - QString name; - QIcon icon; - bool is_builtin; - QString filename; -}; - -class Private : public QObject -{ - Q_OBJECT -public: - QMap index; - QVector icons; - Private() - { - } -}; - -IconList::IconList() : QAbstractListModel(), d(new Private()) -{ - QDir instance_icons(":/icons/instances/"); - auto file_info_list = instance_icons.entryInfoList(QDir::Files, QDir::Name); - for (auto file_info : file_info_list) - { - QString key = file_info.baseName(); - addIcon(key, key, file_info.absoluteFilePath(), true); - } - - // FIXME: get from settings - ensureFolderPathExists("icons"); - QDir user_icons("icons"); - file_info_list = user_icons.entryInfoList(QDir::Files, QDir::Name); - for (auto file_info : file_info_list) - { - QString filename = file_info.absoluteFilePath(); - QString key = file_info.baseName(); - addIcon(key, key, filename); - } -} - -IconList::~IconList() -{ - delete d; - d = nullptr; -} - -QStringList IconList::mimeTypes() const -{ - QStringList types; - types << "text/uri-list"; - return types; -} -Qt::DropActions IconList::supportedDropActions() const -{ - return Qt::CopyAction; -} - -bool IconList::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, - const QModelIndex &parent) -{ - if (action == Qt::IgnoreAction) - return true; - // check if the action is supported - if (!data || !(action & supportedDropActions())) - return false; - - // files dropped from outside? - if (data->hasUrls()) - { - /* - bool was_watching = is_watching; - if(was_watching) - stopWatching(); - */ - auto urls = data->urls(); - QStringList iconFiles; - for (auto url : urls) - { - // only local files may be dropped... - if (!url.isLocalFile()) - continue; - iconFiles += url.toLocalFile(); - } - installIcons(iconFiles); - /* - if(was_watching) - startWatching(); - */ - return true; - } - return false; -} - -Qt::ItemFlags IconList::flags(const QModelIndex &index) const -{ - Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index); - if (index.isValid()) - return Qt::ItemIsDropEnabled | defaultFlags; - else - return Qt::ItemIsDropEnabled | defaultFlags; -} - -QVariant IconList::data(const QModelIndex &index, int role) const -{ - if (!index.isValid()) - return QVariant(); - - int row = index.row(); - - if (row < 0 || row >= d->icons.size()) - return QVariant(); - - switch (role) - { - case Qt::DecorationRole: - return d->icons[row].icon; - case Qt::DisplayRole: - return d->icons[row].name; - case Qt::UserRole: - return d->icons[row].key; - default: - return QVariant(); - } -} - -int IconList::rowCount(const QModelIndex &parent) const -{ - return d->icons.size(); -} - -void IconList::installIcons(QStringList iconFiles) -{ - for (QString file : iconFiles) - { - QFileInfo fileinfo(file); - if (!fileinfo.isReadable() || !fileinfo.isFile()) - continue; - QString target = PathCombine("icons", fileinfo.fileName()); - - QString suffix = fileinfo.suffix(); - if (suffix != "jpeg" && suffix != "png" && suffix != "jpg") - continue; - - if (!QFile::copy(file, target)) - continue; - - QString key = fileinfo.baseName(); - addIcon(key, key, target); - } -} - -bool IconList::deleteIcon(QString key) -{ - int iconIdx = getIconIndex(key); - if (iconIdx == -1) - return false; - auto &iconEntry = d->icons[iconIdx]; - if (iconEntry.is_builtin) - return false; - if (QFile::remove(iconEntry.filename)) - { - beginRemoveRows(QModelIndex(), iconIdx, iconIdx); - d->icons.remove(iconIdx); - reindex(); - endRemoveRows(); - } - return true; -} - -bool IconList::addIcon(QString key, QString name, QString path, bool is_builtin) -{ - auto iter = d->index.find(key); - if (iter != d->index.end()) - { - if (d->icons[*iter].is_builtin) - return false; - - QIcon icon(path); - if (icon.isNull()) - return false; - - auto &oldOne = d->icons[*iter]; - - if (!QFile::remove(oldOne.filename)) - return false; - - // replace the icon - oldOne = {key, name, icon, is_builtin, path}; - dataChanged(index(*iter), index(*iter)); - return true; - } - else - { - QIcon icon(path); - if (icon.isNull()) - return false; - - // add a new icon - beginInsertRows(QModelIndex(), d->icons.size(), d->icons.size()); - d->icons.push_back({key, name, icon, is_builtin, path}); - d->index[key] = d->icons.size() - 1; - endInsertRows(); - return true; - } -} - -void IconList::reindex() -{ - d->index.clear(); - int i = 0; - for (auto &iter : d->icons) - { - d->index[iter.key] = i; - i++; - } -} - -QIcon IconList::getIcon(QString key) -{ - int icon_index = getIconIndex(key); - - if (icon_index != -1) - return d->icons[icon_index].icon; - - // Fallback for icons that don't exist. - icon_index = getIconIndex("infinity"); - - if (icon_index != -1) - return d->icons[icon_index].icon; - return QIcon(); -} - -int IconList::getIconIndex(QString key) -{ - if (key == "default") - key = "infinity"; - - auto iter = d->index.find(key); - if (iter != d->index.end()) - return *iter; - - return -1; -} - -#include "IconList.moc" \ No newline at end of file diff --git a/logic/lists/InstanceList.cpp b/logic/lists/InstanceList.cpp index 15fd10ba7..48a2865ac 100644 --- a/logic/lists/InstanceList.cpp +++ b/logic/lists/InstanceList.cpp @@ -22,11 +22,14 @@ #include #include #include +#include +#include #include #include "MultiMC.h" #include "logic/lists/InstanceList.h" -#include "logic/lists/IconList.h" +#include "logic/icons/IconList.h" +#include "logic/lists/MinecraftVersionList.h" #include "logic/BaseInstance.h" #include "logic/InstanceFactory.h" #include "logger/QsLog.h" @@ -42,6 +45,9 @@ InstanceList::InstanceList(const QString &instDir, QObject *parent) { QDir::current().mkpath(m_instDir); } + + connect(MMC->minecraftlist().get(), &MinecraftVersionList::modelReset, this, + &InstanceList::loadList); } InstanceList::~InstanceList() @@ -276,6 +282,125 @@ void InstanceList::loadGroupList(QMap &groupMap) } } +struct FTBRecord +{ + QString dir; + QString name; + QString logo; + QString mcVersion; + QString description; +}; + +void InstanceList::loadForgeInstances(QMap groupMap) +{ + QList records; + QDir dir = QDir(MMC->settings()->get("FTBLauncherRoot").toString()); + QDir dataDir = QDir(MMC->settings()->get("FTBRoot").toString()); + if (!dir.exists()) + { + QLOG_INFO() << "The FTB launcher directory specified does not exist. Please check your " + "settings."; + return; + } + else if (!dataDir.exists()) + { + QLOG_INFO() << "The FTB directory specified does not exist. Please check your settings"; + return; + } + + dir.cd("ModPacks"); + QFile f(dir.absoluteFilePath("modpacks.xml")); + if (!f.open(QFile::ReadOnly)) + return; + + // read the FTB packs XML. + QXmlStreamReader reader(&f); + while (!reader.atEnd()) + { + switch (reader.readNext()) + { + case QXmlStreamReader::StartElement: + { + if (reader.name() == "modpack") + { + QXmlStreamAttributes attrs = reader.attributes(); + FTBRecord record; + record.dir = attrs.value("dir").toString(); + record.name = attrs.value("name").toString(); + record.logo = attrs.value("logo").toString(); + record.mcVersion = attrs.value("mcVersion").toString(); + record.description = attrs.value("description").toString(); + records.append(record); + } + break; + } + case QXmlStreamReader::EndElement: + break; + case QXmlStreamReader::Characters: + break; + default: + break; + } + } + f.close(); + + // process the records we acquired. + for (auto record : records) + { + auto instanceDir = dataDir.absoluteFilePath(record.dir); + auto templateDir = dir.absoluteFilePath(record.dir); + if (!QFileInfo(instanceDir).exists()) + { + continue; + } + + QString iconKey = record.logo; + iconKey.remove(QRegularExpression("\\..*")); + MMC->icons()->addIcon(iconKey, iconKey, PathCombine(templateDir, record.logo), + MMCIcon::Transient); + + if (!QFileInfo(PathCombine(instanceDir, "instance.cfg")).exists()) + { + BaseInstance *instPtr = NULL; + auto &factory = InstanceFactory::get(); + auto version = MMC->minecraftlist()->findVersion(record.mcVersion); + if (!version) + { + QLOG_ERROR() << "Can't load instance " << instanceDir + << " because minecraft version " << record.mcVersion + << " can't be resolved."; + continue; + } + auto error = factory.createInstance(instPtr, version, instanceDir, + InstanceFactory::FTBInstance); + + if (!instPtr || error != InstanceFactory::NoCreateError) + continue; + + instPtr->setGroupInitial("FTB"); + instPtr->setName(record.name); + instPtr->setIconKey(iconKey); + instPtr->setIntendedVersionId(record.mcVersion); + instPtr->setNotes(record.description); + continueProcessInstance(instPtr, error, instanceDir, groupMap); + } + else + { + BaseInstance *instPtr = NULL; + auto error = InstanceFactory::get().loadInstance(instPtr, instanceDir); + if (!instPtr || error != InstanceFactory::NoCreateError) + continue; + instPtr->setGroupInitial("FTB"); + instPtr->setName(record.name); + instPtr->setIconKey(iconKey); + if (instPtr->intendedVersionId() != record.mcVersion) + instPtr->setIntendedVersionId(record.mcVersion); + instPtr->setNotes(record.description); + continueProcessInstance(instPtr, error, instanceDir, groupMap); + } + } +} + InstanceList::InstListError InstanceList::loadList() { // load the instance groups @@ -285,57 +410,27 @@ InstanceList::InstListError InstanceList::loadList() beginResetModel(); m_instances.clear(); - QDir dir(m_instDir); - QDirIterator iter(m_instDir, QDir::Dirs | QDir::NoDot | QDir::NoDotDot | QDir::Readable, - QDirIterator::FollowSymlinks); - while (iter.hasNext()) + { - QString subDir = iter.next(); - if (!QFileInfo(PathCombine(subDir, "instance.cfg")).exists()) - continue; - - BaseInstance *instPtr = NULL; - auto &loader = InstanceFactory::get(); - auto error = loader.loadInstance(instPtr, subDir); - - if (error != InstanceFactory::NoLoadError && error != InstanceFactory::NotAnInstance) + QDirIterator iter(m_instDir, QDir::Dirs | QDir::NoDot | QDir::NoDotDot | QDir::Readable, + QDirIterator::FollowSymlinks); + while (iter.hasNext()) { - QString errorMsg = QString("Failed to load instance %1: ") - .arg(QFileInfo(subDir).baseName()) - .toUtf8(); + QString subDir = iter.next(); + if (!QFileInfo(PathCombine(subDir, "instance.cfg")).exists()) + continue; - switch (error) - { - default: - errorMsg += QString("Unknown instance loader error %1").arg(error); - break; - } - QLOG_ERROR() << errorMsg.toUtf8(); - } - else if (!instPtr) - { - QLOG_ERROR() << QString("Error loading instance %1. Instance loader returned null.") - .arg(QFileInfo(subDir).baseName()) - .toUtf8(); - } - else - { - std::shared_ptr inst(instPtr); - auto iter = groupMap.find(inst->id()); - if (iter != groupMap.end()) - { - inst->setGroupInitial((*iter)); - } - QLOG_INFO() << "Loaded instance " << inst->name(); - inst->setParent(this); - m_instances.append(inst); - connect(instPtr, SIGNAL(propertiesChanged(BaseInstance *)), this, - SLOT(propertiesChanged(BaseInstance *))); - connect(instPtr, SIGNAL(groupChanged()), this, SLOT(groupChanged())); - connect(instPtr, SIGNAL(nuked(BaseInstance *)), this, - SLOT(instanceNuked(BaseInstance *))); + BaseInstance *instPtr = NULL; + auto error = InstanceFactory::get().loadInstance(instPtr, subDir); + continueProcessInstance(instPtr, error, subDir, groupMap); } } + + if (MMC->settings()->get("TrackFTBInstances").toBool()) + { + loadForgeInstances(groupMap); + } + endResetModel(); emit dataIsInvalid(); return NoError; @@ -409,6 +504,47 @@ int InstanceList::getInstIndex(BaseInstance *inst) const return -1; } +void InstanceList::continueProcessInstance(BaseInstance *instPtr, const int error, + const QDir &dir, QMap &groupMap) +{ + if (error != InstanceFactory::NoLoadError && error != InstanceFactory::NotAnInstance) + { + QString errorMsg = QString("Failed to load instance %1: ") + .arg(QFileInfo(dir.absolutePath()).baseName()) + .toUtf8(); + + switch (error) + { + default: + errorMsg += QString("Unknown instance loader error %1").arg(error); + break; + } + QLOG_ERROR() << errorMsg.toUtf8(); + } + else if (!instPtr) + { + QLOG_ERROR() << QString("Error loading instance %1. Instance loader returned null.") + .arg(QFileInfo(dir.absolutePath()).baseName()) + .toUtf8(); + } + else + { + auto iter = groupMap.find(instPtr->id()); + if (iter != groupMap.end()) + { + instPtr->setGroupInitial((*iter)); + } + QLOG_INFO() << "Loaded instance " << instPtr->name(); + instPtr->setParent(this); + m_instances.append(std::shared_ptr(instPtr)); + connect(instPtr, SIGNAL(propertiesChanged(BaseInstance *)), this, + SLOT(propertiesChanged(BaseInstance *))); + connect(instPtr, SIGNAL(groupChanged()), this, SLOT(groupChanged())); + connect(instPtr, SIGNAL(nuked(BaseInstance *)), this, + SLOT(instanceNuked(BaseInstance *))); + } +} + void InstanceList::instanceNuked(BaseInstance *inst) { int i = getInstIndex(inst); diff --git a/logic/lists/InstanceList.h b/logic/lists/InstanceList.h index f23b7763b..0ce808e5e 100644 --- a/logic/lists/InstanceList.h +++ b/logic/lists/InstanceList.h @@ -25,6 +25,8 @@ class BaseInstance; +class QDir; + class InstanceList : public QAbstractListModel { Q_OBJECT @@ -65,11 +67,6 @@ public: return m_instDir; } - /*! - * \brief Loads the instance list. Triggers notifications. - */ - InstListError loadList(); - /*! * \brief Get the instance at index */ @@ -108,6 +105,12 @@ public slots: void on_InstFolderChanged(const Setting &setting, QVariant value); + /*! + * \brief Loads the instance list. Triggers notifications. + */ + InstListError loadList(); + void loadForgeInstances(QMap groupMap); + private slots: void propertiesChanged(BaseInstance *inst); @@ -117,6 +120,9 @@ slots: private: int getInstIndex(BaseInstance *inst) const; + void continueProcessInstance(BaseInstance *instPtr, const int error, const QDir &dir, + QMap &groupMap); + protected: QString m_instDir; QList m_instances; diff --git a/logic/lists/JavaVersionList.cpp b/logic/lists/JavaVersionList.cpp index d2f0972c7..e8c5acd06 100644 --- a/logic/lists/JavaVersionList.cpp +++ b/logic/lists/JavaVersionList.cpp @@ -172,14 +172,14 @@ JavaListLoadTask::~JavaListLoadTask() void JavaListLoadTask::executeTask() { - setStatus("Detecting Java installations..."); + setStatus(tr("Detecting Java installations...")); JavaUtils ju; QList candidate_paths = ju.FindJavaPaths(); - auto job = new JavaCheckerJob("Java detection"); - connect(job, SIGNAL(finished(QList)), this, SLOT(javaCheckerFinished(QList))); - connect(job, SIGNAL(progress(int, int)), this, SLOT(checkerProgress(int, int))); + m_job = std::shared_ptr(new JavaCheckerJob("Java detection")); + connect(m_job.get(), SIGNAL(finished(QList)), this, SLOT(javaCheckerFinished(QList))); + connect(m_job.get(), SIGNAL(progress(int, int)), this, SLOT(checkerProgress(int, int))); QLOG_DEBUG() << "Probing the following Java paths: "; for(QString candidate : candidate_paths) @@ -188,10 +188,10 @@ void JavaListLoadTask::executeTask() auto candidate_checker = new JavaChecker(); candidate_checker->path = candidate; - job->addJavaCheckerAction(JavaCheckerPtr(candidate_checker)); + m_job->addJavaCheckerAction(JavaCheckerPtr(candidate_checker)); } - job->start(); + m_job->start(); } void JavaListLoadTask::checkerProgress(int current, int total) @@ -203,6 +203,7 @@ void JavaListLoadTask::checkerProgress(int current, int total) void JavaListLoadTask::javaCheckerFinished(QList results) { QList candidates; + m_job.reset(); QLOG_DEBUG() << "Found the following valid Java installations:"; for(JavaCheckResult result : results) diff --git a/logic/lists/JavaVersionList.h b/logic/lists/JavaVersionList.h index 879b24807..e6cc8e5f2 100644 --- a/logic/lists/JavaVersionList.h +++ b/logic/lists/JavaVersionList.h @@ -90,6 +90,7 @@ public slots: void checkerProgress(int current, int total); protected: + std::shared_ptr m_job; JavaVersionList *m_list; JavaVersion *m_currentRecommended; }; diff --git a/logic/lists/MinecraftVersionList.cpp b/logic/lists/MinecraftVersionList.cpp index 523b81ac1..91f86df07 100644 --- a/logic/lists/MinecraftVersionList.cpp +++ b/logic/lists/MinecraftVersionList.cpp @@ -139,7 +139,7 @@ MCVListLoadTask::~MCVListLoadTask() void MCVListLoadTask::executeTask() { - setStatus("Loading instance version list..."); + setStatus(tr("Loading instance version list...")); auto worker = MMC->qnam(); vlistReply = worker->get(QNetworkRequest(QUrl("http://" + URLConstants::AWS_DOWNLOAD_VERSIONS + "versions.json"))); connect(vlistReply, SIGNAL(finished()), this, SLOT(list_downloaded())); diff --git a/logic/net/ForgeXzDownload.cpp b/logic/net/ForgeXzDownload.cpp index 1771d304d..359ad858c 100644 --- a/logic/net/ForgeXzDownload.cpp +++ b/logic/net/ForgeXzDownload.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include "logger/QsLog.h" ForgeXzDownload::ForgeXzDownload(QString relative_path, MetaEntryPtr entry) : NetAction() @@ -310,16 +311,51 @@ void ForgeXzDownload::decompressAndInstall() m_pack200_xz_file.remove(); // revert pack200 - pack200_file.close(); - QString pack_name = pack200_file.fileName(); + pack200_file.seek(0); + int handle_in = pack200_file.handle(); + // FIXME: dispose of file handles, pointers and the like. Ideally wrap in objects. + if(handle_in == -1) + { + QLOG_ERROR() << "Error reopening " << pack200_file.fileName(); + failAndTryNextMirror(); + return; + } + FILE * file_in = fdopen(handle_in,"r"); + if(!file_in) + { + QLOG_ERROR() << "Error reopening " << pack200_file.fileName(); + failAndTryNextMirror(); + return; + } + QFile qfile_out(m_target_path); + if(!qfile_out.open(QIODevice::WriteOnly)) + { + QLOG_ERROR() << "Error opening " << qfile_out.fileName(); + failAndTryNextMirror(); + return; + } + int handle_out = qfile_out.handle(); + if(handle_out == -1) + { + QLOG_ERROR() << "Error opening " << qfile_out.fileName(); + failAndTryNextMirror(); + return; + } + FILE * file_out = fdopen(handle_out,"w"); + if(!file_out) + { + QLOG_ERROR() << "Error opening " << qfile_out.fileName(); + failAndTryNextMirror(); + return; + } try { - unpack_200(pack_name.toStdString(), m_target_path.toStdString()); + unpack_200(file_in, file_out); } catch (std::runtime_error &err) { m_status = Job_Failed; - QLOG_ERROR() << "Error unpacking " << pack_name.toUtf8() << " : " << err.what(); + QLOG_ERROR() << "Error unpacking " << pack200_file.fileName() << " : " << err.what(); QFile f(m_target_path); if (f.exists()) f.remove(); diff --git a/logic/net/MD5EtagDownload.cpp b/logic/net/MD5EtagDownload.cpp index 435e854e6..63583e8d7 100644 --- a/logic/net/MD5EtagDownload.cpp +++ b/logic/net/MD5EtagDownload.cpp @@ -23,7 +23,6 @@ MD5EtagDownload::MD5EtagDownload(QUrl url, QString target_path) : NetAction() { m_url = url; m_target_path = target_path; - m_check_md5 = false; m_status = Job_NotStarted; } @@ -34,22 +33,26 @@ void MD5EtagDownload::start() // if there already is a file and md5 checking is in effect and it can be opened if (m_output_file.exists() && m_output_file.open(QIODevice::ReadOnly)) { - // check the md5 against the expected one - QString hash = + // get the md5 of the local file. + m_local_md5 = QCryptographicHash::hash(m_output_file.readAll(), QCryptographicHash::Md5) .toHex() .constData(); m_output_file.close(); - // skip this file if they match - if (m_check_md5 && hash == m_expected_md5) + // if we are expecting some md5sum, compare it with the local one + if (!m_expected_md5.isEmpty()) { - QLOG_INFO() << "Skipping " << m_url.toString() << ": md5 match."; - emit succeeded(m_index_within_job); - return; + // skip if they match + if(m_local_md5 == m_expected_md5) + { + QLOG_INFO() << "Skipping " << m_url.toString() << ": md5 match."; + emit succeeded(m_index_within_job); + return; + } } else { - m_expected_md5 = hash; + // no expected md5. we use the local md5sum as an ETag } } if (!ensureFilePathExists(filename)) @@ -58,9 +61,18 @@ void MD5EtagDownload::start() return; } - QLOG_INFO() << "Downloading " << m_url.toString() << " expecting " << m_expected_md5; QNetworkRequest request(m_url); - request.setRawHeader(QString("If-None-Match").toLatin1(), m_expected_md5.toLatin1()); + + QLOG_INFO() << "Downloading " << m_url.toString() << " got " << m_local_md5; + + if(!m_local_md5.isEmpty()) + { + QLOG_INFO() << "Got " << m_local_md5; + request.setRawHeader(QString("If-None-Match").toLatin1(), m_local_md5.toLatin1()); + } + if(!m_expected_md5.isEmpty()) + QLOG_INFO() << "Expecting " << m_expected_md5; + request.setHeader(QNetworkRequest::UserAgentHeader, "MultiMC/5.0 (Uncached)"); // Go ahead and try to open the file. @@ -107,7 +119,10 @@ void MD5EtagDownload::downloadFinished() m_status = Job_Finished; m_output_file.close(); + // FIXME: compare with the real written data md5sum + // this is just an ETag QLOG_INFO() << "Finished " << m_url.toString() << " got " << m_reply->rawHeader("ETag").constData(); + m_reply.reset(); emit succeeded(m_index_within_job); return; @@ -116,6 +131,7 @@ void MD5EtagDownload::downloadFinished() else { m_output_file.close(); + m_output_file.remove(); m_reply.reset(); emit failed(m_index_within_job); return; diff --git a/logic/net/MD5EtagDownload.h b/logic/net/MD5EtagDownload.h index 416ab9de7..d5aed0ca4 100644 --- a/logic/net/MD5EtagDownload.h +++ b/logic/net/MD5EtagDownload.h @@ -23,12 +23,10 @@ class MD5EtagDownload : public NetAction { Q_OBJECT public: - /// if true, check the md5sum against a provided md5sum - /// also, if a file exists, perform an md5sum first and don't download only if they don't - /// match - bool m_check_md5; - /// the expected md5 checksum + /// the expected md5 checksum. Only set from outside QString m_expected_md5; + /// the md5 checksum of a file that already exists. + QString m_local_md5; /// if saving to file, use the one specified in this string QString m_target_path; /// this is the output file, if any diff --git a/logic/net/URLConstants.h b/logic/net/URLConstants.h index dcd5c2b15..9579198d3 100644 --- a/logic/net/URLConstants.h +++ b/logic/net/URLConstants.h @@ -29,4 +29,6 @@ const QString RESOURCE_BASE("resources.download.minecraft.net/"); const QString LIBRARY_BASE("libraries.minecraft.net/"); const QString SKINS_BASE("skins.minecraft.net/MinecraftSkins/"); const QString AUTH_BASE("authserver.mojang.com/"); +const QString FORGE_LEGACY_URL("http://files.minecraftforge.net/minecraftforge/json"); +const QString FORGE_GRADLE_URL("http://files.minecraftforge.net/maven/net/minecraftforge/forge/json"); } diff --git a/logic/tasks/SequentialTask.cpp b/logic/tasks/SequentialTask.cpp new file mode 100644 index 000000000..63025eee8 --- /dev/null +++ b/logic/tasks/SequentialTask.cpp @@ -0,0 +1,77 @@ +#include "SequentialTask.h" + +SequentialTask::SequentialTask(QObject *parent) : + Task(parent), m_currentIndex(-1) +{ + +} + +QString SequentialTask::getStatus() const +{ + if (m_queue.isEmpty() || m_currentIndex >= m_queue.size()) + { + return QString(); + } + return m_queue.at(m_currentIndex)->getStatus(); +} + +void SequentialTask::getProgress(qint64 ¤t, qint64 &total) +{ + current = 0; + total = 0; + for (int i = 0; i < m_queue.size(); ++i) + { + qint64 subCurrent, subTotal; + m_queue.at(i)->getProgress(subCurrent, subTotal); + current += subCurrent; + total += subTotal; + } +} + +void SequentialTask::addTask(std::shared_ptr task) +{ + m_queue.append(task); +} + +void SequentialTask::executeTask() +{ + m_currentIndex = -1; + startNext(); +} + +void SequentialTask::startNext() +{ + if (m_currentIndex != -1) + { + std::shared_ptr previous = m_queue[m_currentIndex]; + disconnect(previous.get(), 0, this, 0); + } + m_currentIndex++; + if (m_queue.isEmpty() || m_currentIndex >= m_queue.size()) + { + emitSucceeded(); + return; + } + std::shared_ptr next = m_queue[m_currentIndex]; + connect(next.get(), SIGNAL(failed(QString)), this, SLOT(subTaskFailed(QString))); + connect(next.get(), SIGNAL(status(QString)), this, SLOT(subTaskStatus(QString))); + connect(next.get(), SIGNAL(progress(qint64,qint64)), this, SLOT(subTaskProgress())); + connect(next.get(), SIGNAL(succeeded()), this, SLOT(startNext())); + next->start(); + emit status(getStatus()); +} + +void SequentialTask::subTaskFailed(const QString &msg) +{ + emitFailed(msg); +} +void SequentialTask::subTaskStatus(const QString &msg) +{ + setStatus(msg); +} +void SequentialTask::subTaskProgress() +{ + qint64 current, total; + getProgress(current, total); + setProgress(100 * current / total); +} diff --git a/logic/tasks/SequentialTask.h b/logic/tasks/SequentialTask.h new file mode 100644 index 000000000..7f0469289 --- /dev/null +++ b/logic/tasks/SequentialTask.h @@ -0,0 +1,32 @@ +#pragma once + +#include "Task.h" + +#include +#include + +class SequentialTask : public Task +{ + Q_OBJECT +public: + explicit SequentialTask(QObject *parent = 0); + + virtual QString getStatus() const; + virtual void getProgress(qint64 ¤t, qint64 &total); + + void addTask(std::shared_ptr task); + +protected: + void executeTask(); + +private +slots: + void startNext(); + void subTaskFailed(const QString &msg); + void subTaskStatus(const QString &msg); + void subTaskProgress(); + +private: + QQueue > m_queue; + int m_currentIndex; +}; diff --git a/logic/tasks/ThreadTask.cpp b/logic/tasks/ThreadTask.cpp new file mode 100644 index 000000000..ddd1dee5f --- /dev/null +++ b/logic/tasks/ThreadTask.cpp @@ -0,0 +1,41 @@ +#include "ThreadTask.h" +#include +ThreadTask::ThreadTask(Task * internal, QObject *parent) : Task(parent), m_internal(internal) +{ +} + +void ThreadTask::start() +{ + connect(m_internal, SIGNAL(failed(QString)), SLOT(iternal_failed(QString))); + connect(m_internal, SIGNAL(progress(qint64,qint64)), SLOT(iternal_progress(qint64,qint64))); + connect(m_internal, SIGNAL(started()), SLOT(iternal_started())); + connect(m_internal, SIGNAL(status(QString)), SLOT(iternal_status(QString))); + connect(m_internal, SIGNAL(succeeded()), SLOT(iternal_succeeded())); + m_running = true; + QtConcurrent::run(m_internal, &Task::start); +} + +void ThreadTask::iternal_failed(QString reason) +{ + emitFailed(reason); +} + +void ThreadTask::iternal_progress(qint64 current, qint64 total) +{ + progress(current, total); +} + +void ThreadTask::iternal_started() +{ + emit started(); +} + +void ThreadTask::iternal_status(QString status) +{ + setStatus(status); +} + +void ThreadTask::iternal_succeeded() +{ + emitSucceeded(); +} diff --git a/logic/tasks/ThreadTask.h b/logic/tasks/ThreadTask.h new file mode 100644 index 000000000..718dbc91a --- /dev/null +++ b/logic/tasks/ThreadTask.h @@ -0,0 +1,25 @@ +#pragma once + +#include "Task.h" + +class ThreadTask : public Task +{ + Q_OBJECT +public: + explicit ThreadTask(Task * internal, QObject * parent = nullptr); + +protected: + void executeTask() {}; + +public slots: + virtual void start(); + +private slots: + void iternal_started(); + void iternal_progress(qint64 current, qint64 total); + void iternal_succeeded(); + void iternal_failed(QString reason); + void iternal_status(QString status); +private: + Task * m_internal; +}; \ No newline at end of file diff --git a/logic/updater/DownloadUpdateTask.cpp b/logic/updater/DownloadUpdateTask.cpp index d9aab8264..6e0a92f0f 100644 --- a/logic/updater/DownloadUpdateTask.cpp +++ b/logic/updater/DownloadUpdateTask.cpp @@ -26,9 +26,8 @@ #include - -DownloadUpdateTask::DownloadUpdateTask(QString repoUrl, int versionId, QObject* parent) : - Task(parent) +DownloadUpdateTask::DownloadUpdateTask(QString repoUrl, int versionId, QObject *parent) + : Task(parent) { m_cVersionId = MMC->version().build; @@ -45,68 +44,69 @@ void DownloadUpdateTask::executeTask() findCurrentVersionInfo(); } +void DownloadUpdateTask::processChannels() +{ + auto checker = MMC->updateChecker(); + + // Now, check the channel list again. + if (!checker->hasChannels()) + { + // We still couldn't load the channel list. Give up. Call loadVersionInfo and return. + QLOG_INFO() << "Reloading the channel list didn't work. Giving up."; + loadVersionInfo(); + return; + } + + QList channels = checker->getChannelList(); + QString channelId = MMC->version().channel; + + // Search through the channel list for a channel with the correct ID. + for (auto channel : channels) + { + if (channel.id == channelId) + { + QLOG_INFO() << "Found matching channel."; + m_cRepoUrl = fixPathForTests(channel.url); + break; + } + } + + // Now that we've done that, load version info. + loadVersionInfo(); +} + void DownloadUpdateTask::findCurrentVersionInfo() { - setStatus(tr("Finding information about the current version.")); + setStatus(tr("Finding information about the current version...")); auto checker = MMC->updateChecker(); - // This runs after we've tried loading the channel list. - // If the channel list doesn't need to be loaded, this will be called immediately. - // If the channel list does need to be loaded, this will be called when it's done. - auto processFunc = [this, &checker] () -> void - { - // Now, check the channel list again. - if (checker->hasChannels()) - { - // We still couldn't load the channel list. Give up. Call loadVersionInfo and return. - QLOG_INFO() << "Reloading the channel list didn't work. Giving up."; - loadVersionInfo(); - return; - } - - QList channels = checker->getChannelList(); - QString channelId = MMC->version().channel; - - // Search through the channel list for a channel with the correct ID. - for (auto channel : channels) - { - if (channel.id == channelId) - { - QLOG_INFO() << "Found matching channel."; - m_cRepoUrl = channel.url; - break; - } - } - - // Now that we've done that, load version info. - loadVersionInfo(); - }; - - if (checker->hasChannels()) + if (!checker->hasChannels()) { // Load the channel list and wait for it to finish loading. QLOG_INFO() << "No channel list entries found. Will try reloading it."; - QObject::connect(checker.get(), &UpdateChecker::channelListLoaded, processFunc); + QObject::connect(checker.get(), &UpdateChecker::channelListLoaded, this, + &DownloadUpdateTask::processChannels); checker->updateChanList(); } else { - processFunc(); + processChannels(); } } void DownloadUpdateTask::loadVersionInfo() { - setStatus(tr("Loading version information.")); + setStatus(tr("Loading version information...")); // Create the net job for loading version info. - NetJob* netJob = new NetJob("Version Info"); - + NetJob *netJob = new NetJob("Version Info"); + // Find the index URL. QUrl newIndexUrl = QUrl(m_nRepoUrl).resolved(QString::number(m_nVersionId) + ".json"); - + QLOG_DEBUG() << m_nRepoUrl << " turns into " << newIndexUrl; + // Add a net action to download the version info for the version we're updating to. netJob->addNetAction(ByteArrayDownload::make(newIndexUrl)); @@ -115,10 +115,12 @@ void DownloadUpdateTask::loadVersionInfo() { QUrl cIndexUrl = QUrl(m_cRepoUrl).resolved(QString::number(m_cVersionId) + ".json"); netJob->addNetAction(ByteArrayDownload::make(cIndexUrl)); + QLOG_DEBUG() << m_cRepoUrl << " turns into " << cIndexUrl; } // Connect slots so we know when it's done. - QObject::connect(netJob, &NetJob::succeeded, this, &DownloadUpdateTask::vinfoDownloadFinished); + QObject::connect(netJob, &NetJob::succeeded, this, + &DownloadUpdateTask::vinfoDownloadFinished); QObject::connect(netJob, &NetJob::failed, this, &DownloadUpdateTask::vinfoDownloadFailed); // Store the NetJob in a class member. We don't want to lose it! @@ -136,7 +138,8 @@ void DownloadUpdateTask::vinfoDownloadFinished() void DownloadUpdateTask::vinfoDownloadFailed() { - // Something failed. We really need the second download (current version info), so parse downloads anyways as long as the first one succeeded. + // Something failed. We really need the second download (current version info), so parse + // downloads anyways as long as the first one succeeded. if (m_vinfoNetJob->first()->m_status != Job_Failed) { parseDownloadedVersionInfo(); @@ -150,61 +153,73 @@ void DownloadUpdateTask::vinfoDownloadFailed() void DownloadUpdateTask::parseDownloadedVersionInfo() { - setStatus(tr("Reading file lists.")); + setStatus(tr("Reading file list for new version...")); + QLOG_DEBUG() << "Reading file list for new version..."; + QString error; + if (!parseVersionInfo( + std::dynamic_pointer_cast(m_vinfoNetJob->first())->m_data, + &m_nVersionFileList, &error)) + { + emitFailed(error); + return; + } - parseVersionInfo(NEW_VERSION, &m_nVersionFileList); - - // If there is a second entry in the network job's list, load it as the current version's info. + // If there is a second entry in the network job's list, load it as the current version's + // info. if (m_vinfoNetJob->size() >= 2 && m_vinfoNetJob->operator[](1)->m_status != Job_Failed) { - parseVersionInfo(CURRENT_VERSION, &m_cVersionFileList); + setStatus(tr("Reading file list for current version...")); + QLOG_DEBUG() << "Reading file list for current version..."; + QString error; + parseVersionInfo( + std::dynamic_pointer_cast(m_vinfoNetJob->operator[](1))->m_data, + &m_cVersionFileList, &error); } // We don't need this any more. m_vinfoNetJob.reset(); - // Now that we're done loading version info, we can move on to the next step. Process file lists and download files. + // Now that we're done loading version info, we can move on to the next step. Process file + // lists and download files. processFileLists(); } -void DownloadUpdateTask::parseVersionInfo(VersionInfoFileEnum vfile, VersionFileList* list) +bool DownloadUpdateTask::parseVersionInfo(const QByteArray &data, VersionFileList *list, + QString *error) { - if (vfile == CURRENT_VERSION) setStatus(tr("Reading file list for current version.")); - else if (vfile == NEW_VERSION) setStatus(tr("Reading file list for new version.")); - - QLOG_DEBUG() << "Reading file list for" << (vfile == NEW_VERSION ? "new" : "current") << "version."; - - QByteArray data; - { - ByteArrayDownloadPtr dl = std::dynamic_pointer_cast( - vfile == NEW_VERSION ? m_vinfoNetJob->first() : m_vinfoNetJob->operator[](1)); - data = dl->m_data; - } - QJsonParseError jsonError; QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError); if (jsonError.error != QJsonParseError::NoError) { - QLOG_ERROR() << "Failed to parse version info JSON:" << jsonError.errorString() << "at" << jsonError.offset; - return; + *error = QString("Failed to parse version info JSON: %1 at %2") + .arg(jsonError.errorString()) + .arg(jsonError.offset); + QLOG_ERROR() << error; + return false; } QJsonObject json = jsonDoc.object(); + QLOG_DEBUG() << data; QLOG_DEBUG() << "Loading version info from JSON."; QJsonArray filesArray = json.value("Files").toArray(); for (QJsonValue fileValue : filesArray) { QJsonObject fileObj = fileValue.toObject(); - VersionFileEntry file{ - fileObj.value("Path").toString(), - fileObj.value("Perms").toVariant().toInt(), - FileSourceList(), - fileObj.value("MD5").toString(), - }; + QString file_path = fileObj.value("Path").toString(); +#ifdef Q_OS_MAC + // On OSX, the paths for the updater need to be fixed. + // basically, anything that isn't in the .app folder is ignored. + // everything else is changed so the code that processes the files actually finds + // them and puts the replacements in the right spots. + if (!fixPathForOSX(file_path)) + continue; +#endif + VersionFileEntry file{file_path, fileObj.value("Perms").toVariant().toInt(), + FileSourceList(), fileObj.value("MD5").toString(), }; QLOG_DEBUG() << "File" << file.path << "with perms" << file.mode; - + QJsonArray sourceArray = fileObj.value("Sources").toArray(); for (QJsonValue val : sourceArray) { @@ -213,11 +228,14 @@ void DownloadUpdateTask::parseVersionInfo(VersionInfoFileEnum vfile, VersionFile QString type = sourceObj.value("SourceType").toString(); if (type == "http") { - file.sources.append(FileSource("http", sourceObj.value("Url").toString())); + file.sources.append( + FileSource("http", fixPathForTests(sourceObj.value("Url").toString()))); } else if (type == "httpc") { - file.sources.append(FileSource("httpc", sourceObj.value("Url").toString(), sourceObj.value("CompressionType").toString())); + file.sources.append( + FileSource("httpc", fixPathForTests(sourceObj.value("Url").toString()), + sourceObj.value("CompressionType").toString())); } else { @@ -229,78 +247,26 @@ void DownloadUpdateTask::parseVersionInfo(VersionInfoFileEnum vfile, VersionFile list->append(file); } + + return true; } void DownloadUpdateTask::processFileLists() { - setStatus(tr("Processing file lists. Figuring out how to install the update.")); - - // First, if we've loaded the current version's file list, we need to iterate through it and - // delete anything in the current one version's list that isn't in the new version's list. - for (VersionFileEntry entry : m_cVersionFileList) - { - bool keep = false; - for (VersionFileEntry newEntry : m_nVersionFileList) - { - if (newEntry.path == entry.path) - { - QLOG_DEBUG() << "Not deleting" << entry.path << "because it is still present in the new version."; - keep = true; - break; - } - } - // If the loop reaches the end and we didn't find a match, delete the file. - if(!keep) - m_operationList.append(UpdateOperation::DeleteOp(entry.path)); - } - // Create a network job for downloading files. - NetJob* netJob = new NetJob("Update Files"); + NetJob *netJob = new NetJob("Update Files"); - // Next, check each file in MultiMC's folder and see if we need to update them. - for (VersionFileEntry entry : m_nVersionFileList) + if (!processFileLists(netJob, m_cVersionFileList, m_nVersionFileList, m_operationList)) { - // TODO: Let's not MD5sum a ton of files on the GUI thread. We should probably find a way to do this in the background. - QString fileMD5; - QFile entryFile(entry.path); - if (entryFile.open(QFile::ReadOnly)) - { - QCryptographicHash hash(QCryptographicHash::Md5); - hash.addData(entryFile.readAll()); - fileMD5 = hash.result().toHex(); - } - - if (!entryFile.exists() || fileMD5.isEmpty() || fileMD5 != entry.md5) - { - QLOG_DEBUG() << "Found file" << entry.path << "that needs updating."; - - // Go through the sources list and find one to use. - // TODO: Make a NetAction that takes a source list and tries each of them until one works. For now, we'll just use the first http one. - for (FileSource source : entry.sources) - { - if (source.type == "http") - { - QLOG_DEBUG() << "Will download" << entry.path << "from" << source.url; - - // Download it to updatedir/- where filepath is the file's path with slashes replaced by underscores. - QString dlPath = PathCombine(m_updateFilesDir.path(), QString(entry.path).replace("/", "_")); - - // We need to download the file to the updatefiles folder and add a task to copy it to its install path. - auto download = MD5EtagDownload::make(source.url, dlPath); - download->m_check_md5 = true; - download->m_expected_md5 = entry.md5; - netJob->addNetAction(download); - - // Now add a copy operation to our operations list to install the file. - m_operationList.append(UpdateOperation::CopyOp(dlPath, entry.path, entry.mode)); - } - } - } + emitFailed(tr("Failed to process update lists...")); + return; } // Add listeners to wait for the downloads to finish. - QObject::connect(netJob, &NetJob::succeeded, this, &DownloadUpdateTask::fileDownloadFinished); - QObject::connect(netJob, &NetJob::progress, this, &DownloadUpdateTask::fileDownloadProgressChanged); + QObject::connect(netJob, &NetJob::succeeded, this, + &DownloadUpdateTask::fileDownloadFinished); + QObject::connect(netJob, &NetJob::progress, this, + &DownloadUpdateTask::fileDownloadProgressChanged); QObject::connect(netJob, &NetJob::failed, this, &DownloadUpdateTask::fileDownloadFailed); // Now start the download. @@ -312,7 +278,154 @@ void DownloadUpdateTask::processFileLists() writeInstallScript(m_operationList, PathCombine(m_updateFilesDir.path(), "file_list.xml")); } -void DownloadUpdateTask::writeInstallScript(UpdateOperationList& opsList, QString scriptFile) +bool +DownloadUpdateTask::processFileLists(NetJob *job, + const DownloadUpdateTask::VersionFileList ¤tVersion, + const DownloadUpdateTask::VersionFileList &newVersion, + DownloadUpdateTask::UpdateOperationList &ops) +{ + setStatus(tr("Processing file lists - figuring out how to install the update...")); + + // First, if we've loaded the current version's file list, we need to iterate through it and + // delete anything in the current one version's list that isn't in the new version's list. + for (VersionFileEntry entry : currentVersion) + { + QFileInfo toDelete(entry.path); + if (!toDelete.exists()) + { + QLOG_ERROR() << "Expected file " << toDelete.absoluteFilePath() + << " doesn't exist!"; + QLOG_ERROR() << "CWD: " << QDir::currentPath(); + } + bool keep = false; + + // + for (VersionFileEntry newEntry : newVersion) + { + if (newEntry.path == entry.path) + { + QLOG_DEBUG() << "Not deleting" << entry.path + << "because it is still present in the new version."; + keep = true; + break; + } + } + + // If the loop reaches the end and we didn't find a match, delete the file. + if (!keep) + { + QFileInfo toDelete(entry.path); + if (toDelete.exists()) + ops.append(UpdateOperation::DeleteOp(entry.path)); + } + } + + // Next, check each file in MultiMC's folder and see if we need to update them. + for (VersionFileEntry entry : newVersion) + { + // TODO: Let's not MD5sum a ton of files on the GUI thread. We should probably find a + // way to do this in the background. + QString fileMD5; + QFile entryFile(entry.path); + QFileInfo entryInfo(entry.path); + + bool needs_upgrade = false; + if (!entryFile.exists()) + { + needs_upgrade = true; + } + else + { + bool pass = true; + if (!entryInfo.isReadable()) + { + QLOG_ERROR() << "File " << entry.path << " is not readable."; + pass = false; + } + if (!entryInfo.isWritable()) + { + QLOG_ERROR() << "File " << entry.path << " is not writable."; + pass = false; + } + if (!entryFile.open(QFile::ReadOnly)) + { + QLOG_ERROR() << "File " << entry.path << " cannot be opened for reading."; + pass = false; + } + if (!pass) + { + QLOG_ERROR() << "CWD: " << QDir::currentPath(); + ops.clear(); + return false; + } + } + + QCryptographicHash hash(QCryptographicHash::Md5); + auto foo = entryFile.readAll(); + + hash.addData(foo); + fileMD5 = hash.result().toHex(); + if ((fileMD5 != entry.md5)) + { + QLOG_DEBUG() << "MD5Sum does not match!"; + QLOG_DEBUG() << "Expected:'" << entry.md5 << "'"; + QLOG_DEBUG() << "Got: '" << fileMD5 << "'"; + needs_upgrade = true; + } + + // skip file. it doesn't need an upgrade. + if (!needs_upgrade) + { + QLOG_DEBUG() << "File" << entry.path << " does not need updating."; + continue; + } + + // yep. this file actually needs an upgrade. PROCEED. + QLOG_DEBUG() << "Found file" << entry.path << " that needs updating."; + + // if it's the updater we want to treat it separately + bool isUpdater = entry.path.endsWith("updater") || entry.path.endsWith("updater.exe"); + + // Go through the sources list and find one to use. + // TODO: Make a NetAction that takes a source list and tries each of them until one + // works. For now, we'll just use the first http one. + for (FileSource source : entry.sources) + { + if (source.type == "http") + { + QLOG_DEBUG() << "Will download" << entry.path << "from" << source.url; + + // Download it to updatedir/- where filepath is the file's + // path with slashes replaced by underscores. + QString dlPath = + PathCombine(m_updateFilesDir.path(), QString(entry.path).replace("/", "_")); + + if (isUpdater) + { + auto cache_entry = MMC->metacache()->resolveEntry("root", entry.path); + QLOG_DEBUG() << "Updater will be in " << cache_entry->getFullPath(); + if(cache_entry->stale) + { + auto download = CacheDownload::make(QUrl(source.url), cache_entry); + job->addNetAction(download); + } + } + else + { + // We need to download the file to the updatefiles folder and add a task + // to copy it to its install path. + auto download = MD5EtagDownload::make(source.url, dlPath); + download->m_expected_md5 = entry.md5; + job->addNetAction(download); + ops.append(UpdateOperation::CopyOp(dlPath, entry.path, entry.mode)); + } + } + } + } + return true; +} + +bool DownloadUpdateTask::writeInstallScript(UpdateOperationList &opsList, QString scriptFile) { // Build the base structure of the XML document. QDomDocument doc; @@ -334,36 +447,38 @@ void DownloadUpdateTask::writeInstallScript(UpdateOperationList& opsList, QStrin switch (op.type) { - case UpdateOperation::OP_COPY: - { - // Install the file. - QDomElement name = doc.createElement("source"); - QDomElement path = doc.createElement("dest"); - QDomElement mode = doc.createElement("mode"); - name.appendChild(doc.createTextNode(op.file)); - path.appendChild(doc.createTextNode(op.dest)); - // We need to add a 0 at the beginning here, because Qt doesn't convert to octal correctly. - mode.appendChild(doc.createTextNode("0" + QString::number(op.mode, 8))); - file.appendChild(name); - file.appendChild(path); - file.appendChild(mode); - installFiles.appendChild(file); - QLOG_DEBUG() << "Will install file" << op.file; - } - break; + case UpdateOperation::OP_COPY: + { + // Install the file. + QDomElement name = doc.createElement("source"); + QDomElement path = doc.createElement("dest"); + QDomElement mode = doc.createElement("mode"); + name.appendChild(doc.createTextNode(op.file)); + path.appendChild(doc.createTextNode(op.dest)); + // We need to add a 0 at the beginning here, because Qt doesn't convert to octal + // correctly. + mode.appendChild(doc.createTextNode("0" + QString::number(op.mode, 8))); + file.appendChild(name); + file.appendChild(path); + file.appendChild(mode); + installFiles.appendChild(file); + QLOG_DEBUG() << "Will install file " << op.file << " to " << op.dest; + } + break; - case UpdateOperation::OP_DELETE: - { - // Delete the file. - file.appendChild(doc.createTextNode(op.file)); - removeFiles.appendChild(file); - QLOG_DEBUG() << "Will remove file" << op.file; - } - break; + case UpdateOperation::OP_DELETE: + { + // Delete the file. + file.appendChild(doc.createTextNode(op.file)); + removeFiles.appendChild(file); + QLOG_DEBUG() << "Will remove file" << op.file; + } + break; - default: - QLOG_WARN() << "Can't write update operation of type" << op.type << "to file. Not implemented."; - continue; + default: + QLOG_WARN() << "Can't write update operation of type" << op.type + << "to file. Not implemented."; + continue; } } @@ -377,6 +492,36 @@ void DownloadUpdateTask::writeInstallScript(UpdateOperationList& opsList, QStrin else { emitFailed(tr("Failed to write update script file.")); + return false; + } + + return true; +} + +QString DownloadUpdateTask::fixPathForTests(const QString &path) +{ + if (path.startsWith("$PWD")) + { + QString foo = path; + foo.replace("$PWD", qApp->applicationDirPath()); + return QUrl::fromLocalFile(foo).toString(QUrl::FullyEncoded); + } + return path; +} + +bool DownloadUpdateTask::fixPathForOSX(QString &path) +{ + if (path.startsWith("MultiMC.app/")) + { + // remove the prefix and add a new, more appropriate one. + path.remove(0, 12); + path = QString("../../") + path; + return true; + } + else + { + QLOG_ERROR() << "Update path not within .app: " << path; + return false; } } @@ -394,11 +539,10 @@ void DownloadUpdateTask::fileDownloadFailed() void DownloadUpdateTask::fileDownloadProgressChanged(qint64 current, qint64 total) { - setProgress((int)(((float)current / (float)total)*100)); + setProgress((int)(((float)current / (float)total) * 100)); } QString DownloadUpdateTask::updateFilesDir() { return m_updateFilesDir.path(); } - diff --git a/logic/updater/DownloadUpdateTask.h b/logic/updater/DownloadUpdateTask.h index f5b23d126..b1d14846f 100644 --- a/logic/updater/DownloadUpdateTask.h +++ b/logic/updater/DownloadUpdateTask.h @@ -34,7 +34,8 @@ public: */ QString updateFilesDir(); -protected: +public: + // TODO: We should probably put these data structures into a separate header... /*! @@ -53,7 +54,6 @@ protected: QString url; QString compressionType; }; - typedef QList FileSourceList; /*! @@ -66,10 +66,8 @@ protected: FileSourceList sources; QString md5; }; - typedef QList VersionFileList; - /*! * Structure that describes an operation to perform when installing updates. */ @@ -100,9 +98,12 @@ protected: // Yeah yeah, polymorphism blah blah inheritance, blah blah object oriented. I'm lazy, OK? }; - typedef QList UpdateOperationList; +protected: + friend class DownloadUpdateTaskTest; + + /*! * Used for arguments to parseVersionInfo and friends to specify which version info file to parse. */ @@ -119,6 +120,13 @@ protected: */ virtual void findCurrentVersionInfo(); + /*! + * This runs after we've tried loading the channel list. + * If the channel list doesn't need to be loaded, this will be called immediately. + * If the channel list does need to be loaded, this will be called when it's done. + */ + void processChannels(); + /*! * Downloads the version info files from the repository. * The files for both the current build, and the build that we're updating to need to be downloaded. @@ -142,20 +150,25 @@ protected: /*! * Loads the file list from the given version info JSON object into the given list. */ - virtual void parseVersionInfo(VersionInfoFileEnum vfile, VersionFileList* list); + virtual bool parseVersionInfo(const QByteArray &data, VersionFileList* list, QString *error); /*! * Takes a list of file entries for the current version's files and the new version's files * and populates the downloadList and operationList with information about how to download and install the update. */ + virtual bool processFileLists(NetJob *job, const VersionFileList ¤tVersion, const VersionFileList &newVersion, UpdateOperationList &ops); + + /*! + * Calls \see processFileLists to populate the \see m_operationList and a NetJob, and then executes + * the NetJob to fetch all needed files + */ virtual void processFileLists(); /*! * Takes the operations list and writes an install script for the updater to the update files directory. */ - virtual void writeInstallScript(UpdateOperationList& opsList, QString scriptFile); + virtual bool writeInstallScript(UpdateOperationList& opsList, QString scriptFile); - VersionFileList m_downloadList; UpdateOperationList m_operationList; VersionFileList m_nVersionFileList; @@ -181,6 +194,26 @@ protected: */ QTemporaryDir m_updateFilesDir; + /*! + * Filters paths + * Path of the format $PWD/path, it is converted to a file:///$PWD/ URL + */ + static QString fixPathForTests(const QString &path); + + /*! + * Filters paths + * This fixes destination paths for OSX. + * The updater runs in MultiMC.app/Contents/MacOs by default + * The destination paths are such as this: MultiMC.app/blah/blah + * + * Therefore we chop off the 'MultiMC.app' prefix and prepend ../.. + * + * Returns false if the path couldn't be fixed (is invalid) + * + * Has no effect on systems that aren't OSX + */ + static bool fixPathForOSX(QString &path); + protected slots: void vinfoDownloadFinished(); void vinfoDownloadFailed(); diff --git a/logic/updater/UpdateChecker.cpp b/logic/updater/UpdateChecker.cpp index 5ff1898e6..d0795c0dd 100644 --- a/logic/updater/UpdateChecker.cpp +++ b/logic/updater/UpdateChecker.cpp @@ -44,17 +44,19 @@ QList UpdateChecker::getChannelList() const bool UpdateChecker::hasChannels() const { - return m_channels.isEmpty(); + return !m_channels.isEmpty(); } -void UpdateChecker::checkForUpdate() +void UpdateChecker::checkForUpdate(bool notifyNoUpdate) { QLOG_DEBUG() << "Checking for updates."; - // If the channel list hasn't loaded yet, load it and defer checking for updates until later. + // If the channel list hasn't loaded yet, load it and defer checking for updates until + // later. if (!m_chanListLoaded) { - QLOG_DEBUG() << "Channel list isn't loaded yet. Loading channel list and deferring update check."; + QLOG_DEBUG() << "Channel list isn't loaded yet. Loading channel list and deferring " + "update check."; m_checkUpdateWaiting = true; updateChanList(); return; @@ -72,7 +74,8 @@ void UpdateChecker::checkForUpdate() // TODO: Allow user to select channels. For now, we'll just use the current channel. QString updateChannel = m_currentChannel; - // Find the desired channel within the channel list and get its repo URL. If if cannot be found, error. + // Find the desired channel within the channel list and get its repo URL. If if cannot be + // found, error. m_repoUrl = ""; for (ChannelListEntry entry : m_channels) { @@ -91,20 +94,22 @@ void UpdateChecker::checkForUpdate() auto job = new NetJob("GoUpdate Repository Index"); job->addNetAction(ByteArrayDownload::make(indexUrl)); - connect(job, SIGNAL(succeeded()), SLOT(updateCheckFinished())); + connect(job, &NetJob::succeeded, [this, notifyNoUpdate]() + { updateCheckFinished(notifyNoUpdate); }); connect(job, SIGNAL(failed()), SLOT(updateCheckFailed())); indexJob.reset(job); job->start(); } -void UpdateChecker::updateCheckFinished() +void UpdateChecker::updateCheckFinished(bool notifyNoUpdate) { QLOG_DEBUG() << "Finished downloading repo index. Checking for new versions."; QJsonParseError jsonError; QByteArray data; { - ByteArrayDownloadPtr dl = std::dynamic_pointer_cast(indexJob->first()); + ByteArrayDownloadPtr dl = + std::dynamic_pointer_cast(indexJob->first()); data = dl->m_data; indexJob.reset(); } @@ -112,7 +117,8 @@ void UpdateChecker::updateCheckFinished() QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError); if (jsonError.error != QJsonParseError::NoError || !jsonDoc.isObject()) { - QLOG_ERROR() << "Failed to parse GoUpdate repository index. JSON error" << jsonError.errorString() << "at offset" << jsonError.offset; + QLOG_ERROR() << "Failed to parse GoUpdate repository index. JSON error" + << jsonError.errorString() << "at offset" << jsonError.offset; return; } @@ -122,7 +128,8 @@ void UpdateChecker::updateCheckFinished() int apiVersion = object.value("ApiVersion").toVariant().toInt(&success); if (apiVersion != API_VERSION || !success) { - QLOG_ERROR() << "Failed to check for updates. API version mismatch. We're using" << API_VERSION << "server has" << apiVersion; + QLOG_ERROR() << "Failed to check for updates. API version mismatch. We're using" + << API_VERSION << "server has" << apiVersion; return; } @@ -132,19 +139,27 @@ void UpdateChecker::updateCheckFinished() for (QJsonValue versionVal : versions) { QJsonObject version = versionVal.toObject(); - if (newestVersion.value("Id").toVariant().toInt() < version.value("Id").toVariant().toInt()) + if (newestVersion.value("Id").toVariant().toInt() < + version.value("Id").toVariant().toInt()) { - QLOG_DEBUG() << "Found newer version with ID" << version.value("Id").toVariant().toInt(); + QLOG_DEBUG() << "Found newer version with ID" + << version.value("Id").toVariant().toInt(); newestVersion = version; } } - // We've got the version with the greatest ID number. Now compare it to our current build number and update if they're different. + // We've got the version with the greatest ID number. Now compare it to our current build + // number and update if they're different. int newBuildNumber = newestVersion.value("Id").toVariant().toInt(); if (newBuildNumber != MMC->version().build) { // Update! - emit updateAvailable(m_repoUrl, newestVersion.value("Name").toVariant().toString(), newBuildNumber); + emit updateAvailable(m_repoUrl, newestVersion.value("Name").toVariant().toString(), + newBuildNumber); + } + else if (notifyNoUpdate) + { + emit noUpdateFound(); } m_updateChecking = false; @@ -163,12 +178,13 @@ void UpdateChecker::updateChanList() if (m_channelListUrl.isEmpty()) { QLOG_ERROR() << "Failed to update channel list. No channel list URL set." - << "If you'd like to use MultiMC's update system, please pass the channel list URL to CMake at compile time."; + << "If you'd like to use MultiMC's update system, please pass the channel " + "list URL to CMake at compile time."; return; } m_chanListLoading = true; - NetJob* job = new NetJob("Update System Channel List"); + NetJob *job = new NetJob("Update System Channel List"); job->addNetAction(ByteArrayDownload::make(QUrl(m_channelListUrl))); QObject::connect(job, &NetJob::succeeded, this, &UpdateChecker::chanListDownloadFinished); QObject::connect(job, &NetJob::failed, this, &UpdateChecker::chanListDownloadFailed); @@ -180,7 +196,8 @@ void UpdateChecker::chanListDownloadFinished() { QByteArray data; { - ByteArrayDownloadPtr dl = std::dynamic_pointer_cast(chanListJob->first()); + ByteArrayDownloadPtr dl = + std::dynamic_pointer_cast(chanListJob->first()); data = dl->m_data; chanListJob.reset(); } @@ -190,17 +207,20 @@ void UpdateChecker::chanListDownloadFinished() if (jsonError.error != QJsonParseError::NoError) { // TODO: Report errors to the user. - QLOG_ERROR() << "Failed to parse channel list JSON:" << jsonError.errorString() << "at" << jsonError.offset; + QLOG_ERROR() << "Failed to parse channel list JSON:" << jsonError.errorString() << "at" + << jsonError.offset; return; } - + QJsonObject object = jsonDoc.object(); bool success = false; int formatVersion = object.value("format_version").toVariant().toInt(&success); if (formatVersion != CHANLIST_FORMAT || !success) { - QLOG_ERROR() << "Failed to check for updates. Channel list format version mismatch. We're using" << CHANLIST_FORMAT << "server has" << formatVersion; + QLOG_ERROR() + << "Failed to check for updates. Channel list format version mismatch. We're using" + << CHANLIST_FORMAT << "server has" << formatVersion; return; } @@ -210,12 +230,10 @@ void UpdateChecker::chanListDownloadFinished() for (QJsonValue chanVal : channelArray) { QJsonObject channelObj = chanVal.toObject(); - ChannelListEntry entry{ - channelObj.value("id").toVariant().toString(), - channelObj.value("name").toVariant().toString(), - channelObj.value("description").toVariant().toString(), - channelObj.value("url").toVariant().toString() - }; + ChannelListEntry entry{channelObj.value("id").toVariant().toString(), + channelObj.value("name").toVariant().toString(), + channelObj.value("description").toVariant().toString(), + channelObj.value("url").toVariant().toString()}; if (entry.id.isEmpty() || entry.name.isEmpty() || entry.url.isEmpty()) { QLOG_ERROR() << "Channel list entry with empty ID, name, or URL. Skipping."; @@ -233,7 +251,7 @@ void UpdateChecker::chanListDownloadFinished() // If we're waiting to check for updates, do that now. if (m_checkUpdateWaiting) - checkForUpdate(); + checkForUpdate(false); emit channelListLoaded(); } @@ -244,4 +262,3 @@ void UpdateChecker::chanListDownloadFailed() QLOG_ERROR() << "Failed to download channel list."; emit channelListLoaded(); } - diff --git a/logic/updater/UpdateChecker.h b/logic/updater/UpdateChecker.h index 59fb8e47c..a47e89030 100644 --- a/logic/updater/UpdateChecker.h +++ b/logic/updater/UpdateChecker.h @@ -25,7 +25,10 @@ class UpdateChecker : public QObject public: UpdateChecker(); - void checkForUpdate(); + void checkForUpdate(bool notifyNoUpdate); + + void setCurrentChannel(const QString &channel) { m_currentChannel = channel; } + void setChannelListUrl(const QString &url) { m_channelListUrl = url; } /*! * Causes the update checker to download the channel list from the URL specified in config.h (generated by CMake). @@ -62,14 +65,18 @@ signals: //! Signal emitted when the channel list finishes loading or fails to load. void channelListLoaded(); + void noUpdateFound(); + private slots: - void updateCheckFinished(); + void updateCheckFinished(bool notifyNoUpdate); void updateCheckFailed(); void chanListDownloadFinished(); void chanListDownloadFailed(); private: + friend class UpdateCheckerTest; + NetJobPtr indexJob; NetJobPtr chanListJob; diff --git a/main.cpp b/main.cpp index fb75765a5..89eef72f8 100644 --- a/main.cpp +++ b/main.cpp @@ -8,6 +8,7 @@ int main_gui(MultiMC &app) mainWin.restoreState(QByteArray::fromBase64(MMC->settings()->get("MainWindowState").toByteArray())); mainWin.restoreGeometry(QByteArray::fromBase64(MMC->settings()->get("MainWindowGeometry").toByteArray())); mainWin.show(); + mainWin.checkMigrateLegacyAssets(); mainWin.checkSetDefaultJava(); auto exitCode = app.exec(); diff --git a/mmc_updater/CMakeLists.txt b/mmc_updater/CMakeLists.txt index 61c8cd098..971ac153f 100644 --- a/mmc_updater/CMakeLists.txt +++ b/mmc_updater/CMakeLists.txt @@ -9,6 +9,14 @@ include_directories(depends) if (WIN32) include_directories(depends/win32cpp) + # static all the things. The updater must have no dependencies, or it will fail. + if (MINGW) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -static-libgcc -static") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -static-libgcc -static-libstdc++ -static") +#set(CMAKE_SHARED_LIBRARY_LINK_C_FLAGS "${CMAKE_SHARED_LIBRARY_LINK_C_FLAGS} -static-libgcc -s") +#set(CMAKE_SHARED_LIBRARY_LINK_CXX_FLAGS "${CMAKE_SHARED_LIBRARY_LINK_CXX_FLAGS} -static-libgcc -static-libstdc++ -s") + endif() + if(MSVC) # - Link the updater binary statically with the Visual C++ runtime # so that the executable can function standalone. diff --git a/mmc_updater/src/FileUtils.cpp b/mmc_updater/src/FileUtils.cpp index 10435e49f..712c0c5da 100644 --- a/mmc_updater/src/FileUtils.cpp +++ b/mmc_updater/src/FileUtils.cpp @@ -10,6 +10,8 @@ #include #include #include +// this actually works with mingw32, which we use. +#include #ifdef PLATFORM_UNIX #include @@ -19,7 +21,6 @@ #include #include #include -#include #endif FileUtils::IOException::IOException(const std::string& error) @@ -249,59 +250,18 @@ void FileUtils::removeFile(const char* src) throw (IOException) std::string FileUtils::fileName(const char* path) { -#ifdef PLATFORM_UNIX char* pathCopy = strdup(path); std::string basename = ::basename(pathCopy); free(pathCopy); return basename; -#else - char baseName[MAX_PATH]; - char extension[MAX_PATH]; - _splitpath_s(path, - 0, /* drive */ - 0, /* drive length */ - 0, /* dir */ - 0, /* dir length */ - baseName, - MAX_PATH, /* baseName length */ - extension, - MAX_PATH /* extension length */ - ); - return std::string(baseName) + std::string(extension); -#endif } std::string FileUtils::dirname(const char* path) { -#ifdef PLATFORM_UNIX char* pathCopy = strdup(path); std::string dirname = ::dirname(pathCopy); free(pathCopy); return dirname; -#else - char drive[3]; - char dir[MAX_PATH]; - - _splitpath_s(path, - drive, /* drive */ - 3, /* drive length */ - dir, - MAX_PATH, /* dir length */ - 0, /* filename */ - 0, /* filename length */ - 0, /* extension */ - 0 /* extension length */ - ); - - std::string result; - if (drive[0]) - { - result += std::string(drive); - } - result += dir; - - return result; -#endif } void FileUtils::touch(const char* path) throw (IOException) diff --git a/mmc_updater/src/Platform.h b/mmc_updater/src/Platform.h index 6d9afdfbe..97867d6ac 100644 --- a/mmc_updater/src/Platform.h +++ b/mmc_updater/src/Platform.h @@ -13,7 +13,9 @@ // disable warnings about exception specifications, // which are not implemented in Visual C++ - #pragma warning(disable:4290) + #ifdef MSVC + #pragma warning(disable:4290) + #endif #endif #ifdef __APPLE__ diff --git a/mmc_updater/src/UpdateDialogWin32.cpp b/mmc_updater/src/UpdateDialogWin32.cpp index bdc25437e..8b38bed2b 100644 --- a/mmc_updater/src/UpdateDialogWin32.cpp +++ b/mmc_updater/src/UpdateDialogWin32.cpp @@ -85,8 +85,8 @@ UpdateDialogWin32::~UpdateDialogWin32() void UpdateDialogWin32::init(int /* argc */, char** /* argv */) { - int width = 300; - int height = 130; + int width = 400; + int height = 150; DWORD style = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX; m_window.CreateEx(0 /* dwExStyle */, diff --git a/mmc_updater/src/UpdateScript.h b/mmc_updater/src/UpdateScript.h index c825e35df..5c863ff41 100644 --- a/mmc_updater/src/UpdateScript.h +++ b/mmc_updater/src/UpdateScript.h @@ -56,9 +56,7 @@ class UpdateScriptFile } }; -/** Stores information about the packages and files included - * in an update, parsed from an XML file. - */ +/** Stores information about the files included in an update, parsed from an XML file. */ class UpdateScript { public: diff --git a/mmc_updater/src/UpdaterOptions.cpp b/mmc_updater/src/UpdaterOptions.cpp index ae34562da..0945431b8 100644 --- a/mmc_updater/src/UpdaterOptions.cpp +++ b/mmc_updater/src/UpdaterOptions.cpp @@ -142,7 +142,7 @@ void UpdaterOptions::parse(int argc, char** argv) showVersion = parser.getFlag("version"); forceElevated = parser.getFlag("force-elevated"); autoClose = parser.getFlag("auto-close"); - + if (installDir.empty()) { // if no --install-dir argument is present, try parsing @@ -152,3 +152,4 @@ void UpdaterOptions::parse(int argc, char** argv) } } + diff --git a/mmc_updater/src/resources/updater.manifest b/mmc_updater/src/resources/updater.manifest new file mode 100644 index 000000000..cafc47d3d --- /dev/null +++ b/mmc_updater/src/resources/updater.manifest @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + Software updater for MultiMC. + + + + + + + + + + + \ No newline at end of file diff --git a/mmc_updater/src/resources/updater.rc b/mmc_updater/src/resources/updater.rc index 550970a8f..9c7c57112 100644 --- a/mmc_updater/src/resources/updater.rc +++ b/mmc_updater/src/resources/updater.rc @@ -1,30 +1,30 @@ +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include + IDI_APPICON ICON DISCARDABLE "updater.ico" -1 VERSIONINFO -FILEVERSION 0,0,1,0 -PRODUCTVERSION 1,0,1,0 -FILEFLAGSMASK 0X3FL -FILEFLAGS 0X8L -FILEOS 0X40004L -FILETYPE 0X1 -FILESUBTYPE 0 +1 RT_MANIFEST "updater.manifest" + +VS_VERSION_INFO VERSIONINFO +FILEVERSION 1,0,0,0 +FILEOS VOS_NT_WINDOWS32 +FILETYPE VFT_APP BEGIN BLOCK "StringFileInfo" BEGIN BLOCK "000004b0" BEGIN - VALUE "FileVersion", "0.0.1.0" - VALUE "ProductVersion", "1.0.1.0" - VALUE "OriginalFilename", "updater.exe" - VALUE "InternalName", "updater.exe" - VALUE "FileDescription", "Software Update Tool" VALUE "CompanyName", "MultiMC Contributors" + VALUE "FileDescription", "Software Update Tool" + VALUE "FileVersion", "1.0.0.0" VALUE "ProductName", "MultiMC Software Updater" - VALUE "PrivateBuild", "Built by BuildBot" + VALUE "ProductVersion", "1.0" END END BLOCK "VarFileInfo" BEGIN - VALUE "Translation", 0x0000, 0x04b0 + VALUE "Translation", 0x0000, 0x04b0 // Unicode END END \ No newline at end of file diff --git a/mmc_updater/src/tests/CMakeLists.txt b/mmc_updater/src/tests/CMakeLists.txt index 5de9d0961..794022450 100644 --- a/mmc_updater/src/tests/CMakeLists.txt +++ b/mmc_updater/src/tests/CMakeLists.txt @@ -5,21 +5,19 @@ if (APPLE) set(HELPER_SHARED_SOURCES ../StlSymbolsLeopard.cpp) endif() -# Create helper binaries for unit tests -add_executable(oldapp - old_app.cpp - ${HELPER_SHARED_SOURCES} -) -add_executable(newapp - new_app.cpp - ${HELPER_SHARED_SOURCES} -) +# # Create helper binaries for unit tests +# add_executable(oldapp +# old_app.cpp +# ${HELPER_SHARED_SOURCES} +# ) +# add_executable(newapp +# new_app.cpp +# ${HELPER_SHARED_SOURCES} +# ) # Install data files required by unit tests set(TEST_FILES file_list.xml - v2_file_list.xml - test-update.rb ) foreach(TEST_FILE ${TEST_FILES}) @@ -31,21 +29,20 @@ endforeach() # Add unit test binaries macro(ADD_UPDATER_TEST CLASS) - set(TEST_TARGET updater_${CLASS}) - add_executable(${TEST_TARGET} ${CLASS}.cpp) - target_link_libraries(${TEST_TARGET} updatershared) - add_test(NAME ${TEST_TARGET} COMMAND ${TEST_TARGET}) - if (APPLE) - set_target_properties(${TEST_TARGET} PROPERTIES LINK_FLAGS "-framework Security -framework Cocoa") - endif() + set(TEST_TARGET updater_${CLASS}) + unset(srcs) + list(APPEND srcs ${CLASS}.cpp) + if (WIN32) + list(APPEND srcs ${CMAKE_CURRENT_SOURCE_DIR}/test.rc) + endif() + add_executable(${TEST_TARGET} ${srcs}) + target_link_libraries(${TEST_TARGET} updatershared) + add_test(NAME ${TEST_TARGET} COMMAND ${TEST_TARGET}) + if (APPLE) + set_target_properties(${TEST_TARGET} PROPERTIES LINK_FLAGS "-framework Security -framework Cocoa") + endif() endmacro() -add_updater_test(TestUpdateScript) +add_updater_test(TestParseScript) add_updater_test(TestUpdaterOptions) add_updater_test(TestFileUtils) - -# Add updater that that performs a complete update install -# and checks the result -find_program(RUBY_BIN ruby) -add_test(updater_TestUpdateInstall ${RUBY_BIN} test-update.rb) - diff --git a/mmc_updater/src/tests/TestFileUtils.cpp b/mmc_updater/src/tests/TestFileUtils.cpp index 709acc5c3..f8535a287 100644 --- a/mmc_updater/src/tests/TestFileUtils.cpp +++ b/mmc_updater/src/tests/TestFileUtils.cpp @@ -5,10 +5,39 @@ void TestFileUtils::testDirName() { + std::string dirName; + std::string fileName; + #ifdef PLATFORM_WINDOWS - std::string dirName = FileUtils::dirname("E:/Some Dir/App.exe"); - TEST_COMPARE(dirName,"E:/Some Dir/"); + // absolute paths + dirName = FileUtils::dirname("E:/Some Dir/App.exe"); + TEST_COMPARE(dirName,"E:/Some Dir"); + fileName = FileUtils::fileName("E:/Some Dir/App.exe"); + TEST_COMPARE(fileName,"App.exe"); + + dirName = FileUtils::dirname("C:/Users/kitteh/AppData/Local/Temp/MultiMC5-yidaaa/MultiMC.exe"); + TEST_COMPARE(dirName,"C:/Users/kitteh/AppData/Local/Temp/MultiMC5-yidaaa"); + fileName = FileUtils::fileName("C:/Users/kitteh/AppData/Local/Temp/MultiMC5-yidaaa/MultiMC.exe"); + TEST_COMPARE(fileName,"MultiMC.exe"); + +#else + // absolute paths + dirName = FileUtils::dirname("/home/tester/foo bar/baz"); + TEST_COMPARE(dirName,"/home/tester/foo bar"); + fileName = FileUtils::fileName("/home/tester/foo bar/baz"); + TEST_COMPARE(fileName,"baz"); #endif + // current directory + dirName = FileUtils::dirname("App.exe"); + TEST_COMPARE(dirName,"."); + fileName = FileUtils::fileName("App.exe"); + TEST_COMPARE(fileName,"App.exe"); + + // relative paths + dirName = FileUtils::dirname("Foo/App.exe"); + TEST_COMPARE(dirName,"Foo"); + fileName = FileUtils::fileName("Foo/App.exe"); + TEST_COMPARE(fileName,"App.exe"); } void TestFileUtils::testIsRelative() diff --git a/mmc_updater/src/tests/TestParseScript.cpp b/mmc_updater/src/tests/TestParseScript.cpp new file mode 100644 index 000000000..f44539578 --- /dev/null +++ b/mmc_updater/src/tests/TestParseScript.cpp @@ -0,0 +1,24 @@ +#include "TestParseScript.h" + +#include "TestUtils.h" +#include "UpdateScript.h" + +#include +#include + +void TestParseScript::testParse() +{ + UpdateScript script; + + script.parse("file_list.xml"); + + TEST_COMPARE(script.isValid(),true); +} + +int main(int,char**) +{ + TestList tests; + tests.addTest(&TestParseScript::testParse); + return TestUtils::runTest(tests); +} + diff --git a/mmc_updater/src/tests/TestParseScript.h b/mmc_updater/src/tests/TestParseScript.h new file mode 100644 index 000000000..528e97a8c --- /dev/null +++ b/mmc_updater/src/tests/TestParseScript.h @@ -0,0 +1,8 @@ +#pragma once + +class TestParseScript +{ + public: + void testParse(); +}; + diff --git a/mmc_updater/src/tests/TestUpdateScript.cpp b/mmc_updater/src/tests/TestUpdateScript.cpp deleted file mode 100644 index 30a7572ac..000000000 --- a/mmc_updater/src/tests/TestUpdateScript.cpp +++ /dev/null @@ -1,27 +0,0 @@ -#include "TestUpdateScript.h" - -#include "TestUtils.h" -#include "UpdateScript.h" - -#include -#include - -void TestUpdateScript::testV2Script() -{ - UpdateScript newFormat; - UpdateScript oldFormat; - - newFormat.parse("file_list.xml"); - oldFormat.parse("v2_file_list.xml"); - - TEST_COMPARE(newFormat.filesToInstall(),oldFormat.filesToInstall()); - TEST_COMPARE(newFormat.filesToUninstall(),oldFormat.filesToUninstall()); -} - -int main(int,char**) -{ - TestList tests; - tests.addTest(&TestUpdateScript::testV2Script); - return TestUtils::runTest(tests); -} - diff --git a/mmc_updater/src/tests/TestUpdateScript.h b/mmc_updater/src/tests/TestUpdateScript.h deleted file mode 100644 index 513513d51..000000000 --- a/mmc_updater/src/tests/TestUpdateScript.h +++ /dev/null @@ -1,8 +0,0 @@ -#pragma once - -class TestUpdateScript -{ - public: - void testV2Script(); -}; - diff --git a/mmc_updater/src/tests/file_list.xml b/mmc_updater/src/tests/file_list.xml index dff4b54f9..06ba501d2 100644 --- a/mmc_updater/src/tests/file_list.xml +++ b/mmc_updater/src/tests/file_list.xml @@ -1,20 +1,5 @@ - 2.0 - Test - - - - - - app-pkg - $APP_PACKAGE_HASH - $APP_PACKAGE_SIZE - http://some/dummy/URL - - $APP_FILENAME diff --git a/mmc_updater/src/tests/test-update.rb b/mmc_updater/src/tests/test-update.rb deleted file mode 100755 index 82965cf46..000000000 --- a/mmc_updater/src/tests/test-update.rb +++ /dev/null @@ -1,218 +0,0 @@ -#!/usr/bin/ruby - -require 'fileutils.rb' -require 'find' -require 'rbconfig' -require 'optparse' - -# Install directory - this contains a space to check -# for correct escaping of paths when passing comamnd -# line arguments under Windows -INSTALL_DIR = File.expand_path("install dir/") -PACKAGE_DIR = File.expand_path("package-dir/") -PACKAGE_SRC_DIR = File.expand_path("package-src-dir/") -IS_WINDOWS = RbConfig::CONFIG['host_os'] =~ /mswin|mingw/ - -if IS_WINDOWS - OLDAPP_NAME = "oldapp.exe" - NEWAPP_NAME = "newapp.exe" - APP_NAME = "app.exe" - UPDATER_NAME = "updater.exe" - ZIP_TOOL = File.expand_path("../zip-tool.exe") -else - OLDAPP_NAME = "oldapp" - NEWAPP_NAME = "newapp" - APP_NAME = "app" - UPDATER_NAME = "updater" - ZIP_TOOL = File.expand_path("../zip-tool") -end - -file_list_vars = { - "APP_FILENAME" => APP_NAME, - "UPDATER_FILENAME" => UPDATER_NAME -} - -def replace_vars(src_file,dest_file,vars) - content = File.read(src_file) - vars.each do |key,value| - content.gsub! "$#{key}",value - end - File.open(dest_file,'w') do |file| - file.print content - end -end - -# Returns true if |src_file| and |dest_file| have the same contents, type -# and permissions or false otherwise -def compare_files(src_file, dest_file) - if File.ftype(src_file) != File.ftype(dest_file) - $stderr.puts "Type of file #{src_file} and #{dest_file} differ" - return false - end - - if File.file?(src_file) && !FileUtils.identical?(src_file, dest_file) - $stderr.puts "Contents of file #{src_file} and #{dest_file} differ" - return false - end - - src_stat = File.stat(src_file) - dest_stat = File.stat(dest_file) - - if src_stat.mode != dest_stat.mode - $stderr.puts "Permissions of #{src_file} and #{dest_file} differ" - return false - end - - return true -end - -# Compares the contents of two directories and returns a map of (file path => change type) -# for files and directories which differ between the two -def compare_dirs(src_dir, dest_dir) - src_dir += '/' if !src_dir.end_with?('/') - dest_dir += '/' if !dest_dir.end_with?('/') - - src_file_map = {} - Find.find(src_dir) do |src_file| - src_file = src_file[src_dir.length..-1] - src_file_map[src_file] = nil - end - - change_map = {} - Find.find(dest_dir) do |dest_file| - dest_file = dest_file[dest_dir.length..-1] - - if !src_file_map.include?(dest_file) - change_map[dest_file] = :deleted - elsif !compare_files("#{src_dir}/#{dest_file}", "#{dest_dir}/#{dest_file}") - change_map[dest_file] = :updated - end - - src_file_map.delete(dest_file) - end - - src_file_map.each do |file| - change_map[file] = :added - end - - return change_map -end - -def create_test_file(name, content) - File.open(name, 'w') do |file| - file.puts content - end - return name -end - -force_elevation = false -run_in_debugger = false - -OptionParser.new do |parser| - parser.on("-f","--force-elevated","Force the updater to elevate itself") do - force_elevation = true - end - parser.on("-d","--debug","Run the updater under GDB") do - run_in_debugger = true - end -end.parse! - -# copy 'src' to 'dest', preserving the attributes -# of 'src' -def copy_file(src, dest) - FileUtils.cp src, dest, :preserve => true -end - -# Remove the install and package dirs if they -# already exist -FileUtils.rm_rf(INSTALL_DIR) -FileUtils.rm_rf(PACKAGE_DIR) -FileUtils.rm_rf(PACKAGE_SRC_DIR) - -# Create the install directory with the old app -Dir.mkdir(INSTALL_DIR) -copy_file OLDAPP_NAME, "#{INSTALL_DIR}/#{APP_NAME}" - -# Create a dummy file to uninstall -uninstall_test_file = create_test_file("#{INSTALL_DIR}/file-to-uninstall.txt", "this file should be removed after the update") -uninstall_test_symlink = if not IS_WINDOWS - FileUtils.ln_s("#{INSTALL_DIR}/file-to-uninstall.txt", "#{INSTALL_DIR}/symlink-to-file-to-uninstall.txt") -else - create_test_file("#{INSTALL_DIR}/symlink-to-file-to-uninstall.txt", "dummy file. this is a symlink on Unix") -end - -# Populate package source dir with files to install -Dir.mkdir(PACKAGE_SRC_DIR) -nested_dir_path = "#{PACKAGE_SRC_DIR}/new-dir/new-dir2" -FileUtils.mkdir_p(nested_dir_path) -FileUtils::chmod 0755, "#{PACKAGE_SRC_DIR}/new-dir" -FileUtils::chmod 0755, "#{PACKAGE_SRC_DIR}/new-dir/new-dir2" -nested_dir_test_file = "#{nested_dir_path}/new-file.txt" -File.open(nested_dir_test_file,'w') do |file| - file.puts "this is a new file in a new nested dir" -end -FileUtils::chmod 0644, nested_dir_test_file -copy_file NEWAPP_NAME, "#{PACKAGE_SRC_DIR}/#{APP_NAME}" -FileUtils::chmod 0755, "#{PACKAGE_SRC_DIR}/#{APP_NAME}" - -# Create .zip packages from source files -Dir.mkdir(PACKAGE_DIR) -Dir.chdir(PACKAGE_SRC_DIR) do - if !system("#{ZIP_TOOL} #{PACKAGE_DIR}/app-pkg.zip .") - raise "Unable to create update package" - end -end - -# Copy the install script and updater to the target -# directory -replace_vars("file_list.xml","#{PACKAGE_DIR}/file_list.xml",file_list_vars) -copy_file "../#{UPDATER_NAME}", "#{PACKAGE_DIR}/#{UPDATER_NAME}" - -# Run the updater using the new syntax -# -# Run the application from the install directory to -# make sure that it looks in the correct directory for -# the file_list.xml file and packages -# -install_path = File.expand_path(INSTALL_DIR) -Dir.chdir(INSTALL_DIR) do - flags = "--force-elevated" if force_elevation - debug_flags = "gdb --args" if run_in_debugger - cmd = "#{debug_flags} #{PACKAGE_DIR}/#{UPDATER_NAME} #{flags} --install-dir \"#{install_path}\" --package-dir \"#{PACKAGE_DIR}\" --script file_list.xml --auto-close" - puts "Running '#{cmd}'" - system(cmd) -end - -# TODO - Correctly wait until updater has finished -sleep(1) - -# Check that the app was updated -app_path = "#{INSTALL_DIR}/#{APP_NAME}" -output = `"#{app_path}"` -if (output.strip != "new app starting") - throw "Updated app produced unexpected output: #{output}" -end - -# Check that the packaged dir and install dir match -dir_diff = compare_dirs(PACKAGE_SRC_DIR, INSTALL_DIR) -ignored_files = ["test-dir", "test-dir/app-symlink", UPDATER_NAME] -have_unexpected_change = false -dir_diff.each do |path, change_type| - if !ignored_files.include?(path) - case change_type - when :added - $stderr.puts "File #{path} was not installed" - when :changed - $stderr.puts "File #{path} differs between install and package dir" - when :deleted - $stderr.puts "File #{path} was not uninstalled" - end - have_unexpected_change = true - end -end - -if have_unexpected_change - throw "Unexpected differences between packaging and update dir" -end - -puts "Test passed" diff --git a/mmc_updater/src/tests/test.manifest b/mmc_updater/src/tests/test.manifest new file mode 100644 index 000000000..8b4dbb98b --- /dev/null +++ b/mmc_updater/src/tests/test.manifest @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + Custom Minecraft launcher for managing multiple installs. + + + + + + + + + + + \ No newline at end of file diff --git a/mmc_updater/src/tests/test.rc b/mmc_updater/src/tests/test.rc new file mode 100644 index 000000000..a288dba60 --- /dev/null +++ b/mmc_updater/src/tests/test.rc @@ -0,0 +1,28 @@ +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include + +1 RT_MANIFEST "test.manifest" + +VS_VERSION_INFO VERSIONINFO +FILEVERSION 1,0,0,0 +FILEOS VOS_NT_WINDOWS32 +FILETYPE VFT_APP +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "000004b0" + BEGIN + VALUE "CompanyName", "MultiMC Contributors" + VALUE "FileDescription", "Testcase" + VALUE "FileVersion", "1.0.0.0" + VALUE "ProductName", "MultiMC Testcase" + VALUE "ProductVersion", "5" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x0000, 0x04b0 // Unicode + END +END diff --git a/mmc_updater/src/tests/v2_file_list.xml b/mmc_updater/src/tests/v2_file_list.xml deleted file mode 100644 index 202e5bbe6..000000000 --- a/mmc_updater/src/tests/v2_file_list.xml +++ /dev/null @@ -1,67 +0,0 @@ - - - - - 2.0 - Test - - - - - - app-pkg - $APP_PACKAGE_HASH - $APP_PACKAGE_SIZE - http://some/dummy/URL - - - - - - - - - - $APP_FILENAME - $UPDATED_APP_HASH - $UPDATED_APP_SIZE - 0755 - app-pkg - true - - - $UPDATER_FILENAME - $UPDATER_HASH - $UPDATER_SIZE - 0755 - - - - test-dir/app-symlink - ../app - - - new-dir/new-dir2/new-file.txt - $TEST_FILENAME - $TEST_SIZE - app-pkg - 0644 - - - - - file-to-uninstall.txt - symlink-to-file-to-uninstall.txt - - diff --git a/multimc.rc b/multimc.rc index decf7d3ab..2140e3f4f 100644 --- a/multimc.rc +++ b/multimc.rc @@ -1 +1,29 @@ +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include + IDI_ICON1 ICON DISCARDABLE "resources/icons/MultiMC.ico" +1 RT_MANIFEST "MultiMC.manifest" + +VS_VERSION_INFO VERSIONINFO +FILEVERSION 1,0,0,0 +FILEOS VOS_NT_WINDOWS32 +FILETYPE VFT_APP +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "000004b0" + BEGIN + VALUE "CompanyName", "MultiMC Contributors" + VALUE "FileDescription", "Minecraft Launcher" + VALUE "FileVersion", "1.0.0.0" + VALUE "ProductName", "MultiMC" + VALUE "ProductVersion", "5" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x0000, 0x04b0 // Unicode + END +END diff --git a/package/linux/MultiMC b/package/linux/MultiMC index 8229b24f1..7fb72f989 100755 --- a/package/linux/MultiMC +++ b/package/linux/MultiMC @@ -1,7 +1,20 @@ -#!/bin/sh +#!/bin/bash # Basic start script for running MultiMC with the libs packaged with it. -MMC_DIR=`dirname "$0"` +function printerror { + printf "$1" + if which zenity >/dev/null; then zenity --error --text="$1" &>/dev/null; + elif which kdialog >/dev/null; then kdialog --error "$1" &>/dev/null; + fi +} + +if [[ $EUID -eq 0 ]]; then + printerror "This program should not be run using sudo or as the root user!\n" + exit 1 +fi + + +MMC_DIR="$(dirname "$(readlink -f "$0")")" cd "${MMC_DIR}" echo "MultiMC Dir: ${MMC_DIR}" @@ -11,8 +24,8 @@ export QT_PLUGIN_PATH="${MMC_DIR}/plugins" export QT_FONTPATH="${MMC_DIR}/fonts" # Detect missing dependencies... -DEPS_LIST=`ldd "${MMC_DIR}"/plugins/*/*.so | grep "not found" | awk -vORS=", " '{ print $1 }'` -if [ -z $DEPS_LIST ]; then +DEPS_LIST=`ldd "${MMC_DIR}"/plugins/*/*.so 2>/dev/null | grep "not found" | awk -vORS=", " '{ print $1 }'` +if [ "x$DEPS_LIST" = "x" ]; then # We have all our dependencies. Run MultiMC. echo "No missing dependencies found." @@ -25,9 +38,41 @@ if [ -z $DEPS_LIST ]; then # Exit with MultiMC's exit code. exit $? else - echo "Error: MultiMC is missing the following libraries that it needs to work correctly:" - echo "\t${DEPS_LIST}" - echo "Please install them from your distribution's package manager." + # apt + if which apt-file &>/dev/null; then + LIBRARIES=`echo "$DEPS_LIST" | grep -oP "[^, ]*"` + COMMAND_LIBS=`for LIBRARY in $LIBRARIES; do apt-file -l search $LIBRARY; done` + COMMAND_LIBS=`echo "$COMMAND_LIBS" | awk -vORS=" " '{ print $1 }'` + INSTALL_CMD="sudo apt-get install $COMMAND_LIBS" + # pacman + elif which pkgfile &>/dev/null; then + LIBRARIES=`echo "$DEPS_LIST" | grep -oP "[^, ]*"` + COMMAND_LIBS=`for LIBRARY in $LIBRARIES; do pkgfile $LIBRARY; done` + COMMAND_LIBS=`echo "$COMMAND_LIBS" | awk -vORS=" " '{ print $1 }'` + INSTALL_CMD="sudo pacman -S $COMMAND_LIBS" + # yum + elif which yum &>/dev/null; then + LIBRARIES=`echo "$DEPS_LIST" | grep -oP "[^, ]*"` + COMMAND_LIBS=`for LIBRARY in $LIBRARIES; do yum whatprovides $LIBRARY; done` + COMMAND_LIBS=`echo "$COMMAND_LIBS" | awk -vORS=" " '{ print $1 }'` + INSTALL_CMD="sudo yum install $COMMAND_LIBS" + # zypper + elif which zypper &>/dev/null; then + LIBRARIES=`echo "$DEPS_LIST" | grep -oP "[^, ]*"` + COMMAND_LIBS=`for LIBRARY in $LIBRARIES; do zypper wp $LIBRARY; done` + COMMAND_LIBS=`echo "$COMMAND_LIBS" | awk -vORS=" " '{ print $1 }'` + INSTALL_CMD="sudo zypper install $COMMAND_LIBS" + # emerge + elif which pfl &>/dev/null; then + LIBRARIES=`echo "$DEPS_LIST" | grep -oP "[^, ]*"` + COMMAND_LIBS=`for LIBRARY in $LIBRARIES; do pfl $LIBRARY; done` + COMMAND_LIBS=`echo "$COMMAND_LIBS" | awk -vORS=" " '{ print $1 }'` + INSTALL_CMD="sudo emerge $COMMAND_LIBS" + fi + + MESSAGE="Error: MultiMC is missing the following libraries that it needs to work correctly:\n\t${DEPS_LIST}\nPlease install them from your distribution's package manager." + MESSAGE="$MESSAGE\n\nHint: $INSTALL_CMD\n" + + printerror "$MESSAGE" exit 1 fi - diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e115af17f..2d8514047 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -8,6 +8,9 @@ macro(add_unit_test name) unset(srcs) foreach(arg ${testname} ${ARGN}) list(APPEND srcs ${CMAKE_CURRENT_SOURCE_DIR}/${arg}) + if (WIN32) + list(APPEND srcs ${CMAKE_CURRENT_SOURCE_DIR}/test.rc) + endif() endforeach() add_executable(tst_${name} ${srcs}) qt5_use_modules(tst_${name} Test Core Network Widgets) @@ -20,6 +23,8 @@ endmacro() add_unit_test(pathutils tst_pathutils.cpp) add_unit_test(userutils tst_userutils.cpp) +add_unit_test(UpdateChecker tst_UpdateChecker.cpp) +add_unit_test(DownloadUpdateTask tst_DownloadUpdateTask.cpp) # Tests END # @@ -79,4 +84,9 @@ if(MultiMC_CODE_COVERAGE) add_custom_target(MultiMC_RUN_TESTS DEPENDS MultiMC_GENERATE_COVERAGE_HTML) endif(MultiMC_CODE_COVERAGE) -add_subdirectory(data) + +add_custom_target(MultiMC_Test_Data + ALL + COMMAND ${CMAKE_COMMAND} -E remove_directory ${CMAKE_CURRENT_BINARY_DIR}/data + COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_CURRENT_SOURCE_DIR}/data ${CMAKE_CURRENT_BINARY_DIR}/data +) diff --git a/tests/TestUtil.h b/tests/TestUtil.h index 64ee16754..fd25d24fb 100644 --- a/tests/TestUtil.h +++ b/tests/TestUtil.h @@ -15,15 +15,30 @@ struct TestsInternal f.open(QFile::ReadOnly); return f.readAll(); } + static QString readFileUtf8(const QString &fileName) + { + return QString::fromUtf8(readFile(fileName)); + } }; #define MULTIMC_GET_TEST_FILE(file) TestsInternal::readFile(QFINDTESTDATA( file )) +#define MULTIMC_GET_TEST_FILE_UTF8(file) TestsInternal::readFileUtf8(QFINDTESTDATA( file )) +#ifdef Q_OS_LINUX +# define _MMC_EXTRA_ARGV , "-platform", "offscreen" +# define _MMC_EXTRA_ARGC 2 +#else +# define _MMC_EXTRA_ARGV +# define _MMC_EXTRA_ARGC 0 +#endif + + + #define QTEST_GUILESS_MAIN_MULTIMC(TestObject) \ int main(int argc, char *argv[]) \ { \ - char *argv_[] = { argv[0] }; \ - int argc_ = 1; \ + char *argv_[] = { argv[0] _MMC_EXTRA_ARGV }; \ + int argc_ = 1 + _MMC_EXTRA_ARGC; \ MultiMC app(argc_, argv_, QDir::temp().absoluteFilePath("MultiMC_Test")); \ app.setAttribute(Qt::AA_Use96Dpi, true); \ TestObject tc; \ diff --git a/tests/data/.gitattributes b/tests/data/.gitattributes new file mode 100644 index 000000000..9ac803f03 --- /dev/null +++ b/tests/data/.gitattributes @@ -0,0 +1,2 @@ +* -text -diff + diff --git a/tests/data/1.json b/tests/data/1.json new file mode 100644 index 000000000..f9f99b22f --- /dev/null +++ b/tests/data/1.json @@ -0,0 +1,43 @@ +{ + "ApiVersion": 0, + "Id": 1, + "Name": "1.0.1", + "Files": [ + { + "Path": "fileOne", + "Sources": [ + { + "SourceType": "http", + "Url": "$PWD/tests/data/fileOneA" + } + ], + "Executable": true, + "Perms": 493, + "MD5": "9eb84090956c484e32cb6c08455a667b" + }, + { + "Path": "fileTwo", + "Sources": [ + { + "SourceType": "http", + "Url": "$PWD/tests/data/fileTwo" + } + ], + "Executable": false, + "Perms": 644, + "MD5": "38f94f54fa3eb72b0ea836538c10b043" + }, + { + "Path": "fileThree", + "Sources": [ + { + "SourceType": "http", + "Url": "$PWD/tests/data/fileThree" + } + ], + "Executable": false, + "Perms": "750", + "MD5": "f12df554b21e320be6471d7154130e70" + } + ] +} diff --git a/tests/data/2.json b/tests/data/2.json new file mode 100644 index 000000000..bb59b9b6d --- /dev/null +++ b/tests/data/2.json @@ -0,0 +1,31 @@ +{ + "ApiVersion": 0, + "Id": 1, + "Name": "1.0.1", + "Files": [ + { + "Path": "fileOne", + "Sources": [ + { + "SourceType": "http", + "Url": "$PWD/tests/data/fileOneB" + } + ], + "Executable": true, + "Perms": 493, + "MD5": "42915a71277c9016668cce7b82c6b577" + }, + { + "Path": "fileTwo", + "Sources": [ + { + "SourceType": "http", + "Url": "$PWD/tests/data/fileTwo" + } + ], + "Executable": false, + "Perms": 644, + "MD5": "38f94f54fa3eb72b0ea836538c10b043" + } + ] +} diff --git a/tests/data/CMakeLists.txt b/tests/data/CMakeLists.txt deleted file mode 100644 index eee5a5962..000000000 --- a/tests/data/CMakeLists.txt +++ /dev/null @@ -1,4 +0,0 @@ -add_custom_target(MultiMC_Test_Data - ALL - COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_BINARY_DIR} -) diff --git a/tests/data/channels.json b/tests/data/channels.json new file mode 100644 index 000000000..6bf65a82f --- /dev/null +++ b/tests/data/channels.json @@ -0,0 +1,23 @@ +{ + "format_version": 0, + "channels": [ + { + "id": "develop", + "name": "Develop", + "description": "The channel called \"develop\"", + "url": "$PWD/tests/data/" + }, + { + "id": "stable", + "name": "Stable", + "description": "It's stable at least", + "url": "ftp://username@host/path/to/stuff" + }, + { + "id": "42", + "name": "The Channel", + "description": "This is the channel that is going to answer all of your questions", + "url": "https://dent.me/tea" + } + ] +} diff --git a/tests/data/errorChannels.json b/tests/data/errorChannels.json new file mode 100644 index 000000000..333cd445e --- /dev/null +++ b/tests/data/errorChannels.json @@ -0,0 +1,23 @@ +{ + "format_version": 0, + "channels": [ + { + "id": "", + "name": "Develop", + "description": "The channel called \"develop\"", + "url": "http://example.org/stuff" + }, + { + "id": "stable", + "name": "", + "description": "It's stable at least", + "url": "ftp://username@host/path/to/stuff" + }, + { + "id": "42", + "name": "The Channel", + "description": "This is the channel that is going to answer all of your questions", + "url": "" + } + ] +} diff --git a/tests/data/fileOneA b/tests/data/fileOneA new file mode 100644 index 000000000..f2e41136e --- /dev/null +++ b/tests/data/fileOneA @@ -0,0 +1 @@ +stuff diff --git a/tests/data/fileOneB b/tests/data/fileOneB new file mode 100644 index 000000000..f9aba922a --- /dev/null +++ b/tests/data/fileOneB @@ -0,0 +1,3 @@ +stuff + +more stuff that came in the new version diff --git a/tests/data/fileThree b/tests/data/fileThree new file mode 100644 index 000000000..6353ff161 --- /dev/null +++ b/tests/data/fileThree @@ -0,0 +1 @@ +this is yet another file diff --git a/tests/data/fileTwo b/tests/data/fileTwo new file mode 100644 index 000000000..aad9a93ad --- /dev/null +++ b/tests/data/fileTwo @@ -0,0 +1 @@ +some other stuff diff --git a/tests/data/garbageChannels.json b/tests/data/garbageChannels.json new file mode 100644 index 000000000..1450fb9cf --- /dev/null +++ b/tests/data/garbageChannels.json @@ -0,0 +1,22 @@ +{ + "format_version": 0, + "channels": [ + { + "id": "develop", + "name": "Develop", + "description": "The channel called \"develop\"", +aa "url": "http://example.org/stuff" + }, +a "id": "stable", + "name": "Stable", + "description": "It's stable at least", + "url": "ftp://username@host/path/to/stuff" + }, + { + "id": "42"f + "name": "The Channel", + "description": "This is the channel that is going to answer all of your questions", + "url": "https://dent.me/tea" + } + ] +} diff --git a/tests/data/index.json b/tests/data/index.json new file mode 100644 index 000000000..20ceb9f4d --- /dev/null +++ b/tests/data/index.json @@ -0,0 +1,9 @@ +{ + "ApiVersion": 0, + "Versions": [ + { "Id": 0, "Name": "1.0.0" }, + { "Id": 1, "Name": "1.0.1" }, + { "Id": 2, "Name": "1.0.2" }, + { "Id": 3, "Name": "1.0.3" } + ] +} diff --git a/tests/data/noChannels.json b/tests/data/noChannels.json new file mode 100644 index 000000000..bbb2cb700 --- /dev/null +++ b/tests/data/noChannels.json @@ -0,0 +1,5 @@ +{ + "format_version": 0, + "channels": [ + ] +} diff --git a/tests/data/oneChannel.json b/tests/data/oneChannel.json new file mode 100644 index 000000000..84727ac70 --- /dev/null +++ b/tests/data/oneChannel.json @@ -0,0 +1,11 @@ +{ + "format_version": 0, + "channels": [ + { + "id": "develop", + "name": "Develop", + "description": "The channel called \"develop\"", + "url": "http://example.org/stuff" + } + ] +} diff --git a/tests/data/tst_DownloadUpdateTask-test_writeInstallScript.xml b/tests/data/tst_DownloadUpdateTask-test_writeInstallScript.xml new file mode 100644 index 000000000..09c162cad --- /dev/null +++ b/tests/data/tst_DownloadUpdateTask-test_writeInstallScript.xml @@ -0,0 +1,17 @@ + + + + sourceOne + destOne + 0777 + + + MultiMC.exe + M/u/l/t/i/M/C/e/x/e + 0644 + + + + toDelete.abc + + diff --git a/tests/test.manifest b/tests/test.manifest new file mode 100644 index 000000000..8b4dbb98b --- /dev/null +++ b/tests/test.manifest @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + Custom Minecraft launcher for managing multiple installs. + + + + + + + + + + + \ No newline at end of file diff --git a/tests/test.rc b/tests/test.rc new file mode 100644 index 000000000..a288dba60 --- /dev/null +++ b/tests/test.rc @@ -0,0 +1,28 @@ +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include + +1 RT_MANIFEST "test.manifest" + +VS_VERSION_INFO VERSIONINFO +FILEVERSION 1,0,0,0 +FILEOS VOS_NT_WINDOWS32 +FILETYPE VFT_APP +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "000004b0" + BEGIN + VALUE "CompanyName", "MultiMC Contributors" + VALUE "FileDescription", "Testcase" + VALUE "FileVersion", "1.0.0.0" + VALUE "ProductName", "MultiMC Testcase" + VALUE "ProductVersion", "5" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x0000, 0x04b0 // Unicode + END +END diff --git a/tests/tst_DownloadUpdateTask.cpp b/tests/tst_DownloadUpdateTask.cpp new file mode 100644 index 000000000..3b2c6793c --- /dev/null +++ b/tests/tst_DownloadUpdateTask.cpp @@ -0,0 +1,271 @@ +#include +#include + +#include "TestUtil.h" + +#include "logic/updater/DownloadUpdateTask.h" +#include "logic/updater/UpdateChecker.h" +#include "depends/util/include/pathutils.h" + +DownloadUpdateTask::FileSourceList encodeBaseFile(const char *suffix) +{ + auto base = qApp->applicationDirPath(); + QUrl localFile = QUrl::fromLocalFile(base + suffix); + QString localUrlString = localFile.toString(QUrl::FullyEncoded); + auto item = DownloadUpdateTask::FileSource("http", localUrlString); + return DownloadUpdateTask::FileSourceList({item}); +} + +Q_DECLARE_METATYPE(DownloadUpdateTask::VersionFileList) +Q_DECLARE_METATYPE(DownloadUpdateTask::UpdateOperation) + +bool operator==(const DownloadUpdateTask::FileSource &f1, + const DownloadUpdateTask::FileSource &f2) +{ + return f1.type == f2.type && f1.url == f2.url && f1.compressionType == f2.compressionType; +} +bool operator==(const DownloadUpdateTask::VersionFileEntry &v1, + const DownloadUpdateTask::VersionFileEntry &v2) +{ + return v1.path == v2.path && v1.mode == v2.mode && v1.sources == v2.sources && + v1.md5 == v2.md5; +} +bool operator==(const DownloadUpdateTask::UpdateOperation &u1, + const DownloadUpdateTask::UpdateOperation &u2) +{ + return u1.type == u2.type && u1.file == u2.file && u1.dest == u2.dest && u1.mode == u2.mode; +} + +QDebug operator<<(QDebug dbg, const DownloadUpdateTask::FileSource &f) +{ + dbg.nospace() << "FileSource(type=" << f.type << " url=" << f.url + << " comp=" << f.compressionType << ")"; + return dbg.maybeSpace(); +} + +QDebug operator<<(QDebug dbg, const DownloadUpdateTask::VersionFileEntry &v) +{ + dbg.nospace() << "VersionFileEntry(path=" << v.path << " mode=" << v.mode + << " md5=" << v.md5 << " sources=" << v.sources << ")"; + return dbg.maybeSpace(); +} + +QDebug operator<<(QDebug dbg, const DownloadUpdateTask::UpdateOperation::Type &t) +{ + switch (t) + { + case DownloadUpdateTask::UpdateOperation::OP_COPY: + dbg << "OP_COPY"; + break; + case DownloadUpdateTask::UpdateOperation::OP_DELETE: + dbg << "OP_DELETE"; + break; + case DownloadUpdateTask::UpdateOperation::OP_MOVE: + dbg << "OP_MOVE"; + break; + case DownloadUpdateTask::UpdateOperation::OP_CHMOD: + dbg << "OP_CHMOD"; + break; + } + return dbg.maybeSpace(); +} + +QDebug operator<<(QDebug dbg, const DownloadUpdateTask::UpdateOperation &u) +{ + dbg.nospace() << "UpdateOperation(type=" << u.type << " file=" << u.file + << " dest=" << u.dest << " mode=" << u.mode << ")"; + return dbg.maybeSpace(); +} + +class DownloadUpdateTaskTest : public QObject +{ + Q_OBJECT +private +slots: + void initTestCase() + { + } + void cleanupTestCase() + { + } + + void test_writeInstallScript() + { + DownloadUpdateTask task( + QUrl::fromLocalFile(QDir::current().absoluteFilePath("tests/data/")).toString(), 0); + + DownloadUpdateTask::UpdateOperationList ops; + + ops << DownloadUpdateTask::UpdateOperation::CopyOp("sourceOne", "destOne", 0777) + << DownloadUpdateTask::UpdateOperation::CopyOp("MultiMC.exe", "M/u/l/t/i/M/C/e/x/e") + << DownloadUpdateTask::UpdateOperation::DeleteOp("toDelete.abc"); + auto testFile = "tests/data/tst_DownloadUpdateTask-test_writeInstallScript.xml"; + const QString script = QDir::temp().absoluteFilePath("MultiMCUpdateScript.xml"); + QVERIFY(task.writeInstallScript(ops, script)); + QCOMPARE(TestsInternal::readFileUtf8(script).replace(QRegExp("[\r\n]+"), "\n"), + MULTIMC_GET_TEST_FILE_UTF8(testFile).replace(QRegExp("[\r\n]+"), "\n")); + } + + void test_parseVersionInfo_data() + { + QTest::addColumn("data"); + QTest::addColumn("list"); + QTest::addColumn("error"); + QTest::addColumn("ret"); + + QTest::newRow("one") + << MULTIMC_GET_TEST_FILE("tests/data/1.json") + << (DownloadUpdateTask::VersionFileList() + << DownloadUpdateTask::VersionFileEntry{"fileOne", + 493, + encodeBaseFile("/tests/data/fileOneA"), + "9eb84090956c484e32cb6c08455a667b"} + << DownloadUpdateTask::VersionFileEntry{"fileTwo", + 644, + encodeBaseFile("/tests/data/fileTwo"), + "38f94f54fa3eb72b0ea836538c10b043"} + << DownloadUpdateTask::VersionFileEntry{"fileThree", + 750, + encodeBaseFile("/tests/data/fileThree"), + "f12df554b21e320be6471d7154130e70"}) + << QString() << true; + QTest::newRow("two") + << MULTIMC_GET_TEST_FILE("tests/data/2.json") + << (DownloadUpdateTask::VersionFileList() + << DownloadUpdateTask::VersionFileEntry{"fileOne", + 493, + encodeBaseFile("/tests/data/fileOneB"), + "42915a71277c9016668cce7b82c6b577"} + << DownloadUpdateTask::VersionFileEntry{"fileTwo", + 644, + encodeBaseFile("/tests/data/fileTwo"), + "38f94f54fa3eb72b0ea836538c10b043"}) + << QString() << true; + } + void test_parseVersionInfo() + { + QFETCH(QByteArray, data); + QFETCH(DownloadUpdateTask::VersionFileList, list); + QFETCH(QString, error); + QFETCH(bool, ret); + + DownloadUpdateTask::VersionFileList outList; + QString outError; + bool outRet = DownloadUpdateTask("", 0).parseVersionInfo(data, &outList, &outError); + QCOMPARE(outRet, ret); + QCOMPARE(outList, list); + QCOMPARE(outError, error); + } + + void test_processFileLists_data() + { + QTest::addColumn("downloader"); + QTest::addColumn("currentVersion"); + QTest::addColumn("newVersion"); + QTest::addColumn("expectedOperations"); + + DownloadUpdateTask *downloader = new DownloadUpdateTask(QString(), -1); + + // update fileOne, keep fileTwo, remove fileThree + QTest::newRow("test 1") + << downloader << (DownloadUpdateTask::VersionFileList() + << DownloadUpdateTask::VersionFileEntry{ + "tests/data/fileOne", 493, + DownloadUpdateTask::FileSourceList() + << DownloadUpdateTask::FileSource( + "http", "http://host/path/fileOne-1"), + "9eb84090956c484e32cb6c08455a667b"} + << DownloadUpdateTask::VersionFileEntry{ + "tests/data/fileTwo", 644, + DownloadUpdateTask::FileSourceList() + << DownloadUpdateTask::FileSource( + "http", "http://host/path/fileTwo-1"), + "38f94f54fa3eb72b0ea836538c10b043"} + << DownloadUpdateTask::VersionFileEntry{ + "tests/data/fileThree", 420, + DownloadUpdateTask::FileSourceList() + << DownloadUpdateTask::FileSource( + "http", "http://host/path/fileThree-1"), + "f12df554b21e320be6471d7154130e70"}) + << (DownloadUpdateTask::VersionFileList() + << DownloadUpdateTask::VersionFileEntry{ + "tests/data/fileOne", 493, + DownloadUpdateTask::FileSourceList() + << DownloadUpdateTask::FileSource("http", + "http://host/path/fileOne-2"), + "42915a71277c9016668cce7b82c6b577"} + << DownloadUpdateTask::VersionFileEntry{ + "tests/data/fileTwo", 644, + DownloadUpdateTask::FileSourceList() + << DownloadUpdateTask::FileSource("http", + "http://host/path/fileTwo-2"), + "38f94f54fa3eb72b0ea836538c10b043"}) + << (DownloadUpdateTask::UpdateOperationList() + << DownloadUpdateTask::UpdateOperation::DeleteOp("tests/data/fileThree") + << DownloadUpdateTask::UpdateOperation::CopyOp( + PathCombine(downloader->updateFilesDir(), + QString("tests/data/fileOne").replace("/", "_")), + "tests/data/fileOne", 493)); + } + void test_processFileLists() + { + QFETCH(DownloadUpdateTask *, downloader); + QFETCH(DownloadUpdateTask::VersionFileList, currentVersion); + QFETCH(DownloadUpdateTask::VersionFileList, newVersion); + QFETCH(DownloadUpdateTask::UpdateOperationList, expectedOperations); + + DownloadUpdateTask::UpdateOperationList operations; + + downloader->processFileLists(new NetJob("Dummy"), currentVersion, newVersion, + operations); + qDebug() << (operations == expectedOperations); + qDebug() << operations; + qDebug() << expectedOperations; + QCOMPARE(operations, expectedOperations); + } + + void test_masterTest() + { + QLOG_INFO() << "#####################"; + MMC->m_version.build = 1; + MMC->m_version.channel = "develop"; + auto channels = + QUrl::fromLocalFile(QDir::current().absoluteFilePath("tests/data/channels.json")); + auto root = QUrl::fromLocalFile(QDir::current().absoluteFilePath("tests/data/")); + QLOG_DEBUG() << "channels: " << channels; + QLOG_DEBUG() << "root: " << root; + MMC->updateChecker()->setChannelListUrl(channels.toString()); + MMC->updateChecker()->setCurrentChannel("develop"); + + DownloadUpdateTask task(root.toString(), 2); + + QSignalSpy succeededSpy(&task, SIGNAL(succeeded())); + + task.start(); + + QVERIFY(succeededSpy.wait()); + } + + void test_OSXPathFixup() + { + QString path, pathOrig; + bool result; + // Proper OSX path + pathOrig = path = "MultiMC.app/Foo/Bar/Baz"; + qDebug() << "Proper OSX path: " << path; + result = DownloadUpdateTask::fixPathForOSX(path); + QCOMPARE(path, QString("../../Foo/Bar/Baz")); + QCOMPARE(result, true); + + // Bad OSX path + pathOrig = path = "translations/klingon.lol"; + qDebug() << "Bad OSX path: " << path; + result = DownloadUpdateTask::fixPathForOSX(path); + QCOMPARE(path, pathOrig); + QCOMPARE(result, false); + } +}; + +QTEST_GUILESS_MAIN_MULTIMC(DownloadUpdateTaskTest) + +#include "tst_DownloadUpdateTask.moc" diff --git a/tests/tst_UpdateChecker.cpp b/tests/tst_UpdateChecker.cpp new file mode 100644 index 000000000..162d00097 --- /dev/null +++ b/tests/tst_UpdateChecker.cpp @@ -0,0 +1,163 @@ +#include +#include + +#include "TestUtil.h" +#include "logic/updater/UpdateChecker.h" + +Q_DECLARE_METATYPE(UpdateChecker::ChannelListEntry) + +bool operator==(const UpdateChecker::ChannelListEntry &e1, const UpdateChecker::ChannelListEntry &e2) +{ + return e1.id == e2.id && + e1.name == e2.name && + e1.description == e2.description && + e1.url == e2.url; +} + +QDebug operator<<(QDebug dbg, const UpdateChecker::ChannelListEntry &c) +{ + dbg.nospace() << "ChannelListEntry(id=" << c.id << " name=" << c.name << " description=" << c.description << " url=" << c.url << ")"; + return dbg.maybeSpace(); +} + +class UpdateCheckerTest : public QObject +{ + Q_OBJECT +private +slots: + void initTestCase() + { + + } + void cleanupTestCase() + { + + } + + static QString findTestDataUrl(const char *file) + { + return QUrl::fromLocalFile(QFINDTESTDATA(file)).toString(); + } + void tst_ChannelListParsing_data() + { + QTest::addColumn("channel"); + QTest::addColumn("channelUrl"); + QTest::addColumn("hasChannels"); + QTest::addColumn("valid"); + QTest::addColumn >("result"); + + QTest::newRow("garbage") + << QString() + << findTestDataUrl("tests/data/garbageChannels.json") + << false + << false + << QList(); + QTest::newRow("errors") + << QString() + << findTestDataUrl("tests/data/errorChannels.json") + << false + << true + << QList(); + QTest::newRow("no channels") + << QString() + << findTestDataUrl("tests/data/noChannels.json") + << false + << true + << QList(); + QTest::newRow("one channel") + << QString("develop") + << findTestDataUrl("tests/data/oneChannel.json") + << true + << true + << (QList() << UpdateChecker::ChannelListEntry{"develop", "Develop", "The channel called \"develop\"", "http://example.org/stuff"}); + QTest::newRow("several channels") + << QString("develop") + << findTestDataUrl("tests/data/channels.json") + << true + << true + << (QList() + << UpdateChecker::ChannelListEntry{"develop", "Develop", "The channel called \"develop\"", "$PWD/tests/data/"} + << UpdateChecker::ChannelListEntry{"stable", "Stable", "It's stable at least", "ftp://username@host/path/to/stuff"} + << UpdateChecker::ChannelListEntry{"42", "The Channel", "This is the channel that is going to answer all of your questions", "https://dent.me/tea"}); + } + void tst_ChannelListParsing() + { + QFETCH(QString, channel); + QFETCH(QString, channelUrl); + QFETCH(bool, hasChannels); + QFETCH(bool, valid); + QFETCH(QList, result); + + UpdateChecker checker; + + QSignalSpy channelListLoadedSpy(&checker, SIGNAL(channelListLoaded())); + QVERIFY(channelListLoadedSpy.isValid()); + + checker.setCurrentChannel(channel); + checker.setChannelListUrl(channelUrl); + + checker.updateChanList(); + + if (valid) + { + QVERIFY(channelListLoadedSpy.wait()); + QCOMPARE(channelListLoadedSpy.size(), 1); + } + else + { + channelListLoadedSpy.wait(); + QCOMPARE(channelListLoadedSpy.size(), 0); + } + + QCOMPARE(checker.hasChannels(), hasChannels); + QCOMPARE(checker.getChannelList(), result); + } + + void tst_UpdateChecking_data() + { + QTest::addColumn("channel"); + QTest::addColumn("channelUrl"); + QTest::addColumn("currentBuild"); + QTest::addColumn >("result"); + + QTest::newRow("valid channel") + << "develop" << findTestDataUrl("tests/data/channels.json") + << 2 + << (QList() << QString() << "1.0.3" << 3); + } + + void tst_UpdateChecking() + { + QFETCH(QString, channel); + QFETCH(QString, channelUrl); + QFETCH(int, currentBuild); + QFETCH(QList, result); + + MMC->m_version.build = currentBuild; + + UpdateChecker checker; + checker.setCurrentChannel(channel); + checker.setChannelListUrl(channelUrl); + + QSignalSpy updateAvailableSpy(&checker, SIGNAL(updateAvailable(QString,QString,int))); + QVERIFY(updateAvailableSpy.isValid()); + QSignalSpy channelListLoadedSpy(&checker, SIGNAL(channelListLoaded())); + QVERIFY(channelListLoadedSpy.isValid()); + + checker.updateChanList(); + QVERIFY(channelListLoadedSpy.wait()); + + checker.m_channels[0].url = QUrl::fromLocalFile(QDir::current().absoluteFilePath("tests/data/")).toString(); + + checker.checkForUpdate(false); + + QVERIFY(updateAvailableSpy.wait()); + QList res = result; + res[0] = checker.m_channels[0].url; + QCOMPARE(updateAvailableSpy.first(), res); + } +}; + +QTEST_GUILESS_MAIN_MULTIMC(UpdateCheckerTest) + +#include "tst_UpdateChecker.moc" diff --git a/tests/tst_pathutils.cpp b/tests/tst_pathutils.cpp index 1e4a83bfe..a1310d005 100644 --- a/tests/tst_pathutils.cpp +++ b/tests/tst_pathutils.cpp @@ -23,13 +23,12 @@ slots: QTest::addColumn("path1"); QTest::addColumn("path2"); -#if defined(Q_OS_UNIX) - QTest::newRow("unix 1") << "/abc/def/ghi/jkl" << "/abc/def" << "ghi/jkl"; - QTest::newRow("unix 2") << "/abc/def/ghi/jkl" << "/abc/def/" << "ghi/jkl"; -#elif defined(Q_OS_WIN) - QTest::newRow("win, from C:") << "C:\\abc" << "C:" << "abc\\def"; - QTest::newRow("win 1") << "C:\\abc\\def\\ghi\\jkl" << "C:\\abc\\def" << "ghi\\jkl"; - QTest::newRow("win 2") << "C:\\abc\\def\\ghi\\jkl" << "C:\\abc\\def\\" << "ghi\\jkl"; + QTest::newRow("qt 1") << "/abc/def/ghi/jkl" << "/abc/def" << "ghi/jkl"; + QTest::newRow("qt 2") << "/abc/def/ghi/jkl" << "/abc/def/" << "ghi/jkl"; +#if defined(Q_OS_WIN) + QTest::newRow("win native, from C:") << "C:/abc" << "C:" << "abc"; + QTest::newRow("win native 1") << "C:/abc/def/ghi/jkl" << "C:\\abc\\def" << "ghi\\jkl"; + QTest::newRow("win native 2") << "C:/abc/def/ghi/jkl" << "C:\\abc\\def\\" << "ghi\\jkl"; #endif } void test_PathCombine1() @@ -48,16 +47,15 @@ slots: QTest::addColumn("path2"); QTest::addColumn("path3"); -#if defined(Q_OS_UNIX) - QTest::newRow("unix 1") << "/abc/def/ghi/jkl" << "/abc" << "def" << "ghi/jkl"; - QTest::newRow("unix 2") << "/abc/def/ghi/jkl" << "/abc/" << "def" << "ghi/jkl"; - QTest::newRow("unix 3") << "/abc/def/ghi/jkl" << "/abc" << "def/" << "ghi/jkl"; - QTest::newRow("unix 4") << "/abc/def/ghi/jkl" << "/abc/" << "def/" << "ghi/jkl"; -#elif defined(Q_OS_WIN) - QTest::newRow("win 1") << "C:\\abc\\def\\ghi\\jkl" << "C:\\abc" << "def" << "ghi\\jkl"; - QTest::newRow("win 2") << "C:\\abc\\def\\ghi\\jkl" << "C:\\abc\\" << "def" << "ghi\\jkl"; - QTest::newRow("win 3") << "C:\\abc\\def\\ghi\\jkl" << "C:\\abc" << "def\\" << "ghi\\jkl"; - QTest::newRow("win 4") << "C:\\abc\\def\\ghi\\jkl" << "C:\\abc\\" << "def" << "ghi\\jkl"; + QTest::newRow("qt 1") << "/abc/def/ghi/jkl" << "/abc" << "def" << "ghi/jkl"; + QTest::newRow("qt 2") << "/abc/def/ghi/jkl" << "/abc/" << "def" << "ghi/jkl"; + QTest::newRow("qt 3") << "/abc/def/ghi/jkl" << "/abc" << "def/" << "ghi/jkl"; + QTest::newRow("qt 4") << "/abc/def/ghi/jkl" << "/abc/" << "def/" << "ghi/jkl"; +#if defined(Q_OS_WIN) + QTest::newRow("win 1") << "C:/abc/def/ghi/jkl" << "C:\\abc" << "def" << "ghi\\jkl"; + QTest::newRow("win 2") << "C:/abc/def/ghi/jkl" << "C:\\abc\\" << "def" << "ghi\\jkl"; + QTest::newRow("win 3") << "C:/abc/def/ghi/jkl" << "C:\\abc" << "def\\" << "ghi\\jkl"; + QTest::newRow("win 4") << "C:/abc/def/ghi/jkl" << "C:\\abc\\" << "def" << "ghi\\jkl"; #endif } void test_PathCombine2() diff --git a/tests/tst_userutils.cpp b/tests/tst_userutils.cpp index 62bee9850..3bc980c07 100644 --- a/tests/tst_userutils.cpp +++ b/tests/tst_userutils.cpp @@ -23,6 +23,9 @@ slots: QCOMPARE(Util::getDesktopDir(), QStandardPaths::writableLocation(QStandardPaths::DesktopLocation)); } +// this is only valid on linux +// FIXME: implement on windows, OSX, then test. +#if defined(Q_OS_LINUX) void test_createShortcut_data() { QTest::addColumn("location"); @@ -40,7 +43,7 @@ slots: #if defined(Q_OS_LINUX) << MULTIMC_GET_TEST_FILE("data/tst_userutils-test_createShortcut-unix") #elif defined(Q_OS_WIN) - << QString() + << QByteArray() #endif ; } @@ -59,8 +62,10 @@ slots: //QDir().remove(location); } +#endif }; + QTEST_GUILESS_MAIN_MULTIMC(UserUtilsTest) #include "tst_userutils.moc"