NOISSUE Some work on JRE downloading

This commit is contained in:
Petr Mrázek 2020-10-13 21:46:09 +02:00
parent 9f3c2921f2
commit afd19099ef
38 changed files with 1314 additions and 311 deletions

View File

@ -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()

View File

@ -77,6 +77,8 @@
#include <sys.h>
#include <Secrets.h>
#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<GenericPageProvider>(tr("Settings"));
m_globalSettingsProvider->addPage<LauncherPage>();
m_globalSettingsProvider->addPage<MinecraftPage>();
m_globalSettingsProvider->addPage<JavaPage>();
m_globalSettingsProvider->addPage<LanguagePage>();
m_globalSettingsProvider->addPage<CustomCommandsPage>();
m_globalSettingsProvider->addPage<ProxyPage>();
m_globalSettingsProvider->addPage<ExternalToolsPage>();
m_globalSettingsProvider->addPage<AccountListPage>();
m_globalSettingsProvider->addPage<PasteEEPage>();
}
qDebug() << "<> Settings loaded.";
}
initializeSettings();
#ifndef QT_NO_ACCESSIBILITY
QAccessible::installFactory(groupViewAccessibleFactory);
@ -884,44 +731,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
}
// Initialize analytics
[this]()
{
const int analyticsVersion = 2;
if(BuildConfig.ANALYTICS_ID.isEmpty())
{
return;
}
auto analyticsSetting = m_settings->getSetting("Analytics");
connect(analyticsSetting.get(), &Setting::SettingChanged, this, &Application::analyticsSettingChanged);
QString clientID = m_settings->get("AnalyticsClientID").toString();
if(clientID.isEmpty())
{
clientID = QUuid::createUuid().toString();
clientID.remove(QLatin1Char('{'));
clientID.remove(QLatin1Char('}'));
m_settings->set("AnalyticsClientID", clientID);
}
m_analytics = new GAnalytics(BuildConfig.ANALYTICS_ID, clientID, analyticsVersion, this);
m_analytics->setLogLevel(GAnalytics::Debug);
m_analytics->setAnonymizeIPs(true);
// FIXME: the ganalytics library has no idea about our fancy shared pointers...
m_analytics->setNetworkAccessManager(network().get());
if(m_settings->get("AnalyticsSeen").toInt() < m_analytics->version())
{
qDebug() << "Analytics info not seen by user yet (or old version).";
return;
}
if(!m_settings->get("Analytics").toBool())
{
qDebug() << "Analytics disabled by user.";
return;
}
m_analytics->enable();
qDebug() << "<> Initialized analytics with tid" << BuildConfig.ANALYTICS_ID;
}();
initializeAnalytics();
if(createSetupWizard())
{
@ -930,6 +740,68 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
performMainStartupAction();
}
void Application::initializeAnalytics()
{
const int analyticsVersion = 2;
if(BuildConfig.ANALYTICS_ID.isEmpty())
{
return;
}
auto analyticsSetting = m_settings->getSetting("Analytics");
connect(analyticsSetting.get(), &Setting::SettingChanged, this, &Application::analyticsSettingChanged);
QString clientID = m_settings->get("AnalyticsClientID").toString();
if(clientID.isEmpty())
{
clientID = QUuid::createUuid().toString();
clientID.remove(QLatin1Char('{'));
clientID.remove(QLatin1Char('}'));
m_settings->set("AnalyticsClientID", clientID);
}
m_analytics = new GAnalytics(BuildConfig.ANALYTICS_ID, clientID, analyticsVersion, this);
m_analytics->setLogLevel(GAnalytics::Debug);
m_analytics->setAnonymizeIPs(true);
// FIXME: the ganalytics library has no idea about our fancy shared pointers...
m_analytics->setNetworkAccessManager(network().get());
if(m_settings->get("AnalyticsSeen").toInt() < m_analytics->version())
{
qDebug() << "Analytics info not seen by user yet (or old version).";
return;
}
if(!m_settings->get("Analytics").toBool())
{
qDebug() << "Analytics disabled by user.";
return;
}
m_analytics->enable();
qDebug() << "<> Initialized analytics with tid" << BuildConfig.ANALYTICS_ID;
}
void Application::initializeSettings()
{
// Initialize application settings
m_settings.reset(new ApplicationSettings(BuildConfig.LAUNCHER_CONFIGFILE, this));
// Init page provider
{
m_globalSettingsProvider = std::make_shared<GenericPageProvider>(tr("Settings"));
m_globalSettingsProvider->addPage<LauncherPage>();
m_globalSettingsProvider->addPage<MinecraftPage>();
m_globalSettingsProvider->addPage<JavaPage>();
m_globalSettingsProvider->addPage<LanguagePage>();
m_globalSettingsProvider->addPage<CustomCommandsPage>();
m_globalSettingsProvider->addPage<ProxyPage>();
m_globalSettingsProvider->addPage<ExternalToolsPage>();
m_globalSettingsProvider->addPage<AccountListPage>();
m_globalSettingsProvider->addPage<PasteEEPage>();
}
qDebug() << "<> Settings loaded.";
}
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)

