diff --git a/CMakeLists.txt b/CMakeLists.txt index b50716f036..28c72ff528 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -180,3 +180,9 @@ add_subdirectory(buildconfig) # NOTE: this must always be last to appease the CMake deity of quirky install command evaluation order. add_subdirectory(launcher) + +option(BUILD_TOOLS "Build miscellaneous tools" OFF) + +if(BUILD_TOOLS) + add_subdirectory(tools) +endif() diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 1f9a62841a..442925e4b2 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -77,6 +77,8 @@ #include #include +#include "ApplicationSettings.h" +#include "jreclient/JREClientDialog.h" #define STRINGIFY(x) #x @@ -562,162 +564,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) } } while(false); - // Initialize application settings - { - m_settings.reset(new INISettingsObject(BuildConfig.LAUNCHER_CONFIGFILE, this)); - // Updates - m_settings->registerSetting("AutoUpdate", true); - - // Theming - m_settings->registerSetting("IconTheme", QString("multimc")); - m_settings->registerSetting("ApplicationTheme", QString("system")); - - // Notifications - m_settings->registerSetting("ShownNotifications", QString()); - - // Remembered state - m_settings->registerSetting("LastUsedGroupForNewInstance", QString()); - - QString defaultMonospace; - int defaultSize = 11; -#ifdef Q_OS_WIN32 - defaultMonospace = "Courier"; - defaultSize = 10; -#elif defined(Q_OS_MAC) - defaultMonospace = "Menlo"; -#else - defaultMonospace = "Monospace"; -#endif - - // resolve the font so the default actually matches - QFont consoleFont; - consoleFont.setFamily(defaultMonospace); - consoleFont.setStyleHint(QFont::Monospace); - consoleFont.setFixedPitch(true); - QFontInfo consoleFontInfo(consoleFont); - QString resolvedDefaultMonospace = consoleFontInfo.family(); - QFont resolvedFont(resolvedDefaultMonospace); - qDebug() << "Detected default console font:" << resolvedDefaultMonospace - << ", substitutions:" << resolvedFont.substitutions().join(','); - - m_settings->registerSetting("ConsoleFont", resolvedDefaultMonospace); - m_settings->registerSetting("ConsoleFontSize", defaultSize); - m_settings->registerSetting("ConsoleMaxLines", 100000); - m_settings->registerSetting("ConsoleOverflowStop", true); - - // Folders - m_settings->registerSetting("InstanceDir", "instances"); - m_settings->registerSetting({"CentralModsDir", "ModsDir"}, "mods"); - m_settings->registerSetting("IconsDir", "icons"); - - // Editors - m_settings->registerSetting("JsonEditor", QString()); - - // Language - m_settings->registerSetting("Language", QString()); - - // Console - m_settings->registerSetting("ShowConsole", false); - m_settings->registerSetting("AutoCloseConsole", false); - m_settings->registerSetting("ShowConsoleOnError", true); - m_settings->registerSetting("LogPrePostOutput", true); - - // Window Size - m_settings->registerSetting({"LaunchMaximized", "MCWindowMaximize"}, false); - m_settings->registerSetting({"MinecraftWinWidth", "MCWindowWidth"}, 854); - m_settings->registerSetting({"MinecraftWinHeight", "MCWindowHeight"}, 480); - - // Proxy Settings - m_settings->registerSetting("ProxyType", "None"); - m_settings->registerSetting({"ProxyAddr", "ProxyHostName"}, "127.0.0.1"); - m_settings->registerSetting("ProxyPort", 8080); - m_settings->registerSetting({"ProxyUser", "ProxyUsername"}, ""); - m_settings->registerSetting({"ProxyPass", "ProxyPassword"}, ""); - - // Memory - m_settings->registerSetting({"MinMemAlloc", "MinMemoryAlloc"}, 512); - m_settings->registerSetting({"MaxMemAlloc", "MaxMemoryAlloc"}, 1024); - m_settings->registerSetting("PermGen", 128); - - // Java Settings - m_settings->registerSetting("JavaPath", ""); - m_settings->registerSetting("JavaTimestamp", 0); - m_settings->registerSetting("JavaArchitecture", ""); - m_settings->registerSetting("JavaVersion", ""); - m_settings->registerSetting("JavaVendor", ""); - m_settings->registerSetting("LastHostname", ""); - m_settings->registerSetting("JvmArgs", ""); - - // Native library workarounds - m_settings->registerSetting("UseNativeOpenAL", false); - m_settings->registerSetting("UseNativeGLFW", false); - - // Game time - m_settings->registerSetting("ShowGameTime", true); - m_settings->registerSetting("ShowGlobalGameTime", true); - m_settings->registerSetting("RecordGameTime", true); - m_settings->registerSetting("ShowGameTimeHours", false); - - // Minecraft launch method - m_settings->registerSetting("MCLaunchMethod", "LauncherPart"); - - // Minecraft offline player name - m_settings->registerSetting("LastOfflinePlayerName", ""); - - // Wrapper command for launch - m_settings->registerSetting("WrapperCommand", ""); - - // Custom Commands - m_settings->registerSetting({"PreLaunchCommand", "PreLaunchCmd"}, ""); - m_settings->registerSetting({"PostExitCommand", "PostExitCmd"}, ""); - - // The cat - m_settings->registerSetting("TheCat", false); - - m_settings->registerSetting("InstSortMode", "Name"); - m_settings->registerSetting("SelectedInstance", QString()); - - // Window state and geometry - m_settings->registerSetting("MainWindowState", ""); - m_settings->registerSetting("MainWindowGeometry", ""); - - m_settings->registerSetting("ConsoleWindowState", ""); - m_settings->registerSetting("ConsoleWindowGeometry", ""); - - m_settings->registerSetting("SettingsGeometry", ""); - - m_settings->registerSetting("PagedGeometry", ""); - - m_settings->registerSetting("NewInstanceGeometry", ""); - - m_settings->registerSetting("UpdateDialogGeometry", ""); - - // paste.ee API key - m_settings->registerSetting("PasteEEAPIKey", "multimc"); - - if(!BuildConfig.ANALYTICS_ID.isEmpty()) - { - // Analytics - m_settings->registerSetting("Analytics", true); - m_settings->registerSetting("AnalyticsSeen", 0); - m_settings->registerSetting("AnalyticsClientID", QString()); - } - - // Init page provider - { - m_globalSettingsProvider = std::make_shared(tr("Settings")); - m_globalSettingsProvider->addPage(); - m_globalSettingsProvider->addPage(); - m_globalSettingsProvider->addPage(); - m_globalSettingsProvider->addPage(); - m_globalSettingsProvider->addPage(); - m_globalSettingsProvider->addPage(); - m_globalSettingsProvider->addPage(); - m_globalSettingsProvider->addPage(); - m_globalSettingsProvider->addPage(); - } - qDebug() << "<> Settings loaded."; - } + initializeSettings(); #ifndef QT_NO_ACCESSIBILITY QAccessible::installFactory(groupViewAccessibleFactory); @@ -884,52 +731,77 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) } // Initialize analytics - [this]() - { - const int analyticsVersion = 2; - if(BuildConfig.ANALYTICS_ID.isEmpty()) - { - return; - } + initializeAnalytics(); - auto analyticsSetting = m_settings->getSetting("Analytics"); - connect(analyticsSetting.get(), &Setting::SettingChanged, this, &Application::analyticsSettingChanged); - QString clientID = m_settings->get("AnalyticsClientID").toString(); - if(clientID.isEmpty()) - { - clientID = QUuid::createUuid().toString(); - clientID.remove(QLatin1Char('{')); - clientID.remove(QLatin1Char('}')); - m_settings->set("AnalyticsClientID", clientID); - } - m_analytics = new GAnalytics(BuildConfig.ANALYTICS_ID, clientID, analyticsVersion, this); - m_analytics->setLogLevel(GAnalytics::Debug); - m_analytics->setAnonymizeIPs(true); - // FIXME: the ganalytics library has no idea about our fancy shared pointers... - m_analytics->setNetworkAccessManager(network().get()); + if(createSetupWizard()) + { + return; + } + performMainStartupAction(); +} - if(m_settings->get("AnalyticsSeen").toInt() < m_analytics->version()) - { - qDebug() << "Analytics info not seen by user yet (or old version)."; - return; - } - if(!m_settings->get("Analytics").toBool()) - { - qDebug() << "Analytics disabled by user."; - return; - } +void Application::initializeAnalytics() +{ + const int analyticsVersion = 2; + if(BuildConfig.ANALYTICS_ID.isEmpty()) + { + return; + } - m_analytics->enable(); - qDebug() << "<> Initialized analytics with tid" << BuildConfig.ANALYTICS_ID; - }(); + auto analyticsSetting = m_settings->getSetting("Analytics"); + connect(analyticsSetting.get(), &Setting::SettingChanged, this, &Application::analyticsSettingChanged); + QString clientID = m_settings->get("AnalyticsClientID").toString(); + if(clientID.isEmpty()) + { + clientID = QUuid::createUuid().toString(); + clientID.remove(QLatin1Char('{')); + clientID.remove(QLatin1Char('}')); + m_settings->set("AnalyticsClientID", clientID); + } + m_analytics = new GAnalytics(BuildConfig.ANALYTICS_ID, clientID, analyticsVersion, this); + m_analytics->setLogLevel(GAnalytics::Debug); + m_analytics->setAnonymizeIPs(true); + // FIXME: the ganalytics library has no idea about our fancy shared pointers... + m_analytics->setNetworkAccessManager(network().get()); - if(createSetupWizard()) + if(m_settings->get("AnalyticsSeen").toInt() < m_analytics->version()) { + qDebug() << "Analytics info not seen by user yet (or old version)."; return; } - performMainStartupAction(); + if(!m_settings->get("Analytics").toBool()) + { + qDebug() << "Analytics disabled by user."; + return; + } + + m_analytics->enable(); + qDebug() << "<> Initialized analytics with tid" << BuildConfig.ANALYTICS_ID; } + +void Application::initializeSettings() +{ + // Initialize application settings + m_settings.reset(new ApplicationSettings(BuildConfig.LAUNCHER_CONFIGFILE, this)); + + // Init page provider + { + m_globalSettingsProvider = std::make_shared(tr("Settings")); + m_globalSettingsProvider->addPage(); + m_globalSettingsProvider->addPage(); + m_globalSettingsProvider->addPage(); + m_globalSettingsProvider->addPage(); + m_globalSettingsProvider->addPage(); + m_globalSettingsProvider->addPage(); + m_globalSettingsProvider->addPage(); + m_globalSettingsProvider->addPage(); + m_globalSettingsProvider->addPage(); + } + qDebug() << "<> Settings loaded."; +} + + bool Application::createSetupWizard() { bool javaRequired = [&]() @@ -1406,6 +1278,13 @@ void Application::ShowGlobalSettings(class QWidget* parent, QString open_page) emit globalSettingsClosed(); } +void Application::ShowJREs(class QWidget* parent) +{ + JREClientDialog dlg(parent); + dlg.exec(); +} + + MainWindow* Application::showMainWindow(bool minimized) { if(m_mainWindow) diff --git a/launcher/Application.h b/launcher/Application.h index 75c206c405..f79dfc2542 100644 --- a/launcher/Application.h +++ b/launcher/Application.h @@ -142,6 +142,7 @@ class Application : public QApplication bool updatesAreAllowed(); void ShowGlobalSettings(class QWidget * parent, QString open_page = QString()); + void ShowJREs(class QWidget * parent); signals: void updateAllowedChanged(bool status); @@ -168,6 +169,8 @@ private slots: void setupWizardFinished(int status); private: + void initializeSettings(); + void initializeAnalytics(); bool createSetupWizard(); void performMainStartupAction(); diff --git a/launcher/ApplicationSettings.cpp b/launcher/ApplicationSettings.cpp new file mode 100644 index 0000000000..734d10d31b --- /dev/null +++ b/launcher/ApplicationSettings.cpp @@ -0,0 +1,146 @@ + +#include "ApplicationSettings.h" +#include +#include + +ApplicationSettings::ApplicationSettings(const QString & path, QObject * parent) : INISettingsObject(path, parent) +{ + // Updates + registerSetting("AutoUpdate", true); + + // Theming + registerSetting("IconTheme", QString("multimc")); + registerSetting("ApplicationTheme", QString("system")); + + // Notifications + registerSetting("ShownNotifications", QString()); + + // Remembered state + registerSetting("LastUsedGroupForNewInstance", QString()); + + QString defaultMonospace; + int defaultSize = 11; +#ifdef Q_OS_WIN32 + defaultMonospace = "Courier"; + defaultSize = 10; +#elif defined(Q_OS_MAC) + defaultMonospace = "Menlo"; +#else + defaultMonospace = "Monospace"; +#endif + + // resolve the font so the default actually matches + QFont consoleFont; + consoleFont.setFamily(defaultMonospace); + consoleFont.setStyleHint(QFont::Monospace); + consoleFont.setFixedPitch(true); + QFontInfo consoleFontInfo(consoleFont); + QString resolvedDefaultMonospace = consoleFontInfo.family(); + QFont resolvedFont(resolvedDefaultMonospace); + qDebug() << "Detected default console font:" << resolvedDefaultMonospace << ", substitutions:" << resolvedFont.substitutions().join(','); + + registerSetting("ConsoleFont", resolvedDefaultMonospace); + registerSetting("ConsoleFontSize", defaultSize); + registerSetting("ConsoleMaxLines", 100000); + registerSetting("ConsoleOverflowStop", true); + + // Folders + registerSetting("InstanceDir", "instances"); + registerSetting({"CentralModsDir", "ModsDir"}, "mods"); + registerSetting("IconsDir", "icons"); + + // Editors + registerSetting("JsonEditor", QString()); + + // Language + registerSetting("Language", QString()); + + // Console + registerSetting("ShowConsole", false); + registerSetting("AutoCloseConsole", false); + registerSetting("ShowConsoleOnError", true); + registerSetting("LogPrePostOutput", true); + + // Window Size + registerSetting({"LaunchMaximized", "MCWindowMaximize"}, false); + registerSetting({"MinecraftWinWidth", "MCWindowWidth"}, 854); + registerSetting({"MinecraftWinHeight", "MCWindowHeight"}, 480); + + // Proxy Settings + registerSetting("ProxyType", "None"); + registerSetting({"ProxyAddr", "ProxyHostName"}, "127.0.0.1"); + registerSetting("ProxyPort", 8080); + registerSetting({"ProxyUser", "ProxyUsername"}, ""); + registerSetting({"ProxyPass", "ProxyPassword"}, ""); + + // Memory + registerSetting({"MinMemAlloc", "MinMemoryAlloc"}, 512); + registerSetting({"MaxMemAlloc", "MaxMemoryAlloc"}, 1024); + registerSetting("PermGen", 128); + + // Java Settings + registerSetting("JavaPath", ""); + registerSetting("JavaTimestamp", 0); + registerSetting("JavaArchitecture", ""); + registerSetting("JavaVersion", ""); + registerSetting("JavaVendor", ""); + registerSetting("LastHostname", ""); + registerSetting("JvmArgs", ""); + + // Native library workarounds + registerSetting("UseNativeOpenAL", false); + registerSetting("UseNativeGLFW", false); + + // Game time + registerSetting("ShowGameTime", true); + registerSetting("ShowGlobalGameTime", true); + registerSetting("RecordGameTime", true); + registerSetting("ShowGameTimeHours", false); + + // Minecraft launch method + registerSetting("MCLaunchMethod", "LauncherPart"); + + // Minecraft offline player name + registerSetting("LastOfflinePlayerName", ""); + + // Wrapper command for launch + registerSetting("WrapperCommand", ""); + + // Custom Commands + registerSetting({"PreLaunchCommand", "PreLaunchCmd"}, ""); + registerSetting({"PostExitCommand", "PostExitCmd"}, ""); + + // The cat + registerSetting("TheCat", false); + + registerSetting("InstSortMode", "Name"); + registerSetting("SelectedInstance", QString()); + + // Window state and geometry + registerSetting("MainWindowState", ""); + registerSetting("MainWindowGeometry", ""); + + registerSetting("ConsoleWindowState", ""); + registerSetting("ConsoleWindowGeometry", ""); + + registerSetting("SettingsGeometry", ""); + + registerSetting("PagedGeometry", ""); + + registerSetting("NewInstanceGeometry", ""); + + registerSetting("UpdateDialogGeometry", ""); + + registerSetting("JREClientDialogGeometry", ""); + + // paste.ee API key + registerSetting("PasteEEAPIKey", "multimc"); + + if(!BuildConfig.ANALYTICS_ID.isEmpty()) + { + // Analytics + registerSetting("Analytics", true); + registerSetting("AnalyticsSeen", 0); + registerSetting("AnalyticsClientID", QString()); + } +} diff --git a/launcher/ApplicationSettings.h b/launcher/ApplicationSettings.h new file mode 100644 index 0000000000..66a90c787b --- /dev/null +++ b/launcher/ApplicationSettings.h @@ -0,0 +1,10 @@ +#pragma once + +#include "settings/INISettingsObject.h" + +class ApplicationSettings: public INISettingsObject { + Q_OBJECT +public: + ApplicationSettings(const QString & path, QObject * parent = nullptr); + virtual ~ApplicationSettings() = default; +}; diff --git a/launcher/ApplicationThemes.cpp b/launcher/ApplicationThemes.cpp new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/launcher/ApplicationThemes.cpp @@ -0,0 +1 @@ + diff --git a/launcher/ApplicationThemes.h b/launcher/ApplicationThemes.h new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/launcher/ApplicationThemes.h @@ -0,0 +1 @@ + diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 42bb02d4e4..ee9a76ad85 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -352,8 +352,14 @@ set(MINECRAFT_SOURCES minecraft/services/SkinDelete.cpp minecraft/services/SkinDelete.h - mojang/PackageManifest.h + mojang/ComponentsManifest.cpp + mojang/ComponentsManifest.h + mojang/PackageInstallTask.cpp + mojang/PackageInstallTask.h mojang/PackageManifest.cpp + mojang/PackageManifest.h + mojang/Path.cpp + mojang/Path.h ) add_unit_test(GradleSpecifier @@ -568,10 +574,12 @@ SET(LAUNCHER_SOURCES # Application base Application.h Application.cpp - UpdateController.cpp - UpdateController.h ApplicationMessage.h ApplicationMessage.cpp + ApplicationSettings.cpp + ApplicationSettings.h + UpdateController.cpp + UpdateController.h # GUI - general utilities DesktopServices.h @@ -653,6 +661,11 @@ SET(LAUNCHER_SOURCES JavaCommon.h JavaCommon.cpp + jreclient/JREClient.h + jreclient/JREClient.cpp + jreclient/JREClientDialog.h + jreclient/JREClientDialog.cpp + # GUI - paged dialog base ui/pages/BasePage.h ui/pages/BasePageContainer.h @@ -886,6 +899,8 @@ qt_wrap_ui(LAUNCHER_UI ui/dialogs/EditAccountDialog.ui ui/dialogs/CreateShortcutDialog.ui ui/dialogs/ModrinthExportDialog.ui + + jreclient/JREClientDialog.ui ) qt_add_resources(LAUNCHER_RESOURCES @@ -937,6 +952,7 @@ target_link_libraries(Launcher_logic ) target_link_libraries(Launcher_logic secrets) +target_include_directories(Launcher_logic PUBLIC .) add_executable(${Launcher_Name} MACOSX_BUNDLE WIN32 main.cpp ${LAUNCHER_RCS}) target_link_libraries(${Launcher_Name} Launcher_logic) diff --git a/launcher/MMCStrings.cpp b/launcher/MMCStrings.cpp index dc91c8d6a1..3e23bd18cd 100644 --- a/launcher/MMCStrings.cpp +++ b/launcher/MMCStrings.cpp @@ -9,7 +9,7 @@ static inline QChar getNextChar(const QString &s, int location) /// TAKEN FROM Qt, because it doesn't expose it intelligently int Strings::naturalCompare(const QString &s1, const QString &s2, Qt::CaseSensitivity cs) { - for (int l1 = 0, l2 = 0; l1 <= s1.count() && l2 <= s2.count(); ++l1, ++l2) + for (int l1 = 0, l2 = 0; l1 <= s1.length() && l2 <= s2.length(); ++l1, ++l2) { // skip spaces, tabs and 0's QChar c1 = getNextChar(s1, l1); diff --git a/launcher/java/JavaInstall.cpp b/launcher/java/JavaInstall.cpp index 5bcf7bcbf7..4c48916373 100644 --- a/launcher/java/JavaInstall.cpp +++ b/launcher/java/JavaInstall.cpp @@ -3,9 +3,19 @@ bool JavaInstall::operator<(const JavaInstall &rhs) { + // prefer remote + if(remote < rhs.remote) { + return true; + } + if(remote > rhs.remote) { + return false; + } + // FIXME: make this prefer native arch auto archCompare = Strings::naturalCompare(arch, rhs.arch, Qt::CaseInsensitive); if(archCompare != 0) return archCompare < 0; + + // FIXME: make this a version compare if(id < rhs.id) { return true; @@ -19,7 +29,7 @@ bool JavaInstall::operator<(const JavaInstall &rhs) bool JavaInstall::operator==(const JavaInstall &rhs) { - return arch == rhs.arch && id == rhs.id && path == rhs.path; + return arch == rhs.arch && id == rhs.id && path == rhs.path && remote == rhs.remote; } bool JavaInstall::operator>(const JavaInstall &rhs) diff --git a/launcher/java/JavaInstall.h b/launcher/java/JavaInstall.h index 64be40d190..14dbfa6a1e 100644 --- a/launcher/java/JavaInstall.h +++ b/launcher/java/JavaInstall.h @@ -33,6 +33,10 @@ struct JavaInstall : public BaseVersion QString arch; QString path; bool recommended = false; + + bool remote = false; + QString url; + QString installRoot; }; typedef std::shared_ptr JavaInstallPtr; diff --git a/launcher/java/JavaInstallList.cpp b/launcher/java/JavaInstallList.cpp index 88e1ca96a8..ebcc98ba4f 100644 --- a/launcher/java/JavaInstallList.cpp +++ b/launcher/java/JavaInstallList.cpp @@ -24,6 +24,7 @@ #include "java/JavaUtils.h" #include "MMCStrings.h" #include "minecraft/VersionFilterData.h" +#include "sys.h" JavaInstallList::JavaInstallList(QObject *parent) : BaseVersionList(parent) { @@ -191,16 +192,72 @@ void JavaListLoadTask::javaCheckerFinished() } } + auto kernelInfo = Sys::getKernelInfo(); + + // FIXME: data-drive + // FIXME: architecture being '32' or '64' is dumb + // FIXME: limited. + JavaInstallPtr remoteJava; + if(kernelInfo.kernelName == "Windows") { + remoteJava.reset(new JavaInstall()); + remoteJava->id = "1.8.0_51"; + if(Sys::isSystem64bit()) { + remoteJava->arch = "64"; + remoteJava->url = "https://launchermeta.mojang.com/v1/packages/ddc568a50326d2cf85765abb61e752aab191c366/manifest.json"; + } + else { + remoteJava->arch = "32"; + remoteJava->url = "https://launchermeta.mojang.com/v1/packages/baa62193c2785f54d877d871d9859c67d65f08ba/manifest.json"; + } + } + + if(kernelInfo.kernelName == "Linux") { + remoteJava.reset(new JavaInstall()); + remoteJava->id = "1.8.0_202"; + if(Sys::isSystem64bit()) { + remoteJava->arch = "64"; + remoteJava->url = "https://launchermeta.mojang.com/v1/packages/a1c15cc788f8893fba7e988eb27404772f699a84/manifest.json"; + } + else { + remoteJava->arch = "32"; + remoteJava->url = "https://launchermeta.mojang.com/v1/packages/64c6a0b8e3427c6c3f3ce82729aada8b2634a955/manifest.json"; + } + } + + if(kernelInfo.kernelName == "Darwin") { + if(Sys::isSystem64bit()) { + remoteJava.reset(new JavaInstall()); + remoteJava->id = "1.8.0_74"; + remoteJava->arch = "64"; + remoteJava->url = "https://launchermeta.mojang.com/v1/packages/341663b48a0d4e1c448dc789463fced6ba0962e1/manifest.json"; + } + } + + if(remoteJava) { + remoteJava->remote = true; + remoteJava->recommended = true; + + // Example: "runtimes/Windows/64/1.8.0_51" + auto rootPath = QString("runtimes/%1/%2/%3").arg(kernelInfo.kernelName, remoteJava->arch, remoteJava->id.toString()); + remoteJava->installRoot = rootPath; + + if(kernelInfo.kernelName == "Windows") { + remoteJava->path = rootPath + "/data/bin/javaw.exe"; + } + else if (kernelInfo.kernelName == "Darwin") { + remoteJava->path = rootPath + "/data/jre.bundle/Contents/Home/bin/java"; + } + else if (kernelInfo.kernelName == "Linux") { + remoteJava->path = rootPath + "/data/bin/java"; + } + candidates.append(remoteJava); + } + + QList javas_bvp; for (auto java : candidates) { - //qDebug() << java->id << java->arch << " at " << java->path; - BaseVersionPtr bp_java = std::dynamic_pointer_cast(java); - - if (bp_java) - { - javas_bvp.append(java); - } + javas_bvp.append(java); } m_list->updateListData(javas_bvp); diff --git a/launcher/jreclient/JREClient.cpp b/launcher/jreclient/JREClient.cpp new file mode 100644 index 0000000000..13fdf7381e --- /dev/null +++ b/launcher/jreclient/JREClient.cpp @@ -0,0 +1,16 @@ +/* + * Copyright 2023 Petr Mrázek + * + * This source is subject to the Microsoft Permissive License (MS-PL). + * Please see the COPYING.md file for more information. + */ + +#include "JREClient.h" + +JREClient::JREClient(QObject* parent) : QObject(parent) { +} + +void JREClient::refreshManifest() { + +} + diff --git a/launcher/jreclient/JREClient.h b/launcher/jreclient/JREClient.h new file mode 100644 index 0000000000..9a4d42989a --- /dev/null +++ b/launcher/jreclient/JREClient.h @@ -0,0 +1,32 @@ +/* + * Copyright 2023 Petr Mrázek + * + * This source is subject to the Microsoft Permissive License (MS-PL). + * Please see the COPYING.md file for more information. + */ + +#pragma once + +#include +#include +#include + +class JREClient: public QObject { +protected: + Q_OBJECT + +public: + JREClient(QObject * parent); + virtual ~JREClient() = default; + + using Ptr = shared_qobject_ptr; + + void refreshManifest(); + +public signals: + void busyChanged(bool busy); + +private: + Task::Ptr m_manifestTask; + Task::Ptr m_runtimeTask; +}; diff --git a/launcher/jreclient/JREClientDialog.cpp b/launcher/jreclient/JREClientDialog.cpp new file mode 100644 index 0000000000..489845b5cb --- /dev/null +++ b/launcher/jreclient/JREClientDialog.cpp @@ -0,0 +1,33 @@ +/* + * Copyright 2023 Petr Mrázek + * + * This source is subject to the Microsoft Permissive License (MS-PL). + * Please see the COPYING.md file for more information. + */ + +#include "JREClientDialog.h" +#include "ui_JREClientDialog.h" +#include +#include "Application.h" +#include +#include + +#include "BuildConfig.h" + +JREClientDialog::JREClientDialog(QWidget *parent) : QDialog(parent), ui(new Ui::JREClientDialog) +{ + ui->setupUi(this); + client.reset(new JREClient(this)); + restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("JREClientDialogGeometry").toByteArray())); +} + +JREClientDialog::~JREClientDialog() +{ +} + +void JREClientDialog::closeEvent(QCloseEvent* evt) +{ + APPLICATION->settings()->set("JREClientDialogGeometry", saveGeometry().toBase64()); + QDialog::closeEvent(evt); +} + diff --git a/launcher/jreclient/JREClientDialog.h b/launcher/jreclient/JREClientDialog.h new file mode 100644 index 0000000000..08c905c845 --- /dev/null +++ b/launcher/jreclient/JREClientDialog.h @@ -0,0 +1,34 @@ +/* + * Copyright 2023 Petr Mrázek + * + * This source is subject to the Microsoft Permissive License (MS-PL). + * Please see the COPYING.md file for more information. + */ + +#pragma once + +#include +#include "net/NetJob.h" +#include "JREClient.h" + +namespace Ui +{ +class JREClientDialog; +} + + +class JREClientDialog : public QDialog +{ + Q_OBJECT + +public: + explicit JREClientDialog(QWidget *parent = 0); + ~JREClientDialog(); + +protected: + void closeEvent(QCloseEvent * ) override; + +private: + Ui::JREClientDialog *ui; + JREClient::Ptr client; +}; diff --git a/launcher/jreclient/JREClientDialog.ui b/launcher/jreclient/JREClientDialog.ui new file mode 100644 index 0000000000..2b48b9071e --- /dev/null +++ b/launcher/jreclient/JREClientDialog.ui @@ -0,0 +1,22 @@ + + + JREClientDialog + + + + 0 + 0 + 1250 + 1269 + + + + Java Runtime Client + + + + + + + + diff --git a/launcher/mojang/ComponentsManifest.cpp b/launcher/mojang/ComponentsManifest.cpp new file mode 100644 index 0000000000..91249065f9 --- /dev/null +++ b/launcher/mojang/ComponentsManifest.cpp @@ -0,0 +1,113 @@ +/* + * Copyright 2023 Petr Mrázek + * + * This source is subject to the Microsoft Permissive License (MS-PL). + * Please see the COPYING.md file for more information. + */ + +#include "ComponentsManifest.h" + +#include +#include + +// https://launchermeta.mojang.com/v1/products/java-runtime/2ec0cc96c44e5a76b9c8b7c39df7210883d12871/all.json + +namespace mojang_files { + +namespace { +void fromJson(QJsonDocument & doc, AllPlatformsManifest & out) { + if (!doc.isObject()) + { + throw JSONValidationError("file manifest is not an object"); + } + QJsonObject root = doc.object(); + + auto platformIter = root.begin(); + while (platformIter != root.end()) { + QString platformName = platformIter.key(); + auto platformValue = platformIter.value(); + platformIter++; + if (!platformValue.isObject()) + { + throw JSONValidationError("platform entry inside manifest is not an object: " + platformName); + } + auto platformObject = platformValue.toObject(); + auto componentIter = platformObject.begin(); + ComponentsPlatform outPlatform; + while (componentIter != platformObject.end()) { + QString componentName = componentIter.key(); + auto componentValue = componentIter.value(); + componentIter++; + if (!componentValue.isArray()) + { + throw JSONValidationError("component version list inside manifest is not an array: " + componentName); + } + auto versionArray = componentValue.toArray(); + VersionList outVersionList; + int i = 0; + for (auto versionValue: versionArray) { + if (!versionValue.isObject()) + { + throw JSONValidationError("version is not an object: " + componentName + "[" + QString::number(i) + "]"); + } + i++; + auto versionObject = versionValue.toObject(); + ComponentVersion outVersion; + auto availaibility = Json::requireObject(versionObject, "availability"); + outVersion.availability_group = Json::requireInteger(availaibility, "group"); + outVersion.availability_progress = Json::requireInteger(availaibility, "progress"); + auto manifest = Json::requireObject(versionObject, "manifest"); + outVersion.manifest_sha1 = Json::requireString(manifest, "sha1"); + outVersion.manifest_size = Json::requireInteger(manifest, "size"); + outVersion.manifest_url = Json::requireUrl(manifest, "url"); + auto version = Json::requireObject(versionObject, "version"); + outVersion.version_name = Json::requireString(version, "name"); + outVersion.version_released = Json::requireDateTime(version, "released"); + outVersionList.versions.push_back(outVersion); + } + if(outVersionList.versions.size()) { + outPlatform.components[componentName] = std::move(outVersionList); + } + } + if(outPlatform.components.size()) { + out.platforms[platformName] = outPlatform; + } + } + out.valid = true; +} +} + +AllPlatformsManifest AllPlatformsManifest::fromManifestContents(const QByteArray& contents) +{ + AllPlatformsManifest out; + try + { + auto doc = Json::requireDocument(contents, "AllPlatformsManifest"); + fromJson(doc, out); + return out; + } + catch (const Exception &e) + { + qDebug() << QString("Unable to parse manifest: %1").arg(e.cause()); + out.valid = false; + return out; + } +} + +AllPlatformsManifest AllPlatformsManifest::fromManifestFile(const QString & filename) { + AllPlatformsManifest out; + try + { + auto doc = Json::requireDocument(filename, filename); + fromJson(doc, out); + return out; + } + catch (const Exception &e) + { + qDebug() << QString("Unable to parse manifest file %1: %2").arg(filename, e.cause()); + out.valid = false; + return out; + } +} + +} diff --git a/launcher/mojang/ComponentsManifest.h b/launcher/mojang/ComponentsManifest.h new file mode 100644 index 0000000000..be0cc5b6cf --- /dev/null +++ b/launcher/mojang/ComponentsManifest.h @@ -0,0 +1,55 @@ +/* + * Copyright 2023 Petr Mrázek + * + * This source is subject to the Microsoft Permissive License (MS-PL). + * Please see the COPYING.md file for more information. + */ + +#pragma once +#include +#include +#include +#include +#include +#include +#include "tasks/Task.h" + + +#include "Path.h" + +namespace mojang_files { + +struct ComponentVersion { + int availability_group = 0; + int availability_progress = 0; + + QString manifest_sha1; + size_t manifest_size = 0; + QUrl manifest_url; + + QString version_name; + QDateTime version_released; +}; + +struct VersionList { + std::vector versions; +}; + +struct ComponentsPlatform { + std::map components; +}; + +struct AllPlatformsManifest { + static AllPlatformsManifest fromManifestFile(const QString &path); + static AllPlatformsManifest fromManifestContents(const QByteArray& contents); + + explicit operator bool() const + { + return valid; + } + + std::map platforms; + bool valid = false; +}; + +} diff --git a/launcher/mojang/ComponentsManifest_test.cpp b/launcher/mojang/ComponentsManifest_test.cpp new file mode 100644 index 0000000000..4db9309f9a --- /dev/null +++ b/launcher/mojang/ComponentsManifest_test.cpp @@ -0,0 +1,110 @@ +/* + * Copyright 2023 Petr Mrázek + * + * This source is subject to the Microsoft Permissive License (MS-PL). + * Please see the COPYING.md file for more information. + */ + +#include +#include +#include "TestUtil.h" + +#include "mojang/ComponentsManifest.h" + +using namespace mojang_files; + +class ComponentsManifestTest : public QObject +{ + Q_OBJECT + +private slots: + void test_parse(); + void test_parse_file(); +}; + +namespace { +QByteArray basic_manifest = R"END( +{ + "gamecore": { + "java-runtime-alpha": [], + "jre-legacy": [], + "minecraft-java-exe": [] + }, + "linux": { + "java-runtime-alpha": [{ + "availability": { + "group": 5851, + "progress": 100 + }, + "manifest": { + "sha1": "e968e71afd3360e5032deac19e1c14d7aa32f5bb", + "size": 81882, + "url": "https://launchermeta.mojang.com/v1/packages/e968e71afd3360e5032deac19e1c14d7aa32f5bb/manifest.json" + }, + "version": { + "name": "16.0.1.9.1", + "released": "2021-05-10T16:43:02+00:00" + } + }], + "jre-legacy": [{ + "availability": { + "group": 6513, + "progress": 100 + }, + "manifest": { + "sha1": "a1c15cc788f8893fba7e988eb27404772f699a84", + "size": 125581, + "url": "https://launchermeta.mojang.com/v1/packages/a1c15cc788f8893fba7e988eb27404772f699a84/manifest.json" + }, + "version": { + "name": "8u202", + "released": "2020-11-17T19:26:25+00:00" + } + }], + "minecraft-java-exe": [] + } +} +)END"; +} + +void ComponentsManifestTest::test_parse() +{ + auto manifest = AllPlatformsManifest::fromManifestContents(basic_manifest); + QVERIFY(manifest.valid == true); + QVERIFY(manifest.platforms.count("gamecore") == 0); + QVERIFY(manifest.platforms.count("linux") == 1); + /* + QVERIFY(manifest.files.size() == 1); + QVERIFY(manifest.files.count(Path("a/b.txt"))); + auto &file = manifest.files[Path("a/b.txt")]; + QVERIFY(file.executable == true); + QVERIFY(file.hash == "da39a3ee5e6b4b0d3255bfef95601890afd80709"); + QVERIFY(file.size == 0); + QVERIFY(manifest.folders.size() == 4); + QVERIFY(manifest.folders.count(Path("."))); + QVERIFY(manifest.folders.count(Path("a"))); + QVERIFY(manifest.folders.count(Path("a/b"))); + QVERIFY(manifest.folders.count(Path("a/b/c"))); + QVERIFY(manifest.symlinks.size() == 1); + auto symlinkPath = Path("a/b/c.txt"); + QVERIFY(manifest.symlinks.count(symlinkPath)); + auto &symlink = manifest.symlinks[symlinkPath]; + QVERIFY(symlink == Path("../b.txt")); + QVERIFY(manifest.sources.size() == 1); + */ +} + +void ComponentsManifestTest::test_parse_file() { + auto path = QFINDTESTDATA("testdata/all.json"); + auto manifest = AllPlatformsManifest::fromManifestFile(path); + QVERIFY(manifest.valid == true); + QVERIFY(manifest.platforms.count("gamecore") == 0); + QVERIFY(manifest.platforms.count("linux") == 1); + /* + QVERIFY(manifest.sources.count("c725183c757011e7ba96c83c1e86ee7e8b516a2b") == 1); + */ +} + +QTEST_GUILESS_MAIN(ComponentsManifestTest) + +#include "ComponentsManifest_test.moc" diff --git a/launcher/mojang/PackageInstallTask.cpp b/launcher/mojang/PackageInstallTask.cpp new file mode 100644 index 0000000000..8d1ca24007 --- /dev/null +++ b/launcher/mojang/PackageInstallTask.cpp @@ -0,0 +1,260 @@ +/* + * Copyright 2023 Petr Mrázek + * + * This source is subject to the Microsoft Permissive License (MS-PL). + * Please see the COPYING.md file for more information. + */ + +#include "PackageInstallTask.h" + +#include +#include +#include +#include +#include + +#include +#include + +#include "PackageManifest.h" +#include + +using Package = mojang_files::Package; +using UpdateOperations = mojang_files::UpdateOperations; + +struct PackageInstallTaskData +{ + QString root; + QString version; + QString packageURL; + Net::Mode netmode; + QFuture inspectionFuture; + QFutureWatcher inspectionWatcher; + Package inspectedPackage; + + NetJob::Ptr manifestDownloadJob; + bool manifestDone = false; + Package downloadedPackage; + + UpdateOperations updateOps; + NetJob::Ptr mainDownloadJob; + shared_qobject_ptr network; +}; + +namespace { +class InspectFolder +{ +public: + InspectFolder(const QString & folder) : folder(folder) {} + Package operator()() + { + return Package::fromInspectedFolder(folder); + } +private: + QString folder; +}; + +class ParsingValidator : public Net::Validator +{ +public: /* con/des */ + ParsingValidator(Package &package) : m_package(package) + { + } + virtual ~ParsingValidator() = default; + +public: /* methods */ + bool init(QNetworkRequest &) override + { + m_package.valid = false; + return true; + } + bool write(QByteArray & data) override + { + this->data.append(data); + return true; + } + bool abort() override + { + return true; + } + bool validate(QNetworkReply &) override + { + m_package = Package::fromManifestContents(data); + return m_package.valid; + } + +private: /* data */ + QByteArray data; + Package &m_package; +}; + +} + +PackageInstallTask::PackageInstallTask( + shared_qobject_ptr network, + Net::Mode netmode, + QString version, + QString packageURL, + QString targetPath, + QObject* parent +) : Task(parent) { + d.reset(new PackageInstallTaskData); + d->network = network; + d->netmode = netmode; + d->root = targetPath; + d->packageURL = packageURL; + d->version = version; +} + +PackageInstallTask::~PackageInstallTask() {} + +void PackageInstallTask::executeTask() { + // inspect the data folder in a thread + d->inspectionFuture = QtConcurrent::run(QThreadPool::globalInstance(), InspectFolder(FS::PathCombine(d->root, "data"))); + connect(&d->inspectionWatcher, &QFutureWatcher::finished, this, &PackageInstallTask::inspectionFinished); + d->inspectionWatcher.setFuture(d->inspectionFuture); + + // while inspecting, grab the manifest from remote + d->manifestDownloadJob.reset(new NetJob(QObject::tr("Download of package manifest %1").arg(d->packageURL), d->network)); + auto url = d->packageURL; + auto dl = Net::Download::makeFile(url, FS::PathCombine(d->root, "manifest.json")); + /* + * The validator parses the file and loads it into the object. + * If that fails, the file is not written to storage. + */ + dl->addValidator(new ParsingValidator(d->downloadedPackage)); + d->manifestDownloadJob->addNetAction(dl); + auto job = d->manifestDownloadJob.get(); + connect(job, &NetJob::finished, this, &PackageInstallTask::manifestFetchFinished); + d->manifestDownloadJob->start(); +} + +void PackageInstallTask::inspectionFinished() { + d->inspectedPackage = d->inspectionWatcher.result(); + processInputs(); +} + +void PackageInstallTask::manifestFetchFinished() +{ + d->manifestDone = true; + d->manifestDownloadJob.reset(); + processInputs(); +} + +void PackageInstallTask::processInputs() +{ + if(!d->manifestDone) { + return; + } + if(!d->inspectionFuture.isFinished()) { + return; + } + + if(!d->downloadedPackage.valid) { + emitFailed("Downloading package manifest failed..."); + return; + } + + if(!d->inspectedPackage.valid) { + emitFailed("Inspecting local data folder failed..."); + return; + } + + d->updateOps = UpdateOperations::resolve(d->inspectedPackage, d->downloadedPackage); + + if(!d->updateOps.valid) { + emitFailed("Unable to determine update actions..."); + return; + } + + if(d->updateOps.empty()) { + emitSucceeded(); + } + + auto dataRoot = FS::PathCombine(d->root, "data"); + + // first, ensure data path exists + QDir temp; + temp.mkpath(dataRoot); + + for(auto & rm: d->updateOps.deletes) { + auto filePath = FS::PathCombine(dataRoot, rm.toString()); + qDebug() << "RM" << filePath; + QFile::remove(filePath); + } + + for(auto & rmdir: d->updateOps.rmdirs) { + auto folderPath = FS::PathCombine(dataRoot, rmdir.toString()); + qDebug() << "RMDIR" << folderPath; + QDir dir; + dir.rmdir(folderPath); + } + + for(auto & mkdir: d->updateOps.mkdirs) { + auto folderPath = FS::PathCombine(dataRoot, mkdir.toString()); + qDebug() << "MKDIR" << folderPath; + QDir dir; + dir.mkdir(folderPath); + } + + for(auto & mklink: d->updateOps.mklinks) { + auto linkPath = FS::PathCombine(dataRoot, mklink.first.toString()); + auto linkTarget = mklink.second.toString(); + qDebug() << "MKLINK" << linkPath << "->" << linkTarget; + QFile::link(linkTarget, linkPath); + } + + for(auto & fix: d->updateOps.executable_fixes) { + const auto &path = fix.first; + bool executable = fix.second; + auto targetPath = FS::PathCombine(dataRoot, path.toString()); + qDebug() << "FIX_EXEC" << targetPath << "->" << (executable ? "EXECUTABLE" : "REGULAR"); + + auto perms = QFile::permissions(targetPath); + if(executable) { + perms |= QFileDevice::ExeUser | QFileDevice::ExeGroup | QFileDevice::ExeOther; + } + else { + perms &= ~(QFileDevice::ExeUser | QFileDevice::ExeGroup | QFileDevice::ExeOther); + } + QFile::setPermissions(targetPath, perms); + } + + if(!d->updateOps.downloads.size()) { + emitSucceeded(); + return; + } + + // we download. + d->manifestDownloadJob.reset(new NetJob(QObject::tr("Download of files for %1").arg(d->packageURL), d->network)); + connect(d->manifestDownloadJob.get(), &NetJob::succeeded, this, &PackageInstallTask::downloadsSucceeded); + connect(d->manifestDownloadJob.get(), &NetJob::failed, this, &PackageInstallTask::downloadsFailed); + connect(d->manifestDownloadJob.get(), &NetJob::progress, [&](qint64 current, qint64 total) { + setProgress(current, total); + }); + + using Option = Net::Download::Option; + for(auto & download: d->updateOps.downloads) { + const auto &path = download.first; + const auto &object = download.second; + auto targetPath = FS::PathCombine(dataRoot, path.toString()); + auto dl = Net::Download::makeFile(object.url, targetPath, object.executable ? Option::SetExecutable : Option::NoOptions); + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, QByteArray::fromHex(object.hash.toLatin1()))); + dl->m_total_progress = object.size; + d->manifestDownloadJob->addNetAction(dl); + qDebug() << "DOWNLOAD" << object.url << "to" << targetPath << (object.executable ? "(EXECUTABLE)" : "(REGULAR)"); + } + d->manifestDownloadJob->start(); +} + +void PackageInstallTask::downloadsFailed(QString reason) +{ + d->manifestDownloadJob.reset(); + emitFailed(reason); +} + +void PackageInstallTask::downloadsSucceeded() +{ + d->manifestDownloadJob.reset(); + emitSucceeded(); +} diff --git a/launcher/mojang/PackageInstallTask.h b/launcher/mojang/PackageInstallTask.h new file mode 100644 index 0000000000..f8959ee1ff --- /dev/null +++ b/launcher/mojang/PackageInstallTask.h @@ -0,0 +1,55 @@ +/* + * Copyright 2023 Petr Mrázek + * + * This source is subject to the Microsoft Permissive License (MS-PL). + * Please see the COPYING.md file for more information. + */ + +#pragma once + +#include "tasks/Task.h" +#include "net/Mode.h" + +#include +#include + +struct PackageInstallTaskData; + +class PackageInstallTask : public Task +{ + Q_OBJECT +public: + enum class Mode + { + Launch, + Resolution + }; + +public: + explicit PackageInstallTask( + shared_qobject_ptr network, + Net::Mode netmode, + QString version, + QString packageURL, + QString targetPath, + QObject *parent = 0 + ); + virtual ~PackageInstallTask(); + +protected: + void executeTask() override; + +private slots: + void inspectionFinished(); + void manifestFetchFinished(); + + void processInputs(); + + void downloadsSucceeded(); + void downloadsFailed(QString reason); + +private: + std::unique_ptr d; +}; + + diff --git a/launcher/mojang/PackageManifest.cpp b/launcher/mojang/PackageManifest.cpp index 6c039438b8..bd048f84f9 100644 --- a/launcher/mojang/PackageManifest.cpp +++ b/launcher/mojang/PackageManifest.cpp @@ -1,3 +1,10 @@ +/* + * Copyright 2023 Petr Mrázek + * + * This source is subject to the Microsoft Permissive License (MS-PL). + * Please see the COPYING.md file for more information. + */ + /* * Copyright 2020 Petr Mrázek * @@ -74,8 +81,8 @@ void Package::addLink(const Path& path, const Path& target) { symlinks[path] = target; } -void Package::addSource(const FileSource& source) { - sources[source.hash] = source; +void Package::addSource(const Hash & rawHash, const FileSource& source) { + sources[rawHash] = source; } @@ -131,6 +138,8 @@ void fromJson(QJsonDocument & doc, Package & out) { } else if (compression == "lzma") { source.compression = Compression::Lzma; + // FIXME: remove this line when we implement LZMA filter for downloads again + continue; } else { continue; @@ -141,11 +150,10 @@ void fromJson(QJsonDocument & doc, Package & out) { throw JSONValidationError("No valid compression method for file " + iter.key()); } out.addFile(objectPath, file); - out.addSource(bestSource); + out.addSource(file.hash, bestSource); } else if(type == "link") { auto target = Json::requireString(fileObject, "target"); - out.symlinks[objectPath] = target; out.addLink(objectPath, target); } else { @@ -245,7 +253,8 @@ Package Package::fromInspectedFolder(const QString& folderPath) iterator.next(); auto fileInfo = iterator.fileInfo(); - auto relPath = root.relativeFilePath(fileInfo.filePath()); + auto itemPath = fileInfo.absoluteFilePath(); + auto relPath = root.relativeFilePath(itemPath); // FIXME: this is probably completely busted on Windows anyway, so just disable it. // Qt makes shit up and doesn't understand the platform details // TODO: Actually use a filesystem library that isn't terrible and has decen license. @@ -253,7 +262,7 @@ Package Package::fromInspectedFolder(const QString& folderPath) #ifndef Q_OS_WIN32 if(fileInfo.isSymLink()) { Path targetPath; - if(!actually_read_symlink_target(fileInfo.filePath(), targetPath)) { + if(!actually_read_symlink_target(fileInfo.absoluteFilePath(), targetPath)) { qCritical() << "Folder inspection: Unknown filesystem object:" << fileInfo.absoluteFilePath(); out.valid = false; } @@ -371,12 +380,21 @@ UpdateOperations UpdateOperations::resolve(const Package& from, const Package& t } } for(auto iter = to.files.begin(); iter != to.files.end(); iter++) { - auto path = iter->first; + auto & path = iter->first; + auto & file = iter->second; + auto & fileHash = file.hash; + auto executable = file.executable; + if(!to.sources.count(fileHash)) { + out.valid = false; + return out; + } + auto & source = to.sources.at(fileHash); + // it wasn't there before, it is there now... therefore we fill it in if(!from.files.count(path)) { out.downloads.emplace( std::pair{ path, - FileDownload(to.sources.at(iter->second.hash), iter->second.executable) + FileDownload(source, executable) } ); } @@ -418,13 +436,14 @@ UpdateOperations UpdateOperations::resolve(const Package& from, const Package& t const auto &new_target = iter2->second; if (current_target != new_target) { out.deletes.push_back(path); - out.mklinks[path] = iter2->second; + out.mklinks[path] = new_target; } } for(auto iter = to.symlinks.begin(); iter != to.symlinks.end(); iter++) { auto path = iter->first; + const auto &new_target = iter->second; if(!from.symlinks.count(path)) { - out.mklinks[path] = iter->second; + out.mklinks[path] = new_target; } } out.valid = true; diff --git a/launcher/mojang/PackageManifest.h b/launcher/mojang/PackageManifest.h index c495ac9816..e5a8ea0ed1 100644 --- a/launcher/mojang/PackageManifest.h +++ b/launcher/mojang/PackageManifest.h @@ -13,97 +13,13 @@ #include #include "tasks/Task.h" +#include "Path.h" + namespace mojang_files { using Hash = QString; extern const Hash empty_hash; -// simple-ish path implementation. assumes always relative and does not allow '..' entries -class Path -{ -public: - using parts_type = QStringList; - - Path() = default; - Path(QString string) { - auto parts_in = string.split('/'); - for(auto & part: parts_in) { - if(part.isEmpty() || part == ".") { - continue; - } - if(part == "..") { - if(parts.size()) { - parts.pop_back(); - } - continue; - } - parts.push_back(part); - } - } - - bool has_parent_path() const - { - return parts.size() > 0; - } - - Path parent_path() const - { - if (parts.empty()) - return Path(); - return Path(parts.begin(), std::prev(parts.end())); - } - - bool empty() const - { - return parts.empty(); - } - - int length() const - { - return parts.length(); - } - - bool operator==(const Path & rhs) const { - return parts == rhs.parts; - } - - bool operator!=(const Path & rhs) const { - return parts != rhs.parts; - } - - inline bool operator<(const Path& rhs) const - { - return compare(rhs) < 0; - } - - parts_type::const_iterator begin() const - { - return parts.begin(); - } - - parts_type::const_iterator end() const - { - return parts.end(); - } - - QString toString() const { - return parts.join("/"); - } - -private: - Path(const parts_type::const_iterator & start, const parts_type::const_iterator & end) { - auto cursor = start; - while(cursor != end) { - parts.push_back(*cursor); - cursor++; - } - } - int compare(const Path& p) const; - - parts_type parts; -}; - - enum class Compression { Raw, Lzma, @@ -146,7 +62,7 @@ struct Package { void addFolder(Path folder); void addFile(const Path & path, const File & file); void addLink(const Path & path, const Path & target); - void addSource(const FileSource & source); + void addSource(const Hash & rawHash, const FileSource & source); std::map sources; bool valid = true; @@ -173,6 +89,19 @@ struct UpdateOperations { std::map downloads; std::map mklinks; std::map executable_fixes; + + bool empty() const { + if(!valid) { + return true; + } + return + deletes.empty() && + rmdirs.empty() && + mkdirs.empty() && + downloads.empty() && + mklinks.empty() && + executable_fixes.empty(); + } }; } diff --git a/launcher/mojang/PackageManifest_test.cpp b/launcher/mojang/PackageManifest_test.cpp index 47c7cbbf42..8134b8d6c1 100644 --- a/launcher/mojang/PackageManifest_test.cpp +++ b/launcher/mojang/PackageManifest_test.cpp @@ -93,6 +93,7 @@ void PackageManifestTest::test_parse_file() { auto path = QFINDTESTDATA("testdata/1.8.0_202-x64.json"); auto manifest = Package::fromManifestFile(path); QVERIFY(manifest.valid == true); + QVERIFY(manifest.sources.count("c725183c757011e7ba96c83c1e86ee7e8b516a2b") == 1); } diff --git a/launcher/mojang/Path.cpp b/launcher/mojang/Path.cpp new file mode 100644 index 0000000000..dd631155d4 --- /dev/null +++ b/launcher/mojang/Path.cpp @@ -0,0 +1,8 @@ +/* + * Copyright 2023 Petr Mrázek + * + * This source is subject to the Microsoft Permissive License (MS-PL). + * Please see the COPYING.md file for more information. + */ + +// NOOP diff --git a/launcher/mojang/Path.h b/launcher/mojang/Path.h new file mode 100644 index 0000000000..bb2a8105ea --- /dev/null +++ b/launcher/mojang/Path.h @@ -0,0 +1,100 @@ +/* + * Copyright 2023 Petr Mrázek + * + * This source is subject to the Microsoft Permissive License (MS-PL). + * Please see the COPYING.md file for more information. + */ + +#pragma once + +#include +#include + +namespace mojang_files { + +// simple-ish path implementation. assumes always relative and does not allow '..' entries +class Path +{ +public: + using parts_type = QStringList; + + Path() = default; + Path(QString string) { + auto parts_in = string.split('/'); + for(auto & part: parts_in) { + if(part.isEmpty() || part == ".") { + continue; + } + if(part == "..") { + if(parts.size()) { + parts.pop_back(); + continue; + } + } + parts.push_back(part); + } + } + + bool has_parent_path() const + { + return parts.size() > 0; + } + + Path parent_path() const + { + if (parts.empty()) + return Path(); + return Path(parts.begin(), std::prev(parts.end())); + } + + bool empty() const + { + return parts.empty(); + } + + int length() const + { + return parts.length(); + } + + bool operator==(const Path & rhs) const { + return parts == rhs.parts; + } + + bool operator!=(const Path & rhs) const { + return parts != rhs.parts; + } + + inline bool operator<(const Path& rhs) const + { + return compare(rhs) < 0; + } + + parts_type::const_iterator begin() const + { + return parts.begin(); + } + + parts_type::const_iterator end() const + { + return parts.end(); + } + + QString toString() const { + return parts.join("/"); + } + +private: + Path(const parts_type::const_iterator & start, const parts_type::const_iterator & end) { + auto cursor = start; + while(cursor != end) { + parts.push_back(*cursor); + cursor++; + } + } + int compare(const Path& p) const; + + parts_type parts; +}; + +} diff --git a/launcher/mojang/testdata/all.json b/launcher/mojang/testdata/all.json new file mode 100644 index 0000000000..84f1f1300a --- /dev/null +++ b/launcher/mojang/testdata/all.json @@ -0,0 +1,186 @@ +{ + "gamecore": { + "java-runtime-alpha": [], + "jre-legacy": [], + "minecraft-java-exe": [] + }, + "linux": { + "java-runtime-alpha": [{ + "availability": { + "group": 5851, + "progress": 100 + }, + "manifest": { + "sha1": "e968e71afd3360e5032deac19e1c14d7aa32f5bb", + "size": 81882, + "url": "https://launchermeta.mojang.com/v1/packages/e968e71afd3360e5032deac19e1c14d7aa32f5bb/manifest.json" + }, + "version": { + "name": "16.0.1.9.1", + "released": "2021-05-10T16:43:02+00:00" + } + }], + "jre-legacy": [{ + "availability": { + "group": 6513, + "progress": 100 + }, + "manifest": { + "sha1": "a1c15cc788f8893fba7e988eb27404772f699a84", + "size": 125581, + "url": "https://launchermeta.mojang.com/v1/packages/a1c15cc788f8893fba7e988eb27404772f699a84/manifest.json" + }, + "version": { + "name": "8u202", + "released": "2020-11-17T19:26:25+00:00" + } + }], + "minecraft-java-exe": [] + }, + "linux-i386": { + "java-runtime-alpha": [], + "jre-legacy": [{ + "availability": { + "group": 4119, + "progress": 100 + }, + "manifest": { + "sha1": "64c6a0b8e3427c6c3f3ce82729aada8b2634a955", + "size": 126498, + "url": "https://launchermeta.mojang.com/v1/packages/64c6a0b8e3427c6c3f3ce82729aada8b2634a955/manifest.json" + }, + "version": { + "name": "8u202", + "released": "2020-11-17T19:28:39+00:00" + } + }], + "minecraft-java-exe": [] + }, + "mac-os": { + "java-runtime-alpha": [{ + "availability": { + "group": 3212, + "progress": 100 + }, + "manifest": { + "sha1": "5a480fde2214534ab0b51ae78c70455ffd7c0e6a", + "size": 95323, + "url": "https://launchermeta.mojang.com/v1/packages/5a480fde2214534ab0b51ae78c70455ffd7c0e6a/manifest.json" + }, + "version": { + "name": "16.0.1.9.1_3", + "released": "2021-05-11T15:03:25+00:00" + } + }], + "jre-legacy": [{ + "availability": { + "group": 8727, + "progress": 100 + }, + "manifest": { + "sha1": "341663b48a0d4e1c448dc789463fced6ba0962e1", + "size": 77382, + "url": "https://launchermeta.mojang.com/v1/packages/341663b48a0d4e1c448dc789463fced6ba0962e1/manifest.json" + }, + "version": { + "name": "8u74", + "released": "2020-11-17T19:06:44+00:00" + } + }], + "minecraft-java-exe": [] + }, + "windows-x64": { + "java-runtime-alpha": [{ + "availability": { + "group": 4219, + "progress": 100 + }, + "manifest": { + "sha1": "0586267ba40236e176925da17ca0d29dead3d30d", + "size": 145480, + "url": "https://launchermeta.mojang.com/v1/packages/0586267ba40236e176925da17ca0d29dead3d30d/manifest.json" + }, + "version": { + "name": "16.0.1.9.1", + "released": "2021-05-10T16:48:07+00:00" + } + }], + "jre-legacy": [{ + "availability": { + "group": 3654, + "progress": 100 + }, + "manifest": { + "sha1": "ddc568a50326d2cf85765abb61e752aab191c366", + "size": 78978, + "url": "https://launchermeta.mojang.com/v1/packages/ddc568a50326d2cf85765abb61e752aab191c366/manifest.json" + }, + "version": { + "name": "8u51", + "released": "2020-11-17T19:12:13+00:00" + } + }], + "minecraft-java-exe": [{ + "availability": { + "group": 23, + "progress": 100 + }, + "manifest": { + "sha1": "956d6567682740ab6327a1e8357658d8f7b86421", + "size": 455, + "url": "https://launchermeta.mojang.com/v1/packages/956d6567682740ab6327a1e8357658d8f7b86421/manifest.json" + }, + "version": { + "name": "14", + "released": "2021-03-25T22:47:03+00:00" + } + }] + }, + "windows-x86": { + "java-runtime-alpha": [{ + "availability": { + "group": 3889, + "progress": 100 + }, + "manifest": { + "sha1": "85c8e2653b5197bbb0fc9257932b670c630e1c77", + "size": 143786, + "url": "https://launchermeta.mojang.com/v1/packages/85c8e2653b5197bbb0fc9257932b670c630e1c77/manifest.json" + }, + "version": { + "name": "16.0.1.9.1", + "released": "2021-05-10T16:45:24+00:00" + } + }], + "jre-legacy": [{ + "availability": { + "group": 4866, + "progress": 100 + }, + "manifest": { + "sha1": "baa62193c2785f54d877d871d9859c67d65f08ba", + "size": 80613, + "url": "https://launchermeta.mojang.com/v1/packages/baa62193c2785f54d877d871d9859c67d65f08ba/manifest.json" + }, + "version": { + "name": "8u51", + "released": "2020-11-17T19:10:36+00:00" + } + }], + "minecraft-java-exe": [{ + "availability": { + "group": 5779, + "progress": 100 + }, + "manifest": { + "sha1": "5d5cf8b579ccf4a3567eef481b12479ee8c35369", + "size": 455, + "url": "https://launchermeta.mojang.com/v1/packages/5d5cf8b579ccf4a3567eef481b12479ee8c35369/manifest.json" + }, + "version": { + "name": "12", + "released": "2021-03-25T22:47:02+00:00" + } + }] + } +} diff --git a/launcher/net/ChecksumValidator.h b/launcher/net/ChecksumValidator.h index 0d6b19c21f..1142b21e60 100644 --- a/launcher/net/ChecksumValidator.h +++ b/launcher/net/ChecksumValidator.h @@ -34,7 +34,7 @@ class ChecksumValidator: public Validator { if(m_expected.size() && m_expected != hash()) { - qWarning() << "Checksum mismatch, download is bad."; + qWarning() << "Checksum mismatch. expected:" << m_expected << "got:" << hash(); return false; } return true; @@ -52,4 +52,4 @@ class ChecksumValidator: public Validator QCryptographicHash m_checksum; QByteArray m_expected; }; -} \ No newline at end of file +} diff --git a/launcher/net/Download.cpp b/launcher/net/Download.cpp index a4e3e50661..7709be46e5 100644 --- a/launcher/net/Download.cpp +++ b/launcher/net/Download.cpp @@ -59,7 +59,7 @@ Download::Ptr Download::makeFile(QUrl url, QString path, Options options) Download * dl = new Download(); dl->m_url = url; dl->m_options = options; - dl->m_sink.reset(new FileSink(path)); + dl->m_sink.reset(new FileSink(path, options & Option::SetExecutable)); return dl; } diff --git a/launcher/net/Download.h b/launcher/net/Download.h index 4478dc30de..5c1dfc02ff 100644 --- a/launcher/net/Download.h +++ b/launcher/net/Download.h @@ -32,7 +32,8 @@ class Download : public NetAction enum class Option { NoOptions = 0, - AcceptLocalFiles = 1 + AcceptLocalFiles = 1, + SetExecutable = 2 }; Q_DECLARE_FLAGS(Options, Option) diff --git a/launcher/net/FileSink.cpp b/launcher/net/FileSink.cpp index 7e9b8929f5..042b7edd1d 100644 --- a/launcher/net/FileSink.cpp +++ b/launcher/net/FileSink.cpp @@ -5,8 +5,8 @@ namespace Net { -FileSink::FileSink(QString filename) - :m_filename(filename) +FileSink::FileSink(QString filename, bool setExecutable) + :m_filename(filename), setExecutable(setExecutable) { // nil } @@ -94,6 +94,10 @@ JobStatus FileSink::finalize(QNetworkReply& reply) m_output_file->cancelWriting(); return Job_Failed; } + if(setExecutable) { + auto permissions = QFile::permissions(m_filename); + QFile::setPermissions(m_filename, permissions | QFileDevice::ExeUser | QFileDevice::ExeGroup | QFileDevice::ExeOther); + } } // then get rid of the save file m_output_file.reset(); diff --git a/launcher/net/FileSink.h b/launcher/net/FileSink.h index 875fe5110f..ca94a0b47c 100644 --- a/launcher/net/FileSink.h +++ b/launcher/net/FileSink.h @@ -6,7 +6,7 @@ namespace Net { class FileSink : public Sink { public: /* con/des */ - FileSink(QString filename); + FileSink(QString filename, bool setExecutable = false); virtual ~FileSink(); public: /* methods */ @@ -24,5 +24,6 @@ class FileSink : public Sink QString m_filename; bool wroteAnyData = false; std::unique_ptr m_output_file; + bool setExecutable = false; }; } diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index e3e22d8381..353e6d51e0 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -200,7 +200,6 @@ class MainWindow::Ui { public: TranslatedAction actionAddInstance; - //TranslatedAction actionRefresh; TranslatedAction actionCheckUpdate; TranslatedAction actionSettings; TranslatedAction actionPatreon; @@ -220,6 +219,7 @@ class MainWindow::Ui TranslatedAction actionDeleteInstance; TranslatedAction actionConfig_Folder; TranslatedAction actionCAT; + TranslatedAction actionJREs; TranslatedAction actionCopyInstance; TranslatedAction actionLaunchInstanceOffline; TranslatedAction actionScreenshots; @@ -421,6 +421,15 @@ class MainWindow::Ui all_actions.append(&actionCAT); mainToolBar->addAction(actionCAT); + actionJREs = TranslatedAction(MainWindow); + actionJREs->setObjectName(QStringLiteral("actionJREs")); + actionJREs->setIcon(APPLICATION->getThemedIcon("java")); + actionJREs.setTextId(QT_TRANSLATE_NOOP("MainWindow", "JREs")); + actionJREs.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the Java Runtime manager")); + actionJREs->setPriority(QAction::LowPriority); + all_actions.append(&actionJREs); + mainToolBar->addAction(actionJREs); + // profile menu and its actions actionManageAccounts = TranslatedAction(MainWindow); actionManageAccounts->setObjectName(QStringLiteral("actionManageAccounts")); @@ -1681,6 +1690,11 @@ void MainWindow::on_actionSettings_triggered() APPLICATION->ShowGlobalSettings(this, "global-settings"); } +void MainWindow::on_actionJREs_triggered() +{ + APPLICATION->ShowJREs(this); +} + void MainWindow::globalSettingsClosed() { // FIXME: quick HACK to make this work. improve, optimize. diff --git a/launcher/ui/MainWindow.h b/launcher/ui/MainWindow.h index 685adba983..d9c599a046 100644 --- a/launcher/ui/MainWindow.h +++ b/launcher/ui/MainWindow.h @@ -103,6 +103,8 @@ private slots: void on_actionSettings_triggered(); + void on_actionJREs_triggered(); + void on_actionInstanceSettings_triggered(); void on_actionManageAccounts_triggered(); diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt new file mode 100644 index 0000000000..9dc14e05b5 --- /dev/null +++ b/tools/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(grabjre) diff --git a/tools/grabjre/CMakeLists.txt b/tools/grabjre/CMakeLists.txt new file mode 100644 index 0000000000..5cd081f0b8 --- /dev/null +++ b/tools/grabjre/CMakeLists.txt @@ -0,0 +1,2 @@ +add_executable(GrabJRE GrabJRE.cpp) +target_link_libraries(GrabJRE Launcher_logic) diff --git a/tools/grabjre/GrabJRE.cpp b/tools/grabjre/GrabJRE.cpp new file mode 100644 index 0000000000..0019651574 --- /dev/null +++ b/tools/grabjre/GrabJRE.cpp @@ -0,0 +1,58 @@ +#include +#include + +#include "mojang/PackageManifest.h" +#include "mojang/PackageInstallTask.h" +#include + +int main(int argc, char ** argv) { + QCoreApplication app(argc, argv); + QCoreApplication::setApplicationName("GrabJRE"); + QCoreApplication::setApplicationVersion("1.0"); + + QCommandLineParser parser; + parser.setApplicationDescription("Stupid thing that grabs a piston package and updates a local folder with it"); + parser.addHelpOption(); + parser.addVersionOption(); + + parser.addPositionalArgument("url", "Source URL"); + parser.addPositionalArgument("version", "Source version"); + parser.addPositionalArgument("destination", "Destination folder to update"); + + parser.process(app); + + const QStringList args = parser.positionalArguments(); + if (args.size() != 3) { + parser.showHelp(1); + } + + // Run like ./GrabJRE "https://launchermeta.mojang.com/v1/packages/a1c15cc788f8893fba7e988eb27404772f699a84/manifest.json" "1.8.0_202" "test" + + shared_qobject_ptr nam = new QNetworkAccessManager(); + auto url = args[0]; + auto version = args[1]; + auto destination = args[2]; + PackageInstallTask installTask( + nam, + Net::Mode::Online, + version, + url, + destination + ); + installTask.start(); + QCoreApplication::connect(&installTask, &PackageInstallTask::progress, [&](qint64 now, qint64 total) { + static int percentage = 0; + if(total > 0) { + int newPercentage = (now * 100.0f) / double(total); + if(newPercentage != percentage) { + percentage = newPercentage; + QTextStream(stdout) << "Downloading: " << percentage << "% done\n"; + } + } + }); + QCoreApplication::connect(&installTask, &PackageInstallTask::finished, [&]() { + app.exit(installTask.wasSuccessful() ? 0 : 1); + }); + app.exec(); + return 0; +}