View File

@ -142,6 +142,7 @@ public:
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();

View File

@ -0,0 +1,146 @@
#include "ApplicationSettings.h"
#include <BuildConfig.h>
#include <QFontInfo>
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());
}
}

View File

@ -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;
};

View File

@ -0,0 +1 @@

View File

@ -0,0 +1 @@

View File

@ -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)

View File

@ -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);

View File

@ -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)

View File

@ -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<JavaInstall> JavaInstallPtr;

View File

@ -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<BaseVersionPtr> javas_bvp;
for (auto java : candidates)
{
//qDebug() << java->id << java->arch << " at " << java->path;
BaseVersionPtr bp_java = std::dynamic_pointer_cast<BaseVersion>(java);
if (bp_java)
{
javas_bvp.append(java);
}
javas_bvp.append(java);
}
m_list->updateListData(javas_bvp);

View File

@ -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() {
}

View File

@ -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 <QObject>
#include <QObjectPtr.h>
#include <tasks/Task.h>
class JREClient: public QObject {
protected:
Q_OBJECT
public:
JREClient(QObject * parent);
virtual ~JREClient() = default;
using Ptr = shared_qobject_ptr<JREClient>;
void refreshManifest();
public signals:
void busyChanged(bool busy);
private:
Task::Ptr m_manifestTask;
Task::Ptr m_runtimeTask;
};

View File

@ -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 <QDebug>
#include "Application.h"
#include <settings/SettingsObject.h>
#include <Json.h>
#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);
}

View File

@ -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 <QDialog>
#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;
};

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>JREClientDialog</class>
<widget class="QDialog" name="JREClientDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1250</width>
<height>1269</height>
</rect>
</property>
<property name="windowTitle">
<string>Java Runtime Client</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout"/>
</widget>
<resources>
<include location="../resources/multimc/multimc.qrc"/>
</resources>
<connections/>
</ui>

View File

@ -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 <Json.h>
#include <QDebug>
// 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;
}
}
}

View File

@ -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 <QString>
#include <map>
#include <set>
#include <QStringList>
#include <QDateTime>
#include <QUrl>
#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<ComponentVersion> versions;
};
struct ComponentsPlatform {
std::map<QString, VersionList> components;
};
struct AllPlatformsManifest {
static AllPlatformsManifest fromManifestFile(const QString &path);
static AllPlatformsManifest fromManifestContents(const QByteArray& contents);
explicit operator bool() const
{
return valid;
}
std::map<QString, ComponentsPlatform> platforms;
bool valid = false;
};
}

View File

@ -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 <QTest>
#include <QDebug>
#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"

View File

@ -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 <QThread>
#include <QFuture>
#include <QFutureWatcher>
#include <QRunnable>
#include <QtConcurrent>
#include <net/NetJob.h>
#include <net/ChecksumValidator.h>
#include "PackageManifest.h"
#include <FileSystem.h>
using Package = mojang_files::Package;
using UpdateOperations = mojang_files::UpdateOperations;
struct PackageInstallTaskData
{
QString root;
QString version;
QString packageURL;
Net::Mode netmode;
QFuture<Package> inspectionFuture;
QFutureWatcher<Package> inspectionWatcher;
Package inspectedPackage;
NetJob::Ptr manifestDownloadJob;
bool manifestDone = false;
Package downloadedPackage;
UpdateOperations updateOps;
NetJob::Ptr mainDownloadJob;
shared_qobject_ptr<QNetworkAccessManager> 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<QNetworkAccessManager> 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<Package>::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();
}

View File

@ -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 <memory>
#include <net/NetJob.h>
struct PackageInstallTaskData;
class PackageInstallTask : public Task
{
Q_OBJECT
public:
enum class Mode
{
Launch,
Resolution
};
public:
explicit PackageInstallTask(
shared_qobject_ptr<QNetworkAccessManager> 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<PackageInstallTaskData> d;
};

View File

@ -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>{
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;

View File

@ -13,97 +13,13 @@
#include <QStringList>
#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<Hash, FileSource> sources;
bool valid = true;
@ -173,6 +89,19 @@ struct UpdateOperations {
std::map<Path, FileDownload> downloads;
std::map<Path, Path> mklinks;
std::map<Path, bool> executable_fixes;
bool empty() const {
if(!valid) {
return true;
}
return
deletes.empty() &&
rmdirs.empty() &&
mkdirs.empty() &&
downloads.empty() &&
mklinks.empty() &&
executable_fixes.empty();
}
};
}

View File

@ -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);
}

8
launcher/mojang/Path.cpp Normal file
View File

@ -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

100
launcher/mojang/Path.h Normal file
View File

@ -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 <QString>
#include <QStringList>
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;
};
}

BIN
launcher/mojang/testdata/all.json vendored Normal file

Binary file not shown.

View File

@ -34,7 +34,7 @@ public: /* methods */
{
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 @@ private: /* data */
QCryptographicHash m_checksum;
QByteArray m_expected;
};
}
}

View File

@ -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;
}

View File

@ -32,7 +32,8 @@ public: /* types */
enum class Option
{
NoOptions = 0,
AcceptLocalFiles = 1
AcceptLocalFiles = 1,
SetExecutable = 2
};
Q_DECLARE_FLAGS(Options, Option)

View File

@ -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();

View File

@ -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 @@ protected: /* data */
QString m_filename;
bool wroteAnyData = false;
std::unique_ptr<QSaveFile> m_output_file;
bool setExecutable = false;
};
}

View File

@ -200,7 +200,6 @@ class MainWindow::Ui
{
public:
TranslatedAction actionAddInstance;
//TranslatedAction actionRefresh;
TranslatedAction actionCheckUpdate;
TranslatedAction actionSettings;
TranslatedAction actionPatreon;
@ -220,6 +219,7 @@ public:
TranslatedAction actionDeleteInstance;
TranslatedAction actionConfig_Folder;
TranslatedAction actionCAT;
TranslatedAction actionJREs;
TranslatedAction actionCopyInstance;
TranslatedAction actionLaunchInstanceOffline;
TranslatedAction actionScreenshots;
@ -421,6 +421,15 @@ public:
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.

View File

@ -103,6 +103,8 @@ private slots:
void on_actionSettings_triggered();
void on_actionJREs_triggered();
void on_actionInstanceSettings_triggered();
void on_actionManageAccounts_triggered();

1
tools/CMakeLists.txt Normal file
View File

@ -0,0 +1 @@
add_subdirectory(grabjre)

View File

@ -0,0 +1,2 @@
add_executable(GrabJRE GrabJRE.cpp)
target_link_libraries(GrabJRE Launcher_logic)

58
tools/grabjre/GrabJRE.cpp Normal file
View File

@ -0,0 +1,58 @@
#include <QTextStream>
#include <QCoreApplication>
#include "mojang/PackageManifest.h"
#include "mojang/PackageInstallTask.h"
#include <QCommandLineParser>
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<QNetworkAccessManager> 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;
}