From b3e1691c017d8d791f2f66411fc634e6bfc330d0 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Wed, 20 Apr 2022 18:33:33 +0200 Subject: [PATCH 001/157] fix: hide LauncherLoginStep tokens for non-Debug builds --- launcher/minecraft/auth/steps/LauncherLoginStep.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/launcher/minecraft/auth/steps/LauncherLoginStep.cpp b/launcher/minecraft/auth/steps/LauncherLoginStep.cpp index c978bd078..f56972233 100644 --- a/launcher/minecraft/auth/steps/LauncherLoginStep.cpp +++ b/launcher/minecraft/auth/steps/LauncherLoginStep.cpp @@ -50,7 +50,9 @@ void LauncherLoginStep::onRequestDone( auto requestor = qobject_cast(QObject::sender()); requestor->deleteLater(); +#ifndef NDEBUG qDebug() << data; +#endif if (error != QNetworkReply::NoError) { qWarning() << "Reply error:" << error; #ifndef NDEBUG From 3ec511010fbff31c392090548396080e44b8389c Mon Sep 17 00:00:00 2001 From: flow Date: Thu, 21 Apr 2022 09:34:44 -0300 Subject: [PATCH 002/157] fix: Build iconfix as static library On CI we build using the bundled Quazip, and automatically set -DBUILD_STATIC_LIBS to true, so it build iconfix statically as well. However, since we recently added support for using the system quazip, this flag is not set anymore, and PolyMC fails to run because iconfix neither is statically linked, nor it creates a .so file for dynamic linking. Since most other libs are built statically, we should make this one static as well. Maybe we should consider allowing for dynamic linking of libs now that quazip is not much of an issue anymore. :^) --- libraries/iconfix/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/iconfix/CMakeLists.txt b/libraries/iconfix/CMakeLists.txt index 084412038..97a591297 100644 --- a/libraries/iconfix/CMakeLists.txt +++ b/libraries/iconfix/CMakeLists.txt @@ -12,7 +12,7 @@ internal/qiconloader.cpp internal/qiconloader_p.h ) -add_library(Launcher_iconfix ${ICONFIX_SOURCES}) +add_library(Launcher_iconfix STATIC ${ICONFIX_SOURCES}) target_include_directories(Launcher_iconfix PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} "${CMAKE_CURRENT_BINARY_DIR}" ) target_link_libraries(Launcher_iconfix Qt5::Core Qt5::Widgets) From 908e6364c9c156f53146df9d32506ace15bd0360 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Thu, 21 Apr 2022 22:52:05 +0200 Subject: [PATCH 003/157] fix: update README --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 4c73c47e1..c493293d0 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,8 @@ PolyMC is a custom launcher for Minecraft that focuses on predictability, long term stability and simplicity. -This is a **fork** of the MultiMC Launcher and not endorsed by MultiMC. The PolyMC community felt that the maintainer was not acting in the spirit of Free Software so this fork was made. +This is a **fork** of the MultiMC Launcher and not endorsed by MultiMC. +If you want to read about why this fork was created, check out [our FAQ page](https://polymc.org/wiki/overview/faq/).
# Installation @@ -81,8 +82,8 @@ To modify download information or change packaging information send a pull reque Do whatever you want, we don't care. Just follow the license. If you have any questions about this feel free to ask in an issue. -All launcher code is available under the GPL-3 license. +All launcher code is available under the GPL-3.0-only license. -[Source for the website](https://github.com/PolyMC/polymc.github.io) is hosted under the AGPL-3 License. +[Source for the website](https://github.com/PolyMC/polymc.github.io) is hosted under the AGPL-3.0-or-later License. The logo and related assets are under the CC BY-SA 4.0 license. From 234a9e48e9abfdd67fe55760e2182dbca80fe7b4 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Thu, 21 Apr 2022 22:53:44 +0200 Subject: [PATCH 004/157] chore: add FUNDING --- .github/FUNDING.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..247675fca --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +open_collective: polymc From c1386bcb048beac3d51ea0734c7e6acc00a31962 Mon Sep 17 00:00:00 2001 From: Daniel Schemp Date: Fri, 22 Apr 2022 00:12:20 +0200 Subject: [PATCH 005/157] added: Mnemonics for Settings/Launcher --- launcher/ui/pages/global/LauncherPage.ui | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index 4cc2a113b..086de17b3 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -196,7 +196,7 @@ - By &last launched + &By last launched sortingModeGroup @@ -293,7 +293,7 @@ - Colors + &Colors themeComboBoxColors @@ -334,7 +334,7 @@ The menubar is more friendly for keyboard-driven interaction. - Replace toolbar with menubar + &Replace toolbar with menubar @@ -370,21 +370,21 @@ - Show console while the game is running? + Show console while the game is &running? - Automatically close console when the game quits? + &Automatically close console when the game quits? - Show console when the game crashes? + Show console when the game &crashes? @@ -394,13 +394,13 @@ - History limit + &History limit - Stop logging when log overflows + &Stop logging when log overflows @@ -441,7 +441,7 @@ - Console font + Console &font From f52b7c030ff537d6465417adc722c23da7423bc3 Mon Sep 17 00:00:00 2001 From: Daniel Schemp Date: Fri, 22 Apr 2022 00:14:24 +0200 Subject: [PATCH 006/157] added: Mnemonics for Settings/Minecraft+ --- launcher/ui/pages/global/MinecraftPage.ui | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/launcher/ui/pages/global/MinecraftPage.ui b/launcher/ui/pages/global/MinecraftPage.ui index c18ab34b3..353390bd4 100644 --- a/launcher/ui/pages/global/MinecraftPage.ui +++ b/launcher/ui/pages/global/MinecraftPage.ui @@ -51,7 +51,7 @@ - Start Minecraft maximized? + Start Minecraft &maximized? @@ -60,7 +60,7 @@ - Window hei&ght: + Window &height: windowHeightSpinBox @@ -70,7 +70,7 @@ - W&indow width: + Window &width: windowWidthSpinBox @@ -120,14 +120,14 @@ - Use system installation of GLFW + Use system installation of &GLFW - Use system installation of OpenAL + Use system installation of &OpenAL @@ -143,21 +143,21 @@ - Show time spent playing instances + Show time spent &playing instances - Show time spent playing across all instances + Show time spent playing across &all instances - Record time spent playing instances + &Record time spent playing instances @@ -176,7 +176,7 @@ <html><head/><body><p>The launcher will automatically reopen when the game crashes or exits.</p></body></html> - Close the launcher after game window opens + &Close the launcher after game window opens @@ -186,7 +186,7 @@ <html><head/><body><p>The launcher will automatically quit after the game exits or crashes.</p></body></html> - Quit the launcher after game window closes + &Quit the launcher after game window closes From 75826aca13d5dcafb630c955f8349c3354503003 Mon Sep 17 00:00:00 2001 From: Daniel Schemp Date: Fri, 22 Apr 2022 00:16:11 +0200 Subject: [PATCH 007/157] added: Mnemonics for Settings/Java --- launcher/ui/pages/global/JavaPage.ui | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/launcher/ui/pages/global/JavaPage.ui b/launcher/ui/pages/global/JavaPage.ui index d27b200fa..bb1957705 100644 --- a/launcher/ui/pages/global/JavaPage.ui +++ b/launcher/ui/pages/global/JavaPage.ui @@ -70,14 +70,14 @@ - Minimum memory allocation: + &Minimum memory allocation: - Maximum memory allocation: + Ma&ximum memory allocation: @@ -106,7 +106,7 @@ - PermGen: + &PermGen: @@ -150,7 +150,7 @@ - Java path: + &Java path: @@ -192,7 +192,7 @@ - JVM arguments: + J&VM arguments: @@ -205,7 +205,7 @@ - Auto-detect... + &Auto-detect... @@ -218,7 +218,7 @@ - Test + &Test @@ -234,7 +234,7 @@ If enabled, the launcher will not check if an instance is compatible with the selected Java version. - Skip Java compatibility checks + &Skip Java compatibility checks From 5a5797d9144d377e92ffaf9776b49ed60a2e8acb Mon Sep 17 00:00:00 2001 From: Daniel Schemp Date: Fri, 22 Apr 2022 00:18:39 +0200 Subject: [PATCH 008/157] added: Mnemonics for Settings/Custom Commands --- launcher/ui/widgets/CustomCommands.ui | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/launcher/ui/widgets/CustomCommands.ui b/launcher/ui/widgets/CustomCommands.ui index 650a9cc15..68e680a54 100644 --- a/launcher/ui/widgets/CustomCommands.ui +++ b/launcher/ui/widgets/CustomCommands.ui @@ -29,7 +29,7 @@ true - Cus&tom Commands + &Custom Commands true @@ -41,7 +41,7 @@ - Post-exit command: + P&ost-exit command: @@ -51,7 +51,7 @@ - Pre-launch command: + &Pre-launch command: @@ -61,7 +61,7 @@ - Wrapper command: + &Wrapper command: From 717067e9ebaaaa25d818da9942e54f0deb081781 Mon Sep 17 00:00:00 2001 From: Daniel Schemp Date: Fri, 22 Apr 2022 00:19:54 +0200 Subject: [PATCH 009/157] added: Mnemonics for Settings/Proxy --- launcher/ui/pages/global/ProxyPage.ui | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/launcher/ui/pages/global/ProxyPage.ui b/launcher/ui/pages/global/ProxyPage.ui index 347fa86c7..5a2fc73d4 100644 --- a/launcher/ui/pages/global/ProxyPage.ui +++ b/launcher/ui/pages/global/ProxyPage.ui @@ -81,7 +81,7 @@ - SOC&KS5 + &SOCKS5 proxyGroup @@ -91,7 +91,7 @@ - H&TTP + &HTTP proxyGroup @@ -104,7 +104,7 @@ - Address and Port + &Address and Port @@ -145,14 +145,14 @@ - Username: + &Username: - Password: + &Password: From 94a655b055cd977b405223d3e549d61a4b11658b Mon Sep 17 00:00:00 2001 From: Daniel Schemp Date: Fri, 22 Apr 2022 00:20:54 +0200 Subject: [PATCH 010/157] added: Mnemonics for Settings/External Tools --- launcher/ui/pages/global/ExternalToolsPage.ui | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/launcher/ui/pages/global/ExternalToolsPage.ui b/launcher/ui/pages/global/ExternalToolsPage.ui index e79e93889..8609d4696 100644 --- a/launcher/ui/pages/global/ExternalToolsPage.ui +++ b/launcher/ui/pages/global/ExternalToolsPage.ui @@ -36,7 +36,7 @@ - JProfiler + J&Profiler @@ -73,7 +73,7 @@ - JVisualVM + J&VisualVM @@ -110,7 +110,7 @@ - MCEdit + &MCEdit @@ -156,7 +156,7 @@ - Text Editor: + &Text Editor: From 08b1b2669a864873c52d604994bb8ff373b81dbc Mon Sep 17 00:00:00 2001 From: Daniel Schemp Date: Fri, 22 Apr 2022 00:22:50 +0200 Subject: [PATCH 011/157] added: Mnemonics for Settings/Accounts --- launcher/ui/pages/global/AccountListPage.ui | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/launcher/ui/pages/global/AccountListPage.ui b/launcher/ui/pages/global/AccountListPage.ui index d21a92e23..469955b51 100644 --- a/launcher/ui/pages/global/AccountListPage.ui +++ b/launcher/ui/pages/global/AccountListPage.ui @@ -65,17 +65,17 @@ - Add Mojang + Add &Mojang - Remove + Remo&ve - Set Default + &Set Default @@ -83,17 +83,17 @@ true - No Default + &No Default - Upload Skin + &Upload Skin - Delete Skin + &Delete Skin Delete the currently active skin and go back to the default one @@ -101,17 +101,17 @@ - Add Microsoft + &Add Microsoft - Add Offline + Add &Offline - Refresh + &Refresh Refresh the account tokens From c86ec0bd36f3ac445a234b38df19f7b1bf300fbc Mon Sep 17 00:00:00 2001 From: Daniel Schemp Date: Fri, 22 Apr 2022 00:23:36 +0200 Subject: [PATCH 012/157] added: Mnemonics for Settings/APIs --- launcher/ui/pages/global/APIPage.ui | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/launcher/ui/pages/global/APIPage.ui b/launcher/ui/pages/global/APIPage.ui index 7a9088d18..acde9aef8 100644 --- a/launcher/ui/pages/global/APIPage.ui +++ b/launcher/ui/pages/global/APIPage.ui @@ -36,7 +36,7 @@ - Pastebin URL + &Pastebin URL @@ -98,7 +98,7 @@ - Microsoft Authentication + &Microsoft Authentication From 71777e7a6f5cc6a641c38867a4d087efdb644606 Mon Sep 17 00:00:00 2001 From: Daniel Schemp Date: Fri, 22 Apr 2022 00:31:03 +0200 Subject: [PATCH 013/157] added and fixed some Mnemonics in MainWindow --- launcher/ui/MainWindow.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 7ac4d2d44..f34cf1ab9 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -294,14 +294,14 @@ class MainWindow::Ui actionViewInstanceFolder = TranslatedAction(MainWindow); actionViewInstanceFolder->setObjectName(QStringLiteral("actionViewInstanceFolder")); actionViewInstanceFolder->setIcon(APPLICATION->getThemedIcon("viewfolder")); - actionViewInstanceFolder.setTextId(QT_TRANSLATE_NOOP("MainWindow", "View Instance Folder")); + actionViewInstanceFolder.setTextId(QT_TRANSLATE_NOOP("MainWindow", "&View Instance Folder")); actionViewInstanceFolder.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the instance folder in a file browser.")); all_actions.append(&actionViewInstanceFolder); actionViewCentralModsFolder = TranslatedAction(MainWindow); actionViewCentralModsFolder->setObjectName(QStringLiteral("actionViewCentralModsFolder")); actionViewCentralModsFolder->setIcon(APPLICATION->getThemedIcon("centralmods")); - actionViewCentralModsFolder.setTextId(QT_TRANSLATE_NOOP("MainWindow", "View Central Mods Folder")); + actionViewCentralModsFolder.setTextId(QT_TRANSLATE_NOOP("MainWindow", "View &Central Mods Folder")); actionViewCentralModsFolder.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the central mods folder in a file browser.")); all_actions.append(&actionViewCentralModsFolder); @@ -326,7 +326,7 @@ class MainWindow::Ui actionSettings->setObjectName(QStringLiteral("actionSettings")); actionSettings->setIcon(APPLICATION->getThemedIcon("settings")); actionSettings->setMenuRole(QAction::PreferencesRole); - actionSettings.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Settings...")); + actionSettings.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Setti&ngs...")); actionSettings.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Change settings.")); actionSettings->setShortcut(QKeySequence::Preferences); all_actions.append(&actionSettings); @@ -542,7 +542,7 @@ class MainWindow::Ui actionOpenWiki = TranslatedAction(MainWindow); actionOpenWiki->setObjectName(QStringLiteral("actionOpenWiki")); - actionOpenWiki.setTextId(QT_TRANSLATE_NOOP("MainWindow", "%1 He&lp")); + actionOpenWiki.setTextId(QT_TRANSLATE_NOOP("MainWindow", "%1 &Help")); actionOpenWiki.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the %1 wiki")); connect(actionOpenWiki, &QAction::triggered, MainWindow, &MainWindow::on_actionOpenWiki_triggered); all_actions.append(&actionOpenWiki); From 45783c1661c8d39880e69b33ab014f94250bb67e Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Thu, 7 Apr 2022 19:46:41 +0100 Subject: [PATCH 014/157] ATLauncher: Support using share codes --- buildconfig/BuildConfig.h | 2 + launcher/CMakeLists.txt | 2 + .../modplatform/atlauncher/ATLShareCode.cpp | 60 ++++++++ .../modplatform/atlauncher/ATLShareCode.h | 47 ++++++ .../atlauncher/AtlOptionalModDialog.cpp | 144 ++++++++++++++++-- .../atlauncher/AtlOptionalModDialog.h | 50 ++++-- .../atlauncher/AtlOptionalModDialog.ui | 16 +- 7 files changed, 290 insertions(+), 31 deletions(-) create mode 100644 launcher/modplatform/atlauncher/ATLShareCode.cpp create mode 100644 launcher/modplatform/atlauncher/ATLShareCode.h diff --git a/buildconfig/BuildConfig.h b/buildconfig/BuildConfig.h index 6304387cb..c1d347087 100644 --- a/buildconfig/BuildConfig.h +++ b/buildconfig/BuildConfig.h @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify @@ -139,6 +140,7 @@ class Config QString LEGACY_FTB_CDN_BASE_URL = "https://dist.creeper.host/FTB2/"; QString ATL_DOWNLOAD_SERVER_URL = "https://download.nodecdn.net/containers/atl/"; + QString ATL_API_BASE_URL = "https://api.atlauncher.com/v1/"; QString TECHNIC_API_BASE_URL = "https://api.technicpack.net/"; /** diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 6ed86726b..e60c4d45a 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -557,6 +557,8 @@ set(ATLAUNCHER_SOURCES modplatform/atlauncher/ATLPackInstallTask.h modplatform/atlauncher/ATLPackManifest.cpp modplatform/atlauncher/ATLPackManifest.h + modplatform/atlauncher/ATLShareCode.cpp + modplatform/atlauncher/ATLShareCode.h ) add_unit_test(Index diff --git a/launcher/modplatform/atlauncher/ATLShareCode.cpp b/launcher/modplatform/atlauncher/ATLShareCode.cpp new file mode 100644 index 000000000..59030c873 --- /dev/null +++ b/launcher/modplatform/atlauncher/ATLShareCode.cpp @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "ATLShareCode.h" + +#include "Json.h" + +namespace ATLauncher { + +static void loadShareCodeMod(ShareCodeMod& m, QJsonObject& obj) +{ + m.selected = Json::requireBoolean(obj, "selected"); + m.name = Json::requireString(obj, "name"); +} + +static void loadShareCode(ShareCode& c, QJsonObject& obj) +{ + c.pack = Json::requireString(obj, "pack"); + c.version = Json::requireString(obj, "version"); + + auto mods = Json::requireObject(obj, "mods"); + auto optional = Json::requireArray(mods, "optional"); + for (const auto modRaw : optional) { + auto modObj = Json::requireObject(modRaw); + ShareCodeMod mod; + loadShareCodeMod(mod, modObj); + c.mods.append(mod); + } +} + +void loadShareCodeResponse(ShareCodeResponse& r, QJsonObject& obj) +{ + r.error = Json::requireBoolean(obj, "error"); + r.code = Json::requireInteger(obj, "code"); + + if (obj.contains("message") && !obj.value("message").isNull()) + r.message = Json::requireString(obj, "message"); + + if (!r.error) { + auto dataRaw = Json::requireObject(obj, "data"); + loadShareCode(r.data, dataRaw); + } +} + +} diff --git a/launcher/modplatform/atlauncher/ATLShareCode.h b/launcher/modplatform/atlauncher/ATLShareCode.h new file mode 100644 index 000000000..88c30c98e --- /dev/null +++ b/launcher/modplatform/atlauncher/ATLShareCode.h @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include + +namespace ATLauncher { + +struct ShareCodeMod { + bool selected; + QString name; +}; + +struct ShareCode { + QString pack; + QString version; + QVector mods; +}; + +struct ShareCodeResponse { + bool error; + int code; + QString message; + ShareCode data; +}; + +void loadShareCodeResponse(ShareCodeResponse& r, QJsonObject& obj); + +} diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp index ac3869dca..39ecf1798 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp @@ -1,30 +1,56 @@ +// SPDX-License-Identifier: GPL-3.0-only /* - * Copyright 2021 Jamie Mansfield + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2021 Jamie Mansfield + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #include "AtlOptionalModDialog.h" #include "ui_AtlOptionalModDialog.h" +#include +#include +#include "BuildConfig.h" +#include "Json.h" +#include "modplatform/atlauncher/ATLShareCode.h" +#include "Application.h" + AtlOptionalModListModel::AtlOptionalModListModel(QWidget *parent, QVector mods) : QAbstractListModel(parent), m_mods(mods) { - // fill mod index for (int i = 0; i < m_mods.size(); i++) { auto mod = m_mods.at(i); m_index[mod.name] = i; } + // set initial state for (int i = 0; i < m_mods.size(); i++) { auto mod = m_mods.at(i); @@ -77,7 +103,7 @@ QVariant AtlOptionalModListModel::data(const QModelIndex &index, int role) const } } - return QVariant(); + return {}; } bool AtlOptionalModListModel::setData(const QModelIndex &index, const QVariant &value, int role) { @@ -104,7 +130,7 @@ QVariant AtlOptionalModListModel::headerData(int section, Qt::Orientation orient } } - return QVariant(); + return {}; } Qt::ItemFlags AtlOptionalModListModel::flags(const QModelIndex &index) const { @@ -115,6 +141,69 @@ Qt::ItemFlags AtlOptionalModListModel::flags(const QModelIndex &index) const { return flags; } +void AtlOptionalModListModel::useShareCode(const QString& code) { + m_jobPtr.reset(new NetJob("Atl::Request", APPLICATION->network())); + auto url = QString(BuildConfig.ATL_API_BASE_URL + "share-codes/" + code); + m_jobPtr->addNetAction(Net::Download::makeByteArray(QUrl(url), &m_response)); + + connect(m_jobPtr.get(), &NetJob::succeeded, + this, &AtlOptionalModListModel::shareCodeSuccess); + connect(m_jobPtr.get(), &NetJob::failed, + this, &AtlOptionalModListModel::shareCodeFailure); + + m_jobPtr->start(); +} + +void AtlOptionalModListModel::shareCodeSuccess() { + m_jobPtr.reset(); + + QJsonParseError parse_error {}; + auto doc = QJsonDocument::fromJson(m_response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from ATL at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << m_response; + return; + } + auto obj = doc.object(); + + ATLauncher::ShareCodeResponse response; + try { + ATLauncher::loadShareCodeResponse(response, obj); + } + catch (const JSONValidationError& e) { + qDebug() << QString::fromUtf8(m_response); + qWarning() << "Error while reading response from ATLauncher: " << e.cause(); + return; + } + + if (response.error) { + // fixme: plumb in an error message + qWarning() << "ATLauncher API Response Error" << response.message; + return; + } + + // FIXME: verify pack and version, error if not matching. + + // Clear the current selection + for (const auto& mod : m_mods) { + m_selection[mod.name] = false; + } + + // Make the selections, as per the share code. + for (const auto& mod : response.data.mods) { + m_selection[mod.name] = mod.selected; + } + + emit dataChanged(AtlOptionalModListModel::index(0, EnabledColumn), + AtlOptionalModListModel::index(m_mods.size() - 1, EnabledColumn)); +} + +void AtlOptionalModListModel::shareCodeFailure(const QString& reason) { + m_jobPtr.reset(); + + // fixme: plumb in an error message +} + void AtlOptionalModListModel::selectRecommended() { for (const auto& mod : m_mods) { m_selection[mod.name] = mod.recommended; @@ -212,6 +301,8 @@ AtlOptionalModDialog::AtlOptionalModDialog(QWidget *parent, QVectortreeView->header()->setSectionResizeMode( AtlOptionalModListModel::DescriptionColumn, QHeaderView::Stretch); + connect(ui->shareCodeButton, &QPushButton::pressed, + this, &AtlOptionalModDialog::useShareCode); connect(ui->selectRecommendedButton, &QPushButton::pressed, listModel, &AtlOptionalModListModel::selectRecommended); connect(ui->clearAllButton, &QPushButton::pressed, @@ -223,3 +314,30 @@ AtlOptionalModDialog::AtlOptionalModDialog(QWidget *parent, QVectoruseShareCode(shareCode); +} diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h index 9832014cd..953b288ea 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h +++ b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h @@ -1,17 +1,36 @@ +// SPDX-License-Identifier: GPL-3.0-only /* - * Copyright 2021 Jamie Mansfield + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2021 Jamie Mansfield + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #pragma once @@ -20,6 +39,7 @@ #include #include "modplatform/atlauncher/ATLPackIndex.h" +#include "net/NetJob.h" namespace Ui { class AtlOptionalModDialog; @@ -49,7 +69,12 @@ class AtlOptionalModListModel : public QAbstractListModel { Qt::ItemFlags flags(const QModelIndex &index) const override; + void useShareCode(const QString& code); + public slots: + void shareCodeSuccess(); + void shareCodeFailure(const QString& reason); + void selectRecommended(); void clearAll(); @@ -58,6 +83,9 @@ public slots: void setMod(ATLauncher::VersionMod mod, int index, bool enable, bool shouldEmit = true); private: + NetJob::Ptr m_jobPtr; + QByteArray m_response; + QVector m_mods; QMap m_selection; QMap m_index; @@ -75,6 +103,8 @@ class AtlOptionalModDialog : public QDialog { return listModel->getResult(); } + void useShareCode(); + private: Ui::AtlOptionalModDialog *ui; diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.ui b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.ui index 4c5c2ec5e..d9496142a 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.ui +++ b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.ui @@ -24,23 +24,23 @@ - - - - Select Recommended - - - - false + true Use Share Code + + + + Select Recommended + + + From ba9059c7c8332d469a09515b4d16589909cf9bfd Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Fri, 22 Apr 2022 20:33:42 +0100 Subject: [PATCH 015/157] ATLauncher: Replace usage of QPushButton::pressed with ::clicked --- .../pages/modplatform/atlauncher/AtlOptionalModDialog.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp index 39ecf1798..26aa60af5 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp @@ -301,13 +301,13 @@ AtlOptionalModDialog::AtlOptionalModDialog(QWidget *parent, QVectortreeView->header()->setSectionResizeMode( AtlOptionalModListModel::DescriptionColumn, QHeaderView::Stretch); - connect(ui->shareCodeButton, &QPushButton::pressed, + connect(ui->shareCodeButton, &QPushButton::clicked, this, &AtlOptionalModDialog::useShareCode); - connect(ui->selectRecommendedButton, &QPushButton::pressed, + connect(ui->selectRecommendedButton, &QPushButton::clicked, listModel, &AtlOptionalModListModel::selectRecommended); - connect(ui->clearAllButton, &QPushButton::pressed, + connect(ui->clearAllButton, &QPushButton::clicked, listModel, &AtlOptionalModListModel::clearAll); - connect(ui->installButton, &QPushButton::pressed, + connect(ui->installButton, &QPushButton::clicked, this, &QDialog::close); } From 8bcbe07c87ee4b776d9ba743bb598f22ee80dda0 Mon Sep 17 00:00:00 2001 From: TheCodex6824 Date: Thu, 21 Apr 2022 16:01:55 -0400 Subject: [PATCH 016/157] Fix Mojang auth failing due to Mojang rejecting requests to the profile endpoint --- launcher/CMakeLists.txt | 2 + launcher/minecraft/auth/Parsers.cpp | 170 ++++++++++++++++++ launcher/minecraft/auth/Parsers.h | 1 + launcher/minecraft/auth/Yggdrasil.cpp | 22 +++ launcher/minecraft/auth/flows/Mojang.cpp | 6 +- .../auth/steps/MinecraftProfileStepMojang.cpp | 94 ++++++++++ .../auth/steps/MinecraftProfileStepMojang.h | 22 +++ 7 files changed, 314 insertions(+), 3 deletions(-) create mode 100644 launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp create mode 100644 launcher/minecraft/auth/steps/MinecraftProfileStepMojang.h diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 6ed86726b..075c183a7 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -235,6 +235,8 @@ set(MINECRAFT_SOURCES minecraft/auth/steps/MigrationEligibilityStep.h minecraft/auth/steps/MinecraftProfileStep.cpp minecraft/auth/steps/MinecraftProfileStep.h + minecraft/auth/steps/MinecraftProfileStepMojang.cpp + minecraft/auth/steps/MinecraftProfileStepMojang.h minecraft/auth/steps/MSAStep.cpp minecraft/auth/steps/MSAStep.h minecraft/auth/steps/XboxAuthorizationStep.cpp diff --git a/launcher/minecraft/auth/Parsers.cpp b/launcher/minecraft/auth/Parsers.cpp index 2dd36562f..82f23559a 100644 --- a/launcher/minecraft/auth/Parsers.cpp +++ b/launcher/minecraft/auth/Parsers.cpp @@ -212,6 +212,176 @@ bool parseMinecraftProfile(QByteArray & data, MinecraftProfile &output) { return true; } +namespace { + // these skin URLs are for the MHF_Steve and MHF_Alex accounts (made by a Mojang employee) + // they are needed because the session server doesn't return skin urls for default skins + static const QString SKIN_URL_STEVE = "http://textures.minecraft.net/texture/1a4af718455d4aab528e7a61f86fa25e6a369d1768dcb13f7df319a713eb810b"; + static const QString SKIN_URL_ALEX = "http://textures.minecraft.net/texture/83cee5ca6afcdb171285aa00e8049c297b2dbeba0efb8ff970a5677a1b644032"; + + bool isDefaultModelSteve(QString uuid) { + // need to calculate *Java* hashCode of UUID + // if number is even, skin/model is steve, otherwise it is alex + + // just in case dashes are in the id + uuid.remove('-'); + + if (uuid.size() != 32) { + return true; + } + + // qulonglong is guaranteed to be 64 bits + // we need to use unsigned numbers to guarantee truncation below + qulonglong most = uuid.left(16).toULongLong(nullptr, 16); + qulonglong least = uuid.right(16).toULongLong(nullptr, 16); + qulonglong xored = most ^ least; + return ((static_cast(xored >> 32)) ^ static_cast(xored)) % 2 == 0; + } +} + +/** +Uses session server for skin/cape lookup instead of profile, +because locked Mojang accounts cannot access profile endpoint +(https://api.minecraftservices.com/minecraft/profile/) + +ref: https://wiki.vg/Mojang_API#UUID_to_Profile_and_Skin.2FCape + +{ + "id": "", + "name": "", + "properties": [ + { + "name": "textures", + "value": "" + } + ] +} + +decoded base64 "value": +{ + "timestamp": , + "profileId": "", + "profileName": "", + "textures": { + "SKIN": { + "url": "" + }, + "CAPE": { + "url": "" + } + } +} +*/ + +bool parseMinecraftProfileMojang(QByteArray & data, MinecraftProfile &output) { + qDebug() << "Parsing Minecraft profile..."; +#ifndef NDEBUG + qDebug() << data; +#endif + + QJsonParseError jsonError; + QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); + if(jsonError.error) { + qWarning() << "Failed to parse response as JSON: " << jsonError.errorString(); + return false; + } + + auto obj = doc.object(); + if(!getString(obj.value("id"), output.id)) { + qWarning() << "Minecraft profile id is not a string"; + return false; + } + + if(!getString(obj.value("name"), output.name)) { + qWarning() << "Minecraft profile name is not a string"; + return false; + } + + auto propsArray = obj.value("properties").toArray(); + QByteArray texturePayload; + for( auto p : propsArray) { + auto pObj = p.toObject(); + auto name = pObj.value("name"); + if (!name.isString() || name.toString() != "textures") { + continue; + } + + auto value = pObj.value("value"); + if (value.isString()) { + texturePayload = QByteArray::fromBase64(value.toString().toUtf8(), QByteArray::AbortOnBase64DecodingErrors); + } + + if (!texturePayload.isEmpty()) { + break; + } + } + + if (texturePayload.isNull()) { + qWarning() << "No texture payload data"; + return false; + } + + doc = QJsonDocument::fromJson(texturePayload, &jsonError); + if(jsonError.error) { + qWarning() << "Failed to parse response as JSON: " << jsonError.errorString(); + return false; + } + + obj = doc.object(); + auto textures = obj.value("textures"); + if (!textures.isObject()) { + qWarning() << "No textures array in response"; + return false; + } + + Skin skinOut; + // fill in default skin info ourselves, as this endpoint doesn't provide it + bool steve = isDefaultModelSteve(output.id); + skinOut.variant = steve ? "classic" : "slim"; + skinOut.url = steve ? SKIN_URL_STEVE : SKIN_URL_ALEX; + // sadly we can't figure this out, but I don't think it really matters... + skinOut.id = "00000000-0000-0000-0000-000000000000"; + Cape capeOut; + auto tObj = textures.toObject(); + for (auto idx = tObj.constBegin(); idx != tObj.constEnd(); ++idx) { + if (idx->isObject()) { + if (idx.key() == "SKIN") { + auto skin = idx->toObject(); + if (!getString(skin.value("url"), skinOut.url)) { + qWarning() << "Skin url is not a string"; + return false; + } + + auto maybeMeta = skin.find("metadata"); + if (maybeMeta != skin.end() && maybeMeta->isObject()) { + auto meta = maybeMeta->toObject(); + // might not be present + getString(meta.value("model"), skinOut.variant); + } + } + else if (idx.key() == "CAPE") { + auto cape = idx->toObject(); + if (!getString(cape.value("url"), capeOut.url)) { + qWarning() << "Cape url is not a string"; + return false; + } + + // we don't know the cape ID as it is not returned from the session server + // so just fake it - changing capes is probably locked anyway :( + capeOut.alias = "cape"; + } + } + } + + output.skin = skinOut; + if (capeOut.alias == "cape") { + output.capes = QMap({{capeOut.alias, capeOut}}); + output.currentCape = capeOut.alias; + } + + output.validity = Katabasis::Validity::Certain; + return true; +} + bool parseMinecraftEntitlements(QByteArray & data, MinecraftEntitlement &output) { qDebug() << "Parsing Minecraft entitlements..."; #ifndef NDEBUG diff --git a/launcher/minecraft/auth/Parsers.h b/launcher/minecraft/auth/Parsers.h index dac7f69bf..2666d890c 100644 --- a/launcher/minecraft/auth/Parsers.h +++ b/launcher/minecraft/auth/Parsers.h @@ -14,6 +14,7 @@ namespace Parsers bool parseMojangResponse(QByteArray &data, Katabasis::Token &output); bool parseMinecraftProfile(QByteArray &data, MinecraftProfile &output); + bool parseMinecraftProfileMojang(QByteArray &data, MinecraftProfile &output); bool parseMinecraftEntitlements(QByteArray &data, MinecraftEntitlement &output); bool parseRolloutResponse(QByteArray &data, bool& result); } diff --git a/launcher/minecraft/auth/Yggdrasil.cpp b/launcher/minecraft/auth/Yggdrasil.cpp index 7ac842a67..299784119 100644 --- a/launcher/minecraft/auth/Yggdrasil.cpp +++ b/launcher/minecraft/auth/Yggdrasil.cpp @@ -209,6 +209,28 @@ void Yggdrasil::processResponse(QJsonObject responseData) { m_data->yggdrasilToken.validity = Katabasis::Validity::Certain; m_data->yggdrasilToken.issueInstant = QDateTime::currentDateTimeUtc(); + // Get UUID here since we need it for later + auto profile = responseData.value("selectedProfile"); + if (!profile.isObject()) { + changeState(AccountTaskState::STATE_FAILED_HARD, tr("Authentication server didn't send a selected profile.")); + return; + } + + auto profileObj = profile.toObject(); + for (auto i = profileObj.constBegin(); i != profileObj.constEnd(); ++i) { + if (i.key() == "name" && i.value().isString()) { + m_data->minecraftProfile.name = i->toString(); + } + else if (i.key() == "id" && i.value().isString()) { + m_data->minecraftProfile.id = i->toString(); + } + } + + if (m_data->minecraftProfile.id.isEmpty()) { + changeState(AccountTaskState::STATE_FAILED_HARD, tr("Authentication server didn't send a UUID in selected profile.")); + return; + } + // We've made it through the minefield of possible errors. Return true to indicate that // we've succeeded. qDebug() << "Finished reading authentication response."; diff --git a/launcher/minecraft/auth/flows/Mojang.cpp b/launcher/minecraft/auth/flows/Mojang.cpp index 4661dbe23..b86b0936a 100644 --- a/launcher/minecraft/auth/flows/Mojang.cpp +++ b/launcher/minecraft/auth/flows/Mojang.cpp @@ -1,7 +1,7 @@ #include "Mojang.h" #include "minecraft/auth/steps/YggdrasilStep.h" -#include "minecraft/auth/steps/MinecraftProfileStep.h" +#include "minecraft/auth/steps/MinecraftProfileStepMojang.h" #include "minecraft/auth/steps/MigrationEligibilityStep.h" #include "minecraft/auth/steps/GetSkinStep.h" @@ -10,7 +10,7 @@ MojangRefresh::MojangRefresh( QObject *parent ) : AuthFlow(data, parent) { m_steps.append(new YggdrasilStep(m_data, QString())); - m_steps.append(new MinecraftProfileStep(m_data)); + m_steps.append(new MinecraftProfileStepMojang(m_data)); m_steps.append(new MigrationEligibilityStep(m_data)); m_steps.append(new GetSkinStep(m_data)); } @@ -21,7 +21,7 @@ MojangLogin::MojangLogin( QObject *parent ): AuthFlow(data, parent), m_password(password) { m_steps.append(new YggdrasilStep(m_data, m_password)); - m_steps.append(new MinecraftProfileStep(m_data)); + m_steps.append(new MinecraftProfileStepMojang(m_data)); m_steps.append(new MigrationEligibilityStep(m_data)); m_steps.append(new GetSkinStep(m_data)); } diff --git a/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp b/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp new file mode 100644 index 000000000..d30352725 --- /dev/null +++ b/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp @@ -0,0 +1,94 @@ +#include "MinecraftProfileStepMojang.h" + +#include + +#include "minecraft/auth/AuthRequest.h" +#include "minecraft/auth/Parsers.h" + +MinecraftProfileStepMojang::MinecraftProfileStepMojang(AccountData* data) : AuthStep(data) { + +} + +MinecraftProfileStepMojang::~MinecraftProfileStepMojang() noexcept = default; + +QString MinecraftProfileStepMojang::describe() { + return tr("Fetching the Minecraft profile."); +} + + +void MinecraftProfileStepMojang::perform() { + if (m_data->minecraftProfile.id.isEmpty()) { + emit finished(AccountTaskState::STATE_FAILED_HARD, tr("A UUID is required to get the profile.")); + return; + } + + // use session server instead of profile due to profile endpoint being locked for locked Mojang accounts + QUrl url = QUrl("https://sessionserver.mojang.com/session/minecraft/profile/" + m_data->minecraftProfile.id); + QNetworkRequest req = QNetworkRequest(url); + AuthRequest *request = new AuthRequest(this); + connect(request, &AuthRequest::finished, this, &MinecraftProfileStepMojang::onRequestDone); + request->get(req); +} + +void MinecraftProfileStepMojang::rehydrate() { + // NOOP, for now. We only save bools and there's nothing to check. +} + +void MinecraftProfileStepMojang::onRequestDone( + QNetworkReply::NetworkError error, + QByteArray data, + QList headers +) { + auto requestor = qobject_cast(QObject::sender()); + requestor->deleteLater(); + +#ifndef NDEBUG + qDebug() << data; +#endif + if (error == QNetworkReply::ContentNotFoundError) { + // NOTE: Succeed even if we do not have a profile. This is a valid account state. + if(m_data->type == AccountType::Mojang) { + m_data->minecraftEntitlement.canPlayMinecraft = false; + m_data->minecraftEntitlement.ownsMinecraft = false; + } + m_data->minecraftProfile = MinecraftProfile(); + emit finished( + AccountTaskState::STATE_SUCCEEDED, + tr("Account has no Minecraft profile.") + ); + return; + } + if (error != QNetworkReply::NoError) { + qWarning() << "Error getting profile:"; + qWarning() << " HTTP Status: " << requestor->httpStatus_; + qWarning() << " Internal error no.: " << error; + qWarning() << " Error string: " << requestor->errorString_; + + qWarning() << " Response:"; + qWarning() << QString::fromUtf8(data); + + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("Minecraft Java profile acquisition failed.") + ); + return; + } + if(!Parsers::parseMinecraftProfileMojang(data, m_data->minecraftProfile)) { + m_data->minecraftProfile = MinecraftProfile(); + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("Minecraft Java profile response could not be parsed") + ); + return; + } + + if(m_data->type == AccountType::Mojang) { + auto validProfile = m_data->minecraftProfile.validity == Katabasis::Validity::Certain; + m_data->minecraftEntitlement.canPlayMinecraft = validProfile; + m_data->minecraftEntitlement.ownsMinecraft = validProfile; + } + emit finished( + AccountTaskState::STATE_WORKING, + tr("Minecraft Java profile acquisition succeeded.") + ); +} diff --git a/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.h b/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.h new file mode 100644 index 000000000..e06b30ab0 --- /dev/null +++ b/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.h @@ -0,0 +1,22 @@ +#pragma once +#include + +#include "QObjectPtr.h" +#include "minecraft/auth/AuthStep.h" + + +class MinecraftProfileStepMojang : public AuthStep { + Q_OBJECT + +public: + explicit MinecraftProfileStepMojang(AccountData *data); + virtual ~MinecraftProfileStepMojang() noexcept; + + void perform() override; + void rehydrate() override; + + QString describe() override; + +private slots: + void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList); +}; From e56f0db11bc096909bc17eac1a5cdf5de06817dc Mon Sep 17 00:00:00 2001 From: TheCodex6824 Date: Sat, 23 Apr 2022 10:32:52 -0400 Subject: [PATCH 017/157] Remove base64 decode option that was added in Qt 5.15 --- launcher/minecraft/auth/Parsers.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/minecraft/auth/Parsers.cpp b/launcher/minecraft/auth/Parsers.cpp index 82f23559a..60458b9b8 100644 --- a/launcher/minecraft/auth/Parsers.cpp +++ b/launcher/minecraft/auth/Parsers.cpp @@ -307,7 +307,7 @@ bool parseMinecraftProfileMojang(QByteArray & data, MinecraftProfile &output) { auto value = pObj.value("value"); if (value.isString()) { - texturePayload = QByteArray::fromBase64(value.toString().toUtf8(), QByteArray::AbortOnBase64DecodingErrors); + texturePayload = QByteArray::fromBase64(value.toString().toUtf8()); } if (!texturePayload.isEmpty()) { From a0bafa49520195512c388ebe8d5e5b307d0a10be Mon Sep 17 00:00:00 2001 From: TheCodex6824 Date: Sat, 23 Apr 2022 11:11:55 -0400 Subject: [PATCH 018/157] Re-add base64 decode option for Qt versions that support it --- launcher/minecraft/auth/Parsers.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/launcher/minecraft/auth/Parsers.cpp b/launcher/minecraft/auth/Parsers.cpp index 60458b9b8..ea882b564 100644 --- a/launcher/minecraft/auth/Parsers.cpp +++ b/launcher/minecraft/auth/Parsers.cpp @@ -213,8 +213,8 @@ bool parseMinecraftProfile(QByteArray & data, MinecraftProfile &output) { } namespace { - // these skin URLs are for the MHF_Steve and MHF_Alex accounts (made by a Mojang employee) - // they are needed because the session server doesn't return skin urls for default skins + // these skin URLs are for the MHF_Steve and MHF_Alex accounts (made by a Mojang employee) + // they are needed because the session server doesn't return skin urls for default skins static const QString SKIN_URL_STEVE = "http://textures.minecraft.net/texture/1a4af718455d4aab528e7a61f86fa25e6a369d1768dcb13f7df319a713eb810b"; static const QString SKIN_URL_ALEX = "http://textures.minecraft.net/texture/83cee5ca6afcdb171285aa00e8049c297b2dbeba0efb8ff970a5677a1b644032"; @@ -307,7 +307,11 @@ bool parseMinecraftProfileMojang(QByteArray & data, MinecraftProfile &output) { auto value = pObj.value("value"); if (value.isString()) { +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + texturePayload = QByteArray::fromBase64(value.toString().toUtf8(), QByteArray::AbortOnBase64DecodingErrors); +#else texturePayload = QByteArray::fromBase64(value.toString().toUtf8()); +#endif } if (!texturePayload.isEmpty()) { From c968c1be7892fbc1ba0571d30a03b20e3f8a5abc Mon Sep 17 00:00:00 2001 From: icelimetea Date: Sun, 24 Apr 2022 14:45:01 +0100 Subject: [PATCH 019/157] Refactor some parts of NewLaunch --- .../launcher/org/multimc/EntryPoint.java | 155 ++++++++---------- libraries/launcher/org/multimc/Launcher.java | 2 +- .../launcher/org/multimc/ParamBucket.java | 51 +++--- 3 files changed, 95 insertions(+), 113 deletions(-) diff --git a/libraries/launcher/org/multimc/EntryPoint.java b/libraries/launcher/org/multimc/EntryPoint.java index 0f904f5f8..c923bbdef 100644 --- a/libraries/launcher/org/multimc/EntryPoint.java +++ b/libraries/launcher/org/multimc/EntryPoint.java @@ -16,22 +16,24 @@ import org.multimc.onesix.OneSixLauncher; -import java.io.*; -import java.nio.charset.Charset; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; public class EntryPoint { - private enum Action - { - Proceed, - Launch, - Abort - } + + private final ParamBucket params = new ParamBucket(); + + private org.multimc.Launcher launcher; public static void main(String[] args) { EntryPoint listener = new EntryPoint(); + int retCode = listener.listen(); + if (retCode != 0) { System.out.println("Exiting with " + retCode); @@ -41,111 +43,92 @@ public static void main(String[] args) private Action parseLine(String inData) throws ParseException { - String[] pair = inData.split(" ", 2); + String[] pair = inData.split("\\s+", 2); - if(pair.length == 1) - { - String command = pair[0]; - if (pair[0].equals("launch")) + if (pair.length == 0) + throw new ParseException("Unexpected empty string!"); + + switch (pair[0]) { + case "launch": { return Action.Launch; + } - else if (pair[0].equals("abort")) + case "abort": { return Action.Abort; + } - else throw new ParseException("Error while parsing:" + pair[0]); - } + case "launcher": { + if (pair.length != 2) + throw new ParseException("Expected 2 tokens, got 1!"); - if(pair.length != 2) - throw new ParseException("Pair length is not 2."); + if (pair[1].equals("onesix")) { + launcher = new OneSixLauncher(); - String command = pair[0]; - String param = pair[1]; + Utils.log("Using onesix launcher."); + + return Action.Proceed; + } else { + throw new ParseException("Invalid launcher type: " + pair[1]); + } + } + + default: { + if (pair.length != 2) + throw new ParseException("Error while parsing:" + pair[0]); + + params.add(pair[0], pair[1]); - if(command.equals("launcher")) - { - if(param.equals("onesix")) - { - m_launcher = new OneSixLauncher(); - Utils.log("Using onesix launcher."); - Utils.log(); return Action.Proceed; } - else - throw new ParseException("Invalid launcher type: " + param); } - - m_params.add(command, param); - //System.out.println(command + " : " + param); - return Action.Proceed; } public int listen() { - BufferedReader buffer; - try - { - buffer = new BufferedReader(new InputStreamReader(System.in, "UTF-8")); - } catch (UnsupportedEncodingException e) - { - System.err.println("For some reason, your java does not support UTF-8. Consider living in the current century."); + Action action = Action.Proceed; + + try (BufferedReader reader = new BufferedReader(new InputStreamReader( + System.in, + StandardCharsets.UTF_8 + ))) { + String line; + + while (action == Action.Proceed) { + if ((line = reader.readLine()) != null) { + action = parseLine(line); + } else { + action = Action.Abort; + } + } + } catch (IOException | ParseException e) { + Utils.log("Launcher ABORT due to exception:"); + e.printStackTrace(); + return 1; } - boolean isListening = true; - boolean isAborted = false; + // Main loop - while (isListening) - { - String inData; - try - { - // Read from the pipe one line at a time - inData = buffer.readLine(); - if (inData != null) - { - Action a = parseLine(inData); - if(a == Action.Abort) - { - isListening = false; - isAborted = true; - } - if(a == Action.Launch) - { - isListening = false; - } - } - else - { - isListening = false; - isAborted = true; - } - } - catch (IOException e) - { - System.err.println("Launcher ABORT due to IO exception:"); - e.printStackTrace(); - return 1; - } - catch (ParseException e) - { - System.err.println("Launcher ABORT due to PARSE exception:"); - e.printStackTrace(); - return 1; - } - } - if(isAborted) + if (action == Action.Abort) { System.err.println("Launch aborted by the launcher."); return 1; } - if(m_launcher != null) + + if (launcher != null) { - return m_launcher.launch(m_params); + return launcher.launch(params); } + System.err.println("No valid launcher implementation specified."); + return 1; } - private ParamBucket m_params = new ParamBucket(); - private org.multimc.Launcher m_launcher; + private enum Action { + Proceed, + Launch, + Abort + } + } diff --git a/libraries/launcher/org/multimc/Launcher.java b/libraries/launcher/org/multimc/Launcher.java index d8cb6d1ba..c5e8fbc10 100644 --- a/libraries/launcher/org/multimc/Launcher.java +++ b/libraries/launcher/org/multimc/Launcher.java @@ -18,5 +18,5 @@ public interface Launcher { - abstract int launch(ParamBucket params); + int launch(ParamBucket params); } diff --git a/libraries/launcher/org/multimc/ParamBucket.java b/libraries/launcher/org/multimc/ParamBucket.java index 2fde13296..8ff03ddc7 100644 --- a/libraries/launcher/org/multimc/ParamBucket.java +++ b/libraries/launcher/org/multimc/ParamBucket.java @@ -19,62 +19,62 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Map; public class ParamBucket { + + private final Map> paramsMap = new HashMap<>(); + public void add(String key, String value) { - List coll = null; - if(!m_params.containsKey(key)) - { - coll = new ArrayList(); - m_params.put(key, coll); - } - else - { - coll = m_params.get(key); - } - coll.add(value); + paramsMap.computeIfAbsent(key, k -> new ArrayList<>()) + .add(value); } public List all(String key) throws NotFoundException { - if(!m_params.containsKey(key)) + List params = paramsMap.get(key); + + if (params == null) throw new NotFoundException(); - return m_params.get(key); + + return params; } public List allSafe(String key, List def) { - if(!m_params.containsKey(key) || m_params.get(key).size() < 1) - { + List params = paramsMap.get(key); + + if (params == null || params.isEmpty()) return def; - } - return m_params.get(key); + + return params; } public List allSafe(String key) { - return allSafe(key, new ArrayList()); + return allSafe(key, new ArrayList<>()); } public String first(String key) throws NotFoundException { List list = all(key); - if(list.size() < 1) - { + + if (list.isEmpty()) throw new NotFoundException(); - } + return list.get(0); } public String firstSafe(String key, String def) { - if(!m_params.containsKey(key) || m_params.get(key).size() < 1) - { + List params = paramsMap.get(key); + + if (params == null || params.isEmpty()) return def; - } - return m_params.get(key).get(0); + + return params.get(0); } public String firstSafe(String key) @@ -82,5 +82,4 @@ public String firstSafe(String key) return firstSafe(key, ""); } - private HashMap> m_params = new HashMap>(); } From b0a469baab54c38a80607a4567b4c0f6eb825245 Mon Sep 17 00:00:00 2001 From: icelimetea Date: Sun, 24 Apr 2022 15:10:35 +0100 Subject: [PATCH 020/157] Use java.util.logging instead of custom logging --- .../launcher/org/multimc/EntryPoint.java | 18 +++++--- libraries/launcher/org/multimc/Utils.java | 33 ------------- .../org/multimc/onesix/OneSixLauncher.java | 46 +++++++++++-------- 3 files changed, 38 insertions(+), 59 deletions(-) diff --git a/libraries/launcher/org/multimc/EntryPoint.java b/libraries/launcher/org/multimc/EntryPoint.java index c923bbdef..85cf37026 100644 --- a/libraries/launcher/org/multimc/EntryPoint.java +++ b/libraries/launcher/org/multimc/EntryPoint.java @@ -20,10 +20,14 @@ import java.io.IOException; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; +import java.util.logging.Level; +import java.util.logging.Logger; public class EntryPoint { + private static final Logger LOGGER = Logger.getLogger("EntryPoint"); + private final ParamBucket params = new ParamBucket(); private org.multimc.Launcher launcher; @@ -36,7 +40,8 @@ public static void main(String[] args) if (retCode != 0) { - System.out.println("Exiting with " + retCode); + LOGGER.info("Exiting with " + retCode); + System.exit(retCode); } } @@ -64,7 +69,7 @@ private Action parseLine(String inData) throws ParseException if (pair[1].equals("onesix")) { launcher = new OneSixLauncher(); - Utils.log("Using onesix launcher."); + LOGGER.info("Using onesix launcher."); return Action.Proceed; } else { @@ -101,9 +106,7 @@ public int listen() } } } catch (IOException | ParseException e) { - Utils.log("Launcher ABORT due to exception:"); - - e.printStackTrace(); + LOGGER.log(Level.SEVERE, "Launcher ABORT due to exception:", e); return 1; } @@ -111,7 +114,8 @@ public int listen() // Main loop if (action == Action.Abort) { - System.err.println("Launch aborted by the launcher."); + LOGGER.info("Launch aborted by the launcher."); + return 1; } @@ -120,7 +124,7 @@ public int listen() return launcher.launch(params); } - System.err.println("No valid launcher implementation specified."); + LOGGER.log(Level.SEVERE, "No valid launcher implementation specified."); return 1; } diff --git a/libraries/launcher/org/multimc/Utils.java b/libraries/launcher/org/multimc/Utils.java index 353af7d39..e48029c25 100644 --- a/libraries/launcher/org/multimc/Utils.java +++ b/libraries/launcher/org/multimc/Utils.java @@ -16,21 +16,10 @@ package org.multimc; -import java.io.*; import java.io.File; import java.lang.reflect.Field; -import java.lang.reflect.Method; import java.lang.reflect.Modifier; -import java.net.URL; -import java.net.URLClassLoader; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.*; -import java.util.Arrays; -import java.util.Enumeration; import java.util.List; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; public class Utils { @@ -93,27 +82,5 @@ public static Field getMCPathField(Class mc) return null; } - /** - * Log to the launcher console - * - * @param message A String containing the message - * @param level A String containing the level name. See MinecraftLauncher::getLevel() - */ - public static void log(String message, String level) - { - // Kinda dirty - String tag = "!![" + level + "]!"; - System.out.println(tag + message.replace("\n", "\n" + tag)); - } - - public static void log(String message) - { - log(message, "Launcher"); - } - - public static void log() - { - System.out.println(); - } } diff --git a/libraries/launcher/org/multimc/onesix/OneSixLauncher.java b/libraries/launcher/org/multimc/onesix/OneSixLauncher.java index ea445995e..0058bd43f 100644 --- a/libraries/launcher/org/multimc/onesix/OneSixLauncher.java +++ b/libraries/launcher/org/multimc/onesix/OneSixLauncher.java @@ -19,14 +19,18 @@ import java.applet.Applet; import java.io.File; -import java.io.IOException; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; public class OneSixLauncher implements Launcher { + + private static final Logger LOGGER = Logger.getLogger("OneSixLauncher"); + // parameters, separated from ParamBucket private List libraries; private List mcparams; @@ -104,7 +108,7 @@ int legacyLaunch() if (f == null) { - System.err.println("Could not find Minecraft path field."); + LOGGER.warning("Could not find Minecraft path field."); } else { @@ -113,8 +117,12 @@ int legacyLaunch() } } catch (Exception e) { - System.err.println("Could not set base folder. Failed to find/access Minecraft main class:"); - e.printStackTrace(System.err); + LOGGER.log( + Level.SEVERE, + "Could not set base folder. Failed to find/access Minecraft main class:", + e + ); + return -1; } @@ -122,7 +130,7 @@ int legacyLaunch() if(!traits.contains("noapplet")) { - Utils.log("Launching with applet wrapper..."); + LOGGER.info("Launching with applet wrapper..."); try { Class MCAppletClass = cl.loadClass(appletClass); @@ -132,10 +140,9 @@ int legacyLaunch() return 0; } catch (Exception e) { - Utils.log("Applet wrapper failed:", "Error"); - e.printStackTrace(System.err); - Utils.log(); - Utils.log("Falling back to using main class."); + LOGGER.log(Level.SEVERE, "Applet wrapper failed:", e); + + LOGGER.warning("Falling back to using main class."); } } @@ -147,8 +154,8 @@ int legacyLaunch() return 0; } catch (Exception e) { - Utils.log("Failed to invoke the Minecraft main class:", "Fatal"); - e.printStackTrace(System.err); + LOGGER.log(Level.SEVERE, "Failed to invoke the Minecraft main class:", e); + return -1; } } @@ -185,8 +192,8 @@ int launchWithMainClass() mc = cl.loadClass(mainClass); } catch (ClassNotFoundException e) { - System.err.println("Failed to find Minecraft main class:"); - e.printStackTrace(System.err); + LOGGER.log(Level.SEVERE, "Failed to find Minecraft main class:", e); + return -1; } @@ -197,8 +204,8 @@ int launchWithMainClass() meth = mc.getMethod("main", String[].class); } catch (NoSuchMethodException e) { - System.err.println("Failed to acquire the main method:"); - e.printStackTrace(System.err); + LOGGER.log(Level.SEVERE, "Failed to acquire the main method:", e); + return -1; } @@ -210,8 +217,8 @@ int launchWithMainClass() meth.invoke(null, (Object) paramsArray); } catch (Exception e) { - System.err.println("Failed to start Minecraft:"); - e.printStackTrace(System.err); + LOGGER.log(Level.SEVERE, "Failed to start Minecraft:", e); + return -1; } return 0; @@ -226,8 +233,8 @@ public int launch(ParamBucket params) processParams(params); } catch (NotFoundException e) { - System.err.println("Not enough arguments."); - e.printStackTrace(System.err); + LOGGER.log(Level.SEVERE, "Not enough arguments!"); + return -1; } @@ -245,4 +252,5 @@ public int launch(ParamBucket params) return launchWithMainClass(); } } + } From 884f7723624b68ffb23b0c30c8c3725a7e126b4a Mon Sep 17 00:00:00 2001 From: icelimetea Date: Mon, 25 Apr 2022 11:22:56 +0100 Subject: [PATCH 021/157] Clarify exception messages --- .../launcher/org/multimc/EntryPoint.java | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/libraries/launcher/org/multimc/EntryPoint.java b/libraries/launcher/org/multimc/EntryPoint.java index 85cf37026..b626d0958 100644 --- a/libraries/launcher/org/multimc/EntryPoint.java +++ b/libraries/launcher/org/multimc/EntryPoint.java @@ -48,12 +48,12 @@ public static void main(String[] args) private Action parseLine(String inData) throws ParseException { - String[] pair = inData.split("\\s+", 2); + String[] tokens = inData.split("\\s+", 2); - if (pair.length == 0) + if (tokens.length == 0) throw new ParseException("Unexpected empty string!"); - switch (pair[0]) { + switch (tokens[0]) { case "launch": { return Action.Launch; } @@ -63,25 +63,25 @@ private Action parseLine(String inData) throws ParseException } case "launcher": { - if (pair.length != 2) - throw new ParseException("Expected 2 tokens, got 1!"); + if (tokens.length != 2) + throw new ParseException("Expected 2 tokens, got " + tokens.length); - if (pair[1].equals("onesix")) { + if (tokens[1].equals("onesix")) { launcher = new OneSixLauncher(); LOGGER.info("Using onesix launcher."); return Action.Proceed; } else { - throw new ParseException("Invalid launcher type: " + pair[1]); + throw new ParseException("Invalid launcher type: " + tokens[1]); } } default: { - if (pair.length != 2) - throw new ParseException("Error while parsing:" + pair[0]); + if (tokens.length != 2) + throw new ParseException("Error while parsing:" + inData); - params.add(pair[0], pair[1]); + params.add(tokens[0], tokens[1]); return Action.Proceed; } From 1ff459d995f685aa5a83fe2c1c4b8f0f3b56ed03 Mon Sep 17 00:00:00 2001 From: TheCodex6824 Date: Mon, 25 Apr 2022 14:08:27 -0400 Subject: [PATCH 022/157] Use suggested error handling --- launcher/minecraft/auth/Parsers.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/launcher/minecraft/auth/Parsers.cpp b/launcher/minecraft/auth/Parsers.cpp index ea882b564..47473899b 100644 --- a/launcher/minecraft/auth/Parsers.cpp +++ b/launcher/minecraft/auth/Parsers.cpp @@ -1,4 +1,5 @@ #include "Parsers.h" +#include "Json.h" #include #include @@ -285,7 +286,7 @@ bool parseMinecraftProfileMojang(QByteArray & data, MinecraftProfile &output) { return false; } - auto obj = doc.object(); + auto obj = Json::requireObject(doc, "mojang minecraft profile"); if(!getString(obj.value("id"), output.id)) { qWarning() << "Minecraft profile id is not a string"; return false; @@ -330,7 +331,7 @@ bool parseMinecraftProfileMojang(QByteArray & data, MinecraftProfile &output) { return false; } - obj = doc.object(); + obj = Json::requireObject(doc, "session texture payload"); auto textures = obj.value("textures"); if (!textures.isObject()) { qWarning() << "No textures array in response"; From ac405aa564821723505b4e46865d0a9ad1e32b99 Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Mon, 25 Apr 2022 19:57:47 -0400 Subject: [PATCH 023/157] Remove old macOS data migration code --- launcher/Application.cpp | 63 ----------------------- launcher/ui/pages/global/LauncherPage.cpp | 14 ----- launcher/ui/pages/global/LauncherPage.h | 1 - launcher/ui/pages/global/LauncherPage.ui | 7 --- 4 files changed, 85 deletions(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 8bd434f03..2d0bba222 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -409,69 +409,6 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) return; } -#if defined(Q_OS_MAC) - // move user data to new location if on macOS and it still exists in Contents/MacOS - QDir fi(applicationDirPath()); - QString originalData = fi.absolutePath(); - // if the config file exists in Contents/MacOS, then user data is still there and needs to moved - if (QFileInfo::exists(FS::PathCombine(originalData, BuildConfig.LAUNCHER_CONFIGFILE))) - { - if (!QFileInfo::exists(FS::PathCombine(originalData, "dontmovemacdata"))) - { - QMessageBox::StandardButton askMoveDialogue; - askMoveDialogue = QMessageBox::question( - nullptr, - BuildConfig.LAUNCHER_DISPLAYNAME, - "Would you like to move application data to a new data location? It will improve the launcher's performance, but if you switch to older versions it will look like instances have disappeared. If you select no, you can migrate later in settings. You should select yes unless you're commonly switching between different versions (eg. develop and stable).", - QMessageBox::Yes | QMessageBox::No, - QMessageBox::Yes - ); - if (askMoveDialogue == QMessageBox::Yes) - { - qDebug() << "On macOS and found config file in old location, moving user data..."; - QDir dir; - QStringList dataFiles { - "*.log", // Launcher log files: ${Launcher_Name}-@.log - "accounts.json", - "accounts", - "assets", - "cache", - "icons", - "instances", - "libraries", - "meta", - "metacache", - "mods", - BuildConfig.LAUNCHER_CONFIGFILE, - "themes", - "translations" - }; - QDirIterator files(originalData, dataFiles); - while (files.hasNext()) { - QString filePath(files.next()); - QString fileName(files.fileName()); - if (!dir.rename(filePath, FS::PathCombine(dataPath, fileName))) - { - qWarning() << "Failed to move " << fileName; - } - } - } - else - { - dataPath = originalData; - QDir::setCurrent(dataPath); - QFile file(originalData + "/dontmovemacdata"); - file.open(QIODevice::WriteOnly); - } - } - else - { - dataPath = originalData; - QDir::setCurrent(dataPath); - } - } -#endif - /* * Establish the mechanism for communication with an already running PolyMC that uses the same data path. * If there is one, tell it what the user actually wanted to do and exit. diff --git a/launcher/ui/pages/global/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp index 097a2bfab..af2e2cd1b 100644 --- a/launcher/ui/pages/global/LauncherPage.cpp +++ b/launcher/ui/pages/global/LauncherPage.cpp @@ -97,13 +97,6 @@ LauncherPage::LauncherPage(QWidget *parent) : QWidget(parent), ui(new Ui::Launch } connect(ui->fontSizeBox, SIGNAL(valueChanged(int)), SLOT(refreshFontPreview())); connect(ui->consoleFont, SIGNAL(currentFontChanged(QFont)), SLOT(refreshFontPreview())); - - //move mac data button - QFile file(QDir::current().absolutePath() + "/dontmovemacdata"); - if (!file.exists()) - { - ui->migrateDataFolderMacBtn->setVisible(false); - } } LauncherPage::~LauncherPage() @@ -190,13 +183,6 @@ void LauncherPage::on_modsDirBrowseBtn_clicked() ui->modsDirTextBox->setText(cooked_dir); } } -void LauncherPage::on_migrateDataFolderMacBtn_clicked() -{ - QFile file(QDir::current().absolutePath() + "/dontmovemacdata"); - file.remove(); - QProcess::startDetached(qApp->arguments()[0]); - qApp->quit(); -} void LauncherPage::refreshUpdateChannelList() { diff --git a/launcher/ui/pages/global/LauncherPage.h b/launcher/ui/pages/global/LauncherPage.h index 63cfe9c30..bbf5d2fee 100644 --- a/launcher/ui/pages/global/LauncherPage.h +++ b/launcher/ui/pages/global/LauncherPage.h @@ -88,7 +88,6 @@ private void on_instDirBrowseBtn_clicked(); void on_modsDirBrowseBtn_clicked(); void on_iconsDirBrowseBtn_clicked(); - void on_migrateDataFolderMacBtn_clicked(); /*! * Updates the list of update channels in the combo box. diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index 086de17b3..ae7eb73fe 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -157,13 +157,6 @@ - - - - Move the data to new location (will restart the launcher) - - - From 0507b56beda250d86aaf9f772c122bceae04f748 Mon Sep 17 00:00:00 2001 From: Ryan Cao <70191398+ryanccn@users.noreply.github.com> Date: Wed, 27 Apr 2022 20:29:40 +0800 Subject: [PATCH 024/157] feat: add PolyMC icon as instance icon --- launcher/resources/multimc/multimc.qrc | 1 + .../multimc/scalable/instances/polymc.svg | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 launcher/resources/multimc/scalable/instances/polymc.svg diff --git a/launcher/resources/multimc/multimc.qrc b/launcher/resources/multimc/multimc.qrc index d31885b94..0fe673ff5 100644 --- a/launcher/resources/multimc/multimc.qrc +++ b/launcher/resources/multimc/multimc.qrc @@ -313,5 +313,6 @@ scalable/instances/fox.svg scalable/instances/bee.svg + scalable/instances/polymc.svg diff --git a/launcher/resources/multimc/scalable/instances/polymc.svg b/launcher/resources/multimc/scalable/instances/polymc.svg new file mode 100644 index 000000000..c192d5031 --- /dev/null +++ b/launcher/resources/multimc/scalable/instances/polymc.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + From efe4e7df06d97e0d08987447f11d187423db1b8d Mon Sep 17 00:00:00 2001 From: DioEgizio <83089242+DioEgizio@users.noreply.github.com> Date: Tue, 26 Apr 2022 18:04:05 +0200 Subject: [PATCH 025/157] fix some appimage issues building with qt 5.15.2 some users are having weird scaling issues since we're using qt 5.12.8 for the appimage --- .github/workflows/build.yml | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e6d1189be..57c04e211 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,6 +17,9 @@ jobs: - os: ubuntu-20.04 + - os: ubuntu-20.04 + appimage: true + - os: windows-2022 name: "Windows-i686" msystem: mingw32 @@ -66,30 +69,25 @@ jobs: ver_short=`git rev-parse --short HEAD` echo "VERSION=$ver_short" >> $GITHUB_ENV - - name: Install OpenJDK - uses: actions/setup-java@v3 - with: - distribution: 'temurin' - java-version: '17' - - name: Install Qt (macOS) if: runner.os == 'macOS' run: | brew update - brew install qt@5 + brew install qt@5 ninja + + - name: Update Qt (AppImage) + if: runner.os == 'Linux' && matrix.appimage == true + run: | + sudo add-apt-repository ppa:savoury1/qt-5-15 - name: Install Qt (Linux) if: runner.os == 'Linux' run: | sudo apt-get -y update - sudo apt-get -y install qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools libqt5core5a libqt5network5 libqt5gui5 - - - name: Install Ninja - if: runner.os != 'Windows' - uses: urkle/action-get-ninja@v1 + sudo apt-get -y install qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools libqt5core5a libqt5network5 libqt5gui5 ninja-build - name: Prepare AppImage (Linux) - if: runner.os == 'Linux' + if: runner.os == 'Linux' && matrix.appimage == true run: | wget "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage" wget "https://github.com/linuxdeploy/linuxdeploy-plugin-appimage/releases/download/continuous/linuxdeploy-plugin-appimage-x86_64.AppImage" @@ -167,7 +165,7 @@ jobs: cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable - name: Package (Linux) - if: runner.os == 'Linux' + if: runner.os == 'Linux' && matrix.appimage != true run: | cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_DIR }} @@ -175,7 +173,7 @@ jobs: tar --owner root --group root -czf ../PolyMC.tar.gz * - name: Package (Linux, portable) - if: runner.os == 'Linux' + if: runner.os == 'Linux' && matrix.appimage != true run: | cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable @@ -184,7 +182,7 @@ jobs: tar -czf ../PolyMC-portable.tar.gz * - name: Package AppImage (Linux) - if: runner.os == 'Linux' + if: runner.os == 'Linux' && matrix.appimage == true shell: bash run: | cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_APPIMAGE_DIR }}/usr @@ -234,21 +232,21 @@ jobs: path: ${{ env.INSTALL_PORTABLE_DIR }}/** - name: Upload binary tarball (Linux) - if: runner.os == 'Linux' + if: runner.os == 'Linux' && matrix.appimage != true uses: actions/upload-artifact@v3 with: name: PolyMC-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }} path: PolyMC.tar.gz - name: Upload binary tarball (Linux, portable) - if: runner.os == 'Linux' + if: runner.os == 'Linux' && matrix.appimage != true uses: actions/upload-artifact@v3 with: name: PolyMC-${{ runner.os }}-Portable-${{ env.VERSION }}-${{ inputs.build_type }} path: PolyMC-portable.tar.gz - name: Upload AppImage (Linux) - if: runner.os == 'Linux' + if: runner.os == 'Linux' && matrix.appimage == true uses: actions/upload-artifact@v3 with: name: PolyMC-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage From b931dc0f9373a0e6887911e3d6f8fb69afbed790 Mon Sep 17 00:00:00 2001 From: txtsd Date: Fri, 29 Apr 2022 01:30:47 +0530 Subject: [PATCH 026/157] fix(mnemonics): Add missing buddies to labels Co-authored-by: Sefa Eyeoglu --- launcher/ui/pages/global/ExternalToolsPage.ui | 3 +++ launcher/ui/pages/global/JavaPage.ui | 15 +++++++++++++++ launcher/ui/pages/global/ProxyPage.ui | 6 ++++++ launcher/ui/widgets/CustomCommands.ui | 9 +++++++++ 4 files changed, 33 insertions(+) diff --git a/launcher/ui/pages/global/ExternalToolsPage.ui b/launcher/ui/pages/global/ExternalToolsPage.ui index 8609d4696..3643094df 100644 --- a/launcher/ui/pages/global/ExternalToolsPage.ui +++ b/launcher/ui/pages/global/ExternalToolsPage.ui @@ -158,6 +158,9 @@ &Text Editor: + + jsonEditorTextBox + diff --git a/launcher/ui/pages/global/JavaPage.ui b/launcher/ui/pages/global/JavaPage.ui index bb1957705..083435d8b 100644 --- a/launcher/ui/pages/global/JavaPage.ui +++ b/launcher/ui/pages/global/JavaPage.ui @@ -72,6 +72,9 @@ &Minimum memory allocation: + + minMemSpinBox + @@ -79,6 +82,9 @@ Ma&ximum memory allocation: + + maxMemSpinBox + @@ -108,6 +114,9 @@ &PermGen: + + permGenSpinBox + @@ -152,6 +161,9 @@ &Java path: + + javaPathTextBox + @@ -194,6 +206,9 @@ J&VM arguments: + + jvmArgsTextBox + diff --git a/launcher/ui/pages/global/ProxyPage.ui b/launcher/ui/pages/global/ProxyPage.ui index 5a2fc73d4..91ba46b3d 100644 --- a/launcher/ui/pages/global/ProxyPage.ui +++ b/launcher/ui/pages/global/ProxyPage.ui @@ -147,6 +147,9 @@ &Username: + + proxyUserEdit + @@ -154,6 +157,9 @@ &Password: + + proxyPassEdit + diff --git a/launcher/ui/widgets/CustomCommands.ui b/launcher/ui/widgets/CustomCommands.ui index 68e680a54..4a39ff7f7 100644 --- a/launcher/ui/widgets/CustomCommands.ui +++ b/launcher/ui/widgets/CustomCommands.ui @@ -43,6 +43,9 @@ P&ost-exit command: + + postExitCmdTextBox + @@ -53,6 +56,9 @@ &Pre-launch command: + + preLaunchCmdTextBox + @@ -63,6 +69,9 @@ &Wrapper command: + + wrapperCmdTextBox + From ece5ca52b211baf4505d113b7ad8f2c939e95c51 Mon Sep 17 00:00:00 2001 From: txtsd Date: Fri, 29 Apr 2022 05:04:26 +0530 Subject: [PATCH 027/157] feat(workflow): Use ccache --- .github/workflows/build.yml | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 57c04e211..042ef27c3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -62,6 +62,32 @@ jobs: cmake:p ninja:p qt5:p + ccache:p + + - name: Setup ccache + if: runner.os != 'Windows' + uses: hendrikmuhs/ccache-action@v1.2.1 + with: + key: ${{ matrix.os }}-${{ matrix.appimage }}-${{ inputs.build_type }} + + - name: Setup ccache (Windows) + if: runner.os == 'Windows' + shell: msys2 {0} + run: | + ccache --set-config=cache_dir='${{ github.workspace }}\.ccache' + ccache --set-config=max_size='500M' + ccache --set-config=compression=true + ccache -p # Show config + ccache -z # Zero stats + + - name: Retrieve ccache cache (Windows) + if: runner.os == 'Windows' + uses: actions/cache@v3.0.2 + with: + path: '${{ github.workspace }}\.ccache' + key: ${{ matrix.os }}-${{ matrix.msystem }}-${{ inputs.build_type }} + restore-keys: | + ${{ matrix.os }}-${{ matrix.msystem }}-${{ inputs.build_type }} - name: Set short version shell: bash @@ -102,18 +128,18 @@ jobs: - name: Configure CMake (macOS) if: runner.os == 'macOS' run: | - cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DQt5_DIR=/usr/local/opt/qt@5 -DCMAKE_PREFIX_PATH=/usr/local/opt/qt@5 -DLauncher_BUILD_PLATFORM=macOS -G Ninja + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DQt5_DIR=/usr/local/opt/qt@5 -DCMAKE_PREFIX_PATH=/usr/local/opt/qt@5 -DLauncher_BUILD_PLATFORM=macOS -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -G Ninja - name: Configure CMake (Windows) if: runner.os == 'Windows' shell: msys2 {0} run: | - cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=${{ matrix.name }} -G Ninja + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=${{ matrix.name }} -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -G Ninja - name: Configure CMake (Linux) if: runner.os == 'Linux' run: | - cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=Linux -G Ninja + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=Linux -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -G Ninja ## # BUILD From dac801c8ac388495bfc8a4376d01457553ef6bad Mon Sep 17 00:00:00 2001 From: dada513 Date: Sat, 30 Apr 2022 15:19:57 +0200 Subject: [PATCH 028/157] add hide java wizard toggle --- launcher/Application.cpp | 6 ++ launcher/ui/pages/global/JavaPage.cpp | 2 + launcher/ui/pages/global/JavaPage.ui | 88 +++++++++++++++------------ share | 1 + 4 files changed, 58 insertions(+), 39 deletions(-) create mode 120000 share diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 8bd434f03..c8f6b7803 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -691,6 +691,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) m_settings->registerSetting("LastHostname", ""); m_settings->registerSetting("JvmArgs", ""); m_settings->registerSetting("IgnoreJavaCompatibility", false); + m_settings->registerSetting("IgnoreJavaWizard", false); // Native library workarounds m_settings->registerSetting("UseNativeOpenAL", false); @@ -936,6 +937,10 @@ bool Application::createSetupWizard() { bool javaRequired = [&]() { + bool ignoreJavaWizard = m_settings->get("IgnoreJavaWizard").toBool(); + if(ignoreJavaWizard) { + return false; + } QString currentHostName = QHostInfo::localHostName(); QString oldHostName = settings()->get("LastHostname").toString(); if (currentHostName != oldHostName) @@ -966,6 +971,7 @@ bool Application::createSetupWizard() { m_setupWizard->addPage(new LanguageWizardPage(m_setupWizard)); } + if (javaRequired) { m_setupWizard->addPage(new JavaWizardPage(m_setupWizard)); diff --git a/launcher/ui/pages/global/JavaPage.cpp b/launcher/ui/pages/global/JavaPage.cpp index f0616db1f..b5e8de6c1 100644 --- a/launcher/ui/pages/global/JavaPage.cpp +++ b/launcher/ui/pages/global/JavaPage.cpp @@ -97,6 +97,7 @@ void JavaPage::applySettings() s->set("JavaPath", ui->javaPathTextBox->text()); s->set("JvmArgs", ui->jvmArgsTextBox->text()); s->set("IgnoreJavaCompatibility", ui->skipCompatibilityCheckbox->isChecked()); + s->set("IgnoreJavaWizard", ui->skipJavaWizardCheckbox->isChecked()); JavaCommon::checkJVMArgs(s->get("JvmArgs").toString(), this->parentWidget()); } void JavaPage::loadSettings() @@ -121,6 +122,7 @@ void JavaPage::loadSettings() ui->javaPathTextBox->setText(s->get("JavaPath").toString()); ui->jvmArgsTextBox->setText(s->get("JvmArgs").toString()); ui->skipCompatibilityCheckbox->setChecked(s->get("IgnoreJavaCompatibility").toBool()); + ui->skipJavaWizardCheckbox->setChecked(s->get("IgnoreJavaWizard").toBool()); } void JavaPage::on_javaDetectBtn_clicked() diff --git a/launcher/ui/pages/global/JavaPage.ui b/launcher/ui/pages/global/JavaPage.ui index bb1957705..7268601f2 100644 --- a/launcher/ui/pages/global/JavaPage.ui +++ b/launcher/ui/pages/global/JavaPage.ui @@ -154,35 +154,6 @@ - - - - - - - - - - 0 - 0 - - - - - 28 - 16777215 - - - - ... - - - - - - - - @@ -196,21 +167,24 @@ - - + + 0 0 + + If enabled, the launcher will not check if an instance is compatible with the selected Java version. + - &Auto-detect... + &Skip Java compatibility checks - - + + 0 @@ -218,23 +192,59 @@ - &Test + &Auto-detect... - - + + + + + + + + + + 0 + 0 + + + + + 28 + 16777215 + + + + ... + + + + + + + 0 0 + + &Test + + + + + + + + - If enabled, the launcher will not check if an instance is compatible with the selected Java version. + If enabled, the launcher will not prompt you to choose a Java version if one isn't found. - &Skip Java compatibility checks + Skip Java Wizard diff --git a/share b/share new file mode 120000 index 000000000..bf797c5fe --- /dev/null +++ b/share @@ -0,0 +1 @@ +cmake-build-debug \ No newline at end of file From 67687683731fecde57042873835bb3ab844e1bce Mon Sep 17 00:00:00 2001 From: dada513 Date: Sat, 30 Apr 2022 15:22:31 +0200 Subject: [PATCH 029/157] Remove symlink --- share | 1 - 1 file changed, 1 deletion(-) delete mode 120000 share diff --git a/share b/share deleted file mode 120000 index bf797c5fe..000000000 --- a/share +++ /dev/null @@ -1 +0,0 @@ -cmake-build-debug \ No newline at end of file From 1e03ef484dafc41a568442186d14dba44c42cc26 Mon Sep 17 00:00:00 2001 From: dada513 Date: Sat, 30 Apr 2022 16:14:48 +0200 Subject: [PATCH 030/157] Update launcher/ui/pages/global/JavaPage.ui Co-authored-by: Sefa Eyeoglu --- launcher/ui/pages/global/JavaPage.ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/pages/global/JavaPage.ui b/launcher/ui/pages/global/JavaPage.ui index 7268601f2..2e553619e 100644 --- a/launcher/ui/pages/global/JavaPage.ui +++ b/launcher/ui/pages/global/JavaPage.ui @@ -244,7 +244,7 @@ If enabled, the launcher will not prompt you to choose a Java version if one isn't found. - Skip Java Wizard + Skip Java &Wizard From 5662d410628f0df43d3b15ae493bed85915ac799 Mon Sep 17 00:00:00 2001 From: dada513 Date: Sat, 30 Apr 2022 16:20:05 +0200 Subject: [PATCH 031/157] Update launcher/ui/pages/global/JavaPage.ui Co-authored-by: Sefa Eyeoglu --- launcher/ui/pages/global/JavaPage.ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/pages/global/JavaPage.ui b/launcher/ui/pages/global/JavaPage.ui index 2e553619e..cfcf90947 100644 --- a/launcher/ui/pages/global/JavaPage.ui +++ b/launcher/ui/pages/global/JavaPage.ui @@ -244,7 +244,7 @@ If enabled, the launcher will not prompt you to choose a Java version if one isn't found. - Skip Java &Wizard + Skip Java &Wizard From 239e4adf29e1190096a82ec81a8f29ff8ebf5713 Mon Sep 17 00:00:00 2001 From: txtsd Date: Sat, 30 Apr 2022 20:42:11 +0530 Subject: [PATCH 032/157] refactor(workflow): Only use ccache on Debug builds --- .github/workflows/build.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 042ef27c3..0590b3480 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -65,13 +65,13 @@ jobs: ccache:p - name: Setup ccache - if: runner.os != 'Windows' + if: runner.os != 'Windows' && inputs.build_type == 'Debug' uses: hendrikmuhs/ccache-action@v1.2.1 with: - key: ${{ matrix.os }}-${{ matrix.appimage }}-${{ inputs.build_type }} + key: ${{ matrix.os }}-${{ matrix.appimage }} - name: Setup ccache (Windows) - if: runner.os == 'Windows' + if: runner.os == 'Windows' && inputs.build_type == 'Debug' shell: msys2 {0} run: | ccache --set-config=cache_dir='${{ github.workspace }}\.ccache' @@ -81,13 +81,13 @@ jobs: ccache -z # Zero stats - name: Retrieve ccache cache (Windows) - if: runner.os == 'Windows' + if: runner.os == 'Windows' && inputs.build_type == 'Debug' uses: actions/cache@v3.0.2 with: path: '${{ github.workspace }}\.ccache' - key: ${{ matrix.os }}-${{ matrix.msystem }}-${{ inputs.build_type }} + key: ${{ matrix.os }}-${{ matrix.msystem }} restore-keys: | - ${{ matrix.os }}-${{ matrix.msystem }}-${{ inputs.build_type }} + ${{ matrix.os }}-${{ matrix.msystem }} - name: Set short version shell: bash From 1a86f7269002e5fb696339f0d709e4832024d741 Mon Sep 17 00:00:00 2001 From: timoreo22 Date: Mon, 2 May 2022 11:13:46 +0200 Subject: [PATCH 033/157] Fix nightly.link pr comment --- .github/workflows/pr-comment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-comment.yml b/.github/workflows/pr-comment.yml index 7e8e4d990..f0f5b8cc1 100644 --- a/.github/workflows/pr-comment.yml +++ b/.github/workflows/pr-comment.yml @@ -1,7 +1,7 @@ name: Comment on pull request on: workflow_run: - workflows: ['Test workflow with upload'] + workflows: ['Build Application'] types: [completed] jobs: pr_comment: From 0b38d878a104029892590ff7a60226ae4077aeb4 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Mon, 2 May 2022 16:27:15 +0200 Subject: [PATCH 034/157] fix: remove in-tree CMake modules where possible --- cmake/BundleUtilities.cmake | 786 --------------------------- cmake/GetPrerequisites.cmake | 902 ------------------------------- cmake/UseJava.cmake | 881 ------------------------------ cmake/UseJavaClassFilelist.cmake | 52 -- cmake/UseJavaSymlinks.cmake | 32 -- 5 files changed, 2653 deletions(-) delete mode 100644 cmake/BundleUtilities.cmake delete mode 100644 cmake/GetPrerequisites.cmake delete mode 100644 cmake/UseJava.cmake delete mode 100644 cmake/UseJavaClassFilelist.cmake delete mode 100644 cmake/UseJavaSymlinks.cmake diff --git a/cmake/BundleUtilities.cmake b/cmake/BundleUtilities.cmake deleted file mode 100644 index e3f50b94c..000000000 --- a/cmake/BundleUtilities.cmake +++ /dev/null @@ -1,786 +0,0 @@ -# - Functions to help assemble a standalone bundle application. -# A collection of CMake utility functions useful for dealing with .app -# bundles on the Mac and bundle-like directories on any OS. -# -# The following functions are provided by this module: -# fixup_bundle -# copy_and_fixup_bundle -# verify_app -# get_bundle_main_executable -# get_dotapp_dir -# get_bundle_and_executable -# get_bundle_all_executables -# get_item_key -# clear_bundle_keys -# set_bundle_key_values -# get_bundle_keys -# copy_resolved_item_into_bundle -# copy_resolved_framework_into_bundle -# fixup_bundle_item -# verify_bundle_prerequisites -# verify_bundle_symlinks -# Requires CMake 2.6 or greater because it uses function, break and -# PARENT_SCOPE. Also depends on GetPrerequisites.cmake. -# -# FIXUP_BUNDLE( ) -# Fix up a bundle in-place and make it standalone, such that it can be -# drag-n-drop copied to another machine and run on that machine as long as all -# of the system libraries are compatible. -# -# If you pass plugins to fixup_bundle as the libs parameter, you should install -# them or copy them into the bundle before calling fixup_bundle. The "libs" -# parameter is a list of libraries that must be fixed up, but that cannot be -# determined by otool output analysis. (i.e., plugins) -# -# Gather all the keys for all the executables and libraries in a bundle, and -# then, for each key, copy each prerequisite into the bundle. Then fix each one -# up according to its own list of prerequisites. -# -# Then clear all the keys and call verify_app on the final bundle to ensure -# that it is truly standalone. -# -# COPY_AND_FIXUP_BUNDLE( ) -# Makes a copy of the bundle at location and then fixes up the -# new copied bundle in-place at ... -# -# VERIFY_APP() -# Verifies that an application appears valid based on running analysis -# tools on it. Calls "message(FATAL_ERROR" if the application is not verified. -# -# GET_BUNDLE_MAIN_EXECUTABLE( ) -# The result will be the full path name of the bundle's main executable file -# or an "error:" prefixed string if it could not be determined. -# -# GET_DOTAPP_DIR( ) -# Returns the nearest parent dir whose name ends with ".app" given the full -# path to an executable. If there is no such parent dir, then simply return -# the dir containing the executable. -# -# The returned directory may or may not exist. -# -# GET_BUNDLE_AND_EXECUTABLE( ) -# Takes either a ".app" directory name or the name of an executable -# nested inside a ".app" directory and returns the path to the ".app" -# directory in and the path to its main executable in -# -# -# GET_BUNDLE_ALL_EXECUTABLES( ) -# Scans the given bundle recursively for all executable files and accumulates -# them into a variable. -# -# GET_ITEM_KEY( ) -# Given a file (item) name, generate a key that should be unique considering -# the set of libraries that need copying or fixing up to make a bundle -# standalone. This is essentially the file name including extension with "." -# replaced by "_" -# -# This key is used as a prefix for CMake variables so that we can associate a -# set of variables with a given item based on its key. -# -# CLEAR_BUNDLE_KEYS() -# Loop over the list of keys, clearing all the variables associated with each -# key. After the loop, clear the list of keys itself. -# -# Caller of get_bundle_keys should call clear_bundle_keys when done with list -# of keys. -# -# SET_BUNDLE_KEY_VALUES( -# ) -# Add a key to the list (if necessary) for the given item. If added, -# also set all the variables associated with that key. -# -# GET_BUNDLE_KEYS( ) -# Loop over all the executable and library files within the bundle (and given -# as extra ) and accumulate a list of keys representing them. Set -# values associated with each key such that we can loop over all of them and -# copy prerequisite libs into the bundle and then do appropriate -# install_name_tool fixups. -# -# COPY_RESOLVED_ITEM_INTO_BUNDLE( ) -# Copy a resolved item into the bundle if necessary. Copy is not necessary if -# the resolved_item is "the same as" the resolved_embedded_item. -# -# COPY_RESOLVED_FRAMEWORK_INTO_BUNDLE( ) -# Copy a resolved framework into the bundle if necessary. Copy is not necessary -# if the resolved_item is "the same as" the resolved_embedded_item. -# -# By default, BU_COPY_FULL_FRAMEWORK_CONTENTS is not set. If you want full -# frameworks embedded in your bundles, set BU_COPY_FULL_FRAMEWORK_CONTENTS to -# ON before calling fixup_bundle. By default, -# COPY_RESOLVED_FRAMEWORK_INTO_BUNDLE copies the framework dylib itself plus -# the framework Resources directory. -# -# FIXUP_BUNDLE_ITEM( ) -# Get the direct/non-system prerequisites of the resolved embedded item. For -# each prerequisite, change the way it is referenced to the value of the -# _EMBEDDED_ITEM keyed variable for that prerequisite. (Most likely changing to -# an "@executable_path" style reference.) -# -# This function requires that the resolved_embedded_item be "inside" the bundle -# already. In other words, if you pass plugins to fixup_bundle as the libs -# parameter, you should install them or copy them into the bundle before -# calling fixup_bundle. The "libs" parameter is a list of libraries that must -# be fixed up, but that cannot be determined by otool output analysis. (i.e., -# plugins) -# -# Also, change the id of the item being fixed up to its own _EMBEDDED_ITEM -# value. -# -# Accumulate changes in a local variable and make *one* call to -# install_name_tool at the end of the function with all the changes at once. -# -# If the BU_CHMOD_BUNDLE_ITEMS variable is set then bundle items will be -# marked writable before install_name_tool tries to change them. -# -# VERIFY_BUNDLE_PREREQUISITES( ) -# Verifies that the sum of all prerequisites of all files inside the bundle -# are contained within the bundle or are "system" libraries, presumed to exist -# everywhere. -# -# VERIFY_BUNDLE_SYMLINKS( ) -# Verifies that any symlinks found in the bundle point to other files that are -# already also in the bundle... Anything that points to an external file causes -# this function to fail the verification. - -#============================================================================= -# Copyright 2008-2009 Kitware, Inc. -# -# Distributed under the OSI-approved BSD License (the "License"); -# see accompanying file Copyright.txt for details. -# -# This software is distributed WITHOUT ANY WARRANTY; without even the -# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. -# See the License for more information. -#============================================================================= -# (To distribute this file outside of CMake, substitute the full -# License text for the above reference.) - -# The functions defined in this file depend on the get_prerequisites function -# (and possibly others) found in: -# -get_filename_component(BundleUtilities_cmake_dir "${CMAKE_CURRENT_LIST_FILE}" PATH) -include("${BundleUtilities_cmake_dir}/GetPrerequisites.cmake") - - -function(get_bundle_main_executable bundle result_var) - set(result "error: '${bundle}/Contents/Info.plist' file does not exist") - - if(EXISTS "${bundle}/Contents/Info.plist") - set(result "error: no CFBundleExecutable in '${bundle}/Contents/Info.plist' file") - set(line_is_main_executable 0) - set(bundle_executable "") - - # Read Info.plist as a list of lines: - # - set(eol_char "E") - file(READ "${bundle}/Contents/Info.plist" info_plist) - string(REGEX REPLACE ";" "\\\\;" info_plist "${info_plist}") - string(REGEX REPLACE "\n" "${eol_char};" info_plist "${info_plist}") - - # Scan the lines for "CFBundleExecutable" - the line after that - # is the name of the main executable. - # - foreach(line ${info_plist}) - if(line_is_main_executable) - string(REGEX REPLACE "^.*(.*).*$" "\\1" bundle_executable "${line}") - break() - endif() - - if(line MATCHES "^.*CFBundleExecutable.*$") - set(line_is_main_executable 1) - endif() - endforeach() - - if(NOT "${bundle_executable}" STREQUAL "") - if(EXISTS "${bundle}/Contents/MacOS/${bundle_executable}") - set(result "${bundle}/Contents/MacOS/${bundle_executable}") - else() - - # Ultimate goal: - # If not in "Contents/MacOS" then scan the bundle for matching files. If - # there is only one executable file that matches, then use it, otherwise - # it's an error... - # - #file(GLOB_RECURSE file_list "${bundle}/${bundle_executable}") - - # But for now, pragmatically, it's an error. Expect the main executable - # for the bundle to be in Contents/MacOS, it's an error if it's not: - # - set(result "error: '${bundle}/Contents/MacOS/${bundle_executable}' does not exist") - endif() - endif() - else() - # - # More inclusive technique... (This one would work on Windows and Linux - # too, if a developer followed the typical Mac bundle naming convention...) - # - # If there is no Info.plist file, try to find an executable with the same - # base name as the .app directory: - # - endif() - - set(${result_var} "${result}" PARENT_SCOPE) -endfunction() - - -function(get_dotapp_dir exe dotapp_dir_var) - set(s "${exe}") - - if(s MATCHES "^.*/.*\\.app/.*$") - # If there is a ".app" parent directory, - # ascend until we hit it: - # (typical of a Mac bundle executable) - # - set(done 0) - while(NOT ${done}) - get_filename_component(snamewe "${s}" NAME_WE) - get_filename_component(sname "${s}" NAME) - get_filename_component(sdir "${s}" PATH) - set(s "${sdir}") - if(sname MATCHES "\\.app$") - set(done 1) - set(dotapp_dir "${sdir}/${sname}") - endif() - endwhile() - else() - # Otherwise use a directory containing the exe - # (typical of a non-bundle executable on Mac, Windows or Linux) - # - is_file_executable("${s}" is_executable) - if(is_executable) - get_filename_component(sdir "${s}" PATH) - set(dotapp_dir "${sdir}") - else() - set(dotapp_dir "${s}") - endif() - endif() - - - set(${dotapp_dir_var} "${dotapp_dir}" PARENT_SCOPE) -endfunction() - - -function(get_bundle_and_executable app bundle_var executable_var valid_var) - set(valid 0) - - if(EXISTS "${app}") - # Is it a directory ending in .app? - if(IS_DIRECTORY "${app}") - if(app MATCHES "\\.app$") - get_bundle_main_executable("${app}" executable) - if(EXISTS "${app}" AND EXISTS "${executable}") - set(${bundle_var} "${app}" PARENT_SCOPE) - set(${executable_var} "${executable}" PARENT_SCOPE) - set(valid 1) - #message(STATUS "info: handled .app directory case...") - else() - message(STATUS "warning: *NOT* handled - .app directory case...") - endif() - else() - message(STATUS "warning: *NOT* handled - directory but not .app case...") - endif() - else() - # Is it an executable file? - is_file_executable("${app}" is_executable) - if(is_executable) - get_dotapp_dir("${app}" dotapp_dir) - if(EXISTS "${dotapp_dir}") - set(${bundle_var} "${dotapp_dir}" PARENT_SCOPE) - set(${executable_var} "${app}" PARENT_SCOPE) - set(valid 1) - #message(STATUS "info: handled executable file in .app dir case...") - else() - get_filename_component(app_dir "${app}" PATH) - set(${bundle_var} "${app_dir}" PARENT_SCOPE) - set(${executable_var} "${app}" PARENT_SCOPE) - set(valid 1) - #message(STATUS "info: handled executable file in any dir case...") - endif() - else() - message(STATUS "warning: *NOT* handled - not .app dir, not executable file...") - endif() - endif() - else() - message(STATUS "warning: *NOT* handled - directory/file ${app} does not exist...") - endif() - - if(NOT valid) - set(${bundle_var} "error: not a bundle" PARENT_SCOPE) - set(${executable_var} "error: not a bundle" PARENT_SCOPE) - endif() - - set(${valid_var} ${valid} PARENT_SCOPE) -endfunction() - - -function(get_bundle_all_executables bundle exes_var) - set(exes "") - - file(GLOB_RECURSE file_list "${bundle}/*") - foreach(f ${file_list}) - is_file_executable("${f}" is_executable) - if(is_executable) - set(exes ${exes} "${f}") - endif() - endforeach() - - set(${exes_var} "${exes}" PARENT_SCOPE) -endfunction() - - -function(get_item_key item key_var) - get_filename_component(item_name "${item}" NAME) - if(WIN32) - string(TOLOWER "${item_name}" item_name) - endif() - string(REGEX REPLACE "\\." "_" ${key_var} "${item_name}") - set(${key_var} ${${key_var}} PARENT_SCOPE) -endfunction() - - -function(clear_bundle_keys keys_var) - foreach(key ${${keys_var}}) - set(${key}_ITEM PARENT_SCOPE) - set(${key}_RESOLVED_ITEM PARENT_SCOPE) - set(${key}_DEFAULT_EMBEDDED_PATH PARENT_SCOPE) - set(${key}_EMBEDDED_ITEM PARENT_SCOPE) - set(${key}_RESOLVED_EMBEDDED_ITEM PARENT_SCOPE) - set(${key}_COPYFLAG PARENT_SCOPE) - endforeach() - set(${keys_var} PARENT_SCOPE) -endfunction() - - -function(set_bundle_key_values keys_var context item exepath dirs copyflag) - get_filename_component(item_name "${item}" NAME) - - get_item_key("${item}" key) - - list(LENGTH ${keys_var} length_before) - gp_append_unique(${keys_var} "${key}") - list(LENGTH ${keys_var} length_after) - - if(NOT length_before EQUAL length_after) - gp_resolve_item("${context}" "${item}" "${exepath}" "${dirs}" resolved_item) - - gp_item_default_embedded_path("${item}" default_embedded_path) - - if(item MATCHES "[^/]+\\.framework/") - # For frameworks, construct the name under the embedded path from the - # opening "${item_name}.framework/" to the closing "/${item_name}": - # - string(REGEX REPLACE "^.*(${item_name}.framework/.*/?${item_name}).*$" "${default_embedded_path}/\\1" embedded_item "${item}") - else() - # For other items, just use the same name as the original, but in the - # embedded path: - # - set(embedded_item "${default_embedded_path}/${item_name}") - endif() - - # Replace @executable_path and resolve ".." references: - # - string(REPLACE "@executable_path" "${exepath}" resolved_embedded_item "${embedded_item}") - get_filename_component(resolved_embedded_item "${resolved_embedded_item}" ABSOLUTE) - - # *But* -- if we are not copying, then force resolved_embedded_item to be - # the same as resolved_item. In the case of multiple executables in the - # original bundle, using the default_embedded_path results in looking for - # the resolved executable next to the main bundle executable. This is here - # so that exes in the other sibling directories (like "bin") get fixed up - # properly... - # - if(NOT copyflag) - set(resolved_embedded_item "${resolved_item}") - endif() - - set(${keys_var} ${${keys_var}} PARENT_SCOPE) - set(${key}_ITEM "${item}" PARENT_SCOPE) - set(${key}_RESOLVED_ITEM "${resolved_item}" PARENT_SCOPE) - set(${key}_DEFAULT_EMBEDDED_PATH "${default_embedded_path}" PARENT_SCOPE) - set(${key}_EMBEDDED_ITEM "${embedded_item}" PARENT_SCOPE) - set(${key}_RESOLVED_EMBEDDED_ITEM "${resolved_embedded_item}" PARENT_SCOPE) - set(${key}_COPYFLAG "${copyflag}" PARENT_SCOPE) - else() - #message("warning: item key '${key}' already in the list, subsequent references assumed identical to first") - endif() -endfunction() - - -function(get_bundle_keys app libs dirs keys_var) - set(${keys_var} PARENT_SCOPE) - - get_bundle_and_executable("${app}" bundle executable valid) - if(valid) - # Always use the exepath of the main bundle executable for @executable_path - # replacements: - # - get_filename_component(exepath "${executable}" PATH) - - # But do fixups on all executables in the bundle: - # - get_bundle_all_executables("${bundle}" exes) - - # For each extra lib, accumulate a key as well and then also accumulate - # any of its prerequisites. (Extra libs are typically dynamically loaded - # plugins: libraries that are prerequisites for full runtime functionality - # but that do not show up in otool -L output...) - # - foreach(lib ${libs}) - set_bundle_key_values(${keys_var} "${lib}" "${lib}" "${exepath}" "${dirs}" 0) - - set(prereqs "") - get_prerequisites("${lib}" prereqs 1 1 "${exepath}" "${dirs}") - foreach(pr ${prereqs}) - set_bundle_key_values(${keys_var} "${lib}" "${pr}" "${exepath}" "${dirs}" 1) - endforeach() - endforeach() - - # For each executable found in the bundle, accumulate keys as we go. - # The list of keys should be complete when all prerequisites of all - # binaries in the bundle have been analyzed. - # - foreach(exe ${exes}) - # Add the exe itself to the keys: - # - set_bundle_key_values(${keys_var} "${exe}" "${exe}" "${exepath}" "${dirs}" 0) - - # Add each prerequisite to the keys: - # - set(prereqs "") - get_prerequisites("${exe}" prereqs 1 1 "${exepath}" "${dirs}") - foreach(pr ${prereqs}) - set_bundle_key_values(${keys_var} "${exe}" "${pr}" "${exepath}" "${dirs}" 1) - endforeach() - endforeach() - - # Propagate values to caller's scope: - # - set(${keys_var} ${${keys_var}} PARENT_SCOPE) - foreach(key ${${keys_var}}) - set(${key}_ITEM "${${key}_ITEM}" PARENT_SCOPE) - set(${key}_RESOLVED_ITEM "${${key}_RESOLVED_ITEM}" PARENT_SCOPE) - set(${key}_DEFAULT_EMBEDDED_PATH "${${key}_DEFAULT_EMBEDDED_PATH}" PARENT_SCOPE) - set(${key}_EMBEDDED_ITEM "${${key}_EMBEDDED_ITEM}" PARENT_SCOPE) - set(${key}_RESOLVED_EMBEDDED_ITEM "${${key}_RESOLVED_EMBEDDED_ITEM}" PARENT_SCOPE) - set(${key}_COPYFLAG "${${key}_COPYFLAG}" PARENT_SCOPE) - endforeach() - endif() -endfunction() - - -function(copy_resolved_item_into_bundle resolved_item resolved_embedded_item) - if(WIN32) - # ignore case on Windows - string(TOLOWER "${resolved_item}" resolved_item_compare) - string(TOLOWER "${resolved_embedded_item}" resolved_embedded_item_compare) - else() - set(resolved_item_compare "${resolved_item}") - set(resolved_embedded_item_compare "${resolved_embedded_item}") - endif() - - if("${resolved_item_compare}" STREQUAL "${resolved_embedded_item_compare}") - message(STATUS "warning: resolved_item == resolved_embedded_item - not copying...") - else() - #message(STATUS "copying COMMAND ${CMAKE_COMMAND} -E copy ${resolved_item} ${resolved_embedded_item}") - execute_process(COMMAND ${CMAKE_COMMAND} -E copy "${resolved_item}" "${resolved_embedded_item}") - if(UNIX AND NOT APPLE) - file(RPATH_REMOVE FILE "${resolved_embedded_item}") - endif() - endif() - -endfunction() - - -function(copy_resolved_framework_into_bundle resolved_item resolved_embedded_item) - if(WIN32) - # ignore case on Windows - string(TOLOWER "${resolved_item}" resolved_item_compare) - string(TOLOWER "${resolved_embedded_item}" resolved_embedded_item_compare) - else() - set(resolved_item_compare "${resolved_item}") - set(resolved_embedded_item_compare "${resolved_embedded_item}") - endif() - - if("${resolved_item_compare}" STREQUAL "${resolved_embedded_item_compare}") - message(STATUS "warning: resolved_item == resolved_embedded_item - not copying...") - else() - if(BU_COPY_FULL_FRAMEWORK_CONTENTS) - # Full Framework (everything): - get_filename_component(resolved_dir "${resolved_item}" PATH) - get_filename_component(resolved_dir "${resolved_dir}/../.." ABSOLUTE) - get_filename_component(resolved_embedded_dir "${resolved_embedded_item}" PATH) - get_filename_component(resolved_embedded_dir "${resolved_embedded_dir}/../.." ABSOLUTE) - #message(STATUS "copying COMMAND ${CMAKE_COMMAND} -E copy_directory '${resolved_dir}' '${resolved_embedded_dir}'") - execute_process(COMMAND ${CMAKE_COMMAND} -E copy_directory "${resolved_dir}" "${resolved_embedded_dir}") - else() - # Framework lib itself: - #message(STATUS "copying COMMAND ${CMAKE_COMMAND} -E copy ${resolved_item} ${resolved_embedded_item}") - execute_process(COMMAND ${CMAKE_COMMAND} -E copy "${resolved_item}" "${resolved_embedded_item}") - - # Plus Resources, if they exist: - string(REGEX REPLACE "^(.*)/[^/]+/[^/]+/[^/]+$" "\\1/Resources" resolved_resources "${resolved_item}") - string(REGEX REPLACE "^(.*)/[^/]+/[^/]+/[^/]+$" "\\1/Resources" resolved_embedded_resources "${resolved_embedded_item}") - if(EXISTS "${resolved_resources}") - #message(STATUS "copying COMMAND ${CMAKE_COMMAND} -E copy_directory '${resolved_resources}' '${resolved_embedded_resources}'") - execute_process(COMMAND ${CMAKE_COMMAND} -E copy_directory "${resolved_resources}" "${resolved_embedded_resources}") - endif() - endif() - if(UNIX AND NOT APPLE) - file(RPATH_REMOVE FILE "${resolved_embedded_item}") - endif() - endif() - -endfunction() - - -function(fixup_bundle_item resolved_embedded_item exepath dirs) - # This item's key is "ikey": - # - get_item_key("${resolved_embedded_item}" ikey) - - # Ensure the item is "inside the .app bundle" -- it should not be fixed up if - # it is not in the .app bundle... Otherwise, we'll modify files in the build - # tree, or in other varied locations around the file system, with our call to - # install_name_tool. Make sure that doesn't happen here: - # - get_dotapp_dir("${exepath}" exe_dotapp_dir) - string(LENGTH "${exe_dotapp_dir}/" exe_dotapp_dir_length) - string(LENGTH "${resolved_embedded_item}" resolved_embedded_item_length) - set(path_too_short 0) - set(is_embedded 0) - if(${resolved_embedded_item_length} LESS ${exe_dotapp_dir_length}) - set(path_too_short 1) - endif() - if(NOT path_too_short) - string(SUBSTRING "${resolved_embedded_item}" 0 ${exe_dotapp_dir_length} item_substring) - if("${exe_dotapp_dir}/" STREQUAL "${item_substring}") - set(is_embedded 1) - endif() - endif() - if(NOT is_embedded) - message(" exe_dotapp_dir/='${exe_dotapp_dir}/'") - message(" item_substring='${item_substring}'") - message(" resolved_embedded_item='${resolved_embedded_item}'") - message("") - message("Install or copy the item into the bundle before calling fixup_bundle.") - message("Or maybe there's a typo or incorrect path in one of the args to fixup_bundle?") - message("") - message(FATAL_ERROR "cannot fixup an item that is not in the bundle...") - endif() - - set(prereqs "") - get_prerequisites("${resolved_embedded_item}" prereqs 1 0 "${exepath}" "${dirs}") - - set(changes "") - - foreach(pr ${prereqs}) - # Each referenced item's key is "rkey" in the loop: - # - get_item_key("${pr}" rkey) - - if(NOT "${${rkey}_EMBEDDED_ITEM}" STREQUAL "") - set(changes ${changes} "-change" "${pr}" "${${rkey}_EMBEDDED_ITEM}") - else() - message("warning: unexpected reference to '${pr}'") - endif() - endforeach() - - if(BU_CHMOD_BUNDLE_ITEMS) - execute_process(COMMAND chmod u+w "${resolved_embedded_item}") - endif() - - # Change this item's id and all of its references in one call - # to install_name_tool: - # - execute_process(COMMAND install_name_tool - ${changes} -id "${${ikey}_EMBEDDED_ITEM}" "${resolved_embedded_item}" - ) -endfunction() - - -function(fixup_bundle app libs dirs) - message(STATUS "fixup_bundle") - message(STATUS " app='${app}'") - message(STATUS " libs='${libs}'") - message(STATUS " dirs='${dirs}'") - - get_bundle_and_executable("${app}" bundle executable valid) - if(valid) - get_filename_component(exepath "${executable}" PATH) - - message(STATUS "fixup_bundle: preparing...") - get_bundle_keys("${app}" "${libs}" "${dirs}" keys) - - message(STATUS "fixup_bundle: copying...") - list(LENGTH keys n) - math(EXPR n ${n}*2) - - set(i 0) - foreach(key ${keys}) - math(EXPR i ${i}+1) - if(${${key}_COPYFLAG}) - message(STATUS "${i}/${n}: copying '${${key}_RESOLVED_ITEM}'") - else() - message(STATUS "${i}/${n}: *NOT* copying '${${key}_RESOLVED_ITEM}'") - endif() - - set(show_status 0) - if(show_status) - message(STATUS "key='${key}'") - message(STATUS "item='${${key}_ITEM}'") - message(STATUS "resolved_item='${${key}_RESOLVED_ITEM}'") - message(STATUS "default_embedded_path='${${key}_DEFAULT_EMBEDDED_PATH}'") - message(STATUS "embedded_item='${${key}_EMBEDDED_ITEM}'") - message(STATUS "resolved_embedded_item='${${key}_RESOLVED_EMBEDDED_ITEM}'") - message(STATUS "copyflag='${${key}_COPYFLAG}'") - message(STATUS "") - endif() - - if(${${key}_COPYFLAG}) - set(item "${${key}_ITEM}") - if(item MATCHES "[^/]+\\.framework/") - copy_resolved_framework_into_bundle("${${key}_RESOLVED_ITEM}" - "${${key}_RESOLVED_EMBEDDED_ITEM}") - else() - copy_resolved_item_into_bundle("${${key}_RESOLVED_ITEM}" - "${${key}_RESOLVED_EMBEDDED_ITEM}") - endif() - endif() - endforeach() - - message(STATUS "fixup_bundle: fixing...") - foreach(key ${keys}) - math(EXPR i ${i}+1) - if(APPLE) - message(STATUS "${i}/${n}: fixing up '${${key}_RESOLVED_EMBEDDED_ITEM}'") - fixup_bundle_item("${${key}_RESOLVED_EMBEDDED_ITEM}" "${exepath}" "${dirs}") - else() - message(STATUS "${i}/${n}: fix-up not required on this platform '${${key}_RESOLVED_EMBEDDED_ITEM}'") - endif() - endforeach() - - message(STATUS "fixup_bundle: cleaning up...") - clear_bundle_keys(keys) - - message(STATUS "fixup_bundle: verifying...") - verify_app("${app}") - else() - message(SEND_ERROR "error: fixup_bundle: not a valid bundle") - endif() - - message(STATUS "fixup_bundle: done") -endfunction() - - -function(copy_and_fixup_bundle src dst libs dirs) - execute_process(COMMAND ${CMAKE_COMMAND} -E copy_directory "${src}" "${dst}") - fixup_bundle("${dst}" "${libs}" "${dirs}") -endfunction() - - -function(verify_bundle_prerequisites bundle result_var info_var) - set(result 1) - set(info "") - set(count 0) - - get_bundle_main_executable("${bundle}" main_bundle_exe) - - file(GLOB_RECURSE file_list "${bundle}/*") - foreach(f ${file_list}) - is_file_executable("${f}" is_executable) - if(is_executable) - get_filename_component(exepath "${f}" PATH) - math(EXPR count "${count} + 1") - - message(STATUS "executable file ${count}: ${f}") - - set(prereqs "") - get_prerequisites("${f}" prereqs 1 1 "${exepath}" "") - - # On the Mac, - # "embedded" and "system" prerequisites are fine... anything else means - # the bundle's prerequisites are not verified (i.e., the bundle is not - # really "standalone") - # - # On Windows (and others? Linux/Unix/...?) - # "local" and "system" prereqs are fine... - # - set(external_prereqs "") - - foreach(p ${prereqs}) - set(p_type "") - gp_file_type("${f}" "${p}" p_type) - - if(APPLE) - if(NOT "${p_type}" STREQUAL "embedded" AND NOT "${p_type}" STREQUAL "system") - set(external_prereqs ${external_prereqs} "${p}") - endif() - else() - if(NOT "${p_type}" STREQUAL "local" AND NOT "${p_type}" STREQUAL "system") - set(external_prereqs ${external_prereqs} "${p}") - endif() - endif() - endforeach() - - if(external_prereqs) - # Found non-system/somehow-unacceptable prerequisites: - set(result 0) - set(info ${info} "external prerequisites found:\nf='${f}'\nexternal_prereqs='${external_prereqs}'\n") - endif() - endif() - endforeach() - - if(result) - set(info "Verified ${count} executable files in '${bundle}'") - endif() - - set(${result_var} "${result}" PARENT_SCOPE) - set(${info_var} "${info}" PARENT_SCOPE) -endfunction() - - -function(verify_bundle_symlinks bundle result_var info_var) - set(result 1) - set(info "") - set(count 0) - - # TODO: implement this function for real... - # Right now, it is just a stub that verifies unconditionally... - - set(${result_var} "${result}" PARENT_SCOPE) - set(${info_var} "${info}" PARENT_SCOPE) -endfunction() - - -function(verify_app app) - set(verified 0) - set(info "") - - get_bundle_and_executable("${app}" bundle executable valid) - - message(STATUS "===========================================================================") - message(STATUS "Analyzing app='${app}'") - message(STATUS "bundle='${bundle}'") - message(STATUS "executable='${executable}'") - message(STATUS "valid='${valid}'") - - # Verify that the bundle does not have any "external" prerequisites: - # - verify_bundle_prerequisites("${bundle}" verified info) - message(STATUS "verified='${verified}'") - message(STATUS "info='${info}'") - message(STATUS "") - - if(verified) - # Verify that the bundle does not have any symlinks to external files: - # - verify_bundle_symlinks("${bundle}" verified info) - message(STATUS "verified='${verified}'") - message(STATUS "info='${info}'") - message(STATUS "") - endif() - - if(NOT verified) - message(FATAL_ERROR "error: verify_app failed") - endif() -endfunction() diff --git a/cmake/GetPrerequisites.cmake b/cmake/GetPrerequisites.cmake deleted file mode 100644 index 39c2cc631..000000000 --- a/cmake/GetPrerequisites.cmake +++ /dev/null @@ -1,902 +0,0 @@ -# - Functions to analyze and list executable file prerequisites. -# This module provides functions to list the .dll, .dylib or .so -# files that an executable or shared library file depends on. (Its -# prerequisites.) -# -# It uses various tools to obtain the list of required shared library files: -# dumpbin (Windows) -# objdump (MinGW on Windows) -# ldd (Linux/Unix) -# otool (Mac OSX) -# The following functions are provided by this module: -# get_prerequisites -# list_prerequisites -# list_prerequisites_by_glob -# gp_append_unique -# is_file_executable -# gp_item_default_embedded_path -# (projects can override with gp_item_default_embedded_path_override) -# gp_resolve_item -# (projects can override with gp_resolve_item_override) -# gp_resolved_file_type -# (projects can override with gp_resolved_file_type_override) -# gp_file_type -# Requires CMake 2.6 or greater because it uses function, break, return and -# PARENT_SCOPE. -# -# GET_PREREQUISITES( -# ) -# Get the list of shared library files required by . The list in -# the variable named should be empty on first entry to -# this function. On exit, will contain the list of -# required shared library files. -# -# is the full path to an executable file. is the -# name of a CMake variable to contain the results. must be 0 -# or 1 indicating whether to include or exclude "system" prerequisites. If -# is set to 1 all prerequisites will be found recursively, if set to -# 0 only direct prerequisites are listed. is the path to the top -# level executable used for @executable_path replacment on the Mac. is -# a list of paths where libraries might be found: these paths are searched -# first when a target without any path info is given. Then standard system -# locations are also searched: PATH, Framework locations, /usr/lib... -# -# LIST_PREREQUISITES( [ [ []]]) -# Print a message listing the prerequisites of . -# -# is the name of a shared library or executable target or the full -# path to a shared library or executable file. If is set to 1 all -# prerequisites will be found recursively, if set to 0 only direct -# prerequisites are listed. must be 0 or 1 indicating whether -# to include or exclude "system" prerequisites. With set to 0 only -# the full path names of the prerequisites are printed, set to 1 extra -# informatin will be displayed. -# -# LIST_PREREQUISITES_BY_GLOB( ) -# Print the prerequisites of shared library and executable files matching a -# globbing pattern. is GLOB or GLOB_RECURSE and is a -# globbing expression used with "file(GLOB" or "file(GLOB_RECURSE" to retrieve -# a list of matching files. If a matching file is executable, its prerequisites -# are listed. -# -# Any additional (optional) arguments provided are passed along as the -# optional arguments to the list_prerequisites calls. -# -# GP_APPEND_UNIQUE( ) -# Append to the list variable only if the value is not -# already in the list. -# -# IS_FILE_EXECUTABLE( ) -# Return 1 in if is a binary executable, 0 otherwise. -# -# GP_ITEM_DEFAULT_EMBEDDED_PATH( ) -# Return the path that others should refer to the item by when the item -# is embedded inside a bundle. -# -# Override on a per-project basis by providing a project-specific -# gp_item_default_embedded_path_override function. -# -# GP_RESOLVE_ITEM( ) -# Resolve an item into an existing full path file. -# -# Override on a per-project basis by providing a project-specific -# gp_resolve_item_override function. -# -# GP_RESOLVED_FILE_TYPE( ) -# Return the type of with respect to . String -# describing type of prerequisite is returned in variable named . -# -# Use and if necessary to resolve non-absolute -# values -- but only for non-embedded items. -# -# Possible types are: -# system -# local -# embedded -# other -# Override on a per-project basis by providing a project-specific -# gp_resolved_file_type_override function. -# -# GP_FILE_TYPE( ) -# Return the type of with respect to . String -# describing type of prerequisite is returned in variable named . -# -# Possible types are: -# system -# local -# embedded -# other - -#============================================================================= -# Copyright 2008-2009 Kitware, Inc. -# -# Distributed under the OSI-approved BSD License (the "License"); -# see accompanying file Copyright.txt for details. -# -# This software is distributed WITHOUT ANY WARRANTY; without even the -# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. -# See the License for more information. -#============================================================================= -# (To distribute this file outside of CMake, substitute the full -# License text for the above reference.) - -function(gp_append_unique list_var value) - set(contains 0) - - foreach(item ${${list_var}}) - if("${item}" STREQUAL "${value}") - set(contains 1) - break() - endif() - endforeach() - - if(NOT contains) - set(${list_var} ${${list_var}} "${value}" PARENT_SCOPE) - endif() -endfunction() - - -function(is_file_executable file result_var) - # - # A file is not executable until proven otherwise: - # - set(${result_var} 0 PARENT_SCOPE) - - get_filename_component(file_full "${file}" ABSOLUTE) - string(TOLOWER "${file_full}" file_full_lower) - - # If file name ends in .exe on Windows, *assume* executable: - # - if(WIN32 AND NOT UNIX) - if("${file_full_lower}" MATCHES "\\.exe$") - set(${result_var} 1 PARENT_SCOPE) - return() - endif() - - # A clause could be added here that uses output or return value of dumpbin - # to determine ${result_var}. In 99%+? practical cases, the exe name - # match will be sufficient... - # - endif() - - # Use the information returned from the Unix shell command "file" to - # determine if ${file_full} should be considered an executable file... - # - # If the file command's output contains "executable" and does *not* contain - # "text" then it is likely an executable suitable for prerequisite analysis - # via the get_prerequisites macro. - # - if(UNIX) - if(NOT file_cmd) - find_program(file_cmd "file") - mark_as_advanced(file_cmd) - endif() - - if(file_cmd) - execute_process(COMMAND "${file_cmd}" "${file_full}" - OUTPUT_VARIABLE file_ov - OUTPUT_STRIP_TRAILING_WHITESPACE - ) - - # Replace the name of the file in the output with a placeholder token - # (the string " _file_full_ ") so that just in case the path name of - # the file contains the word "text" or "executable" we are not fooled - # into thinking "the wrong thing" because the file name matches the - # other 'file' command output we are looking for... - # - string(REPLACE "${file_full}" " _file_full_ " file_ov "${file_ov}") - string(TOLOWER "${file_ov}" file_ov) - - #message(STATUS "file_ov='${file_ov}'") - if("${file_ov}" MATCHES "executable") - #message(STATUS "executable!") - if("${file_ov}" MATCHES "text") - #message(STATUS "but text, so *not* a binary executable!") - else() - set(${result_var} 1 PARENT_SCOPE) - return() - endif() - endif() - - # Also detect position independent executables on Linux, - # where "file" gives "shared object ... (uses shared libraries)" - if("${file_ov}" MATCHES "shared object.*\(uses shared libs\)") - set(${result_var} 1 PARENT_SCOPE) - return() - endif() - - # "file" version 5.22 does not print "(used shared libraries)" - # but uses "interpreter" - if("${file_ov}" MATCHES "shared object.*interpreter") - set(${result_var} 1 PARENT_SCOPE) - return() - endif() - - else() - message(STATUS "warning: No 'file' command, skipping execute_process...") - endif() - endif() -endfunction() - - -function(gp_item_default_embedded_path item default_embedded_path_var) - - # On Windows and Linux, "embed" prerequisites in the same directory - # as the executable by default: - # - set(path "@executable_path") - set(overridden 0) - - # On the Mac, relative to the executable depending on the type - # of the thing we are embedding: - # - if(APPLE) - # - # The assumption here is that all executables in the bundle will be - # in same-level-directories inside the bundle. The parent directory - # of an executable inside the bundle should be MacOS or a sibling of - # MacOS and all embedded paths returned from here will begin with - # "@executable_path/../" and will work from all executables in all - # such same-level-directories inside the bundle. - # - - # By default, embed things right next to the main bundle executable: - # - set(path "@executable_path/../../Contents/MacOS") - - # Embed .dylibs right next to the main bundle executable: - # - if(item MATCHES "\\.dylib$") - set(path "@executable_path/../MacOS") - set(overridden 1) - endif() - - # Embed frameworks in the embedded "Frameworks" directory (sibling of MacOS): - # - if(NOT overridden) - if(item MATCHES "[^/]+\\.framework/") - set(path "@executable_path/../Frameworks") - set(overridden 1) - endif() - endif() - endif() - - # Provide a hook so that projects can override the default embedded location - # of any given library by whatever logic they choose: - # - if(COMMAND gp_item_default_embedded_path_override) - gp_item_default_embedded_path_override("${item}" path) - endif() - - set(${default_embedded_path_var} "${path}" PARENT_SCOPE) -endfunction() - - -function(gp_resolve_item context item exepath dirs resolved_item_var) - set(resolved 0) - set(resolved_item "${item}") - - # Is it already resolved? - # - if(IS_ABSOLUTE "${resolved_item}" AND EXISTS "${resolved_item}") - set(resolved 1) - endif() - - if(NOT resolved) - if(item MATCHES "@executable_path") - # - # @executable_path references are assumed relative to exepath - # - string(REPLACE "@executable_path" "${exepath}" ri "${item}") - get_filename_component(ri "${ri}" ABSOLUTE) - - if(EXISTS "${ri}") - #message(STATUS "info: embedded item exists (${ri})") - set(resolved 1) - set(resolved_item "${ri}") - else() - message(STATUS "warning: embedded item does not exist '${ri}'") - endif() - endif() - endif() - - if(NOT resolved) - if(item MATCHES "@loader_path") - # - # @loader_path references are assumed relative to the - # PATH of the given "context" (presumably another library) - # - get_filename_component(contextpath "${context}" PATH) - string(REPLACE "@loader_path" "${contextpath}" ri "${item}") - get_filename_component(ri "${ri}" ABSOLUTE) - - if(EXISTS "${ri}") - #message(STATUS "info: embedded item exists (${ri})") - set(resolved 1) - set(resolved_item "${ri}") - else() - message(STATUS "warning: embedded item does not exist '${ri}'") - endif() - endif() - endif() - - if(NOT resolved) - if(item MATCHES "@rpath") - # - # @rpath references are relative to the paths built into the binaries with -rpath - # We handle this case like we do for other Unixes - # - string(REPLACE "@rpath/" "" norpath_item "${item}") - - set(ri "ri-NOTFOUND") - find_file(ri "${norpath_item}" ${exepath} ${dirs} NO_DEFAULT_PATH) - if(ri) - #message(STATUS "info: 'find_file' in exepath/dirs (${ri})") - set(resolved 1) - set(resolved_item "${ri}") - set(ri "ri-NOTFOUND") - endif() - - endif() - endif() - - if(NOT resolved) - set(ri "ri-NOTFOUND") - find_file(ri "${item}" ${exepath} ${dirs} NO_DEFAULT_PATH) - find_file(ri "${item}" ${exepath} ${dirs} /usr/lib) - if(ri) - #message(STATUS "info: 'find_file' in exepath/dirs (${ri})") - set(resolved 1) - set(resolved_item "${ri}") - set(ri "ri-NOTFOUND") - endif() - endif() - - if(NOT resolved) - if(item MATCHES "[^/]+\\.framework/") - set(fw "fw-NOTFOUND") - find_file(fw "${item}" - "~/Library/Frameworks" - "/Library/Frameworks" - "/System/Library/Frameworks" - ) - if(fw) - #message(STATUS "info: 'find_file' found framework (${fw})") - set(resolved 1) - set(resolved_item "${fw}") - set(fw "fw-NOTFOUND") - endif() - endif() - endif() - - # Using find_program on Windows will find dll files that are in the PATH. - # (Converting simple file names into full path names if found.) - # - if(WIN32 AND NOT UNIX) - if(NOT resolved) - set(ri "ri-NOTFOUND") - find_program(ri "${item}" PATHS "${exepath};${dirs}" NO_DEFAULT_PATH) - find_program(ri "${item}" PATHS "${exepath};${dirs}") - if(ri) - #message(STATUS "info: 'find_program' in exepath/dirs (${ri})") - set(resolved 1) - set(resolved_item "${ri}") - set(ri "ri-NOTFOUND") - endif() - endif() - endif() - - # Provide a hook so that projects can override item resolution - # by whatever logic they choose: - # - if(COMMAND gp_resolve_item_override) - gp_resolve_item_override("${context}" "${item}" "${exepath}" "${dirs}" resolved_item resolved) - endif() - - if(NOT resolved) - message(STATUS " -warning: cannot resolve item '${item}' - - possible problems: - need more directories? - need to use InstallRequiredSystemLibraries? - run in install tree instead of build tree? -") -# message(STATUS " -#****************************************************************************** -#warning: cannot resolve item '${item}' -# -# possible problems: -# need more directories? -# need to use InstallRequiredSystemLibraries? -# run in install tree instead of build tree? -# -# context='${context}' -# item='${item}' -# exepath='${exepath}' -# dirs='${dirs}' -# resolved_item_var='${resolved_item_var}' -#****************************************************************************** -#") - endif() - - set(${resolved_item_var} "${resolved_item}" PARENT_SCOPE) -endfunction() - - -function(gp_resolved_file_type original_file file exepath dirs type_var) - #message(STATUS "**") - - if(NOT IS_ABSOLUTE "${original_file}") - message(STATUS "warning: gp_resolved_file_type expects absolute full path for first arg original_file") - endif() - - set(is_embedded 0) - set(is_local 0) - set(is_system 0) - - set(resolved_file "${file}") - - if("${file}" MATCHES "^@(executable|loader)_path") - set(is_embedded 1) - endif() - - if(NOT is_embedded) - if(NOT IS_ABSOLUTE "${file}") - gp_resolve_item("${original_file}" "${file}" "${exepath}" "${dirs}" resolved_file) - endif() - - string(TOLOWER "${original_file}" original_lower) - string(TOLOWER "${resolved_file}" lower) - - if(UNIX) - if(resolved_file MATCHES "^(/lib/|/lib32/|/lib64/|/usr/lib/|/usr/lib32/|/usr/lib64/|/usr/X11R6/|/usr/bin/)") - set(is_system 1) - endif() - endif() - - if(APPLE) - if(resolved_file MATCHES "^(/System/Library/|/usr/lib/)") - set(is_system 1) - endif() - endif() - - if(WIN32) - string(TOLOWER "$ENV{SystemRoot}" sysroot) - string(REGEX REPLACE "\\\\" "/" sysroot "${sysroot}") - - string(TOLOWER "$ENV{windir}" windir) - string(REGEX REPLACE "\\\\" "/" windir "${windir}") - - if(lower MATCHES "^(${sysroot}/sys(tem|wow)|${windir}/sys(tem|wow)|(.*/)*msvc[^/]+dll)") - set(is_system 1) - endif() - - if(UNIX) - # if cygwin, we can get the properly formed windows paths from cygpath - find_program(CYGPATH_EXECUTABLE cygpath) - - if(CYGPATH_EXECUTABLE) - execute_process(COMMAND ${CYGPATH_EXECUTABLE} -W - OUTPUT_VARIABLE env_windir - OUTPUT_STRIP_TRAILING_WHITESPACE) - execute_process(COMMAND ${CYGPATH_EXECUTABLE} -S - OUTPUT_VARIABLE env_sysdir - OUTPUT_STRIP_TRAILING_WHITESPACE) - string(TOLOWER "${env_windir}" windir) - string(TOLOWER "${env_sysdir}" sysroot) - - if(lower MATCHES "^(${sysroot}/sys(tem|wow)|${windir}/sys(tem|wow)|(.*/)*msvc[^/]+dll)") - set(is_system 1) - endif() - endif() - endif() - endif() - - if(NOT is_system) - get_filename_component(original_path "${original_lower}" PATH) - get_filename_component(path "${lower}" PATH) - if("${original_path}" STREQUAL "${path}") - set(is_local 1) - else() - string(LENGTH "${original_path}/" original_length) - string(LENGTH "${lower}" path_length) - if(${path_length} GREATER ${original_length}) - string(SUBSTRING "${lower}" 0 ${original_length} path) - if("${original_path}/" STREQUAL "${path}") - set(is_embedded 1) - endif() - endif() - endif() - endif() - endif() - - # Return type string based on computed booleans: - # - set(type "other") - - if(is_system) - set(type "system") - elseif(is_embedded) - set(type "embedded") - elseif(is_local) - set(type "local") - endif() - - #message(STATUS "gp_resolved_file_type: '${file}' '${resolved_file}'") - #message(STATUS " type: '${type}'") - - if(NOT is_embedded) - if(NOT IS_ABSOLUTE "${resolved_file}") - if(lower MATCHES "^msvc[^/]+dll" AND is_system) - message(STATUS "info: non-absolute msvc file '${file}' returning type '${type}'") - else() - message(STATUS "warning: gp_resolved_file_type non-absolute file '${file}' returning type '${type}' -- possibly incorrect") - endif() - endif() - endif() - - # Provide a hook so that projects can override the decision on whether a - # library belongs to the system or not by whatever logic they choose: - # - if(COMMAND gp_resolved_file_type_override) - gp_resolved_file_type_override("${resolved_file}" type) - endif() - - set(${type_var} "${type}" PARENT_SCOPE) - - #message(STATUS "**") -endfunction() - - -function(gp_file_type original_file file type_var) - if(NOT IS_ABSOLUTE "${original_file}") - message(STATUS "warning: gp_file_type expects absolute full path for first arg original_file") - endif() - - get_filename_component(exepath "${original_file}" PATH) - - set(type "") - gp_resolved_file_type("${original_file}" "${file}" "${exepath}" "" type) - - set(${type_var} "${type}" PARENT_SCOPE) -endfunction() - - -function(get_prerequisites target prerequisites_var exclude_system recurse exepath dirs) - set(verbose 0) - set(eol_char "E") - - if(NOT IS_ABSOLUTE "${target}") - message("warning: target '${target}' is not absolute...") - endif() - - if(NOT EXISTS "${target}") - message("warning: target '${target}' does not exist...") - endif() - - set(gp_cmd_paths ${gp_cmd_paths} - "C:/Program Files/Microsoft Visual Studio 9.0/VC/bin" - "C:/Program Files (x86)/Microsoft Visual Studio 9.0/VC/bin" - "C:/Program Files/Microsoft Visual Studio 8/VC/BIN" - "C:/Program Files (x86)/Microsoft Visual Studio 8/VC/BIN" - "C:/Program Files/Microsoft Visual Studio .NET 2003/VC7/BIN" - "C:/Program Files (x86)/Microsoft Visual Studio .NET 2003/VC7/BIN" - "/usr/local/bin" - "/usr/bin" - ) - - # - # - # Try to choose the right tool by default. Caller can set gp_tool prior to - # calling this function to force using a different tool. - # - if("${gp_tool}" STREQUAL "") - set(gp_tool "ldd") - - if(APPLE) - set(gp_tool "otool") - endif() - - if(WIN32 AND NOT UNIX) # This is how to check for cygwin, har! - find_program(gp_dumpbin "dumpbin" PATHS ${gp_cmd_paths}) - if(gp_dumpbin) - set(gp_tool "dumpbin") - else() # Try harder. Maybe we're on MinGW - set(gp_tool "objdump") - endif() - endif() - endif() - - find_program(gp_cmd ${gp_tool} PATHS ${gp_cmd_paths}) - - if(NOT gp_cmd) - message(FATAL_ERROR "FATAL ERROR: could not find '${gp_tool}' - cannot analyze prerequisites!") - return() - endif() - - set(gp_tool_known 0) - - if("${gp_tool}" STREQUAL "ldd") - set(gp_cmd_args "") - set(gp_regex "^[\t ]*[^\t ]+ => ([^\t\(]+) .*${eol_char}$") - set(gp_regex_error "not found${eol_char}$") - set(gp_regex_fallback "^[\t ]*([^\t ]+) => ([^\t ]+).*${eol_char}$") - set(gp_regex_cmp_count 1) - set(gp_tool_known 1) - endif() - - if("${gp_tool}" STREQUAL "otool") - set(gp_cmd_args "-L") - set(gp_regex "^\t([^\t]+) \\(compatibility version ([0-9]+.[0-9]+.[0-9]+), current version ([0-9]+.[0-9]+.[0-9]+)\\)${eol_char}$") - set(gp_regex_error "") - set(gp_regex_fallback "") - set(gp_regex_cmp_count 3) - set(gp_tool_known 1) - endif() - - if("${gp_tool}" STREQUAL "dumpbin") - set(gp_cmd_args "/dependents") - set(gp_regex "^ ([^ ].*[Dd][Ll][Ll])${eol_char}$") - set(gp_regex_error "") - set(gp_regex_fallback "") - set(gp_regex_cmp_count 1) - set(gp_tool_known 1) - endif() - - if("${gp_tool}" STREQUAL "objdump") - set(gp_cmd_args "-p") - set(gp_regex "^\t*DLL Name: (.*\\.[Dd][Ll][Ll])${eol_char}$") - set(gp_regex_error "") - set(gp_regex_fallback "") - set(gp_regex_cmp_count 1) - set(gp_tool_known 1) - endif() - - if(NOT gp_tool_known) - message(STATUS "warning: gp_tool='${gp_tool}' is an unknown tool...") - message(STATUS "CMake function get_prerequisites needs more code to handle '${gp_tool}'") - message(STATUS "Valid gp_tool values are dumpbin, ldd, objdump and otool.") - return() - endif() - - - if("${gp_tool}" STREQUAL "dumpbin") - # When running dumpbin, it also needs the "Common7/IDE" directory in the - # PATH. It will already be in the PATH if being run from a Visual Studio - # command prompt. Add it to the PATH here in case we are running from a - # different command prompt. - # - get_filename_component(gp_cmd_dir "${gp_cmd}" PATH) - get_filename_component(gp_cmd_dlls_dir "${gp_cmd_dir}/../../Common7/IDE" ABSOLUTE) - # Use cmake paths as a user may have a PATH element ending with a backslash. - # This will escape the list delimiter and create havoc! - if(EXISTS "${gp_cmd_dlls_dir}") - # only add to the path if it is not already in the path - set(gp_found_cmd_dlls_dir 0) - file(TO_CMAKE_PATH "$ENV{PATH}" env_path) - foreach(gp_env_path_element ${env_path}) - if("${gp_env_path_element}" STREQUAL "${gp_cmd_dlls_dir}") - set(gp_found_cmd_dlls_dir 1) - endif() - endforeach() - - if(NOT gp_found_cmd_dlls_dir) - file(TO_NATIVE_PATH "${gp_cmd_dlls_dir}" gp_cmd_dlls_dir) - set(ENV{PATH} "$ENV{PATH};${gp_cmd_dlls_dir}") - endif() - endif() - endif() - # - # - - if("${gp_tool}" STREQUAL "ldd") - set(old_ld_env "$ENV{LD_LIBRARY_PATH}") - foreach(dir ${exepath} ${dirs}) - set(ENV{LD_LIBRARY_PATH} "${dir}:$ENV{LD_LIBRARY_PATH}") - endforeach() - endif() - - - # Track new prerequisites at each new level of recursion. Start with an - # empty list at each level: - # - set(unseen_prereqs) - - # Run gp_cmd on the target: - # - execute_process( - COMMAND ${gp_cmd} ${gp_cmd_args} ${target} - OUTPUT_VARIABLE gp_cmd_ov - ) - - if("${gp_tool}" STREQUAL "ldd") - set(ENV{LD_LIBRARY_PATH} "${old_ld_env}") - endif() - - if(verbose) - message(STATUS "") - message(STATUS "gp_cmd_ov='${gp_cmd_ov}'") - message(STATUS "") - endif() - - get_filename_component(target_dir "${target}" PATH) - - # Convert to a list of lines: - # - string(REGEX REPLACE ";" "\\\\;" candidates "${gp_cmd_ov}") - string(REGEX REPLACE "\n" "${eol_char};" candidates "${candidates}") - - # check for install id and remove it from list, since otool -L can include a - # reference to itself - set(gp_install_id) - if("${gp_tool}" STREQUAL "otool") - execute_process( - COMMAND otool -D ${target} - OUTPUT_VARIABLE gp_install_id_ov - ) - # second line is install name - string(REGEX REPLACE ".*:\n" "" gp_install_id "${gp_install_id_ov}") - if(gp_install_id) - # trim - string(REGEX MATCH "[^\n ].*[^\n ]" gp_install_id "${gp_install_id}") - #message("INSTALL ID is \"${gp_install_id}\"") - endif() - endif() - - # Analyze each line for file names that match the regular expression: - # - foreach(candidate ${candidates}) - if("${candidate}" MATCHES "${gp_regex}") - - # Extract information from each candidate: - if(gp_regex_error AND "${candidate}" MATCHES "${gp_regex_error}") - string(REGEX REPLACE "${gp_regex_fallback}" "\\1" raw_item "${candidate}") - else() - string(REGEX REPLACE "${gp_regex}" "\\1" raw_item "${candidate}") - endif() - - if(gp_regex_cmp_count GREATER 1) - string(REGEX REPLACE "${gp_regex}" "\\2" raw_compat_version "${candidate}") - string(REGEX REPLACE "^([0-9]+)\\.([0-9]+)\\.([0-9]+)$" "\\1" compat_major_version "${raw_compat_version}") - string(REGEX REPLACE "^([0-9]+)\\.([0-9]+)\\.([0-9]+)$" "\\2" compat_minor_version "${raw_compat_version}") - string(REGEX REPLACE "^([0-9]+)\\.([0-9]+)\\.([0-9]+)$" "\\3" compat_patch_version "${raw_compat_version}") - endif() - - if(gp_regex_cmp_count GREATER 2) - string(REGEX REPLACE "${gp_regex}" "\\3" raw_current_version "${candidate}") - string(REGEX REPLACE "^([0-9]+)\\.([0-9]+)\\.([0-9]+)$" "\\1" current_major_version "${raw_current_version}") - string(REGEX REPLACE "^([0-9]+)\\.([0-9]+)\\.([0-9]+)$" "\\2" current_minor_version "${raw_current_version}") - string(REGEX REPLACE "^([0-9]+)\\.([0-9]+)\\.([0-9]+)$" "\\3" current_patch_version "${raw_current_version}") - endif() - - # Use the raw_item as the list entries returned by this function. Use the - # gp_resolve_item function to resolve it to an actual full path file if - # necessary. - # - set(item "${raw_item}") - - # Add each item unless it is excluded: - # - set(add_item 1) - - if("${item}" STREQUAL "${gp_install_id}") - set(add_item 0) - endif() - - if(add_item AND ${exclude_system}) - set(type "") - gp_resolved_file_type("${target}" "${item}" "${exepath}" "${dirs}" type) - - if("${type}" STREQUAL "system") - set(add_item 0) - endif() - endif() - - if(add_item) - list(LENGTH ${prerequisites_var} list_length_before_append) - gp_append_unique(${prerequisites_var} "${item}") - list(LENGTH ${prerequisites_var} list_length_after_append) - - if(${recurse}) - # If item was really added, this is the first time we have seen it. - # Add it to unseen_prereqs so that we can recursively add *its* - # prerequisites... - # - # But first: resolve its name to an absolute full path name such - # that the analysis tools can simply accept it as input. - # - if(NOT list_length_before_append EQUAL list_length_after_append) - gp_resolve_item("${target}" "${item}" "${exepath}" "${dirs}" resolved_item) - set(unseen_prereqs ${unseen_prereqs} "${resolved_item}") - endif() - endif() - endif() - else() - if(verbose) - message(STATUS "ignoring non-matching line: '${candidate}'") - endif() - endif() - endforeach() - - list(LENGTH ${prerequisites_var} prerequisites_var_length) - if(prerequisites_var_length GREATER 0) - list(SORT ${prerequisites_var}) - endif() - if(${recurse}) - set(more_inputs ${unseen_prereqs}) - foreach(input ${more_inputs}) - get_prerequisites("${input}" ${prerequisites_var} ${exclude_system} ${recurse} "${exepath}" "${dirs}") - endforeach() - endif() - - set(${prerequisites_var} ${${prerequisites_var}} PARENT_SCOPE) -endfunction() - - -function(list_prerequisites target) - if("${ARGV1}" STREQUAL "") - set(all 1) - else() - set(all "${ARGV1}") - endif() - - if("${ARGV2}" STREQUAL "") - set(exclude_system 0) - else() - set(exclude_system "${ARGV2}") - endif() - - if("${ARGV3}" STREQUAL "") - set(verbose 0) - else() - set(verbose "${ARGV3}") - endif() - - set(count 0) - set(count_str "") - set(print_count "${verbose}") - set(print_prerequisite_type "${verbose}") - set(print_target "${verbose}") - set(type_str "") - - get_filename_component(exepath "${target}" PATH) - - set(prereqs "") - get_prerequisites("${target}" prereqs ${exclude_system} ${all} "${exepath}" "") - - if(print_target) - message(STATUS "File '${target}' depends on:") - endif() - - foreach(d ${prereqs}) - math(EXPR count "${count} + 1") - - if(print_count) - set(count_str "${count}. ") - endif() - - if(print_prerequisite_type) - gp_file_type("${target}" "${d}" type) - set(type_str " (${type})") - endif() - - message(STATUS "${count_str}${d}${type_str}") - endforeach() -endfunction() - - -function(list_prerequisites_by_glob glob_arg glob_exp) - message(STATUS "=============================================================================") - message(STATUS "List prerequisites of executables matching ${glob_arg} '${glob_exp}'") - message(STATUS "") - file(${glob_arg} file_list ${glob_exp}) - foreach(f ${file_list}) - is_file_executable("${f}" is_f_executable) - if(is_f_executable) - message(STATUS "=============================================================================") - list_prerequisites("${f}" ${ARGN}) - message(STATUS "") - endif() - endforeach() -endfunction() diff --git a/cmake/UseJava.cmake b/cmake/UseJava.cmake deleted file mode 100644 index 1a5ef1076..000000000 --- a/cmake/UseJava.cmake +++ /dev/null @@ -1,881 +0,0 @@ -# - Use Module for Java -# This file provides functions for Java. It is assumed that FindJava.cmake -# has already been loaded. See FindJava.cmake for information on how to -# load Java into your CMake project. -# -# add_jar(TARGET_NAME SRC1 SRC2 .. SRCN RCS1 RCS2 .. RCSN) -# -# This command creates a .jar. It compiles the given source -# files (SRC) and adds the given resource files (RCS) to the jar file. -# If only resource files are given then just a jar file is created. -# -# Additional instructions: -# To add compile flags to the target you can set these flags with -# the following variable: -# -# set(CMAKE_JAVA_COMPILE_FLAGS -nowarn) -# -# To add a path or a jar file to the class path you can do this -# with the CMAKE_JAVA_INCLUDE_PATH variable. -# -# set(CMAKE_JAVA_INCLUDE_PATH /usr/share/java/shibboleet.jar) -# -# To use a different output name for the target you can set it with: -# -# set(CMAKE_JAVA_TARGET_OUTPUT_NAME shibboleet.jar) -# add_jar(foobar foobar.java) -# -# To use a different output directory than CMAKE_CURRENT_BINARY_DIR -# you can set it with: -# -# set(CMAKE_JAVA_TARGET_OUTPUT_DIR ${PROJECT_BINARY_DIR}/bin) -# -# To define an entry point in your jar you can set it with: -# -# set(CMAKE_JAVA_JAR_ENTRY_POINT com/examples/MyProject/Main) -# -# To add a VERSION to the target output name you can set it using -# CMAKE_JAVA_TARGET_VERSION. This will create a jar file with the name -# shibboleet-1.0.0.jar and will create a symlink shibboleet.jar -# pointing to the jar with the version information. -# -# set(CMAKE_JAVA_TARGET_VERSION 1.2.0) -# add_jar(shibboleet shibbotleet.java) -# -# If the target is a JNI library, utilize the following commands to -# create a JNI symbolic link: -# -# set(CMAKE_JNI_TARGET TRUE) -# set(CMAKE_JAVA_TARGET_VERSION 1.2.0) -# add_jar(shibboleet shibbotleet.java) -# install_jar(shibboleet ${LIB_INSTALL_DIR}/shibboleet) -# install_jni_symlink(shibboleet ${JAVA_LIB_INSTALL_DIR}) -# -# If a single target needs to produce more than one jar from its -# java source code, to prevent the accumulation of duplicate class -# files in subsequent jars, set/reset CMAKE_JAR_CLASSES_PREFIX prior -# to calling the add_jar() function: -# -# set(CMAKE_JAR_CLASSES_PREFIX com/redhat/foo) -# add_jar(foo foo.java) -# -# set(CMAKE_JAR_CLASSES_PREFIX com/redhat/bar) -# add_jar(bar bar.java) -# -# Target Properties: -# The add_jar() functions sets some target properties. You can get these -# properties with the -# get_property(TARGET PROPERTY ) -# command. -# -# INSTALL_FILES The files which should be installed. This is used by -# install_jar(). -# JNI_SYMLINK The JNI symlink which should be installed. -# This is used by install_jni_symlink(). -# JAR_FILE The location of the jar file so that you can include -# it. -# CLASS_DIR The directory where the class files can be found. For -# example to use them with javah. -# -# find_jar( -# name | NAMES name1 [name2 ...] -# [PATHS path1 [path2 ... ENV var]] -# [VERSIONS version1 [version2]] -# [DOC "cache documentation string"] -# ) -# -# This command is used to find a full path to the named jar. A cache -# entry named by is created to stor the result of this command. If -# the full path to a jar is found the result is stored in the variable -# and the search will not repeated unless the variable is cleared. If -# nothing is found, the result will be -NOTFOUND, and the search -# will be attempted again next time find_jar is invoked with the same -# variable. -# The name of the full path to a file that is searched for is specified -# by the names listed after NAMES argument. Additional search locations -# can be specified after the PATHS argument. If you require special a -# version of a jar file you can specify it with the VERSIONS argument. -# The argument after DOC will be used for the documentation string in -# the cache. -# -# install_jar(TARGET_NAME DESTINATION) -# -# This command installs the TARGET_NAME files to the given DESTINATION. -# It should be called in the same scope as add_jar() or it will fail. -# -# install_jni_symlink(TARGET_NAME DESTINATION) -# -# This command installs the TARGET_NAME JNI symlinks to the given -# DESTINATION. It should be called in the same scope as add_jar() -# or it will fail. -# -# create_javadoc( -# PACKAGES pkg1 [pkg2 ...] -# [SOURCEPATH ] -# [CLASSPATH ] -# [INSTALLPATH ] -# [DOCTITLE "the documentation title"] -# [WINDOWTITLE "the title of the document"] -# [AUTHOR TRUE|FALSE] -# [USE TRUE|FALSE] -# [VERSION TRUE|FALSE] -# ) -# -# Create java documentation based on files or packages. For more -# details please read the javadoc manpage. -# -# There are two main signatures for create_javadoc. The first -# signature works with package names on a path with source files: -# -# Example: -# create_javadoc(my_example_doc -# PACKAGES com.exmaple.foo com.example.bar -# SOURCEPATH "${CMAKE_CURRENT_SOURCE_DIR}" -# CLASSPATH ${CMAKE_JAVA_INCLUDE_PATH} -# WINDOWTITLE "My example" -# DOCTITLE "

My example

" -# AUTHOR TRUE -# USE TRUE -# VERSION TRUE -# ) -# -# The second signature for create_javadoc works on a given list of -# files. -# -# create_javadoc( -# FILES file1 [file2 ...] -# [CLASSPATH ] -# [INSTALLPATH ] -# [DOCTITLE "the documentation title"] -# [WINDOWTITLE "the title of the document"] -# [AUTHOR TRUE|FALSE] -# [USE TRUE|FALSE] -# [VERSION TRUE|FALSE] -# ) -# -# Example: -# create_javadoc(my_example_doc -# FILES ${example_SRCS} -# CLASSPATH ${CMAKE_JAVA_INCLUDE_PATH} -# WINDOWTITLE "My example" -# DOCTITLE "

My example

" -# AUTHOR TRUE -# USE TRUE -# VERSION TRUE -# ) -# -# Both signatures share most of the options. These options are the -# same as what you can find in the javadoc manpage. Please look at -# the manpage for CLASSPATH, DOCTITLE, WINDOWTITLE, AUTHOR, USE and -# VERSION. -# -# The documentation will be by default installed to -# -# ${CMAKE_INSTALL_PREFIX}/share/javadoc/ -# -# if you don't set the INSTALLPATH. -# - -#============================================================================= -# Copyright 2010-2011 Andreas schneider -# Copyright 2010 Ben Boeckel -# -# Distributed under the OSI-approved BSD License (the "License"); -# see accompanying file Copyright.txt for details. -# -# This software is distributed WITHOUT ANY WARRANTY; without even the -# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. -# See the License for more information. -#============================================================================= -# (To distribute this file outside of CMake, substitute the full -# License text for the above reference.) - -function (__java_copy_file src dest comment) - add_custom_command( - OUTPUT ${dest} - COMMAND cmake -E copy_if_different - ARGS ${src} - ${dest} - DEPENDS ${src} - COMMENT ${comment}) -endfunction (__java_copy_file src dest comment) - -# define helper scripts -set(_JAVA_CLASS_FILELIST_SCRIPT ${CMAKE_CURRENT_LIST_DIR}/UseJavaClassFilelist.cmake) -set(_JAVA_SYMLINK_SCRIPT ${CMAKE_CURRENT_LIST_DIR}/UseJavaSymlinks.cmake) - -function(add_jar _TARGET_NAME) - set(_JAVA_SOURCE_FILES ${ARGN}) - - if (NOT DEFINED CMAKE_JAVA_TARGET_OUTPUT_DIR) - set(CMAKE_JAVA_TARGET_OUTPUT_DIR ${CMAKE_CURRENT_BINARY_DIR}) - endif(NOT DEFINED CMAKE_JAVA_TARGET_OUTPUT_DIR) - - if (CMAKE_JAVA_JAR_ENTRY_POINT) - set(_ENTRY_POINT_OPTION e) - set(_ENTRY_POINT_VALUE ${CMAKE_JAVA_JAR_ENTRY_POINT}) - endif (CMAKE_JAVA_JAR_ENTRY_POINT) - - if (LIBRARY_OUTPUT_PATH) - set(CMAKE_JAVA_LIBRARY_OUTPUT_PATH ${LIBRARY_OUTPUT_PATH}) - else (LIBRARY_OUTPUT_PATH) - set(CMAKE_JAVA_LIBRARY_OUTPUT_PATH ${CMAKE_JAVA_TARGET_OUTPUT_DIR}) - endif (LIBRARY_OUTPUT_PATH) - - set(CMAKE_JAVA_INCLUDE_PATH - ${CMAKE_JAVA_INCLUDE_PATH} - ${CMAKE_CURRENT_SOURCE_DIR} - ${CMAKE_JAVA_OBJECT_OUTPUT_PATH} - ${CMAKE_JAVA_LIBRARY_OUTPUT_PATH} - ) - - if (WIN32 AND NOT CYGWIN AND NOT CMAKE_CROSSCOMPILING) - set(CMAKE_JAVA_INCLUDE_FLAG_SEP ";") - else () - set(CMAKE_JAVA_INCLUDE_FLAG_SEP ":") - endif() - - foreach (JAVA_INCLUDE_DIR ${CMAKE_JAVA_INCLUDE_PATH}) - set(CMAKE_JAVA_INCLUDE_PATH_FINAL "${CMAKE_JAVA_INCLUDE_PATH_FINAL}${CMAKE_JAVA_INCLUDE_FLAG_SEP}${JAVA_INCLUDE_DIR}") - endforeach(JAVA_INCLUDE_DIR) - - set(CMAKE_JAVA_CLASS_OUTPUT_PATH "${CMAKE_JAVA_TARGET_OUTPUT_DIR}${CMAKE_FILES_DIRECTORY}/${_TARGET_NAME}.dir") - - set(_JAVA_TARGET_OUTPUT_NAME "${_TARGET_NAME}.jar") - if (CMAKE_JAVA_TARGET_OUTPUT_NAME AND CMAKE_JAVA_TARGET_VERSION) - set(_JAVA_TARGET_OUTPUT_NAME "${CMAKE_JAVA_TARGET_OUTPUT_NAME}-${CMAKE_JAVA_TARGET_VERSION}.jar") - set(_JAVA_TARGET_OUTPUT_LINK "${CMAKE_JAVA_TARGET_OUTPUT_NAME}.jar") - elseif (CMAKE_JAVA_TARGET_VERSION) - set(_JAVA_TARGET_OUTPUT_NAME "${_TARGET_NAME}-${CMAKE_JAVA_TARGET_VERSION}.jar") - set(_JAVA_TARGET_OUTPUT_LINK "${_TARGET_NAME}.jar") - elseif (CMAKE_JAVA_TARGET_OUTPUT_NAME) - set(_JAVA_TARGET_OUTPUT_NAME "${CMAKE_JAVA_TARGET_OUTPUT_NAME}.jar") - endif (CMAKE_JAVA_TARGET_OUTPUT_NAME AND CMAKE_JAVA_TARGET_VERSION) - # reset - set(CMAKE_JAVA_TARGET_OUTPUT_NAME) - - set(_JAVA_CLASS_FILES) - set(_JAVA_COMPILE_FILES) - set(_JAVA_DEPENDS) - set(_JAVA_RESOURCE_FILES) - foreach(_JAVA_SOURCE_FILE ${_JAVA_SOURCE_FILES}) - get_filename_component(_JAVA_EXT ${_JAVA_SOURCE_FILE} EXT) - get_filename_component(_JAVA_FILE ${_JAVA_SOURCE_FILE} NAME_WE) - get_filename_component(_JAVA_PATH ${_JAVA_SOURCE_FILE} PATH) - get_filename_component(_JAVA_FULL ${_JAVA_SOURCE_FILE} ABSOLUTE) - - file(RELATIVE_PATH _JAVA_REL_BINARY_PATH ${CMAKE_JAVA_TARGET_OUTPUT_DIR} ${_JAVA_FULL}) - file(RELATIVE_PATH _JAVA_REL_SOURCE_PATH ${CMAKE_CURRENT_SOURCE_DIR} ${_JAVA_FULL}) - string(LENGTH ${_JAVA_REL_BINARY_PATH} _BIN_LEN) - string(LENGTH ${_JAVA_REL_SOURCE_PATH} _SRC_LEN) - if (${_BIN_LEN} LESS ${_SRC_LEN}) - set(_JAVA_REL_PATH ${_JAVA_REL_BINARY_PATH}) - else (${_BIN_LEN} LESS ${_SRC_LEN}) - set(_JAVA_REL_PATH ${_JAVA_REL_SOURCE_PATH}) - endif (${_BIN_LEN} LESS ${_SRC_LEN}) - get_filename_component(_JAVA_REL_PATH ${_JAVA_REL_PATH} PATH) - - if (_JAVA_EXT MATCHES ".java") - list(APPEND _JAVA_COMPILE_FILES ${_JAVA_SOURCE_FILE}) - set(_JAVA_CLASS_FILE "${CMAKE_JAVA_CLASS_OUTPUT_PATH}/${_JAVA_REL_PATH}/${_JAVA_FILE}.class") - set(_JAVA_CLASS_FILES ${_JAVA_CLASS_FILES} ${_JAVA_CLASS_FILE}) - - elseif (_JAVA_EXT MATCHES ".jar" - OR _JAVA_EXT MATCHES ".war" - OR _JAVA_EXT MATCHES ".ear" - OR _JAVA_EXT MATCHES ".sar") - list(APPEND CMAKE_JAVA_INCLUDE_PATH ${_JAVA_SOURCE_FILE}) - - elseif (_JAVA_EXT STREQUAL "") - list(APPEND CMAKE_JAVA_INCLUDE_PATH ${JAVA_JAR_TARGET_${_JAVA_SOURCE_FILE}} ${JAVA_JAR_TARGET_${_JAVA_SOURCE_FILE}_CLASSPATH}) - list(APPEND _JAVA_DEPENDS ${JAVA_JAR_TARGET_${_JAVA_SOURCE_FILE}}) - - else (_JAVA_EXT MATCHES ".java") - __java_copy_file(${CMAKE_CURRENT_SOURCE_DIR}/${_JAVA_SOURCE_FILE} - ${CMAKE_JAVA_CLASS_OUTPUT_PATH}/${_JAVA_SOURCE_FILE} - "Copying ${_JAVA_SOURCE_FILE} to the build directory") - list(APPEND _JAVA_RESOURCE_FILES ${_JAVA_SOURCE_FILE}) - endif (_JAVA_EXT MATCHES ".java") - endforeach(_JAVA_SOURCE_FILE) - - # create an empty java_class_filelist - if (NOT EXISTS ${CMAKE_JAVA_CLASS_OUTPUT_PATH}/java_class_filelist) - file(WRITE ${CMAKE_JAVA_CLASS_OUTPUT_PATH}/java_class_filelist "") - endif() - - if (_JAVA_COMPILE_FILES) - # Compile the java files and create a list of class files - add_custom_command( - # NOTE: this command generates an artificial dependency file - OUTPUT ${CMAKE_JAVA_CLASS_OUTPUT_PATH}/java_compiled_${_TARGET_NAME} - COMMAND ${Java_JAVAC_EXECUTABLE} - ${CMAKE_JAVA_COMPILE_FLAGS} - -classpath "${CMAKE_JAVA_INCLUDE_PATH_FINAL}" - -d ${CMAKE_JAVA_CLASS_OUTPUT_PATH} - ${_JAVA_COMPILE_FILES} - COMMAND ${CMAKE_COMMAND} -E touch ${CMAKE_JAVA_CLASS_OUTPUT_PATH}/java_compiled_${_TARGET_NAME} - DEPENDS ${_JAVA_COMPILE_FILES} - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} - COMMENT "Building Java objects for ${_TARGET_NAME}.jar" - ) - add_custom_command( - OUTPUT ${CMAKE_JAVA_CLASS_OUTPUT_PATH}/java_class_filelist - COMMAND ${CMAKE_COMMAND} - -DCMAKE_JAVA_CLASS_OUTPUT_PATH=${CMAKE_JAVA_CLASS_OUTPUT_PATH} - -DCMAKE_JAR_CLASSES_PREFIX="${CMAKE_JAR_CLASSES_PREFIX}" - -P ${_JAVA_CLASS_FILELIST_SCRIPT} - DEPENDS ${CMAKE_JAVA_CLASS_OUTPUT_PATH}/java_compiled_${_TARGET_NAME} - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} - ) - endif (_JAVA_COMPILE_FILES) - - # create the jar file - set(_JAVA_JAR_OUTPUT_PATH - ${CMAKE_JAVA_TARGET_OUTPUT_DIR}/${_JAVA_TARGET_OUTPUT_NAME}) - if (CMAKE_JNI_TARGET) - add_custom_command( - OUTPUT ${_JAVA_JAR_OUTPUT_PATH} - COMMAND ${Java_JAR_EXECUTABLE} - -cf${_ENTRY_POINT_OPTION} ${_JAVA_JAR_OUTPUT_PATH} ${_ENTRY_POINT_VALUE} - ${_JAVA_RESOURCE_FILES} @java_class_filelist - COMMAND ${CMAKE_COMMAND} - -D_JAVA_TARGET_DIR=${CMAKE_JAVA_TARGET_OUTPUT_DIR} - -D_JAVA_TARGET_OUTPUT_NAME=${_JAVA_TARGET_OUTPUT_NAME} - -D_JAVA_TARGET_OUTPUT_LINK=${_JAVA_TARGET_OUTPUT_LINK} - -P ${_JAVA_SYMLINK_SCRIPT} - COMMAND ${CMAKE_COMMAND} - -D_JAVA_TARGET_DIR=${CMAKE_JAVA_TARGET_OUTPUT_DIR} - -D_JAVA_TARGET_OUTPUT_NAME=${_JAVA_JAR_OUTPUT_PATH} - -D_JAVA_TARGET_OUTPUT_LINK=${_JAVA_TARGET_OUTPUT_LINK} - -P ${_JAVA_SYMLINK_SCRIPT} - DEPENDS ${_JAVA_RESOURCE_FILES} ${_JAVA_DEPENDS} ${CMAKE_JAVA_CLASS_OUTPUT_PATH}/java_class_filelist - WORKING_DIRECTORY ${CMAKE_JAVA_CLASS_OUTPUT_PATH} - COMMENT "Creating Java archive ${_JAVA_TARGET_OUTPUT_NAME}" - ) - else () - add_custom_command( - OUTPUT ${_JAVA_JAR_OUTPUT_PATH} - COMMAND ${Java_JAR_EXECUTABLE} - -cf${_ENTRY_POINT_OPTION} ${_JAVA_JAR_OUTPUT_PATH} ${_ENTRY_POINT_VALUE} - ${_JAVA_RESOURCE_FILES} @java_class_filelist - COMMAND ${CMAKE_COMMAND} - -D_JAVA_TARGET_DIR=${CMAKE_JAVA_TARGET_OUTPUT_DIR} - -D_JAVA_TARGET_OUTPUT_NAME=${_JAVA_TARGET_OUTPUT_NAME} - -D_JAVA_TARGET_OUTPUT_LINK=${_JAVA_TARGET_OUTPUT_LINK} - -P ${_JAVA_SYMLINK_SCRIPT} - WORKING_DIRECTORY ${CMAKE_JAVA_CLASS_OUTPUT_PATH} - DEPENDS ${_JAVA_RESOURCE_FILES} ${_JAVA_DEPENDS} ${CMAKE_JAVA_CLASS_OUTPUT_PATH}/java_class_filelist - COMMENT "Creating Java archive ${_JAVA_TARGET_OUTPUT_NAME}" - ) - endif (CMAKE_JNI_TARGET) - - # Add the target and make sure we have the latest resource files. - add_custom_target(${_TARGET_NAME} ALL DEPENDS ${_JAVA_JAR_OUTPUT_PATH}) - - set_property( - TARGET - ${_TARGET_NAME} - PROPERTY - INSTALL_FILES - ${_JAVA_JAR_OUTPUT_PATH} - ) - - if (_JAVA_TARGET_OUTPUT_LINK) - set_property( - TARGET - ${_TARGET_NAME} - PROPERTY - INSTALL_FILES - ${_JAVA_JAR_OUTPUT_PATH} - ${CMAKE_JAVA_TARGET_OUTPUT_DIR}/${_JAVA_TARGET_OUTPUT_LINK} - ) - - if (CMAKE_JNI_TARGET) - set_property( - TARGET - ${_TARGET_NAME} - PROPERTY - JNI_SYMLINK - ${CMAKE_JAVA_TARGET_OUTPUT_DIR}/${_JAVA_TARGET_OUTPUT_LINK} - ) - endif (CMAKE_JNI_TARGET) - endif (_JAVA_TARGET_OUTPUT_LINK) - - set_property( - TARGET - ${_TARGET_NAME} - PROPERTY - JAR_FILE - ${_JAVA_JAR_OUTPUT_PATH} - ) - - set_property( - TARGET - ${_TARGET_NAME} - PROPERTY - CLASSDIR - ${CMAKE_JAVA_CLASS_OUTPUT_PATH} - ) - -endfunction(add_jar) - -function(INSTALL_JAR _TARGET_NAME _DESTINATION) - get_property(__FILES - TARGET - ${_TARGET_NAME} - PROPERTY - INSTALL_FILES - ) - - if (__FILES) - install( - FILES - ${__FILES} - DESTINATION - ${_DESTINATION} - ) - else (__FILES) - message(SEND_ERROR "The target ${_TARGET_NAME} is not known in this scope.") - endif (__FILES) -endfunction(INSTALL_JAR _TARGET_NAME _DESTINATION) - -function(INSTALL_JNI_SYMLINK _TARGET_NAME _DESTINATION) - get_property(__SYMLINK - TARGET - ${_TARGET_NAME} - PROPERTY - JNI_SYMLINK - ) - - if (__SYMLINK) - install( - FILES - ${__SYMLINK} - DESTINATION - ${_DESTINATION} - ) - else (__SYMLINK) - message(SEND_ERROR "The target ${_TARGET_NAME} is not known in this scope.") - endif (__SYMLINK) -endfunction(INSTALL_JNI_SYMLINK _TARGET_NAME _DESTINATION) - -function (find_jar VARIABLE) - set(_jar_names) - set(_jar_files) - set(_jar_versions) - set(_jar_paths - /usr/share/java/ - /usr/local/share/java/ - ${Java_JAR_PATHS}) - set(_jar_doc "NOTSET") - - set(_state "name") - - foreach (arg ${ARGN}) - if (${_state} STREQUAL "name") - if (${arg} STREQUAL "VERSIONS") - set(_state "versions") - elseif (${arg} STREQUAL "NAMES") - set(_state "names") - elseif (${arg} STREQUAL "PATHS") - set(_state "paths") - elseif (${arg} STREQUAL "DOC") - set(_state "doc") - else (${arg} STREQUAL "NAMES") - set(_jar_names ${arg}) - if (_jar_doc STREQUAL "NOTSET") - set(_jar_doc "Finding ${arg} jar") - endif (_jar_doc STREQUAL "NOTSET") - endif (${arg} STREQUAL "VERSIONS") - elseif (${_state} STREQUAL "versions") - if (${arg} STREQUAL "NAMES") - set(_state "names") - elseif (${arg} STREQUAL "PATHS") - set(_state "paths") - elseif (${arg} STREQUAL "DOC") - set(_state "doc") - else (${arg} STREQUAL "NAMES") - set(_jar_versions ${_jar_versions} ${arg}) - endif (${arg} STREQUAL "NAMES") - elseif (${_state} STREQUAL "names") - if (${arg} STREQUAL "VERSIONS") - set(_state "versions") - elseif (${arg} STREQUAL "PATHS") - set(_state "paths") - elseif (${arg} STREQUAL "DOC") - set(_state "doc") - else (${arg} STREQUAL "VERSIONS") - set(_jar_names ${_jar_names} ${arg}) - if (_jar_doc STREQUAL "NOTSET") - set(_jar_doc "Finding ${arg} jar") - endif (_jar_doc STREQUAL "NOTSET") - endif (${arg} STREQUAL "VERSIONS") - elseif (${_state} STREQUAL "paths") - if (${arg} STREQUAL "VERSIONS") - set(_state "versions") - elseif (${arg} STREQUAL "NAMES") - set(_state "names") - elseif (${arg} STREQUAL "DOC") - set(_state "doc") - else (${arg} STREQUAL "VERSIONS") - set(_jar_paths ${_jar_paths} ${arg}) - endif (${arg} STREQUAL "VERSIONS") - elseif (${_state} STREQUAL "doc") - if (${arg} STREQUAL "VERSIONS") - set(_state "versions") - elseif (${arg} STREQUAL "NAMES") - set(_state "names") - elseif (${arg} STREQUAL "PATHS") - set(_state "paths") - else (${arg} STREQUAL "VERSIONS") - set(_jar_doc ${arg}) - endif (${arg} STREQUAL "VERSIONS") - endif (${_state} STREQUAL "name") - endforeach (arg ${ARGN}) - - if (NOT _jar_names) - message(FATAL_ERROR "find_jar: No name to search for given") - endif (NOT _jar_names) - - foreach (jar_name ${_jar_names}) - foreach (version ${_jar_versions}) - set(_jar_files ${_jar_files} ${jar_name}-${version}.jar) - endforeach (version ${_jar_versions}) - set(_jar_files ${_jar_files} ${jar_name}.jar) - endforeach (jar_name ${_jar_names}) - - find_file(${VARIABLE} - NAMES ${_jar_files} - PATHS ${_jar_paths} - DOC ${_jar_doc} - NO_DEFAULT_PATH) -endfunction (find_jar VARIABLE) - -function(create_javadoc _target) - set(_javadoc_packages) - set(_javadoc_files) - set(_javadoc_sourcepath) - set(_javadoc_classpath) - set(_javadoc_installpath "${CMAKE_INSTALL_PREFIX}/share/javadoc") - set(_javadoc_doctitle) - set(_javadoc_windowtitle) - set(_javadoc_author FALSE) - set(_javadoc_version FALSE) - set(_javadoc_use FALSE) - - set(_state "package") - - foreach (arg ${ARGN}) - if (${_state} STREQUAL "package") - if (${arg} STREQUAL "PACKAGES") - set(_state "packages") - elseif (${arg} STREQUAL "FILES") - set(_state "files") - elseif (${arg} STREQUAL "SOURCEPATH") - set(_state "sourcepath") - elseif (${arg} STREQUAL "CLASSPATH") - set(_state "classpath") - elseif (${arg} STREQUAL "INSTALLPATH") - set(_state "installpath") - elseif (${arg} STREQUAL "DOCTITLE") - set(_state "doctitle") - elseif (${arg} STREQUAL "WINDOWTITLE") - set(_state "windowtitle") - elseif (${arg} STREQUAL "AUTHOR") - set(_state "author") - elseif (${arg} STREQUAL "USE") - set(_state "use") - elseif (${arg} STREQUAL "VERSION") - set(_state "version") - else () - set(_javadoc_packages ${arg}) - set(_state "packages") - endif () - elseif (${_state} STREQUAL "packages") - if (${arg} STREQUAL "FILES") - set(_state "files") - elseif (${arg} STREQUAL "SOURCEPATH") - set(_state "sourcepath") - elseif (${arg} STREQUAL "CLASSPATH") - set(_state "classpath") - elseif (${arg} STREQUAL "INSTALLPATH") - set(_state "installpath") - elseif (${arg} STREQUAL "DOCTITLE") - set(_state "doctitle") - elseif (${arg} STREQUAL "WINDOWTITLE") - set(_state "windowtitle") - elseif (${arg} STREQUAL "AUTHOR") - set(_state "author") - elseif (${arg} STREQUAL "USE") - set(_state "use") - elseif (${arg} STREQUAL "VERSION") - set(_state "version") - else () - list(APPEND _javadoc_packages ${arg}) - endif () - elseif (${_state} STREQUAL "files") - if (${arg} STREQUAL "PACKAGES") - set(_state "packages") - elseif (${arg} STREQUAL "SOURCEPATH") - set(_state "sourcepath") - elseif (${arg} STREQUAL "CLASSPATH") - set(_state "classpath") - elseif (${arg} STREQUAL "INSTALLPATH") - set(_state "installpath") - elseif (${arg} STREQUAL "DOCTITLE") - set(_state "doctitle") - elseif (${arg} STREQUAL "WINDOWTITLE") - set(_state "windowtitle") - elseif (${arg} STREQUAL "AUTHOR") - set(_state "author") - elseif (${arg} STREQUAL "USE") - set(_state "use") - elseif (${arg} STREQUAL "VERSION") - set(_state "version") - else () - list(APPEND _javadoc_files ${arg}) - endif () - elseif (${_state} STREQUAL "sourcepath") - if (${arg} STREQUAL "PACKAGES") - set(_state "packages") - elseif (${arg} STREQUAL "FILES") - set(_state "files") - elseif (${arg} STREQUAL "CLASSPATH") - set(_state "classpath") - elseif (${arg} STREQUAL "INSTALLPATH") - set(_state "installpath") - elseif (${arg} STREQUAL "DOCTITLE") - set(_state "doctitle") - elseif (${arg} STREQUAL "WINDOWTITLE") - set(_state "windowtitle") - elseif (${arg} STREQUAL "AUTHOR") - set(_state "author") - elseif (${arg} STREQUAL "USE") - set(_state "use") - elseif (${arg} STREQUAL "VERSION") - set(_state "version") - else () - list(APPEND _javadoc_sourcepath ${arg}) - endif () - elseif (${_state} STREQUAL "classpath") - if (${arg} STREQUAL "PACKAGES") - set(_state "packages") - elseif (${arg} STREQUAL "FILES") - set(_state "files") - elseif (${arg} STREQUAL "SOURCEPATH") - set(_state "sourcepath") - elseif (${arg} STREQUAL "INSTALLPATH") - set(_state "installpath") - elseif (${arg} STREQUAL "DOCTITLE") - set(_state "doctitle") - elseif (${arg} STREQUAL "WINDOWTITLE") - set(_state "windowtitle") - elseif (${arg} STREQUAL "AUTHOR") - set(_state "author") - elseif (${arg} STREQUAL "USE") - set(_state "use") - elseif (${arg} STREQUAL "VERSION") - set(_state "version") - else () - list(APPEND _javadoc_classpath ${arg}) - endif () - elseif (${_state} STREQUAL "installpath") - if (${arg} STREQUAL "PACKAGES") - set(_state "packages") - elseif (${arg} STREQUAL "FILES") - set(_state "files") - elseif (${arg} STREQUAL "SOURCEPATH") - set(_state "sourcepath") - elseif (${arg} STREQUAL "DOCTITLE") - set(_state "doctitle") - elseif (${arg} STREQUAL "WINDOWTITLE") - set(_state "windowtitle") - elseif (${arg} STREQUAL "AUTHOR") - set(_state "author") - elseif (${arg} STREQUAL "USE") - set(_state "use") - elseif (${arg} STREQUAL "VERSION") - set(_state "version") - else () - set(_javadoc_installpath ${arg}) - endif () - elseif (${_state} STREQUAL "doctitle") - if (${arg} STREQUAL "PACKAGES") - set(_state "packages") - elseif (${arg} STREQUAL "FILES") - set(_state "files") - elseif (${arg} STREQUAL "SOURCEPATH") - set(_state "sourcepath") - elseif (${arg} STREQUAL "INSTALLPATH") - set(_state "installpath") - elseif (${arg} STREQUAL "CLASSPATH") - set(_state "classpath") - elseif (${arg} STREQUAL "WINDOWTITLE") - set(_state "windowtitle") - elseif (${arg} STREQUAL "AUTHOR") - set(_state "author") - elseif (${arg} STREQUAL "USE") - set(_state "use") - elseif (${arg} STREQUAL "VERSION") - set(_state "version") - else () - set(_javadoc_doctitle ${arg}) - endif () - elseif (${_state} STREQUAL "windowtitle") - if (${arg} STREQUAL "PACKAGES") - set(_state "packages") - elseif (${arg} STREQUAL "FILES") - set(_state "files") - elseif (${arg} STREQUAL "SOURCEPATH") - set(_state "sourcepath") - elseif (${arg} STREQUAL "CLASSPATH") - set(_state "classpath") - elseif (${arg} STREQUAL "INSTALLPATH") - set(_state "installpath") - elseif (${arg} STREQUAL "DOCTITLE") - set(_state "doctitle") - elseif (${arg} STREQUAL "AUTHOR") - set(_state "author") - elseif (${arg} STREQUAL "USE") - set(_state "use") - elseif (${arg} STREQUAL "VERSION") - set(_state "version") - else () - set(_javadoc_windowtitle ${arg}) - endif () - elseif (${_state} STREQUAL "author") - if (${arg} STREQUAL "PACKAGES") - set(_state "packages") - elseif (${arg} STREQUAL "FILES") - set(_state "files") - elseif (${arg} STREQUAL "SOURCEPATH") - set(_state "sourcepath") - elseif (${arg} STREQUAL "CLASSPATH") - set(_state "classpath") - elseif (${arg} STREQUAL "INSTALLPATH") - set(_state "installpath") - elseif (${arg} STREQUAL "DOCTITLE") - set(_state "doctitle") - elseif (${arg} STREQUAL "WINDOWTITLE") - set(_state "windowtitle") - elseif (${arg} STREQUAL "AUTHOR") - set(_state "author") - elseif (${arg} STREQUAL "USE") - set(_state "use") - elseif (${arg} STREQUAL "VERSION") - set(_state "version") - else () - set(_javadoc_author ${arg}) - endif () - elseif (${_state} STREQUAL "use") - if (${arg} STREQUAL "PACKAGES") - set(_state "packages") - elseif (${arg} STREQUAL "FILES") - set(_state "files") - elseif (${arg} STREQUAL "SOURCEPATH") - set(_state "sourcepath") - elseif (${arg} STREQUAL "CLASSPATH") - set(_state "classpath") - elseif (${arg} STREQUAL "INSTALLPATH") - set(_state "installpath") - elseif (${arg} STREQUAL "DOCTITLE") - set(_state "doctitle") - elseif (${arg} STREQUAL "WINDOWTITLE") - set(_state "windowtitle") - elseif (${arg} STREQUAL "AUTHOR") - set(_state "author") - elseif (${arg} STREQUAL "USE") - set(_state "use") - elseif (${arg} STREQUAL "VERSION") - set(_state "version") - else () - set(_javadoc_use ${arg}) - endif () - elseif (${_state} STREQUAL "version") - if (${arg} STREQUAL "PACKAGES") - set(_state "packages") - elseif (${arg} STREQUAL "FILES") - set(_state "files") - elseif (${arg} STREQUAL "SOURCEPATH") - set(_state "sourcepath") - elseif (${arg} STREQUAL "CLASSPATH") - set(_state "classpath") - elseif (${arg} STREQUAL "INSTALLPATH") - set(_state "installpath") - elseif (${arg} STREQUAL "DOCTITLE") - set(_state "doctitle") - elseif (${arg} STREQUAL "WINDOWTITLE") - set(_state "windowtitle") - elseif (${arg} STREQUAL "AUTHOR") - set(_state "author") - elseif (${arg} STREQUAL "USE") - set(_state "use") - elseif (${arg} STREQUAL "VERSION") - set(_state "version") - else () - set(_javadoc_version ${arg}) - endif () - endif (${_state} STREQUAL "package") - endforeach (arg ${ARGN}) - - set(_javadoc_builddir ${CMAKE_CURRENT_BINARY_DIR}/javadoc/${_target}) - set(_javadoc_options -d ${_javadoc_builddir}) - - if (_javadoc_sourcepath) - set(_start TRUE) - foreach(_path ${_javadoc_sourcepath}) - if (_start) - set(_sourcepath ${_path}) - set(_start FALSE) - else (_start) - set(_sourcepath ${_sourcepath}:${_path}) - endif (_start) - endforeach(_path ${_javadoc_sourcepath}) - set(_javadoc_options ${_javadoc_options} -sourcepath ${_sourcepath}) - endif (_javadoc_sourcepath) - - if (_javadoc_classpath) - set(_start TRUE) - foreach(_path ${_javadoc_classpath}) - if (_start) - set(_classpath ${_path}) - set(_start FALSE) - else (_start) - set(_classpath ${_classpath}:${_path}) - endif (_start) - endforeach(_path ${_javadoc_classpath}) - set(_javadoc_options ${_javadoc_options} -classpath "${_classpath}") - endif (_javadoc_classpath) - - if (_javadoc_doctitle) - set(_javadoc_options ${_javadoc_options} -doctitle '${_javadoc_doctitle}') - endif (_javadoc_doctitle) - - if (_javadoc_windowtitle) - set(_javadoc_options ${_javadoc_options} -windowtitle '${_javadoc_windowtitle}') - endif (_javadoc_windowtitle) - - if (_javadoc_author) - set(_javadoc_options ${_javadoc_options} -author) - endif (_javadoc_author) - - if (_javadoc_use) - set(_javadoc_options ${_javadoc_options} -use) - endif (_javadoc_use) - - if (_javadoc_version) - set(_javadoc_options ${_javadoc_options} -version) - endif (_javadoc_version) - - add_custom_target(${_target}_javadoc ALL - COMMAND ${Java_JAVADOC_EXECUTABLE} ${_javadoc_options} - ${_javadoc_files} - ${_javadoc_packages} - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} - ) - - install( - DIRECTORY ${_javadoc_builddir} - DESTINATION ${_javadoc_installpath} - ) -endfunction(create_javadoc) diff --git a/cmake/UseJavaClassFilelist.cmake b/cmake/UseJavaClassFilelist.cmake deleted file mode 100644 index c842bf71a..000000000 --- a/cmake/UseJavaClassFilelist.cmake +++ /dev/null @@ -1,52 +0,0 @@ -# -# This script create a list of compiled Java class files to be added to a -# jar file. This avoids including cmake files which get created in the -# binary directory. -# - -#============================================================================= -# Copyright 2010-2011 Andreas schneider -# -# Distributed under the OSI-approved BSD License (the "License"); -# see accompanying file Copyright.txt for details. -# -# This software is distributed WITHOUT ANY WARRANTY; without even the -# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. -# See the License for more information. -#============================================================================= -# (To distribute this file outside of CMake, substitute the full -# License text for the above reference.) - -if (CMAKE_JAVA_CLASS_OUTPUT_PATH) - if (EXISTS "${CMAKE_JAVA_CLASS_OUTPUT_PATH}") - - set(_JAVA_GLOBBED_FILES) - if (CMAKE_JAR_CLASSES_PREFIX) - foreach(JAR_CLASS_PREFIX ${CMAKE_JAR_CLASSES_PREFIX}) - message(STATUS "JAR_CLASS_PREFIX: ${JAR_CLASS_PREFIX}") - - file(GLOB_RECURSE _JAVA_GLOBBED_TMP_FILES "${CMAKE_JAVA_CLASS_OUTPUT_PATH}/${JAR_CLASS_PREFIX}/*.class") - if (_JAVA_GLOBBED_TMP_FILES) - list(APPEND _JAVA_GLOBBED_FILES ${_JAVA_GLOBBED_TMP_FILES}) - endif (_JAVA_GLOBBED_TMP_FILES) - endforeach(JAR_CLASS_PREFIX ${CMAKE_JAR_CLASSES_PREFIX}) - else() - file(GLOB_RECURSE _JAVA_GLOBBED_FILES "${CMAKE_JAVA_CLASS_OUTPUT_PATH}/*.class") - endif (CMAKE_JAR_CLASSES_PREFIX) - - set(_JAVA_CLASS_FILES) - # file(GLOB_RECURSE foo RELATIVE) is broken so we need this. - foreach(_JAVA_GLOBBED_FILE ${_JAVA_GLOBBED_FILES}) - file(RELATIVE_PATH _JAVA_CLASS_FILE ${CMAKE_JAVA_CLASS_OUTPUT_PATH} ${_JAVA_GLOBBED_FILE}) - set(_JAVA_CLASS_FILES ${_JAVA_CLASS_FILES}${_JAVA_CLASS_FILE}\n) - endforeach(_JAVA_GLOBBED_FILE ${_JAVA_GLOBBED_FILES}) - - # write to file - file(WRITE ${CMAKE_JAVA_CLASS_OUTPUT_PATH}/java_class_filelist ${_JAVA_CLASS_FILES}) - - else (EXISTS "${CMAKE_JAVA_CLASS_OUTPUT_PATH}") - message(SEND_ERROR "FATAL: Java class output path doesn't exist") - endif (EXISTS "${CMAKE_JAVA_CLASS_OUTPUT_PATH}") -else (CMAKE_JAVA_CLASS_OUTPUT_PATH) - message(SEND_ERROR "FATAL: Can't find CMAKE_JAVA_CLASS_OUTPUT_PATH") -endif (CMAKE_JAVA_CLASS_OUTPUT_PATH) diff --git a/cmake/UseJavaSymlinks.cmake b/cmake/UseJavaSymlinks.cmake deleted file mode 100644 index c66ee1ea1..000000000 --- a/cmake/UseJavaSymlinks.cmake +++ /dev/null @@ -1,32 +0,0 @@ -# -# Helper script for UseJava.cmake -# - -#============================================================================= -# Copyright 2010-2011 Andreas schneider -# -# Distributed under the OSI-approved BSD License (the "License"); -# see accompanying file Copyright.txt for details. -# -# This software is distributed WITHOUT ANY WARRANTY; without even the -# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. -# See the License for more information. -#============================================================================= -# (To distribute this file outside of CMake, substitute the full -# License text for the above reference.) - -if (UNIX AND _JAVA_TARGET_OUTPUT_LINK) - if (_JAVA_TARGET_OUTPUT_NAME) - find_program(LN_EXECUTABLE - NAMES - ln - ) - - execute_process( - COMMAND ${LN_EXECUTABLE} -sf "${_JAVA_TARGET_OUTPUT_NAME}" "${_JAVA_TARGET_OUTPUT_LINK}" - WORKING_DIRECTORY ${_JAVA_TARGET_DIR} - ) - else (_JAVA_TARGET_OUTPUT_NAME) - message(SEND_ERROR "FATAL: Can't find _JAVA_TARGET_OUTPUT_NAME") - endif (_JAVA_TARGET_OUTPUT_NAME) -endif (UNIX AND _JAVA_TARGET_OUTPUT_LINK) From 8de63b60b1a9d0ba16f5d45f3198c13637151749 Mon Sep 17 00:00:00 2001 From: icelimetea Date: Mon, 2 May 2022 22:36:55 +0100 Subject: [PATCH 035/157] Refactor some parts of NewLaunch (part 2) --- libraries/launcher/CMakeLists.txt | 13 +- .../launcher/net/minecraft/Launcher.java | 92 +++---- .../launcher/org/multimc/EntryPoint.java | 55 ++-- libraries/launcher/org/multimc/Launcher.java | 7 +- .../launcher/org/multimc/LauncherFactory.java | 34 +++ .../launcher/org/multimc/LegacyFrame.java | 176 ------------ libraries/launcher/org/multimc/Utils.java | 86 ------ .../org/multimc/applet/LegacyFrame.java | 167 ++++++++++++ .../ParameterNotFoundException.java} | 10 +- .../{ => exception}/ParseException.java | 12 +- .../org/multimc/impl/OneSixLauncher.java | 183 +++++++++++++ .../org/multimc/onesix/OneSixLauncher.java | 256 ------------------ .../org/multimc/{ => utils}/ParamBucket.java | 36 +-- .../launcher/org/multimc/utils/Utils.java | 49 ++++ 14 files changed, 542 insertions(+), 634 deletions(-) create mode 100644 libraries/launcher/org/multimc/LauncherFactory.java delete mode 100644 libraries/launcher/org/multimc/LegacyFrame.java delete mode 100644 libraries/launcher/org/multimc/Utils.java create mode 100644 libraries/launcher/org/multimc/applet/LegacyFrame.java rename libraries/launcher/org/multimc/{NotFoundException.java => exception/ParameterNotFoundException.java} (73%) rename libraries/launcher/org/multimc/{ => exception}/ParseException.java (81%) create mode 100644 libraries/launcher/org/multimc/impl/OneSixLauncher.java delete mode 100644 libraries/launcher/org/multimc/onesix/OneSixLauncher.java rename libraries/launcher/org/multimc/{ => utils}/ParamBucket.java (68%) create mode 100644 libraries/launcher/org/multimc/utils/Utils.java diff --git a/libraries/launcher/CMakeLists.txt b/libraries/launcher/CMakeLists.txt index 0eccae8be..e01494829 100644 --- a/libraries/launcher/CMakeLists.txt +++ b/libraries/launcher/CMakeLists.txt @@ -9,12 +9,13 @@ set(CMAKE_JAVA_COMPILE_FLAGS -target 8 -source 8 -Xlint:deprecation -Xlint:unche set(SRC org/multimc/EntryPoint.java org/multimc/Launcher.java - org/multimc/LegacyFrame.java - org/multimc/NotFoundException.java - org/multimc/ParamBucket.java - org/multimc/ParseException.java - org/multimc/Utils.java - org/multimc/onesix/OneSixLauncher.java + org/multimc/LauncherFactory.java + org/multimc/impl/OneSixLauncher.java + org/multimc/applet/LegacyFrame.java + org/multimc/exception/ParameterNotFoundException.java + org/multimc/exception/ParseException.java + org/multimc/utils/ParamBucket.java + org/multimc/utils/Utils.java net/minecraft/Launcher.java ) add_jar(NewLaunch ${SRC}) diff --git a/libraries/launcher/net/minecraft/Launcher.java b/libraries/launcher/net/minecraft/Launcher.java index b6b0a574a..042010474 100644 --- a/libraries/launcher/net/minecraft/Launcher.java +++ b/libraries/launcher/net/minecraft/Launcher.java @@ -16,29 +16,28 @@ package net.minecraft; -import java.util.TreeMap; -import java.util.Map; -import java.net.URL; -import java.awt.Dimension; -import java.awt.BorderLayout; -import java.awt.Graphics; import java.applet.Applet; import java.applet.AppletStub; +import java.awt.*; import java.net.MalformedURLException; +import java.net.URL; +import java.util.Map; +import java.util.TreeMap; + +public class Launcher extends Applet implements AppletStub { + + private final Map params = new TreeMap<>(); -public class Launcher extends Applet implements AppletStub -{ - private Applet wrappedApplet; - private URL documentBase; private boolean active = false; - private final Map params; - public Launcher(Applet applet, URL documentBase) - { - params = new TreeMap(); + private Applet wrappedApplet; + private URL documentBase; + public Launcher(Applet applet, URL documentBase) { this.setLayout(new BorderLayout()); + this.add(applet, "Center"); + this.wrappedApplet = applet; this.documentBase = documentBase; } @@ -48,8 +47,7 @@ public void setParameter(String name, String value) params.put(name, value); } - public void replace(Applet applet) - { + public void replace(Applet applet) { this.wrappedApplet = applet; applet.setStub(this); @@ -65,67 +63,60 @@ public void replace(Applet applet) } @Override - public String getParameter(String name) - { + public String getParameter(String name) { String param = params.get(name); + if (param != null) return param; - try - { + + try { return super.getParameter(name); - } catch (Exception ignore){} + } catch (Exception ignore) {} + return null; } @Override - public boolean isActive() - { + public boolean isActive() { return active; } @Override - public void appletResize(int width, int height) - { + public void appletResize(int width, int height) { wrappedApplet.resize(width, height); } @Override - public void resize(int width, int height) - { + public void resize(int width, int height) { wrappedApplet.resize(width, height); } @Override - public void resize(Dimension d) - { + public void resize(Dimension d) { wrappedApplet.resize(d); } @Override - public void init() - { + public void init() { if (wrappedApplet != null) - { wrappedApplet.init(); - } } @Override - public void start() - { + public void start() { wrappedApplet.start(); + active = true; } @Override - public void stop() - { + public void stop() { wrappedApplet.stop(); + active = false; } - public void destroy() - { + public void destroy() { wrappedApplet.destroy(); } @@ -136,34 +127,35 @@ public URL getCodeBase() { } catch (MalformedURLException e) { e.printStackTrace(); } + return null; } @Override - public URL getDocumentBase() - { + public URL getDocumentBase() { try { // Special case only for Classic versions if (wrappedApplet.getClass().getCanonicalName().startsWith("com.mojang")) { return new URL("http", "www.minecraft.net", 80, "/game/", null); } + return new URL("http://www.minecraft.net/game/"); } catch (MalformedURLException e) { e.printStackTrace(); } + return null; } @Override - public void setVisible(boolean b) - { + public void setVisible(boolean b) { super.setVisible(b); + wrappedApplet.setVisible(b); } - public void update(Graphics paramGraphics) - { - } - public void paint(Graphics paramGraphics) - { - } -} \ No newline at end of file + + public void update(Graphics paramGraphics) {} + + public void paint(Graphics paramGraphics) {} + +} diff --git a/libraries/launcher/org/multimc/EntryPoint.java b/libraries/launcher/org/multimc/EntryPoint.java index b626d0958..be06d1b46 100644 --- a/libraries/launcher/org/multimc/EntryPoint.java +++ b/libraries/launcher/org/multimc/EntryPoint.java @@ -14,7 +14,8 @@ * limitations under the License. */ -import org.multimc.onesix.OneSixLauncher; +import org.multimc.exception.ParseException; +import org.multimc.utils.ParamBucket; import java.io.BufferedReader; import java.io.IOException; @@ -23,31 +24,27 @@ import java.util.logging.Level; import java.util.logging.Logger; -public class EntryPoint -{ +public final class EntryPoint { private static final Logger LOGGER = Logger.getLogger("EntryPoint"); private final ParamBucket params = new ParamBucket(); - private org.multimc.Launcher launcher; + private String launcherType; - public static void main(String[] args) - { + public static void main(String[] args) { EntryPoint listener = new EntryPoint(); int retCode = listener.listen(); - if (retCode != 0) - { + if (retCode != 0) { LOGGER.info("Exiting with " + retCode); System.exit(retCode); } } - private Action parseLine(String inData) throws ParseException - { + private Action parseLine(String inData) throws ParseException { String[] tokens = inData.split("\\s+", 2); if (tokens.length == 0) @@ -66,15 +63,9 @@ private Action parseLine(String inData) throws ParseException if (tokens.length != 2) throw new ParseException("Expected 2 tokens, got " + tokens.length); - if (tokens[1].equals("onesix")) { - launcher = new OneSixLauncher(); + launcherType = tokens[1]; - LOGGER.info("Using onesix launcher."); - - return Action.Proceed; - } else { - throw new ParseException("Invalid launcher type: " + tokens[1]); - } + return Action.Proceed; } default: { @@ -88,8 +79,7 @@ private Action parseLine(String inData) throws ParseException } } - public int listen() - { + public int listen() { Action action = Action.Proceed; try (BufferedReader reader = new BufferedReader(new InputStreamReader( @@ -112,16 +102,31 @@ public int listen() } // Main loop - if (action == Action.Abort) - { + if (action == Action.Abort) { LOGGER.info("Launch aborted by the launcher."); return 1; } - if (launcher != null) - { - return launcher.launch(params); + if (launcherType != null) { + try { + Launcher launcher = + LauncherFactory + .getInstance() + .createLauncher(launcherType, params); + + launcher.launch(); + + return 0; + } catch (IllegalArgumentException e) { + LOGGER.log(Level.SEVERE, "Wrong argument.", e); + + return 1; + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Exception caught from launcher.", e); + + return 1; + } } LOGGER.log(Level.SEVERE, "No valid launcher implementation specified."); diff --git a/libraries/launcher/org/multimc/Launcher.java b/libraries/launcher/org/multimc/Launcher.java index c5e8fbc10..bc0b525eb 100644 --- a/libraries/launcher/org/multimc/Launcher.java +++ b/libraries/launcher/org/multimc/Launcher.java @@ -16,7 +16,8 @@ package org.multimc; -public interface Launcher -{ - int launch(ParamBucket params); +public interface Launcher { + + void launch() throws Exception; + } diff --git a/libraries/launcher/org/multimc/LauncherFactory.java b/libraries/launcher/org/multimc/LauncherFactory.java new file mode 100644 index 000000000..b5d0dd5bd --- /dev/null +++ b/libraries/launcher/org/multimc/LauncherFactory.java @@ -0,0 +1,34 @@ +package org.multimc; + +import org.multimc.impl.OneSixLauncher; +import org.multimc.utils.ParamBucket; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +public final class LauncherFactory { + + private static final LauncherFactory INSTANCE = new LauncherFactory(); + + private final Map> launcherRegistry = new HashMap<>(); + + private LauncherFactory() { + launcherRegistry.put("onesix", OneSixLauncher::new); + } + + public Launcher createLauncher(String name, ParamBucket parameters) { + Function launcherCreator = + launcherRegistry.get(name); + + if (launcherCreator == null) + throw new IllegalArgumentException("Invalid launcher type: " + name); + + return launcherCreator.apply(parameters); + } + + public static LauncherFactory getInstance() { + return INSTANCE; + } + +} diff --git a/libraries/launcher/org/multimc/LegacyFrame.java b/libraries/launcher/org/multimc/LegacyFrame.java deleted file mode 100644 index 985a10e6a..000000000 --- a/libraries/launcher/org/multimc/LegacyFrame.java +++ /dev/null @@ -1,176 +0,0 @@ -package org.multimc;/* - * Copyright 2012-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import net.minecraft.Launcher; - -import javax.imageio.ImageIO; -import java.applet.Applet; -import java.awt.*; -import java.awt.event.WindowEvent; -import java.awt.event.WindowListener; -import java.awt.image.BufferedImage; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.Scanner; - -public class LegacyFrame extends Frame implements WindowListener -{ - private Launcher appletWrap = null; - public LegacyFrame(String title) - { - super ( title ); - BufferedImage image; - try { - image = ImageIO.read ( new File ( "icon.png" ) ); - setIconImage ( image ); - } catch ( IOException e ) { - e.printStackTrace(); - } - this.addWindowListener ( this ); - } - - public void start ( - Applet mcApplet, - String user, - String session, - int winSizeW, - int winSizeH, - boolean maximize, - String serverAddress, - String serverPort - ) - { - try { - appletWrap = new Launcher( mcApplet, new URL ( "http://www.minecraft.net/game" ) ); - } catch ( MalformedURLException ignored ) {} - - // Implements support for launching in to multiplayer on classic servers using a mpticket - // file generated by an external program and stored in the instance's root folder. - File mpticketFile = null; - Scanner fileReader = null; - try { - mpticketFile = new File(System.getProperty("user.dir") + "/../mpticket").getCanonicalFile(); - fileReader = new Scanner(new FileInputStream(mpticketFile), "ascii"); - String[] mpticketParams = new String[3]; - - for(int i=0;i<3;i++) { - if(fileReader.hasNextLine()) { - mpticketParams[i] = fileReader.nextLine(); - } else { - throw new IllegalArgumentException(); - } - } - - // Assumes parameters are valid and in the correct order - appletWrap.setParameter("server", mpticketParams[0]); - appletWrap.setParameter("port", mpticketParams[1]); - appletWrap.setParameter("mppass", mpticketParams[2]); - - fileReader.close(); - mpticketFile.delete(); - } - catch (FileNotFoundException e) {} - catch (IllegalArgumentException e) { - - fileReader.close(); - File mpticketFileCorrupt = new File(System.getProperty("user.dir") + "/../mpticket.corrupt"); - if(mpticketFileCorrupt.exists()) { - mpticketFileCorrupt.delete(); - } - mpticketFile.renameTo(mpticketFileCorrupt); - - System.err.println("Malformed mpticket file, missing argument."); - e.printStackTrace(System.err); - System.exit(-1); - } - catch (Exception e) { - e.printStackTrace(System.err); - System.exit(-1); - } - - if (serverAddress != null) - { - appletWrap.setParameter("server", serverAddress); - appletWrap.setParameter("port", serverPort); - } - - appletWrap.setParameter ( "username", user ); - appletWrap.setParameter ( "sessionid", session ); - appletWrap.setParameter ( "stand-alone", "true" ); // Show the quit button. - appletWrap.setParameter ( "haspaid", "true" ); // Some old versions need this for world saves to work. - appletWrap.setParameter ( "demo", "false" ); - appletWrap.setParameter ( "fullscreen", "false" ); - mcApplet.setStub(appletWrap); - this.add ( appletWrap ); - appletWrap.setPreferredSize ( new Dimension (winSizeW, winSizeH) ); - this.pack(); - this.setLocationRelativeTo ( null ); - this.setResizable ( true ); - if ( maximize ) { - this.setExtendedState ( MAXIMIZED_BOTH ); - } - validate(); - appletWrap.init(); - appletWrap.start(); - setVisible ( true ); - } - - @Override - public void windowActivated ( WindowEvent e ) {} - - @Override - public void windowClosed ( WindowEvent e ) {} - - @Override - public void windowClosing ( WindowEvent e ) - { - new Thread() { - public void run() { - try { - Thread.sleep ( 30000L ); - } catch ( InterruptedException localInterruptedException ) { - localInterruptedException.printStackTrace(); - } - System.out.println ( "FORCING EXIT!" ); - System.exit ( 0 ); - } - } - .start(); - - if ( appletWrap != null ) { - appletWrap.stop(); - appletWrap.destroy(); - } - // old minecraft versions can hang without this >_< - System.exit ( 0 ); - } - - @Override - public void windowDeactivated ( WindowEvent e ) {} - - @Override - public void windowDeiconified ( WindowEvent e ) {} - - @Override - public void windowIconified ( WindowEvent e ) {} - - @Override - public void windowOpened ( WindowEvent e ) {} -} diff --git a/libraries/launcher/org/multimc/Utils.java b/libraries/launcher/org/multimc/Utils.java deleted file mode 100644 index e48029c25..000000000 --- a/libraries/launcher/org/multimc/Utils.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright 2012-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.multimc; - -import java.io.File; -import java.lang.reflect.Field; -import java.lang.reflect.Modifier; -import java.util.List; - -public class Utils -{ - /** - * Combine two parts of a path. - * - * @param path1 - * @param path2 - * @return the paths, combined - */ - public static String combine(String path1, String path2) - { - File file1 = new File(path1); - File file2 = new File(file1, path2); - return file2.getPath(); - } - - /** - * Join a list of strings into a string using a separator! - * - * @param strings the string list to join - * @param separator the glue - * @return the result. - */ - public static String join(List strings, String separator) - { - StringBuilder sb = new StringBuilder(); - String sep = ""; - for (String s : strings) - { - sb.append(sep).append(s); - sep = separator; - } - return sb.toString(); - } - - /** - * Finds a field that looks like a Minecraft base folder in a supplied class - * - * @param mc the class to scan - */ - public static Field getMCPathField(Class mc) - { - Field[] fields = mc.getDeclaredFields(); - - for (Field f : fields) - { - if (f.getType() != File.class) - { - // Has to be File - continue; - } - if (f.getModifiers() != (Modifier.PRIVATE + Modifier.STATIC)) - { - // And Private Static. - continue; - } - return f; - } - return null; - } - -} - diff --git a/libraries/launcher/org/multimc/applet/LegacyFrame.java b/libraries/launcher/org/multimc/applet/LegacyFrame.java new file mode 100644 index 000000000..a5e6c1703 --- /dev/null +++ b/libraries/launcher/org/multimc/applet/LegacyFrame.java @@ -0,0 +1,167 @@ +package org.multimc.applet;/* + * Copyright 2012-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import net.minecraft.Launcher; + +import javax.imageio.ImageIO; +import java.applet.Applet; +import java.awt.*; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.Scanner; +import java.util.logging.Level; +import java.util.logging.Logger; + +public final class LegacyFrame extends Frame { + + private static final Logger LOGGER = Logger.getLogger("LegacyFrame"); + + private Launcher appletWrap; + + public LegacyFrame(String title) { + super(title); + + try { + setIconImage(ImageIO.read(new File("icon.png"))); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Unable to read Minecraft icon!", e); + } + + this.addWindowListener(new ForceExitHandler()); + } + + public void start ( + Applet mcApplet, + String user, + String session, + int winSizeW, + int winSizeH, + boolean maximize, + String serverAddress, + String serverPort + ) { + try { + appletWrap = new Launcher(mcApplet, new URL("http://www.minecraft.net/game")); + } catch (MalformedURLException ignored) {} + + // Implements support for launching in to multiplayer on classic servers using a mpticket + // file generated by an external program and stored in the instance's root folder. + Path mpticketFile = Paths.get(System.getProperty("user.dir") + "/../mpticket"); + Path mpticketFileCorrupt = Paths.get(System.getProperty("user.dir") + "/../mpticket.corrupt"); + + if (Files.exists(mpticketFile)) { + try (Scanner fileScanner = new Scanner( + Files.newInputStream(mpticketFile), + StandardCharsets.US_ASCII.name() + )) { + String[] mpticketParams = new String[3]; + + for (int i = 0; i < mpticketParams.length; i++) { + if (fileScanner.hasNextLine()) { + mpticketParams[i] = fileScanner.nextLine(); + } else { + Files.move( + mpticketFile, + mpticketFileCorrupt, + StandardCopyOption.REPLACE_EXISTING + ); + + throw new IllegalArgumentException("Mpticket file is corrupted!"); + } + } + + Files.delete(mpticketFile); + + // Assumes parameters are valid and in the correct order + appletWrap.setParameter("server", mpticketParams[0]); + appletWrap.setParameter("port", mpticketParams[1]); + appletWrap.setParameter("mppass", mpticketParams[2]); + } catch (IOException e) { + LOGGER.log(Level.WARNING, "Unable to read mpticket file!", e); + } + } + + if (serverAddress != null) { + appletWrap.setParameter("server", serverAddress); + appletWrap.setParameter("port", serverPort); + } + + appletWrap.setParameter("username", user); + appletWrap.setParameter("sessionid", session); + appletWrap.setParameter("stand-alone", "true"); // Show the quit button. + appletWrap.setParameter("haspaid", "true"); // Some old versions need this for world saves to work. + appletWrap.setParameter("demo", "false"); + appletWrap.setParameter("fullscreen", "false"); + + mcApplet.setStub(appletWrap); + + add(appletWrap); + + appletWrap.setPreferredSize(new Dimension(winSizeW, winSizeH)); + + pack(); + + setLocationRelativeTo(null); + setResizable(true); + + if (maximize) + this.setExtendedState(MAXIMIZED_BOTH); + + validate(); + + appletWrap.init(); + appletWrap.start(); + + setVisible(true); + } + + private final class ForceExitHandler extends WindowAdapter { + + @Override + public void windowClosing(WindowEvent e) { + new Thread(() -> { + try { + Thread.sleep(30000L); + } catch (InterruptedException localInterruptedException) { + localInterruptedException.printStackTrace(); + } + + LOGGER.info("Forcing exit!"); + + System.exit(0); + }).start(); + + if (appletWrap != null) { + appletWrap.stop(); + appletWrap.destroy(); + } + + // old minecraft versions can hang without this >_< + System.exit(0); + } + + } + +} diff --git a/libraries/launcher/org/multimc/NotFoundException.java b/libraries/launcher/org/multimc/exception/ParameterNotFoundException.java similarity index 73% rename from libraries/launcher/org/multimc/NotFoundException.java rename to libraries/launcher/org/multimc/exception/ParameterNotFoundException.java index ba12951d6..9edbb8261 100644 --- a/libraries/launcher/org/multimc/NotFoundException.java +++ b/libraries/launcher/org/multimc/exception/ParameterNotFoundException.java @@ -14,8 +14,12 @@ * limitations under the License. */ -package org.multimc; +package org.multimc.exception; + +public final class ParameterNotFoundException extends IllegalArgumentException { + + public ParameterNotFoundException(String key) { + super("Unknown parameter name: " + key); + } -public class NotFoundException extends Exception -{ } diff --git a/libraries/launcher/org/multimc/ParseException.java b/libraries/launcher/org/multimc/exception/ParseException.java similarity index 81% rename from libraries/launcher/org/multimc/ParseException.java rename to libraries/launcher/org/multimc/exception/ParseException.java index 7ea44c1f5..c9a4c8562 100644 --- a/libraries/launcher/org/multimc/ParseException.java +++ b/libraries/launcher/org/multimc/exception/ParseException.java @@ -14,12 +14,16 @@ * limitations under the License. */ -package org.multimc; +package org.multimc.exception; + +public final class ParseException extends IllegalArgumentException { + + public ParseException() { + super(); + } -public class ParseException extends java.lang.Exception -{ - public ParseException() { super(); } public ParseException(String message) { super(message); } + } diff --git a/libraries/launcher/org/multimc/impl/OneSixLauncher.java b/libraries/launcher/org/multimc/impl/OneSixLauncher.java new file mode 100644 index 000000000..d2596a698 --- /dev/null +++ b/libraries/launcher/org/multimc/impl/OneSixLauncher.java @@ -0,0 +1,183 @@ +/* Copyright 2012-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.multimc.impl; + +import org.multimc.Launcher; +import org.multimc.applet.LegacyFrame; +import org.multimc.utils.ParamBucket; +import org.multimc.utils.Utils; + +import java.applet.Applet; +import java.io.File; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +public final class OneSixLauncher implements Launcher { + + private static final int DEFAULT_WINDOW_WIDTH = 854; + private static final int DEFAULT_WINDOW_HEIGHT = 480; + + private static final Logger LOGGER = Logger.getLogger("OneSixLauncher"); + + // parameters, separated from ParamBucket + private final List mcParams; + private final List traits; + private final String appletClass; + private final String mainClass; + private final String userName, sessionId; + private final String windowTitle; + + // secondary parameters + private final int winSizeW; + private final int winSizeH; + private final boolean maximize; + private final String cwd; + + private final String serverAddress; + private final String serverPort; + + private final ClassLoader classLoader; + + public OneSixLauncher(ParamBucket params) { + classLoader = ClassLoader.getSystemClassLoader(); + + mcParams = params.allSafe("param", Collections.emptyList()); + mainClass = params.firstSafe("mainClass", "net.minecraft.client.Minecraft"); + appletClass = params.firstSafe("appletClass", "net.minecraft.client.MinecraftApplet"); + traits = params.allSafe("traits", Collections.emptyList()); + + userName = params.first("userName"); + sessionId = params.first("sessionId"); + windowTitle = params.firstSafe("windowTitle", "Minecraft"); + + serverAddress = params.firstSafe("serverAddress", null); + serverPort = params.firstSafe("serverPort", null); + + cwd = System.getProperty("user.dir"); + + String windowParams = params.firstSafe("windowParams", "854x480"); + + String[] dimStrings = windowParams.split("x"); + + if (windowParams.equalsIgnoreCase("max")) { + maximize = true; + + winSizeW = DEFAULT_WINDOW_WIDTH; + winSizeH = DEFAULT_WINDOW_HEIGHT; + } else if (dimStrings.length == 2) { + maximize = false; + + winSizeW = Integer.parseInt(dimStrings[0]); + winSizeH = Integer.parseInt(dimStrings[1]); + } else { + throw new IllegalArgumentException("Unexpected window size parameter value: " + windowParams); + } + } + + private void invokeMain(Class mainClass) throws Exception { + Method method = mainClass.getMethod("main", String[].class); + + method.invoke(null, (Object) mcParams.toArray(new String[0])); + } + + private void legacyLaunch() throws Exception { + // Get the Minecraft Class and set the base folder + Class minecraftClass = classLoader.loadClass(mainClass); + + Field baseDirField = Utils.getMinecraftBaseDirField(minecraftClass); + + if (baseDirField == null) { + LOGGER.warning("Could not find Minecraft path field."); + } else { + baseDirField.setAccessible(true); + + baseDirField.set(null, new File(cwd)); + } + + System.setProperty("minecraft.applet.TargetDirectory", cwd); + + if (!traits.contains("noapplet")) { + LOGGER.info("Launching with applet wrapper..."); + + try { + Class mcAppletClass = classLoader.loadClass(appletClass); + + Applet mcApplet = (Applet) mcAppletClass.getConstructor().newInstance(); + + LegacyFrame mcWindow = new LegacyFrame(windowTitle); + + mcWindow.start( + mcApplet, + userName, + sessionId, + winSizeW, + winSizeH, + maximize, + serverAddress, + serverPort + ); + + return; + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Applet wrapper failed: ", e); + + LOGGER.warning("Falling back to using main class."); + } + } + + invokeMain(minecraftClass); + } + + void launchWithMainClass() throws Exception { + // window size, title and state, onesix + + // FIXME: there is no good way to maximize the minecraft window in onesix. + // the following often breaks linux screen setups + // mcparams.add("--fullscreen"); + + if (!maximize) { + mcParams.add("--width"); + mcParams.add(Integer.toString(winSizeW)); + mcParams.add("--height"); + mcParams.add(Integer.toString(winSizeH)); + } + + if (serverAddress != null) { + mcParams.add("--server"); + mcParams.add(serverAddress); + mcParams.add("--port"); + mcParams.add(serverPort); + } + + invokeMain(classLoader.loadClass(mainClass)); + } + + @Override + public void launch() throws Exception { + if (traits.contains("legacyLaunch") || traits.contains("alphaLaunch")) { + // legacy launch uses the applet wrapper + legacyLaunch(); + } else { + // normal launch just calls main() + launchWithMainClass(); + } + } + +} diff --git a/libraries/launcher/org/multimc/onesix/OneSixLauncher.java b/libraries/launcher/org/multimc/onesix/OneSixLauncher.java deleted file mode 100644 index 0058bd43f..000000000 --- a/libraries/launcher/org/multimc/onesix/OneSixLauncher.java +++ /dev/null @@ -1,256 +0,0 @@ -/* Copyright 2012-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.multimc.onesix; - -import org.multimc.*; - -import java.applet.Applet; -import java.io.File; -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.List; -import java.util.logging.Level; -import java.util.logging.Logger; - -public class OneSixLauncher implements Launcher -{ - - private static final Logger LOGGER = Logger.getLogger("OneSixLauncher"); - - // parameters, separated from ParamBucket - private List libraries; - private List mcparams; - private List mods; - private List jarmods; - private List coremods; - private List traits; - private String appletClass; - private String mainClass; - private String nativePath; - private String userName, sessionId; - private String windowTitle; - private String windowParams; - - // secondary parameters - private int winSizeW; - private int winSizeH; - private boolean maximize; - private String cwd; - - private String serverAddress; - private String serverPort; - - // the much abused system classloader, for convenience (for further abuse) - private ClassLoader cl; - - private void processParams(ParamBucket params) throws NotFoundException - { - libraries = params.all("cp"); - mcparams = params.allSafe("param", new ArrayList() ); - mainClass = params.firstSafe("mainClass", "net.minecraft.client.Minecraft"); - appletClass = params.firstSafe("appletClass", "net.minecraft.client.MinecraftApplet"); - traits = params.allSafe("traits", new ArrayList()); - nativePath = params.first("natives"); - - userName = params.first("userName"); - sessionId = params.first("sessionId"); - windowTitle = params.firstSafe("windowTitle", "Minecraft"); - windowParams = params.firstSafe("windowParams", "854x480"); - - serverAddress = params.firstSafe("serverAddress", null); - serverPort = params.firstSafe("serverPort", null); - - cwd = System.getProperty("user.dir"); - - winSizeW = 854; - winSizeH = 480; - maximize = false; - - String[] dimStrings = windowParams.split("x"); - - if (windowParams.equalsIgnoreCase("max")) - { - maximize = true; - } - else if (dimStrings.length == 2) - { - try - { - winSizeW = Integer.parseInt(dimStrings[0]); - winSizeH = Integer.parseInt(dimStrings[1]); - } catch (NumberFormatException ignored) {} - } - } - - int legacyLaunch() - { - // Get the Minecraft Class and set the base folder - Class mc; - try - { - mc = cl.loadClass(mainClass); - - Field f = Utils.getMCPathField(mc); - - if (f == null) - { - LOGGER.warning("Could not find Minecraft path field."); - } - else - { - f.setAccessible(true); - f.set(null, new File(cwd)); - } - } catch (Exception e) - { - LOGGER.log( - Level.SEVERE, - "Could not set base folder. Failed to find/access Minecraft main class:", - e - ); - - return -1; - } - - System.setProperty("minecraft.applet.TargetDirectory", cwd); - - if(!traits.contains("noapplet")) - { - LOGGER.info("Launching with applet wrapper..."); - try - { - Class MCAppletClass = cl.loadClass(appletClass); - Applet mcappl = (Applet) MCAppletClass.newInstance(); - LegacyFrame mcWindow = new LegacyFrame(windowTitle); - mcWindow.start(mcappl, userName, sessionId, winSizeW, winSizeH, maximize, serverAddress, serverPort); - return 0; - } catch (Exception e) - { - LOGGER.log(Level.SEVERE, "Applet wrapper failed:", e); - - LOGGER.warning("Falling back to using main class."); - } - } - - // init params for the main method to chomp on. - String[] paramsArray = mcparams.toArray(new String[mcparams.size()]); - try - { - mc.getMethod("main", String[].class).invoke(null, (Object) paramsArray); - return 0; - } catch (Exception e) - { - LOGGER.log(Level.SEVERE, "Failed to invoke the Minecraft main class:", e); - - return -1; - } - } - - int launchWithMainClass() - { - // window size, title and state, onesix - if (maximize) - { - // FIXME: there is no good way to maximize the minecraft window in onesix. - // the following often breaks linux screen setups - // mcparams.add("--fullscreen"); - } - else - { - mcparams.add("--width"); - mcparams.add(Integer.toString(winSizeW)); - mcparams.add("--height"); - mcparams.add(Integer.toString(winSizeH)); - } - - if (serverAddress != null) - { - mcparams.add("--server"); - mcparams.add(serverAddress); - mcparams.add("--port"); - mcparams.add(serverPort); - } - - // Get the Minecraft Class. - Class mc; - try - { - mc = cl.loadClass(mainClass); - } catch (ClassNotFoundException e) - { - LOGGER.log(Level.SEVERE, "Failed to find Minecraft main class:", e); - - return -1; - } - - // get the main method. - Method meth; - try - { - meth = mc.getMethod("main", String[].class); - } catch (NoSuchMethodException e) - { - LOGGER.log(Level.SEVERE, "Failed to acquire the main method:", e); - - return -1; - } - - // init params for the main method to chomp on. - String[] paramsArray = mcparams.toArray(new String[mcparams.size()]); - try - { - // static method doesn't have an instance - meth.invoke(null, (Object) paramsArray); - } catch (Exception e) - { - LOGGER.log(Level.SEVERE, "Failed to start Minecraft:", e); - - return -1; - } - return 0; - } - - @Override - public int launch(ParamBucket params) - { - // get and process the launch script params - try - { - processParams(params); - } catch (NotFoundException e) - { - LOGGER.log(Level.SEVERE, "Not enough arguments!"); - - return -1; - } - - // grab the system classloader and ... - cl = ClassLoader.getSystemClassLoader(); - - if (traits.contains("legacyLaunch") || traits.contains("alphaLaunch") ) - { - // legacy launch uses the applet wrapper - return legacyLaunch(); - } - else - { - // normal launch just calls main() - return launchWithMainClass(); - } - } - -} diff --git a/libraries/launcher/org/multimc/ParamBucket.java b/libraries/launcher/org/multimc/utils/ParamBucket.java similarity index 68% rename from libraries/launcher/org/multimc/ParamBucket.java rename to libraries/launcher/org/multimc/utils/ParamBucket.java index 8ff03ddc7..26ff8eef2 100644 --- a/libraries/launcher/org/multimc/ParamBucket.java +++ b/libraries/launcher/org/multimc/utils/ParamBucket.java @@ -14,36 +14,34 @@ * limitations under the License. */ -package org.multimc; +package org.multimc.utils; + +import org.multimc.exception.ParameterNotFoundException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -public class ParamBucket -{ +public final class ParamBucket { private final Map> paramsMap = new HashMap<>(); - public void add(String key, String value) - { + public void add(String key, String value) { paramsMap.computeIfAbsent(key, k -> new ArrayList<>()) .add(value); } - public List all(String key) throws NotFoundException - { + public List all(String key) throws ParameterNotFoundException { List params = paramsMap.get(key); if (params == null) - throw new NotFoundException(); + throw new ParameterNotFoundException(key); return params; } - public List allSafe(String key, List def) - { + public List allSafe(String key, List def) { List params = paramsMap.get(key); if (params == null || params.isEmpty()) @@ -52,23 +50,16 @@ public List allSafe(String key, List def) return params; } - public List allSafe(String key) - { - return allSafe(key, new ArrayList<>()); - } - - public String first(String key) throws NotFoundException - { + public String first(String key) throws ParameterNotFoundException { List list = all(key); if (list.isEmpty()) - throw new NotFoundException(); + throw new ParameterNotFoundException(key); return list.get(0); } - public String firstSafe(String key, String def) - { + public String firstSafe(String key, String def) { List params = paramsMap.get(key); if (params == null || params.isEmpty()) @@ -77,9 +68,4 @@ public String firstSafe(String key, String def) return params.get(0); } - public String firstSafe(String key) - { - return firstSafe(key, ""); - } - } diff --git a/libraries/launcher/org/multimc/utils/Utils.java b/libraries/launcher/org/multimc/utils/Utils.java new file mode 100644 index 000000000..416eff26b --- /dev/null +++ b/libraries/launcher/org/multimc/utils/Utils.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.multimc.utils; + +import java.io.File; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; + +public final class Utils { + + private Utils() {} + + /** + * Finds a field that looks like a Minecraft base folder in a supplied class + * + * @param clazz the class to scan + */ + public static Field getMinecraftBaseDirField(Class clazz) { + for (Field f : clazz.getDeclaredFields()) { + // Has to be File + if (f.getType() != File.class) + continue; + + // And Private Static. + if (!Modifier.isStatic(f.getModifiers()) || !Modifier.isPrivate(f.getModifiers())) + continue; + + return f; + } + + return null; + } + +} + From eeb5297284494c03f3b8e3927c5ed6cc3ca09a41 Mon Sep 17 00:00:00 2001 From: icelimetea Date: Tue, 3 May 2022 00:25:26 +0100 Subject: [PATCH 036/157] Use only Java 7 features (in order to deal with #515) --- .../launcher/org/multimc/LauncherFactory.java | 23 +++++++++++++------ .../org/multimc/applet/LegacyFrame.java | 19 ++++++++------- .../org/multimc/impl/OneSixLauncher.java | 4 ++-- .../org/multimc/utils/ParamBucket.java | 11 +++++++-- 4 files changed, 38 insertions(+), 19 deletions(-) diff --git a/libraries/launcher/org/multimc/LauncherFactory.java b/libraries/launcher/org/multimc/LauncherFactory.java index b5d0dd5bd..2b3700582 100644 --- a/libraries/launcher/org/multimc/LauncherFactory.java +++ b/libraries/launcher/org/multimc/LauncherFactory.java @@ -5,30 +5,39 @@ import java.util.HashMap; import java.util.Map; -import java.util.function.Function; public final class LauncherFactory { private static final LauncherFactory INSTANCE = new LauncherFactory(); - private final Map> launcherRegistry = new HashMap<>(); + private final Map launcherRegistry = new HashMap<>(); private LauncherFactory() { - launcherRegistry.put("onesix", OneSixLauncher::new); + launcherRegistry.put("onesix", new LauncherProvider() { + @Override + public Launcher provide(ParamBucket parameters) { + return new OneSixLauncher(parameters); + } + }); } public Launcher createLauncher(String name, ParamBucket parameters) { - Function launcherCreator = - launcherRegistry.get(name); + LauncherProvider launcherProvider = launcherRegistry.get(name); - if (launcherCreator == null) + if (launcherProvider == null) throw new IllegalArgumentException("Invalid launcher type: " + name); - return launcherCreator.apply(parameters); + return launcherProvider.provide(parameters); } public static LauncherFactory getInstance() { return INSTANCE; } + public interface LauncherProvider { + + Launcher provide(ParamBucket parameters); + + } + } diff --git a/libraries/launcher/org/multimc/applet/LegacyFrame.java b/libraries/launcher/org/multimc/applet/LegacyFrame.java index a5e6c1703..d250ce268 100644 --- a/libraries/launcher/org/multimc/applet/LegacyFrame.java +++ b/libraries/launcher/org/multimc/applet/LegacyFrame.java @@ -141,16 +141,19 @@ private final class ForceExitHandler extends WindowAdapter { @Override public void windowClosing(WindowEvent e) { - new Thread(() -> { - try { - Thread.sleep(30000L); - } catch (InterruptedException localInterruptedException) { - localInterruptedException.printStackTrace(); - } + new Thread(new Runnable() { + @Override + public void run() { + try { + Thread.sleep(30000L); + } catch (InterruptedException localInterruptedException) { + localInterruptedException.printStackTrace(); + } - LOGGER.info("Forcing exit!"); + LOGGER.info("Forcing exit!"); - System.exit(0); + System.exit(0); + } }).start(); if (appletWrap != null) { diff --git a/libraries/launcher/org/multimc/impl/OneSixLauncher.java b/libraries/launcher/org/multimc/impl/OneSixLauncher.java index d2596a698..19253dc04 100644 --- a/libraries/launcher/org/multimc/impl/OneSixLauncher.java +++ b/libraries/launcher/org/multimc/impl/OneSixLauncher.java @@ -58,10 +58,10 @@ public final class OneSixLauncher implements Launcher { public OneSixLauncher(ParamBucket params) { classLoader = ClassLoader.getSystemClassLoader(); - mcParams = params.allSafe("param", Collections.emptyList()); + mcParams = params.allSafe("param", Collections.emptyList()); mainClass = params.firstSafe("mainClass", "net.minecraft.client.Minecraft"); appletClass = params.firstSafe("appletClass", "net.minecraft.client.MinecraftApplet"); - traits = params.allSafe("traits", Collections.emptyList()); + traits = params.allSafe("traits", Collections.emptyList()); userName = params.first("userName"); sessionId = params.first("sessionId"); diff --git a/libraries/launcher/org/multimc/utils/ParamBucket.java b/libraries/launcher/org/multimc/utils/ParamBucket.java index 26ff8eef2..5dbb8775e 100644 --- a/libraries/launcher/org/multimc/utils/ParamBucket.java +++ b/libraries/launcher/org/multimc/utils/ParamBucket.java @@ -28,8 +28,15 @@ public final class ParamBucket { private final Map> paramsMap = new HashMap<>(); public void add(String key, String value) { - paramsMap.computeIfAbsent(key, k -> new ArrayList<>()) - .add(value); + List params = paramsMap.get(key); + + if (params == null) { + params = new ArrayList<>(); + + paramsMap.put(key, params); + } + + params.add(value); } public List all(String key) throws ParameterNotFoundException { From 4fdb21b41400e789ca44a5cc1079469eb2508370 Mon Sep 17 00:00:00 2001 From: icelimetea Date: Tue, 3 May 2022 00:27:14 +0100 Subject: [PATCH 037/157] Compile with Java 7 in mind --- libraries/launcher/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/launcher/CMakeLists.txt b/libraries/launcher/CMakeLists.txt index e01494829..0a0a541c4 100644 --- a/libraries/launcher/CMakeLists.txt +++ b/libraries/launcher/CMakeLists.txt @@ -4,7 +4,7 @@ find_package(Java 1.7 REQUIRED COMPONENTS Development) include(UseJava) set(CMAKE_JAVA_JAR_ENTRY_POINT org.multimc.EntryPoint) -set(CMAKE_JAVA_COMPILE_FLAGS -target 8 -source 8 -Xlint:deprecation -Xlint:unchecked) +set(CMAKE_JAVA_COMPILE_FLAGS -target 7 -source 7 -Xlint:deprecation -Xlint:unchecked) set(SRC org/multimc/EntryPoint.java From 860a7af6796785898926bcf10b034545caa5401b Mon Sep 17 00:00:00 2001 From: icelimetea Date: Tue, 3 May 2022 00:53:22 +0100 Subject: [PATCH 038/157] Fix method access modifier --- libraries/launcher/org/multimc/impl/OneSixLauncher.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/launcher/org/multimc/impl/OneSixLauncher.java b/libraries/launcher/org/multimc/impl/OneSixLauncher.java index 19253dc04..a87b116c7 100644 --- a/libraries/launcher/org/multimc/impl/OneSixLauncher.java +++ b/libraries/launcher/org/multimc/impl/OneSixLauncher.java @@ -145,7 +145,7 @@ private void legacyLaunch() throws Exception { invokeMain(minecraftClass); } - void launchWithMainClass() throws Exception { + private void launchWithMainClass() throws Exception { // window size, title and state, onesix // FIXME: there is no good way to maximize the minecraft window in onesix. From 9a87ae575ef58bb86d4bbd7bdb8ab7e026ad9a33 Mon Sep 17 00:00:00 2001 From: icelimetea Date: Tue, 3 May 2022 03:19:26 +0100 Subject: [PATCH 039/157] More minor fixes --- libraries/launcher/CMakeLists.txt | 2 +- .../launcher/net/minecraft/Launcher.java | 30 ++++------------ .../launcher/org/multimc/EntryPoint.java | 4 +-- .../launcher/org/multimc/LauncherFactory.java | 8 ++--- .../org/multimc/applet/LegacyFrame.java | 25 +++++++------ .../org/multimc/exception/ParseException.java | 4 --- .../org/multimc/impl/OneSixLauncher.java | 36 +++++++++++-------- .../{ParamBucket.java => Parameters.java} | 2 +- 8 files changed, 47 insertions(+), 64 deletions(-) rename libraries/launcher/org/multimc/utils/{ParamBucket.java => Parameters.java} (98%) diff --git a/libraries/launcher/CMakeLists.txt b/libraries/launcher/CMakeLists.txt index 0a0a541c4..2c859499d 100644 --- a/libraries/launcher/CMakeLists.txt +++ b/libraries/launcher/CMakeLists.txt @@ -14,7 +14,7 @@ set(SRC org/multimc/applet/LegacyFrame.java org/multimc/exception/ParameterNotFoundException.java org/multimc/exception/ParseException.java - org/multimc/utils/ParamBucket.java + org/multimc/utils/Parameters.java org/multimc/utils/Utils.java net/minecraft/Launcher.java ) diff --git a/libraries/launcher/net/minecraft/Launcher.java b/libraries/launcher/net/minecraft/Launcher.java index 042010474..265fa66ac 100644 --- a/libraries/launcher/net/minecraft/Launcher.java +++ b/libraries/launcher/net/minecraft/Launcher.java @@ -24,22 +24,20 @@ import java.util.Map; import java.util.TreeMap; -public class Launcher extends Applet implements AppletStub { +public final class Launcher extends Applet implements AppletStub { private final Map params = new TreeMap<>(); - private boolean active = false; + private final Applet wrappedApplet; - private Applet wrappedApplet; - private URL documentBase; + private boolean active = false; - public Launcher(Applet applet, URL documentBase) { + public Launcher(Applet applet) { this.setLayout(new BorderLayout()); this.add(applet, "Center"); this.wrappedApplet = applet; - this.documentBase = documentBase; } public void setParameter(String name, String value) @@ -47,21 +45,6 @@ public void setParameter(String name, String value) params.put(name, value); } - public void replace(Applet applet) { - this.wrappedApplet = applet; - - applet.setStub(this); - applet.setSize(getWidth(), getHeight()); - - this.setLayout(new BorderLayout()); - this.add(applet, "Center"); - - applet.init(); - active = true; - applet.start(); - validate(); - } - @Override public String getParameter(String name) { String param = params.get(name); @@ -135,9 +118,8 @@ public URL getCodeBase() { public URL getDocumentBase() { try { // Special case only for Classic versions - if (wrappedApplet.getClass().getCanonicalName().startsWith("com.mojang")) { - return new URL("http", "www.minecraft.net", 80, "/game/", null); - } + if (wrappedApplet.getClass().getCanonicalName().startsWith("com.mojang")) + return new URL("http", "www.minecraft.net", 80, "/game/"); return new URL("http://www.minecraft.net/game/"); } catch (MalformedURLException e) { diff --git a/libraries/launcher/org/multimc/EntryPoint.java b/libraries/launcher/org/multimc/EntryPoint.java index be06d1b46..416f21890 100644 --- a/libraries/launcher/org/multimc/EntryPoint.java +++ b/libraries/launcher/org/multimc/EntryPoint.java @@ -15,7 +15,7 @@ */ import org.multimc.exception.ParseException; -import org.multimc.utils.ParamBucket; +import org.multimc.utils.Parameters; import java.io.BufferedReader; import java.io.IOException; @@ -28,7 +28,7 @@ public final class EntryPoint { private static final Logger LOGGER = Logger.getLogger("EntryPoint"); - private final ParamBucket params = new ParamBucket(); + private final Parameters params = new Parameters(); private String launcherType; diff --git a/libraries/launcher/org/multimc/LauncherFactory.java b/libraries/launcher/org/multimc/LauncherFactory.java index 2b3700582..17e0d9058 100644 --- a/libraries/launcher/org/multimc/LauncherFactory.java +++ b/libraries/launcher/org/multimc/LauncherFactory.java @@ -1,7 +1,7 @@ package org.multimc; import org.multimc.impl.OneSixLauncher; -import org.multimc.utils.ParamBucket; +import org.multimc.utils.Parameters; import java.util.HashMap; import java.util.Map; @@ -15,13 +15,13 @@ public final class LauncherFactory { private LauncherFactory() { launcherRegistry.put("onesix", new LauncherProvider() { @Override - public Launcher provide(ParamBucket parameters) { + public Launcher provide(Parameters parameters) { return new OneSixLauncher(parameters); } }); } - public Launcher createLauncher(String name, ParamBucket parameters) { + public Launcher createLauncher(String name, Parameters parameters) { LauncherProvider launcherProvider = launcherRegistry.get(name); if (launcherProvider == null) @@ -36,7 +36,7 @@ public static LauncherFactory getInstance() { public interface LauncherProvider { - Launcher provide(ParamBucket parameters); + Launcher provide(Parameters parameters); } diff --git a/libraries/launcher/org/multimc/applet/LegacyFrame.java b/libraries/launcher/org/multimc/applet/LegacyFrame.java index d250ce268..c50995f67 100644 --- a/libraries/launcher/org/multimc/applet/LegacyFrame.java +++ b/libraries/launcher/org/multimc/applet/LegacyFrame.java @@ -23,8 +23,6 @@ import java.awt.event.WindowEvent; import java.io.File; import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -38,11 +36,15 @@ public final class LegacyFrame extends Frame { private static final Logger LOGGER = Logger.getLogger("LegacyFrame"); - private Launcher appletWrap; + private final Launcher appletWrap; - public LegacyFrame(String title) { + public LegacyFrame(String title, Applet mcApplet) { super(title); + appletWrap = new Launcher(mcApplet); + + mcApplet.setStub(appletWrap); + try { setIconImage(ImageIO.read(new File("icon.png"))); } catch (IOException e) { @@ -53,7 +55,6 @@ public LegacyFrame(String title) { } public void start ( - Applet mcApplet, String user, String session, int winSizeW, @@ -62,14 +63,14 @@ public void start ( String serverAddress, String serverPort ) { - try { - appletWrap = new Launcher(mcApplet, new URL("http://www.minecraft.net/game")); - } catch (MalformedURLException ignored) {} - // Implements support for launching in to multiplayer on classic servers using a mpticket // file generated by an external program and stored in the instance's root folder. - Path mpticketFile = Paths.get(System.getProperty("user.dir") + "/../mpticket"); - Path mpticketFileCorrupt = Paths.get(System.getProperty("user.dir") + "/../mpticket.corrupt"); + + Path mpticketFile = + Paths.get(System.getProperty("user.dir"), "..", "mpticket"); + + Path mpticketFileCorrupt = + Paths.get(System.getProperty("user.dir"), "..", "mpticket.corrupt"); if (Files.exists(mpticketFile)) { try (Scanner fileScanner = new Scanner( @@ -115,8 +116,6 @@ public void start ( appletWrap.setParameter("demo", "false"); appletWrap.setParameter("fullscreen", "false"); - mcApplet.setStub(appletWrap); - add(appletWrap); appletWrap.setPreferredSize(new Dimension(winSizeW, winSizeH)); diff --git a/libraries/launcher/org/multimc/exception/ParseException.java b/libraries/launcher/org/multimc/exception/ParseException.java index c9a4c8562..848b395de 100644 --- a/libraries/launcher/org/multimc/exception/ParseException.java +++ b/libraries/launcher/org/multimc/exception/ParseException.java @@ -18,10 +18,6 @@ public final class ParseException extends IllegalArgumentException { - public ParseException() { - super(); - } - public ParseException(String message) { super(message); } diff --git a/libraries/launcher/org/multimc/impl/OneSixLauncher.java b/libraries/launcher/org/multimc/impl/OneSixLauncher.java index a87b116c7..b981e4ff4 100644 --- a/libraries/launcher/org/multimc/impl/OneSixLauncher.java +++ b/libraries/launcher/org/multimc/impl/OneSixLauncher.java @@ -17,7 +17,7 @@ import org.multimc.Launcher; import org.multimc.applet.LegacyFrame; -import org.multimc.utils.ParamBucket; +import org.multimc.utils.Parameters; import org.multimc.utils.Utils; import java.applet.Applet; @@ -55,7 +55,7 @@ public final class OneSixLauncher implements Launcher { private final ClassLoader classLoader; - public OneSixLauncher(ParamBucket params) { + public OneSixLauncher(Parameters params) { classLoader = ClassLoader.getSystemClassLoader(); mcParams = params.allSafe("param", Collections.emptyList()); @@ -72,22 +72,29 @@ public OneSixLauncher(ParamBucket params) { cwd = System.getProperty("user.dir"); - String windowParams = params.firstSafe("windowParams", "854x480"); + String windowParams = params.firstSafe("windowParams", null); - String[] dimStrings = windowParams.split("x"); + if (windowParams != null) { + String[] dimStrings = windowParams.split("x"); - if (windowParams.equalsIgnoreCase("max")) { - maximize = true; + if (windowParams.equalsIgnoreCase("max")) { + maximize = true; - winSizeW = DEFAULT_WINDOW_WIDTH; - winSizeH = DEFAULT_WINDOW_HEIGHT; - } else if (dimStrings.length == 2) { - maximize = false; + winSizeW = DEFAULT_WINDOW_WIDTH; + winSizeH = DEFAULT_WINDOW_HEIGHT; + } else if (dimStrings.length == 2) { + maximize = false; - winSizeW = Integer.parseInt(dimStrings[0]); - winSizeH = Integer.parseInt(dimStrings[1]); + winSizeW = Integer.parseInt(dimStrings[0]); + winSizeH = Integer.parseInt(dimStrings[1]); + } else { + throw new IllegalArgumentException("Unexpected window size parameter value: " + windowParams); + } } else { - throw new IllegalArgumentException("Unexpected window size parameter value: " + windowParams); + maximize = false; + + winSizeW = DEFAULT_WINDOW_WIDTH; + winSizeH = DEFAULT_WINDOW_HEIGHT; } } @@ -121,10 +128,9 @@ private void legacyLaunch() throws Exception { Applet mcApplet = (Applet) mcAppletClass.getConstructor().newInstance(); - LegacyFrame mcWindow = new LegacyFrame(windowTitle); + LegacyFrame mcWindow = new LegacyFrame(windowTitle, mcApplet); mcWindow.start( - mcApplet, userName, sessionId, winSizeW, diff --git a/libraries/launcher/org/multimc/utils/ParamBucket.java b/libraries/launcher/org/multimc/utils/Parameters.java similarity index 98% rename from libraries/launcher/org/multimc/utils/ParamBucket.java rename to libraries/launcher/org/multimc/utils/Parameters.java index 5dbb8775e..7be790c29 100644 --- a/libraries/launcher/org/multimc/utils/ParamBucket.java +++ b/libraries/launcher/org/multimc/utils/Parameters.java @@ -23,7 +23,7 @@ import java.util.List; import java.util.Map; -public final class ParamBucket { +public final class Parameters { private final Map> paramsMap = new HashMap<>(); From e909cc363d2236ad99601222728bad5b1ea71c31 Mon Sep 17 00:00:00 2001 From: Ryan Cao <70191398+ryanccn@users.noreply.github.com> Date: Wed, 27 Apr 2022 15:55:34 +0800 Subject: [PATCH 040/157] add big sur-style icon --- program_info/genicons.sh | 23 +++++++++++++--- program_info/org.polymc.PolyMC.bigsur.svg | 32 ++++++++++++++++++++++ program_info/polymc.icns | Bin 272578 -> 261369 bytes 3 files changed, 51 insertions(+), 4 deletions(-) create mode 100644 program_info/org.polymc.PolyMC.bigsur.svg diff --git a/program_info/genicons.sh b/program_info/genicons.sh index b553edb56..313bdb539 100755 --- a/program_info/genicons.sh +++ b/program_info/genicons.sh @@ -1,5 +1,7 @@ #/bin/bash +# ICO + inkscape -w 16 -h 16 -o polymc_16.png org.polymc.PolyMC.svg inkscape -w 24 -h 24 -o polymc_24.png org.polymc.PolyMC.svg inkscape -w 32 -h 32 -o polymc_32.png org.polymc.PolyMC.svg @@ -9,11 +11,24 @@ inkscape -w 128 -h 128 -o polymc_128.png org.polymc.PolyMC.svg convert polymc_128.png polymc_64.png polymc_48.png polymc_32.png polymc_24.png polymc_16.png polymc.ico -inkscape -w 256 -h 256 -o polymc_256.png org.polymc.PolyMC.svg -inkscape -w 512 -h 512 -o polymc_512.png org.polymc.PolyMC.svg -inkscape -w 1024 -h 1024 -o polymc_1024.png org.polymc.PolyMC.svg +rm -f polymc_*.png + +inkscape -w 1024 -h 1024 -o polymc_1024.png org.polymc.PolyMC.bigsur.svg + +mkdir polymc.iconset + +sips -z 16 16 polymc_1024.png --out polymc.iconset/icon_16x16.png +sips -z 32 32 polymc_1024.png --out polymc.iconset/icon_16x16@2x.png +sips -z 32 32 polymc_1024.png --out polymc.iconset/icon_32x32.png +sips -z 64 64 polymc_1024.png --out polymc.iconset/icon_32x32@2x.png +sips -z 128 128 polymc_1024.png --out polymc.iconset/icon_128x128.png +sips -z 256 256 polymc_1024.png --out polymc.iconset/icon_128x128@2x.png +sips -z 256 256 polymc_1024.png --out polymc.iconset/icon_256x256.png +sips -z 512 512 polymc_1024.png --out polymc.iconset/icon_256x256@2x.png +sips -z 512 512 polymc_1024.png --out polymc.iconset/icon_512x512.png +cp polymc_1024.png polymc.iconset/icon_512x512@2x.png -png2icns polymc.icns polymc_1024.png polymc_512.png polymc_256.png polymc_128.png polymc_32.png polymc_16.png +iconutil -c icns polymc.iconset rm -f polymc_*.png rm -rf polymc.iconset diff --git a/program_info/org.polymc.PolyMC.bigsur.svg b/program_info/org.polymc.PolyMC.bigsur.svg new file mode 100644 index 000000000..1d6800323 --- /dev/null +++ b/program_info/org.polymc.PolyMC.bigsur.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/program_info/polymc.icns b/program_info/polymc.icns index 84148d1a1877c2dca145124c48a225821735fa8f..a090c1b0d4be086a066f82a1f21514ccb974be0f 100644 GIT binary patch literal 261369 zcmdS9Wl&vF(opJP&kYMuqlbYo*5XR;B7I4D%=XtRA{@^^(fMAE0QY zbV5{kQVWi=&C~A5t*7VQT>XS)?nVN3Q&^*H&H!KE4%1&vS8nZHn|oKc+RUi{bPV+IukD*o(mfO9+|zt}I!0`|a5GB%`;<4TBcYg;53U1|mu=DW68{xpc=4Amm zl4XM^M~)67Z4C!sxktMqPB99X5xBXzk=(OomZ#Si{%!RJL5V2e3{kxk6UrS}ke9!m zj#dL1XJ)O?N1Ux~Y@qW$T~t%hHWAQCAIM^%G;7X5Sg5tj*+RfDNz$^&h=~3qxqZzh zJGrt_0Qjmm@~Zccl3hBuh5;TPzJJd4Hbv%%4;H`)wLpA<`P`HAli;v@I${rCPEP56 zUYucpo{^QchK~x*4g`y`$K0b?{L86RV@~aHy+7ZykqLNRH8wTxxqtMxF_qb~3!M&O z0$jzfmOj}#2ZWz&#ZB8V9sBt78dV?e}?`6(HjzS+tj zip5wE4=7+b9CPeF)hleG8Il_Wo1p+c=cBrKX=MJ;Nv;vg_rIBJI}_|tRw!why|3+U zl#aOb7+b_hr^{xJ9$9Wrq&yZF{uyhN`vGu_+j6?_MKm3wL`r`c<8JJmJ5Gb62qQ6s zvFb$%leyv9TknYh#_gb+p+Te*851)%cc5H%|Aq6%Z_+4v8cElZ+!yl1f(VN#l>0Og5>! zxK3?50f;*2G4Uy@pt_a0{n0x?!w|?pCEFt`g;VuiSGTKb+(`JeQ=usD=S~}dYW67Y z#0!D0>lcIjk*3BZRZb3%(D!IV^TZr~uh!VVr+6Ps7jSa9yyC8f2&3PI zdE@vb04Ru3M2#OyG}4S2r00cqQ&)TG=pL~Sc)_ifw7a`U8PLXq*_Sxjo-y-B!9|!l3Lm%R55@$ktZpy)8@GMow~v=Gvemm ztx1$7UNP9d{u^ctwP_*DS$wKF-u<-DBdLZ5eUmh{L-n#B{ql|zHZAI50k;Ms8c4tT zl*H?b6iuLW#ZsnI+nBx6RlJxU7^n9ndS=P*vWykZb?hP|;vhB^hjizXtX;f#N^}5a zK5biM{Nt&yi!rW;gVeyBwl~Cxjw?7&g?`jJ$lIOzf~f^uH**k0y2Op zptpEhTECIbo!w^gRXUx}S&|3;-OEsjC?Y)^+S4*gHLA5R4x&N;+w>Jl;;D(g5mVBn zl>B_u#oC4H5}(kL5NH;$&!dKtp8)5&JDB|Bz=`c3xn0^NYMK4<2faZ%z&U%MNZ~oF zkDkWn?7L>5a~k*XI)jSO^n8lQ={?|4pFpYCkMf#NNjY(!55^hlkq4e zF_&*7@>Dx0Q4|u4n7~l3#Qo*C8X_ZvkV^hwwO!S44;*U9S0d9FJWP}y4V~2W+LHm< ztSzVzr^IP@R+EUnUri#))>gC|;jQs_CkY*vrqgtfW}m2OfZzVLSv4mD0p8KZKl&=a zYUUxqit&q>diL#Dz5I4ZsQf9Bs)vV~8diUy*XQQPy;bJg0&qFM_3c(LXTa5I$U+V! zTbI&c9+0d1;ip|}GelR|B)6N0fl^|kKc_>dIqn1~w5~&fO(n4vdFo`_iS<761BKRl z8-KV`Qs=>g#yzU~cn;m4-f^;()f<%8%KASf# zRilTFN){AS6y;@>3TOGCFQPXDgkNy~{x)rH z@g4NKOIul2@wxwNHz~7LjPCreL$g{-nG|+%I>4}ut0hdP1SS2^r>C1hq7#y2lRC*b z$pOg&kWH*WY%gLhd!M)OR=?QWRQQh|tj6Ftm46*Itq!l7GI7g!(lLj~|8gV=6SH)} zDHGAeW__`#uojHC)`S@cUPD84=`|R^)1xFcF~>qY5hUQb0pWL=etipCP$V&Xn9_!kvMHMk{i%VqcM2w1Ow@7j;Ar^b6wT45+YV z5c)2xMBE*I$Iq%CpRYVjtg+x3Fyg<-_!{6^JF~?R;D5f}aU~Ux)uII`V92X!uqo?o zEQaG8=;MXH`HYsa+V?n~tuCm^a_+BBy|uKgZ~@AxNO(=V5JT4$g5Y|#UAjoF8*-U@B=mC*?`DAh76`aD4PF8BII|Z zAb8(nzT}O_->%cT1rAVD%v7G*f2ez5Q5Mp_`I$K-9lB+l965n>Fr|a7>paV_?iaeE zx)T7i2*HxK=apEXoCcWE zxU?)N5^emwSC~CY!cAMRUlfU#ny?R&;VNA%pJKdg1PF=sCk@Z+cTaHzpB?@tslc(SsticYYj9dW_sJ zL074wS8K1Lt}~_?y_6WSE#Gc$8pL1XY0ulu zzKW7UJvD-BiE46uqcg&d=M$m`WKGi;~QdR*U+@4AmaCU7q81C=q!RMl+#Ke6SC zeRbs^ZASoTEC4uN4%fadmbA-{)Y{RSqFe<Xio4=|^I!H3ij)^jYi4x+d02rZDs4+C8eC%7_C?`Wr$=-m0 zZeas=p%iCPNFWkaKEha0NgN2Ek@wa^^&(s5kB+@*WwA!ZO8^76v$1x1aYrk@yo=`e zxF5G_-lWL(Sju|0Hg!@AEv7tWvIK3g94HBv8tEf;)cg@r}t z=lYEJq0bc`sgqc7k;htETD&W?=6!2^{`CI$)fIK~D|4!whil8kP$!42p?U)LO&bS! zLhWUso!wZBmmQm>&~cYXlQ~_c62AI9XiZ1L?>bY-`YKh|gelRpF_&RhM`wS3EJYe~ z*becpG2lp`pDOINT$;ciUkon4ZVQIcBPw}ZsMC+&$&M!;+3bd^nm$s5{BpDa{3LZ< zjz8kfoL2@&i;vd)c#HifY2Y8}OTfmqq&Jf55oLmVI-7Av#B-WZ?`)RA&x1Nj3PIxY))?aR0yyLq;0+g*osn_mk-o`Ng-ECBEW)=x@3zVE7Xuy$!t zv?92>|5#M>dU?1yGQTzr6BJf*r)Ynol^~KzgSni!|Mzc`go48PIB`}D4)9Em8G^Ts-8mfcyV{P!ehhWdHt_V%_*L{L!wzDII11@iiQe{bEm znyyyy3+v$@m7%@Ur~yZNPe)tZ!~Rq@zwDPO-8hw2r7F4w;&hcVD6%yZs8=VL7aUK9 zHt^S+wGrB3;`vn{kn!dv4GaV;cq|wuF$OUr?dW|z;jc064dl~uvaB2d4RV|ZA$5y` z#DCtNP9m(9f%{BDl#knuY~ct=N&OL+FUM?)%2aSqM^#o~Lbumiowxl48ssCUR<^d@ zgc-Y7G{AM(4oYHXiNl-^C_8s1IKCYtY{oo{Kf1%SWi$Q#T)(GzrbiX)3RzQV$Kdwfdp z(o--}Q@}cS(V?lRzh8i9={m(wvrdt>)d26Fa$51HW%KqR3(4dPwO8|BUnBJjr1NAx zwf)2>o-)U!jO&kgkiFE6mf>R%TVPM{cWW&c#;F|aeH@BBSgcJLKWM~3wiZ!ezLT54 z3(+gkB$w)I8o!Zb#*_iJoo^3FwWeU>qOc_YwM)5wJvR~bFHJD4kWvKe$Z_GMbLVf; zz(lScq}ImEIr-{iJFcrB!3QgP{lWu0zvVI(69gIxs9YnyZFzZ2?B%aAbo+5@)Io6k zJLVOTkkyl=6aFS{z1lsV#L#eEACi8jjBk8!oKq6xo((l#b;`kj6(T_tIo`c0M#pXW z;0e{?Xr}=u`1`l|Kd53%#BW+%Zpz8i6%ebbBP)J7Ay=A+eDm(jd0GJhME(E<{6#e4sWFmd9GHtOLlb>^JQnDXXFIz9F^8Rm2bj zFnxrR>(i=n!;PAb$?LdrCYkZ~!KHl4(*B>5AQr&zG;Y%e7qRhnyc*b8-DnoKhdC3U zvq!3vWBZ$7-?W*$bKmYI1S~Dko~t8|)XkaLDwXPAqU7aXU)A$2E*4HD^a@hL1v)#p zkfjeCi5ZU9-|}Z8F(g^^SVWay-3nLl<_=igpSrl?`K5FAyuMwkXT%uKa%&x)e0C4+TJzJI*5Y1dZx1R2z zKSGYsM|YA`m~f*t`Wt2ds`WeLFEn*mYn$?tHlNYD={~YnBbh1*KY7&NlFLolvu~x0 zkq}~+!x}0g69Rt+3tO9o!owBJ!%oX$@ejBr4->tNnFh&;-IeTJ&+ekJIJ{N%T`Vl z@_VqxLRNtsdYecp+Nc=~i|lHZF@s*J9L=S-vUH5!Hu(bO@O z!GTFZN17+RI|J2!DFQt6{cqXh$KoT~TG+9P56*pSyMNgtv<>?JNX(Rp8-FVx`Clp6 zQ>U`04m}-w(#sDn7I_K}6ue8yb8NIJkh%_lZ2^o{m6mR3`fp_USL|{4KBwdhGa{&5 z%zh(Z{_6oRx2*e@3^YuLC~sM#Oe)6PH_wcbelB3pBeo-ILRrYAK|CV1_CXlEY3wm5 zKe^p6yqb%)4vVCNLsalmpwavTA7#O6$(XorscGKpH4==NZ+G@F(F$Y`^o5dlhhzzM zJ=mV=XEE=ZS!UwaKKoH)>MaXQt>6V@qX4?hb=@QWCw=o`tn#yq_-ElZ4*Y_Wztoo% zo9+xcm4p&#YGR4<1r=;HKQT$>4K7oIxy)fx`;SsvKagF0Vw9|8 zVJj0~N3l;#X4%e6Js{0nRcz_%^u{U}TyCExz=k0`Sz zjw)E8?$bXrMP{%tSnzeSAm?!*u~;~{1Kq2kY==yVe3Vayv!0TrTh^X2^Bnk7*UF^o zcjkV!BJwlpo^)@!8j{+idJ_o|~N^3h6A9 z{Pkm*2Zh}DqVv?mZ=Q&-4|h3|jBo{JB!5SfB`5@%rSo212IgBCIxCk+%=DNi_zpA@&RTGQ`pri3RwH$b1n4W}?4DTRXu69&W zjmhe%?yR(~9QPfc!t_vrg3wx}UtA7dMe0J?;lfd?J#Ae#?zwKy4kqU>lo%+%c9J`} ze!3IG;n_8Sjuu&GD5bBcpLjL6rdGFk4mlUp97Lb#g-;OInu!>ZpT^I(2{Vz*wN>&I z{DFZcfNb^^Od=Lhq#HjmAqvy>s#;d!ZfaB$#KMkZac8Nn$#-P&3m4*8LGIUOn&-V+ z;Bi-?lj+ZZ4wpk{8}_&#rw)tLp#{xV2U=DZ!%+_F(_PIQi`-CF&y(n69U>T?I2c%w zmvD#My=JP|&-&3ak@ap7}l9RJw{g7oiY*dGOb3y&sP~60{nKqL8%dh#~Er=atZiX zPp0ttz=jiIdpUoRrGkIJ{ospnD(0nHxDG|H1DZ(nEaaZ}0|TmRz&!$EwoYdn3*dg} zV%sEfbpplyB3d1s7>2>;X9M?>NkX>d~Q5mHwiQS^8VXnmZZWlVLQHgxJTg=?E? zr9UpN?HgLVWLsYRF`0nRZ)IBWu3qN8YZkw&r-G3CgP2kNJj+g=Sv7#k3q?SBd7Y;P zxw^2$M+B3ntZ$}P1fBBw%5SjIqZ;U=SIg7Wr0qkRdrqj1K)FnjK$yO~FawAa%>;1n^Kgs+1593?Wj>8m z2sl(V#2SYuUj50Y2uon6E75hy&+G|rphOeNe=GIix`QXN*0b^|s^5uXdn-YC@98(P zLBv&%`6{|!Rb$g1ryE=V2EjIQc1lu3&e>o4bkWuih=k;}NayEFW5*^r6}7Z=BGD5X z72sdmA~&jbEJiQ4_X@NbP`55@d-&pNc-6j!9PjE_7CZlwK;k8Ed=A+$+X5^U!Poyx zgfS?13>|C4WhU0Y(+!BxG|l!~A_}=d-bph8(30wy7x^e0WTTM`(3|S5RtANv1u)Qy zrpORo(nlBXG0FcCu0ZzyDX1XBu#ZF5g>iI&$)&3aUm!GNu10V|sRJe2jL~k~sr9jr zkaONxqgoh$Jpn5S72wJN#}8_q6|}_l9VFmb%U_J$A5y6uC#Aq-Jo%~15daaEK&#~vAkUpR)s1pf zmoYRS!u5Z{*9-qH*gj>h=+BoRJE+6IK2t)sjHhicpJ_scJLn&%zct8_2(3yC{RddZ?rYIDckLgkA6%Sq=F!#7$_NhvsMdnTO1Kj!KQ{)KqHkH8$ied$rB&3NHSEjPXRB! zhmZ;ea+{D<;yp9@KJ#vHNAHPN8I&BQU~r*aT3*z;R_8 zCZh~!b`X1mAu_Yrv+2i4Wl$5P57JoTzb0}A&MhN7=%>=tv851l2tpEc?R2(}hLXRBMyOysfb!K;{<;Y9=9J-5S^2lAa=Xsyocp~@$@E%}& zV7I4-=u7}i4KhN{vd0g6%&6QV2<1Y=8?@p{PAJ zjAzd;rJ%TH)FOb23|bXjIA?bqEx}j9a`i06+ya|R!wqSEm*^;iIcyg>NU?cFj1;z1uB&ked?I(Pap|WGA{KD-Ye=!@_z8VG&)g!(b$R*n;A~iZwJ`{W9;s`>*FS+ z$pb4{KLcQSGp0cZI%}_xjYF>#+L7;Pw|@`Ov^b6pRYj_$*Y3vw+~lUbM*yGHPZGfn zq`_snal7iT`W}z!rj2&|Fl#DHtO?(?dG178q90{bL-+;|5&DZXaaPL{L0LJ5-&?ox z;BE2cmg;0==?Zw&5|9iSTFmb^`gPab9RSJQt4==U?hHD_NZhL)|74k&7H&|A)6>x& zSP5P}*Xm!cYUf}eis@&X*HFehJP9!y7wsP&4Y-ALU@EKY!+j{D_w^iIc8awx`^hvQ z^id0z4bN+f7eN76%a57GuhUE3ro;F{aLBaOuI}s}3Lqax>?RZin2nVtRlfHcQh>$n2WIlw$#g2A^LrtGLG8}f; z1*(x$1uOEIG9Xvjvd#vO2C&Yr{Udava@juk%n2E}ZlFSjXrn_+2eIpbPg<*!9p@yL z8-FSXDksmvYApx_+%1cvb(lo6~#i!;{Q$w{~P-3gM$j&7V)pfNLb|PUIh{iggW8N z7B^&ZGH%}9umR$;h0O0A;$QFb*^i~`NPlne6gdr}eV(Nhbyjm^C2{I^A+-IN;A($< zN|SQEmWq`*!JZkZ8pWY3B3gr_M503f+{rbcJEU3R5~n80d>fDft%mR7!=wm~1Xw$~ zabPY-ezbN`$WXpY?>71x8ty-CYhF+3yxGHUp?M<&W9s~MH~Pd*Rh~IBu>!8EHQ@q| zKFXYI3W}Q+bec+%=0$T2*8*5!bangk@lo67Qo`1Oo?TbgW=WK~CW|NuHdf?ZWqNFe z4Q)acJo9YeTHLRG1>e!IA}kl7BM~yaDvkmEqyD3LO&?3u2xNhy2b_Iw*Tilsh#q(H z?u1l%BR=^<*7-vom0pVx?jL>p=c?&H(JE~jE!Qd%~gZmHuZ`^P&{?)I6e~CTK zd6N|~sDkOcm$oL$av2t0YNtAbnN}u$dUtieAHF9#!f}IpH`%KT!xCxF#)>pi+|Xu_ za@Mx*Aw_$EEwEwmZ+9!PMnuCGPpo^gqy?yPZf6j?mxvtzebldv^vMs`j(PvcPQ{zm zAf$s`_XwHQ`Gs!_cpl>r42K9J(6z0S;xy0W0&70*4~}Iw(sh*M)ZR8%UAB#mNO=cA`r9R?}~W=5PA4o(i{9khb?6M+WS*M-Iv{8~%pU2!c$I zs~@Om}ri@N6c()K&3CA_WY(_d;tBi(c1Q;M*J=PKO9Z!f^KB3mI0~5B$L;NDI-6-m z343{HMU%4jlK|ehhW)Zu{zXJtFU}LuoDO&xwAW1Wn_Z;hW1_EkpbLL+S{4J#sq|M90E~CD^&rCVe z#uT{=?Gh}*&spnmbY=mbpJ&X>v9@0X#S(lJ6{p1Ub{rR4>F~|4ET$H-!ZjHRPd-rx zK{xq7^?un#hx^*#XyA!1YeYZK)znaV{C#f-`@{G#HQYorFU$@;iheskhe24%eFOYgSs!d$>c)B_xjwb;_1 z1L;#9VR<-MF|NNEs5CUo21sgm8860t zi7yu4NxhLq*CMn5DgbARn>Q3>!j-eSozX>h;AF4D&CSg=d>PASFa!9fq#m5vmaFZS z;$^Lj4*0iib2YP8=C@oD2BtEzJ(YT!BYK^1+Jcg8QUdepf+i(l}rsDHs!;{ zeSTKfldRqjR5DWwtX4w~XBI3%d^oJ8E0il19=&LxF2%c}%ZZ~5-PqVzHUpY`e3t$I z&Dr>)4KN7yA;&6G5!S3&Fdq>>&ZYt01;#$x6at{J*nuF^0l|@bu}~{8 zl)deZ{6+bpgJR15JIDoI;gX>omJs{1T1a;W;qOi%YTX1_M6p9X6u_IG8n_lr(0A0Q zy5qS0<)7_$P$GXSz;uGrqWltw@V*&ba&m24+(o{NASb>Y* z`-A@`4*K@MilgjA&YiMTi6k`T5A*_OuEZ_}$VVFs?Mp#*F4q@f+Xz9ZZyYZ`#sM+L ze#-vb%S}uf{HH{z(NH>c#mTZB4YjCk&v;SdRL{H%ZCv0Tc0Xx^Vod=(hchwVLqR7} z5ZoKr%Lni2>OXmB!Lr)2RIUB>r2gHFw|yA>7ZFFbG8bVN!53j5OCTRZoEEH@trX4f zpChS{&MajXWPbc#;*NCx1t5cZqXhhE!pV0o5iB1>DAGN6u5LwEEqs505uCqfQ|V>B z;fmmeEEU4!nO;x$X#>KVV1H&KWA1u-sZ8f|xo(Fc@WW>0ZGY-d5y|o%=J$w2=>3{o zwbJqNgPf&kIpI5%i4yJTgY^YTjak}j^IG8}oXr0RRr){3)XOURU?{=>sa@7k>b=%~ zh~NLBpgFk#0EMdmHwFElx>q3Jzf;ggZspehnf+fVmIVU-i-P{o2>^i2A7c6cRObFq z3OYc)&T35CxW|Y5%Xwu@VO3S<;%2geMJutq9Efww7K8)~qKc%dBkB(htrL$Ge>u90 zgn_Qa*U_9I27!~%VEbjvXgQ=PF|ju*^D4_K|72BlY>6ZsG;_HhjJa=N^Z{NYZj&Hn4x=Ax345;6g@wo&N_3j`NML()Qy<9!#`ZR8D|zV7Ye2Z8A$ zf*;H!Ec582euNT&=fN8aO4yGz)Pc>It)$z2-Z?dJCpyt5E~=!5Si&(eQJ=~AM2}hb z+F1uoLDQfYpV`a-_pE_eT(iBld;=q+oc>pKL`MgIkq0myxi3sW2&9W5VblF97+d++0@ z)i;C?Y_Vnw$pZ0~Yb_RcUY0bM$s2!Lr>+rHxgn7UKI|q8>_f>I!59pEwUb4-h9u|( z(!QAEJMNJf6y&{r_vc<;G^76>b(pe(`6e%+@UT#RwNEvt&b_5fpMFcE3S#QRKz)3C z9`k$e*6dTXr`?wKPfdU>KQIjWp)g@ko0qk-8kSr{T6C=x7%kZ2;x|&m1nBO24MeRL zckSkU%`z2OTghfRM=<{N5em}}t)PAB3|L?a$Rvd%?y%NS5EBzWJ*}*)8KMhcqFq?Zai&reTJJzSieoI0usl=ss&*dLICv64;C6sE=V)`4}n>XDfm}iw(y19=W`{TzUT`@T(7G%^)lo zto*#Rn7<%Rdk)ODC>i!UtC0By6ySw3dh0=xfmS)v0VNf4z8KjxQmFLUZS7d2YUWZROrX>yWkfF&G_<3B#hWx_VEIM)b#Ej=9C;TyMEk<) z5YBX( zyh;!kfUYbM87ew|+Rf|a&-A?J4%PvDha}_)xc(M30e-K99e+nFrYQ7p-X8Y>%{D>I zlD{SX3IY_{5Og2}YS=2{>C7o5*2Ckw;Zf3hch5##ulC!Q8c2UaVVXc@SV16XR5392 z9EfoWc{HHBVf?HvwI7IdF@Qa~J|75Wb{9_C)+GfitX6QkOatHsT)>T8_#@O)>$f+rXmG`Jw^UWvapCrg!?-cN?AqG!9Sl5te2fnpVF?nogV1r!a}hzc6faB9P10A`|i zD22>{P%^08k;Wond-2zot;cVBhxG|N9~{b$Xl(o@xr4mdStn~UTJ999MH;4)!ZBHG_xaLLW8T*-b3WtJR^?g515 zM#nMlU`W9rvm+{10zY02TjVy?N?3$ zkuQSxHa&k_?5}f_nwor_@OSMoNwX+S=*!`NL^}M$_W8XhJGG0zN&7Ib%pGXa*W{(s zEIp1_^v!vV({$L`QY(O|ZjCsix&#f`3}8^*_>IDLvFd#3PWP_>)B<{DF1>s7vp3fZ zsa)~_I-&n!tTeUTAOmF>efMqxf@QjBkUpsY9gB4)g z2-KqeN~ZmV`|P08cYVrF?CXZeQz4CKmrCE%44F?~%lro{o8aqy3IcV#9K~aD2mT8v zH9ev^S`PF&x54h=Yb6%QCE6^(2TP`OpV zv*=sW%1^gN@|>H|u)+_Cv5<)hMGo4hL7430-aHgG?XWuU5sT3Vd8o-ZrzrdVp=3Yf zsj2}~elaNkIS{K^R~x}AIbjGwzV&ENuJryblARg}{A=!T8GOkuaZOc~3Tvou121L~ zHfDx8_S?Ol6kDQC4e0-%tC}e*=9@THmq0n7D>k+*h_Isn`^Ti3SXHHoVm(0ZTV8G> zO$nUsf=cr7@#{L-= zqW#qBx@sC`6KelPiuP%5`V})tmqK6!+ZZb9{?r|6eQcTTa~HJ5$)BI!mj}EvQOJKW zb^#-U$vm{%_5V_VtHr~_s{v;}=s1{`cMY$aww_O(r!Zc}rRUZHSsU1X5}rP18yWp% z{;?&_{#$N0aMZX$KN%5ajuab}^n~XEI`llXF6JZfmxwaP^W?=yp9#5OO&ZYf0}t(!ZSN&7>_sMBUHo$!`jSe0v3<&t6lh&dO^9(QPc8?SEatT%owyyiolgUswP- z;QoA8fSVslyv1uxkLS&P**Skg(0;ii^4&jURq5{9uCcY#{ZnG0;RJHlWU?wkA;R z!Kc~RQRm+{&qw$~+N_8B+*5vvhZmyGVQ2n-5aPtd)kLR|71`!{juMyFxggq?Ohj(n z-Ti*7w6dv<&kpIrj2y{99|T;F2c!=HX{h3xJTb#Zq@oG|0V$88GE#v;2J@X><|x$sdXOYpllWzml)smJ0z z#h2+Gy?V=Jh?qj)m=O2xLg;=|pjE|QM)Un@X2_aQi95YarY8}MniD0B*h!HDl3^Q> z3r3L+j3qdtl-j5tEgCrw^6Dk(==~*f??-dEbkpc*Qjim89QeJ& z_HGg9Fur4{1^;FtA(uz|g#{zK&_bk%hW!Pbi$(8^KE}{+vkM@k^lXax78@Z|jJF#p zOn|9FK2S2|5n3lPCJRqd&O$hSOMcFw`c_o*?!YPJ{NP7>h&wf4Q>4Mf%Z=v^t$u$l zE;uP(PPp~fN2)q8(R&sNI+b!TPo0^ODRF?9D82po;wfs1TPY3ZpBPmFM`x3#KjU3| zPE>@|;CN5@e3Lc$M7DY=emx-Y^%#hX6zDamv;~>7ILQfyFvC-Fw?BW`{7#)Q1{x*L z4{)Xsh0{O*XqfMi(f~9-@c7gn@ zKK|0v`2*-D7G@bzk57`Z9@06X)SL_l3L1MCyY_Anrh*%D4-dqNNAiLf zn`|KNZK$=laNy|g4Hbo`5r1hhTD*{<0T^f*7ei}r^$5>X*faaGT`A#|Zj|}oQsVwm znvb@A3oviMjddZs+eXQfo~Q%GVX9S5wpzv5;q?=p#LB532VD8zp|kIJ7X(xwHqH1K zY@>c2yByl4;4UJlkINv59Ip>?A985Pe!r4304E|fjNZ(Q$>sp`qr8W{=Sj_cF+0`s zIZqRI>@+fLnn$(DJ`tQ|Ksulje0 zv*v@i9p;5Q$<^#D*Bp?Eceaam!xUw6< zH8}O>(v4lVo8WvA3v%B-iH7TubeK8mt$Js$DF7k8x9u&fZw|@*ANI9y5vL2HzjEq} zxOy-I$7v#`CB#@DnYXa9J+Q64*1|M00Sw=LM_}Ym{d;`6iy$-=&aPT43d0Gl#AN#f zEZ9)CnykAKD5Cfw&&2$iHf&K_qO ziI`Cu$ktn?GQ?>8;@E*#HSoI0=LmzMRWxgJ+8!%sR_-B{9)BLuvD!rg5b%R_5K(m* zK71GR#tU70g@_d$>B~2L>gch3o{0bk(QX>%%Dd@B0MUxx1B>GDEx7~t#Z$ke`{U`a z!+*8Ph9%o?N)cY6>I7ApKXTLorgbpgqgG@VE`sumRMQQI(r0Es4=`#`ntQgxQ z;4bk8btTNaa}I(xvw5WwQJC|qv$-G*fFDHec}onxmEskGb@P@l;P@0FT+M}fBInt4 zyZ?!MAcoUe54v=3c7gY40lCmjj7CvtTKTF!<>H%-g9>ZY{7i*O=NrJ>}_{VupVa+GEHX~9%D|86WEzlkc zamTGM%XgcnT|N!in8U-=-@_e83qSRzm1!s`8?d26G3oPj_^C*LW2)%|M%8l=kU$N8 z7vo`vNzl(?2S%Qycr(Lg#NpL{H4`LNO>w<9Q~r9}oHZgYY#A}_k+BTDXwuJ7@q_=| zazUGe3sGqs=CMuP7<7Nx1>O|q`#?xV*PSmyzx#Axe_Gyhyg?T{Jb#J1`5aXSW}I_d ztsTPM6=?oYu1r!k9%<2mPhqB>-qnHog7A~FORRx$9h7Bhw#P~3pk%l`=oL?aYN>}5lu609Z*#A9$;36{VtKN}8&VXn*=S%TKXu>JB z7~dXD#4R3OH1Zy|wpY7TGJ!u_NB>RRdu_b%uzw!7zv_Oq)tn&~s%h`It0`41R?2bs ziGU;>NX{N()m0>=@#wud#!!i3F4?1J9j?JtXVUk-ghlv zbsyZj+%l-A4>df?bWFOs+eRf_i5M z9Ychy_FHT1&8l3=bJ(o^3>#|#MoUx>cuUEUc=z`^I#Cy@>1Q2$?w+cT?-RT6D0;!m z3L_-FTGECD(uh`<+aFR4L{-2?Yme77S z(MW{>+hxQlc6c88pC)9fJ5jQ%h*+>&^FZIz2{F9h#dM-9#Nb-t3Sq#3ni(RAJayP`yv_lWjB#`9z%ivAfoS zb6f+)O)X2oAI%2th}#zPfJM|Y;1m12tfY}p?`G5H+=-hO<6mZ1)nzf0{Y<11%x(@L zALylt=(L+1nuUv)^WJTjHr@?_ZPBaVinJurPE#R`T1?||r_faZn%LlYo~Sb|0)Jsv zai>PnQ?Kpqr{BgV0eGsU>|wr6cpj$M104o24#*ymiXOiD)F8?f2xj+#s}_91=b&( zzxelBFT67EiJUjiq%t29r3)}W-^BJ+6XoLoMsVxv7A+Ufs)#l?O80zT>zU4uxQ{G; zwfJK+^`6PEKfkI?36(}$b_Iho(wspZQ!DKq9tXtrq!^3}iaa6bRYlYK`MxfM@Y|9d zy^=9)Z>LjfG$b3$9bJR#r0T$!85ZfGz^^`Rh!WI`bdRqjJq^#@se>-_jGrkh?_{G%Z z?^Eh$`@@SOd{JNBHzS2c-wK`}?XB~E4E{VeD-`zPd6H>WN1U`Hs-lRnQ6jrxn1Pvm z1W`JXyJ)tz`Nz$6Zm;KT8G#+8$%$0$vR91c?tTM_Db)U@^QSaB-<8bQItX9m315h! zsFnb`0XHUP8$nX9dzw$8h0SK`amo*io{=eXsf9h$w@)D*v)Ed^Ms{v3!uRpYrj~08 zxYjxwkbX)aF4vocuv}kKqH!G20EYhb#Z5L4k($cOb&_GRN^T#9OHFo(k0=TDIqTNqT$t%V(;I02A|SBzJxg?b11OCe-6n5rE-4UT88-&b}+DDsvJuA z#ZeM={SGYFR$C_i#nCeIOU^l}TwG~{T}GhB8i~329T;KHBx;-3uDAQ_ou9sn{#o?rJ3iPgOHXv#JR^e`^@bn&}>6 zjU2Ddvgp-kAO(>PA8W^dBIZBw`oIa3HGg}U{0yI;FP@MyODT*3^PB3*xK5V=I;j%D zSpS!Lg(H21Y>dL=NDo1{67FZ+Dr1pT=`6olvg^V1ilMBhBW>a^OM7GiRs94sBrHDN znmUJ1mF;=n!im`2J%BEwxu-r!3g_Bj^(*tV34RR52aSWUkHNz_; z@9D<1jN=cEf7)ZP8e@^&8Pf!Pz_)XcT*c(%-Q*Iyekr!jV%Cf;B7fna(+VSM7w`YE zz$s<=XWWwZT0U`JP+Fn#xBMn{%17W5=2dpX{0+l=fsK}epYDDJedwKFkdf(Zg*NkJ zF|3m(2FBDL%Ss60;ZOd2LK{g%5e&l^7O|IxKR!rzbyf-ErVD?~VKeZk`S%-mWUX=DAHcDF4tXoc8#Sicrw=vN5UBxia|6 zSL$Pyf82gAO^veX%5!5~<2QS66A}_)U|mW+x@;XRm*$lru(eW9*$xtG_XrNPC9pk+ zB^uK*>364|^3_p{ z?`brWEV1#Qh=_=~h|cLZyDPb~BQu_5VE1@Ij=UtkRSFKq8G&~*^x^{Rxy{r~y?dd6s znsHoT$vtTy@T)uWcqbR*V~NNWt*sWRJG7EoEwoUH=oZWqH!Rg)d1YLBL&lvj`Y|ciPuF<)6;_WQRm+8On&du*!dtY{d>Bkqn zoMZ*}i|`t!;7KS2yI3B29uuKbU8OOTQ|R zljucx=vCnBH4BJ21s~4aj4rF=r=LR#TQnw-i>7ya}CMHZU?(xvpAJE}8QK*4~6v;FUj zEuFL_#m9Ok>m@D$E918z-ZMc-*6J9OQtt|10?yQE*>q2oSM@LFEz7tt->fjJ1a`fO z$4x`j`^@Ay6W}tvqW&xTf{W0D3az?x1c6S;K;In0TkPknqW7*~j`qdpQaZvP(~R8F zt2Nrj!$^NDol(z>i2yRn*zS}EgL>zgi|2^=7 zC?3B@z$ACD;~y_4i;{Fl-TOsJ;GHz5_nRwJc-k$w1TEadkRO0{Ysv4I3JQ(kQ< zJZ=D?9Oda1^f`ia)KXZ9o;vS)w@mhJseG;n>cH5EP^udLm3#0Ttl$Ffw3Pl&$Czxq zqiP-eP@{K8zn`QXJzm)mK=ge>%3iulNI95#kdU?d#BlI$;bR}wvF`$pl{Me}EItw( zj%JcdOjoBn^u%e9o?^Pv6j`=1=B>pwS&^rQu$-pF-v?5lVzmx^qd)E7Y2&_Y;KktL z-MsM<-eJULO!ZOd&Tmdl3{-YX53$GM8Cevx@_kx4BWVpUCf|Mk-8;1zXBDwok(hO_ z%(c&kQ&8I?c{E9>25d2&Dp9b41iaB>Q{<7P5U#uis2zuV zR`+>ITyT^uvkx1x{jQ2h&QtEl!r0)jW+yQR^==aVN={KPK8|{%P#J%TE0=NU*yumP zooDuVHI7TwC(~EU38Fi`bh|hdYjDz&<8Y#JQ2F7&RQN=~9(d&-8!=KW+sxsjJBBUj z_CFT%IEOZT8GXmpRnPuVFtq~QLk3Vgy^ z)drNrp*&6Ob^kYGyC=@9h_^m-Qgs3SRd*nW1e9fF55wN>@f7pfaZTx%e};G&ZYGz0 z%4ms$By+=xK=du$bp3H zR-|zYs~)KFimbuqUfWwP7=Er7QH%{jcK|2!Yy4EV_E|sqAQhMGvcm4p9$sbObYJx* zSWo-HWJNXezO`he5aJRIZV(W5EMLg!;ovXZ_*q8kc{pr)0?Oub7TCMI{!AI``1#eh z!O15sRH@xu%!?L3ENxeov{3q%*Q+{BFfjN|Bp}Kng*zVLT6z^mn;=Ym7V0i!=TD}4 zJ<#Tv)JF$*^7Um-<+fq~OGFZu-Ji!O}d9=+||l`0Ek^;9ha zE(6KG%{aaQj+7gQ`In1GYQTCGAt<*8`^yuP6vXlFbK3IM7m+w`Fag zVjp64_*h(r7+Jwd1;v_vrEExvW_(|xIrfxdb(AYB1~$_9x5M42Hc@!%GGAX_)Qw`* z%78Qo8j58-q*Xr@aF_a0F|}7=BV%;J|2oFP`2*B7s1!Ju2}2_Y4z<>gR%TtC%k;yx zEZN9lz{eGlcy?sFC%4R$8TNwV=f%w4oR#C|RS5OP2vfXWOITp<20z7U#D)~bObRrg z#sqOTX5i^;-BZgjmv=(}d3(VI(k3@=%QEsKE_)F}Ve72zHuvf>eQx8G z5913j2)@1)G_+`^=CqvW>u{R}NwM8Bq+-bzAA3pUs&iQos$!AMd%JP-BC{!~UodQa zi?9mJy>xjCPS~2Urd1Xpz-}L+p~EN@f&Tc=Nge{d7qb5q9rmOKW26JGj5YuvaHYrv zd(%u|Vy;l5lS`S`2}^9*(dNIO9`ND*QQ|7*h#zm(uDe3wwzJU}V7rDv86TBoP92TP zQ;GH!RoAOEg`Cr(@)F1);h^>yS}x3*X0M zE9x!-=&{70|C{8*pGi+vJVXdo)^%oLio8I!M6SoZX}4k#_f5Y1zEQd*o(6ecm+Y%p z^e}A*z_!~Cy3z<*Jwf9kez*(K_wmuRqWjB<$5-?TKvw4>fF$0Y76~AEA zRN>2BH8M-)m?y|aFof#*!y-NfezxZ3$kXP(B2dVWlcjD_BTpT;jVG;BI_Qpo8O_;uF5Qinq!mp$PA$wId9lU}=_zVINUeH{CH&kncp#f;hB zlM_&X0p!n@qOx)B6(jBD>CY$N9m>YcAAyWezC@Y+wE_=}&^EoJC*l0DMU0DV^;Lna z&17TbY+*q2capU;-V3oeZv_<0_~(rlvDEQd@oKkO^gCiLFt8L`(T$KHQK{%k zT?T#Q75OVp-b>CnH_Obd$eC9H*fmuCulZwLhY%FyWG;uJ!d!CUZCS{m zQVV1uOxOGOH=h>ZrN-JTxhuR(4No!P9~r((WNIEFRgrVT&5Ov&bzK;e60oZ8YZY;@ zikP7nAq^xzk96{WOkXa#i4xUoI2bhbgci!KN*0BBw0siLw75%x#Ru1 z{^RH7Yev_yr>*9qGQ3M)jgg`D`uxLpywRQPp-h=bcM%n9OA^1Ijhew}EqsxOPa4Ho z2KQ-3`}-?+%3#RAdNncZPv^75I$M6u(1TWiAFBcksciz(g2M4yQ4LydpOyiN_P25< zdo9yXU_4Q7guww?`1dOPk3y4|L;~~rvEG@BMk%*NtP&Ygcahvl*Mqr7-`a2dVyf!L zzc^K5%eW<9LPzydEM-HC^}Fl8Uf z5Cuf;g!)kjY;JG6x79m%nY9scE2fJeeY7?7P|^eiAB|Kkk!HN@=jpP;qqe|tHehFBCA6HS0QBiuHhvC@)UqgmslHen`U`Jx3z zVnJF&AE?BpKW3)o{u#M~>?wmnEz-NgARP^Yt!o=VYPeUr7M>g_66Hlq4~)O0>q&n* zH$&$r_(+Z?Ia{Ps&g9#g(fNdMU3R{cyc*CN9MC_Pleu_470#KO+7AnW*y9{pX1Hgw zH5?eOV|JQiV@8Qxo8Q$H6pN+Bx{DSZfeRa!YUm-$TpipkNY9Z^f0(RN`f18QXR7*7 zx+4%9r)yi>cGwf15xEsUCP*aB#cPaw3*dkPj-|-uN?U^vLUiWFn~XZ9e71J>SOQc# z`Wh8HmwV***YjI!6LR>QXDo5!OLf4+*TZSJf^%c8KdP+RS@6?kz{I!WqYES!g!Xt| zx$12C!x#L^WESVN((5(vMv0y^Pv~SU@e$xqOa(#$+Cr4?Ab*{b@MrbHBVBH^M&zaS@FV2AfOf>|Pxk6#VB_|) zqtI6HZg6FqE8BU73<*o}nquWuv<;^BOVPihX6beT#*@|Du)8+TCcLEyBO+VtR~G6~ zpdAS=Rr1^pqsZ6a^JP2PIz^a8Mnz%uShFW~lYSsACIZhgI!w9~z77wONH8?*#EKd- z@is=1B-FdTchs)MG%u!9FltpSEud(?UwB0I-v0iC`X?s!UFsl0yapLTzihW?n;!h= zw{Ghl4H|qe7YZ8mJ<&g&fI?v+yCwDz4b0PeD~?ci)3)XYQ4pT&rez#|03Wq-Hy2tL zX4;8F!r2=U?(n6L9MpJL6neUPkHnXICI7l!99c;rUN5Y~y>NQGJBYAh2kGk>ss3j6 zwc&sTb*j~E+X-N6%$D1>7o#T3xd=o*5lPGL=s^>Y@e8$J2h)mMroy0GL6~382-u`} z;VYciqFvlA-4K;v^Z4T!b8fPLAX6%_@{<#sR;-48@(9MJcMkh^c;BZheTPuFkz=_Q zyHvyVt6O}AfYmrT+URTU_9K2xT4lA!%G^)Sk4?fl=Q|6r4u}p-oxTsIO23&{;Op>$ zv*xk4(5BrEMda4*TR86ZL;Zu+;jAchGRt%_>VrO)%I17E`Bzf8gNQfk*5Ryr1n^}h zj?+*0_j>$f(l#2-y;m^d_((tQEB_+wVr*jneF~A^64q!>-N`}JtCk>dcT)M7$UdK{ zREv(PR&_gpFXV1a`uHAvySqmY$F6^Ws^u=6Ji!mX{K5{1t0&wgUzuHPrywoFYtHnK z!>JoLo9Np#A7blkc4wu z{#7h3k-7JqCUSq8M~y(w;?I~uS>L|l?dbc$QeE`IjDL1V?$R?MY&b4q96-Hc`muL& zQ=fEASpMFS#v5&U$*!=?_1xexAJr2WPLC;9)+3@8g7RYeFEp1)VJ--_eGwK(kzFEI7 zmmJ58Gq-k`8|2ek$tHAjV=Q~TgE`QM6R}W;-n=4D7fBiIQ5QIrxc4GOA)(@{DQfz9 z!$scXO(i7{M^TM_t5DX${YAZmMQ8)89))c*{P$qsXYAFFnw4azLu%xfC@u^0w50WT$`<~R+*OZ@EG0O%6V%npk|#)eUa*CD=prZ&vtz5Q0$i9)gg^O~DhaBmD%fvT zI!$x4{CHQsF)iDTv7gS}*0{{XrWj|`lnHa4f4KMFT?#GWGo`ADj{N9`{<^+LM|uJ_ELD6iKC8qlq~8^2$o?>>V`sks^g67f&))4&Usa zrE{vDcH87Tzg2^;8(oHzYRw$|@}2=-Dv%GD7*XSlzYHq!Jo|N3lhY>SH(<&SpA)3sVyUo)F|9hddtma#2X zGG{@nsN-NTWbmHt#7x~;4xU1alkBv}33#}=`*mmcHi4Xs&;I^g)=ED*W1yDr*NQI2 z@O977*u`+b@v8U948Q9vtf?6{1UnWwWF(Y@hpDx1ez+BDL7xnH$7=bv!@bXwH~^7# zSF>moL~g^guKj?nKkh5oWj|)2R%neV z8g%ZSlN$x|)V=Y0TioqX1FIm#!^JgZ;TL#|v~HK*jqz9NOh&6+cZWV!`|32>P~LlY zxd-r($mNIyA9@luA^QDOo7841*DaB*Tb*&fzx*2iQqNXtqlZtAcwpd9H_J-eoE+i^ z$f)4J{_NR(3HwtscEg?kROJR2?vc!$UbkWMyq&JH$D>nLVrjF3Yl~J?M<#(;ux*aZ zxtU(AS_2bK31~14mK0AdrqlfggS7!Ec1Mpak9=ZCTMjHM?ej1J=`O!{m$*L|nx8PB z#9}@aZ1xNn=SP~MN^i%v#vsqZxz@)nhEeXM%i|Io3mkvW+@?rXHx~!9I+8fKFZ{gIQ?F-`ZJ{YlPIRuQ z`7iu;k_ko!al+G(tG_0KVP!+kz7vf$mJ_u>-aVzQD4W=_YKD2;hKxhOTjA})xZ@M^ zQ3j_FZp{4(rrMksMX=Fi0=J}3-h z{f1iwUG*JoL-ZE>vx{bM$p+r1iqVcaN1IIae!(B)0PA94t%5pY*Qj!v`ttWqRJkN$+Jf(=TJoxFpQdm(kPaXAGx+Fh073jSa zzm7Wr`kZr+4GK{ACFtN!CF;*q*v=CU_J@C8EPhmXiP4n z%~sr>Yq^oPed{IN?CF(f8Ci`_KC%dWOdZ);RD(klAu(A&#*#FM z$dTI9BnIB-y%DYP6n2te`LBpZPvvJ_xq##rTC)wmv+EeqkfXE@(KedT{E)5#@7pwh z2h5peC~0iSS~GpwITAc|=j4mJSj_5+x?kuHMPiTmZ~^7R-npvN9*~@KCDAL&8)^zGehFYxnzu!=VpQ7jyL>%J8c_Z z2=g_o1JL}NoHE>PM4%F8H#xN%Ayy8sXEgw&5~y-nb%bv@KglBsr2S}judyy_= zuVVOzcz>XoPm|~){e@f*XRg5Dz2|_(CsE`)WBs8oP7qt0hVSq6m_&2QZ^!Bv&AxNK z{MlDC;2XNRGW60(s)zPoZ0l^QgrAOY6GeoNKk|S>MdrnEtX_$0;Z=xF<^hitV|Du< z(!W}7!0e6~8cH@!%Iw>Tg3A@F$RM?wz5Qbq_g~rdjyLs#T*zt6IIo6w-hZoKbz+a6 zdoQHDE`cHh+pxc%9SJ^YXh1$tB_&;LKm*XQy~xfiw|nhcF~On9t5`A00%oM4*|wCAYLyEW%IQE<_4CgO7CAmTB?N5;Mg!p>k~kYoFp2dIuU zS>+tg+I-=Gv&o)Mq#qu<-P0h!<;F&0ZN-|i<5RTD9a1)R?xPu9_PM}iEf=JQe~i;< zvZeHttiuXUb~STQX5EVCUWo_GDLl2Cc%|VCX$z|%6cn1F9IMhDG5yi>X(4YzBE8bI zs;Ld*{)r3NX_N|EB-n5=|SKKusxU4;WJL9bKU;@C)QI{VLp?rDj&teS9l<& zg2Q%9hnlDWYihJ+56_Nxm7y#Y5oxQEPM~aWd@V0x+iYYY;OQ#^DrU>LLXy!~7CBtZ z-7Bb(6NNleu&UH75~+L+Ma2T_1fo~%Q=2w9{UE$!SO+c0V_+uPPi-mGJbmXq&v08=7$LBAD=ELX=kCP=@c( z8wn{M7~19{cr5}7OWx8U$w1EAW`~Jp2qhaiVBZi#w_LJs0H#rdOI5Q~Oz{#D0fFbY zwE$)FQP0GpM}|^9O|w`;U;EiGh1&)~(_;<>%x-~ z@^Cj}+l>iTh=Kx;>+~dDz$YP~92Opc1J+_^EnkI=1+&Uf$au`u;Ns=wMQ#5(R{D5i zZ_9Cxmh;t9j)q?9Ln4?D87b+s!LPaEYi9HBL+*bZOHsK5Aw%bsw`+vxgy@sYfLPxE zaP-xV1Ln3)@o3s6b{ zppThg_Z8rEmjXJF3uT#ciA8{!jPL-+m@)V>L}P+Q+SV)zY@Rqm7Ha#leU==pxuZ(W zBNinVg#x*1GlLx-?*lY|r6FxS}1V8c6)p4GtLg^o<;Ap`D#w9=hNqp@suA zpBoFN1hdL~fsNu~$-86_k*rMpocfR&c7-5JX(O3CX36(6)}ccP?jXa$tl;AL9y?px z)*(zhQu@ZOP}@zw+5+RTsg&We=EP|cyAc&^wgQ?qgB}DPOj(KX3 z`fvl`7<`qu8bS-v?Z3!L*}f0Iz0Zj4ELyDqU(FpUHCo>9S+ zINU!#VI;qhcMc+2&4qVN_7;CeRq~tUNETZK{1M%KeSL8GDrC=@6c|GM@()?F`E#jP z{fR}odpwdsnk@oeQBtyF+-LKAh6VR`3d-(jip@UTTAu)t_^Twqyi)W z325L~?2C2|Y=~O@_wQ#b|6Ca~0)k4VHQCYMnzaW`L9}C_Fj64BQ#tQaF1LNc5L8L8 z3D!p48%=N?O-ZgqRg=LK+813}CipDPZI<|1(a`bJkqe#Y|4H94*1qGR=mtDK?w+dQ@{zG>@7+(S z27fJp8mu9xi8<=poh^-((w3z!yZwbx+89}%y$6sP@@GdU<1QpBRx06wZ`pE0eRBrPr2k}(Su2fM8&=FjO=AAsjkrHQ{?lNRH}W1vs{n*stPu*fB*iyxXfN*CorbyX~rrQi6G>q*>4{%DX03VW5d1{>?SV%nMhBHw;V0I z&6{%S?mifRF0q?(1&_M2&@Q1UzX9f6xb#w+Q+*7dI~jjHC2-q}0cl&vZF$OeK0R*M zt-#Xeb4@YgSMnosSI&>~$K7W@ANd>wE?|yvkSyvm##iiwRk4^7y3SAr_R!S^HH(fz zjQxN!`ea!k`n^#)$}2W`bGwo7r~JR0(U#$%3%SXDwbG*H@V9ZY>X0_tyv-~3jDAzN zi)wHR&*pg+*nHW*))0lJ!QbsA0d84S?1~#bV}<^&nbEo?T+;FY5Oa!{NeX6Sm0 za@4nnM4a?>+8s0xF3?Pgms4QeGM8h9sGjx|r1ap-_ zYiE~EKAw$1^P1!M=ETU_iBoUqJ%Q>bTU$NZUQCOq^@^pmsYiU!rVBEB)8nuak@G)7 zGu}tJVe0LILax^!u$OfhqGpo@oH?ChXb7x=`UwOy5GJ<6x={g=!Jt zvkpl2!`?)slXzUNsZ;lqdB5#?GEqzr$bmZ9%^3lej7(GRJLTwV7I|GXI{w=(aU>2Z zZP7!dC<{M;(0Nyt6HqSe}L*uX*@D)htL@}+hp|>#)2x$eN}U$D1I?9wCiP2pQu zw8%$A#7^D^uMBDVC!8VU_p7~1Fn3}XPA~`Bb%%|=sU|CiaIT{lL?h6W(DLfIxx+%H$^t zfU0^xRXw1p9#B;esHz84)dQ;P0af*Ys(L_GJ)o){P*o48ss~in1FGr)RrP?XdO%e@ zpsF5FRS&4D2UOJqs_Fq%^?<5+Kvg}Usvb~P52&gKRMi8j>H$^tfU0^xRXw1p9#B;e zsHz84)dQ;P0af*Ys(L_GJ)o){P*o48ss~in1FGr)RrP?XdO%e@psN1wQB^_!fPyNc zsVMsnKqsc8q@tpvBn}5qnCi9EMfdSx0gBRh~;EN%lFaV6f&KJrH=3~c50+6X;Wn*Lg=Y>oS zGY30IC_4%VJ3Ch>fW^jvJj=p@{EUMY24FHUv#>C-8|bnz!C2hr0f3R2neMS`n7$unBIe~xL% zwOT*=m>QV~Qq%r(Ohv_6-jwjlRF{#O3VBRSMMdrvWbbGzPeDaR3qbOdOXZpL6ZZeo4U4w6>T?+}L0(3Ne|1CqOkY`6T!@dI?myB{ zKQh)=e9X(kMEf6Us2O#Q6oq)XY5!XqN=XSpp+{{0BMk*5j0%PF|4}y%RszU6&>R)3 zp{9rfrGz3$Za6QV%WDDv7`Y1uFwl^jQ{Ug$$PLI7Sy6!UVd`z zIHXwjdDD`9*YiGW_0e;!;gi6!1)&L|NYlT@2Ez5xm?8bJA%)M+lf|o@=;&rb0wGN=Z4pNe|EdNx`?NfmuqTk<*&B8ySs6in3#wntR(gadS6N|pJ>-b;059J z1veh7He4tQ@9}DVb@ifn^xdiy68$$T|FzY;{*yn?BxA;371WK^)fqlVe|6#EUwd5T z(w{%@Sje{ZEt(0a=%tyf6rKsG;ias%^MvA=M$iC{-jb`kC?Q|(MXr;f3wqshrfU%c zuTyyk`h=&Am)9v}|BH%}Tpx;^IBm3jlFzZqV#YLdba0GWtdo-yvZJPE-`h*~mG7lg z>p#ANbwfk9%)QJ^;o$oD*R!msClVNcXUFC!soA((nmgwAY3mAM;no2%3;dH?DoFwSKsMR<|SCDj@{ zy4o)IFr2O#7FaUC`$1~xG!cCDq#)!^84n3@!_`LClhw>66Lx=8sX08$AS}t~t(aW& z*SXIcdB4Fp^Pw^6`=ffzI1cu}VY-OtC7Hg|(QeI%W-i}EJ38O*(`y}RbYRD4QRqAO zP1W+BI~;91Zz^LI+BSacb5#mZ~|VB!RS&(zatan zWf;m&pZ`6?WBKMeg7sGjEGq^PH7|v48E2}sd#bjT53fuX+tM`gejZxsx{J5fL*+Ub zwsS@IYzfhHh+vnbJblVs`{N~2ClegfC?H|-OJQ8;V4B?Eo$aE$;55HMTyygBK3XpP zFqOotf(b!LH62#@&SLD&wb`?)%_F&{oM5Ngt#_@dGKjC>Z*5{PVK``N`7aicPhLW- z{GBKXx&5WgH?|sEYaNsoe(K6cG76IBn;6;IkV?SJ-UyLOIERPPO%%z@s%hg~H=1W^ zAyaZz1W;j&&J4hx!Ot8USVLQscS3C;8 zQb0u{E=9}NY&a}U@x>yu<@5_!h&&&1H;q?yt%Hqve_)sEw~+zyreCtznpZ*xTBdTJ zwXce-Ac%vx;qfDj?RF_(_*eOtwkYE@I8Q>6mYk$wb%1*1NxhgM&VYt)YGz zr%uuZT5Qz}`raK=l{XlMzx%gqa!Vm>=(Ju0b2p#G(1a<4rqoKeO z_fp}1lYw0V-jZ|etqXzLq2x;W9W%F>Nspj<9S-E9z;?(4vF=6I5wY9b+aa-h`%M02 zi0f`0ynp0xhHB8gsrfqVPJiZy4JK937LmDta4&sAK|X+-#|xUdj5~Q7P_MY+;I(29d2eQUE1;60Vag` zGJq<*eB1)rl9&BWMJ~wR=-YGb_B=Hip5dXPz3B$`RYbzrwmK7CKcfNBzw&z zdlrF`KfHG);q5`7ijJCGO#Ls-Vu1b!H-*&sE&V~lc>-aE5mo4V~W*9Yo^;0^EF8fzp;13x$)_ukRA`SVN0|dHR$A!b8 ztC_sbza?N46T*)%EhH&<&ykJk3$g>ZV&w^8U?^`@$eB)sfyDrPd4l{yn`*`ThM5EQ zR$l~E@gC3qknFeSe}sRm0ffUGs3U?1!(n>JT7>&;O@QZg)^YUxeQ8+SM4r*;oo+PW zJDhuTJlbX~pMrb%<*)fq?eLowN1T5XU#p3{!Hd)0yiplxuGFh*e}%S`pU!W_{dvtg%|(N=H3>`!m~r(_N^ z!WBiTP%}!gW3!Ik;3tE_!}nZ)ef#4}i{p16Q%n9S_8J`0c3?Myb0!g@Lm^&taU9tO zfux24sfyhwict`(`JyvDG7LV`5^zzjnk9Vai5ikKuRf{#^55pfzZ?c}}t9N@06 zUi}^6w_0+q9ishNRwP`WUbh9%FA|AgCJS3NY#-#U44mdU9F9c(%0}9VuizaW5H`5Z z%J3W=nY||1;Ned>zw39zLEX6HAZfw~^bkq`-W7Ma8M0y>5ZuZcSUM{Grx#&9&k3uk zyAjYE0G%$veYSsnacnIR!!Qp}K?h!7Lyu1xR7nK}7K;wougu=%b}0T=_ajtN3#pM$ zB3S%kSzrXJDqcF+3xDb26E2XAFMi3lH{Wz2Ja>c$Sr+<5D{;1H2lsU@|2Ltgpug}V z>o&QJN0*}Fw3oz7_$OZYZC&IWd@3R>>SpmT=l0+fPwwtq$wga@!V&x-JYKWNdT5SM z+!i`Veti<{rg2eZH=JFN1uh;b0=13uEvNjfgc5{FZJR4bHND=uR=lhZlnqBz2D5lI z{c~j!aegax&^0wLNX5?RCI)gdjY;wvp>knwyxl!ZQ^4LOyAXdu8ADkzV)B zEd;8QC9Lk{6NP_^Qh{z89yq~ix^kIvAN^7=M&?d32SWZ=8Pq8bV0@Aa_!}~kE8%(R zaZCA@IQlyMBUJ@bAK%(woUr@5;B3gHkulU4H|u#gU>J4oA&uYG$1fhjaXC(yaNp~; zhk$pduYS$F_UYI%`?uhrseN zIxd~$EjjJg4IS+0;sqa9F5!Rm1g@1q)LVNBA@GNTqifL((v8yH9fBevsnQ|c-AH$L zcS<9fJ&XQW;SA7ljE?;NPDtT#5FnuTw2(grS}z+rbQV z>jW8ru;d@6msYQA*SmZ%_4d5q`#-IF4>_U!{a^HWIUN*NFv}sT9Z$H!^!QV9>*72} zgh^n~(H++PpzYZpE$PD(L6>6>g~GqaI!Fl&yb#LjlYQ`-2Qgd#?0~PTAt3@t&t$4$ zDbM@Mm1{)B)BfaBmv^TV?B5SDRqD!yUkLQ?$%YU0^KK&W*?b>D7J2!g&5j0~fn*)k z5lGkA*qB8A;fb{30rP(B=!&hV;h7SZe1Sg)gBYN*-0A%!QXIYsgANl!`5NsO^z!iC zlDyCmX5swvO zJUs4;&iB1vc8;`s{s;Iqz(5t=|6XbRsSOGX->5bBe$eIr0UWk{o>kraBS2?2UT7fl zDT}9vli3;Gzjs^3i*j8Hf^u+RjiQSE@f(BtbeO}Bt&be%UypVQ^0`zJzLj)C;1Ye% zDmy9wG0yyLm1hV;LRo*T8aHse7XLB*(&sfS85N@2*8Nzm(2F8x)45K}Uz4IrlmGto zOGO`jSXNAmr5d9Vy3b@D`hHl_`KFDW+*yeb6tS5$w$?rQw9zl&w&(xP5d*#JQwGxSryc`&AQX54(R zbzrdtDZQe|jmb_;yR$Wama7K*e+5-B%%?6<>?VrjD2n&Ieb^d`9>^RBLytNLRvUjFkTd0j}XupXp+W*++mcDRZNtlAjM2sfGUqHQKX zy0G$3)AIjy6{ect3;{39-xNYOqz`Mem*lG7 z{^CGs*96l#8|;K8422KjhL3&BX2Hs!f5V%NbI~Jq!3XcotVfF5wHlOXzA5c8$0h8B z_)z$4;J&_$FqWmge!YOH_Z$5 z=LF@*6_CAyio^PRj##oD1nE|;K49`V{`*;K)i5czrZ8-=?gVIbe>P0ODu)&##Aur! zALl7J`KPtuOi44Xf6D{#ybL~fAaXxF7t$CE_FkerVm|CWS&Xb^pmcg-Vn1kNfSW^k7apJY1c2j zE>F<;JE9dNLIm(ej>CogyzkD-3^0&+oKO-*_y&`hy_$rM9`* z<|>5u3*P=W7{?jW*8=8IUiOX6cLphIhK79}$C%OSMIC>lnw^chBccMcl?X_3E0h7b z;KRybpt0WktcC&$p)#Xt)6oT$A9FA6Kt3Js>u~lBDo1tcj{3K}-HXMZ-|qtoGlVU* z{H_q4{bJeM+fh~g6*AkO@`KP zYWzD&792a@SN%Q+H}S`!zImyOR%Cv;$yIO#Lc2It=38Z|@=3oh5AIl}-nt5_JW^2B7bTt_fPRX0{p+3^-xs=+G+r;C3x@^64v&A$<^y z;ZCE~r(glf=4ErQIw-B-&j*U1m2%4PEqS08YLv%Tn0aKv0t5r}>_VA5_4 z1ICo<*6G#cPjM&~B4|M>_}0n4ZlzR=4%gOqIYsDoQ?JK1#j>gMM#$sR{kPoKreAcXq9|(6+o(B=PhQC2(no6*iRh&Piwy> zj!gi3M}qZ{LIYi19j#6TeTSJZ0c8%aH5p6^UpIU11|c>s`K+F*d-I~`O5K^R_d!xz z*U#<_MPR+}Y@*T~Y;4@>qEVIzRfVK+=H%zyBjs{+Oc4qSkdk+`;(Z(ae1~TmJa0i`-t|y=iC{g%&o* z>)N;6_+UBWCcUydhd(^9A`jpK*_r5~te5k{ zvVWI+@_fjg8K@Kk3+{A2bk(_D5n!zEXJ;Ml@!ifw{YqgYyPJ2$Ju!>RaVLjHlA!RV z*gW2n?9O~o;quAC-ajzNanr>644;PE1=1xLetS}6<8|GatrCCa)^~S#nN9C9bqcc>vXacFVIPundrh^P5Z5YyZ`#E zMD10B`q_q#u)ejMVS4*a;WU!uPi~HN6x}3s*=)1Dlxhw{voO)RfclY(p*1g2Sge3X z6L6J7gB}R@<6w8&BSoWMGGTr%KH9ktKQ$aKKH}CXhW)Z_CY9m03G8D&GR@&=H(PGI zqPv0$z-HZBL5OTs_R!V^&E44Xx{N@Auvp~q+laY4^(cWZ&k>LWt`52bvt({U9WD- z!3;3P{Y~%g{-ATpX9#yOACmnBx5x*S^_LDz4Z&lePY`5|VjI3{opBIcw;AF@R23|! z1r7_vu^a`+#T=R|(ZxiB=sW;-XH;?MmiXC zrL*5MHJx+q=2mmJo({-5#*iW=PQDN*>w<8_B2s(QebgCSR#lHTT`V@SGVpbR2Y0USMX5?mGdxe}NZR8xHfD^$cRPm+77 zHLusOVn>)lz6hZ6>oo)zK&9NO`<_@niAPg#J4pVq6lDdU9QoPVXhZ2Ug{t+R~I z;Y{a#E6Bf6qK!H+>i)P-1S}M#i_op(t+#2|JyvW^ClLwE-Z3KQ4>ahEMt zmWwu2W&?LNc|<(_#5Xba6LYaXu~e)ac)KDo?~z{UD4o|0KFK}7$+AfHg7hyvd**8j$aE|OU2e0u(FQ4*K6+}N5{R6TZCy)#YamSHJ3=!9;D|Z zd`BN@%-bE-FYHSb97D|X>0=I_)jaMln#)!4RVrD^o0Hbgvxa6D6bA%dK|KHpyc9v* zqJUl_^6`R3EUbEwfdcZpm$1?YE}qQtFVMu6>(llT;rGSTD`Jn^o$_3dw-8T$t*YN) zXVYXLV^Ni(4ai+o^VKH4ZP=1&Z;TZr(NtpFoE4ybvwIj+=%TqJ3I3B zTqgAfn)`>u{1;JrA8;4a7_VYkZqxL*$wc|`LkC#zCHvcXBU8tLkI9$41kzCcHk65< zkDu>hjvx1}&#HBrh7NnE935wDI(r%XGL#|+CK z2bAY{zBUdOhvtvO&_5sYIKlp+7o_ClJmjeMAtb?7*l1BHMu9qIWGAK9q6}Ycb4G1? zQq-WvP%B(~-*WIef#V8z&RQ})q}MkWjib$tPGdf_?YKJ0C=*1WFrilnVPuCx<`7yA^=Qkf(n>h+kQE*FHEHLpz9s@^lp46=V|?40le{h=G2* z`ck#H?)N3Jpu6E=J-aknVSD_awp?vow9HKB{rIQ}Av!s7Fov@s$zQ|(%73mFCWMQy zXJDN|f5Oe$-T->*2{4=SSCD*q5CJ#0bs2s!OuLd#)Crx*0Hi&8!Ql7piS;rPUmCnO ziLv6cp&Z^DmEZ9Zw1Jk-$R@s3;yt9u(R9KZZ^9svC?K3LwG$O7LryA6{|Zvar_Rk8&iBaD1B$`I=fz84mvxVkFBD&!s3Y zO>?L8i(N=+;zwLF*%h3nD5q7&S$*SZi7IP)wcUNf#e7D<^o*N=&lqSyX533|ZRYfQ zd)5+k0e>mPOIx47cbaqS`uwcH6pHu>d}k>NbI1~H$(Ma!kUr`w#0TBmQinK+NKSg@ zmnBf2Ldi*+@*)2?GbK-@_d6^jKN4mHKqPU94i$2pGu%Rf|> zL~iXq0=MS8%x%zdmwucwQdF7xIg`t$B!9>$M~ojO65M+y=gp$T+REsFj?SFmzmmDU&0Mo2-doN%oj6qumo& zT~a+wQ!r#x7vL9)U;kdi1-}{Bg^sy}&<(4&8(P_JwCZ&kxB7DbVp91SPpmALZtU&) zv8R7U9(>)4hnADi1esvMfNt5&->6oNs$H+4(Kdzd4Nu0kXEE*#I)dpdr1uU2P?Tb% zIsvw;SGy`M=M;X;7m3PAoD7NPW$YjY3$k(iB^elQkju5n9;BRN#duF<{YsYlI86b5 zP6gaY+yZ7t1HC2v=CsE0K+N5|kWiS(@gApm|DBlU6&Yy?!nD)jXlo(F9Y8J;3Z(H% znWFn0IeA|N=~y|m*s(X?Lc2S4HPcONde*byEsv>h?aN<0qM(VTvf0qEA4SG*DN^nJ zrdSCy3;5HtKEKAX7p*fZ1LHS^Ql1GD!Xa#Zs#fP5x&KMdLtJp%UT!5|{o@(v{H~#_ z#8yLn4>w0mdi-+D_DMw;>Wx+TJF|i7Hjp@f-Pk`z>1ELAIs+Ow#-w2soEU2#zs)Wq z^%px`lN7dRHcGg^7r#=hHS^0a=$GL7di=iDXzdxwp>2vhO+|rwSA}_@3H_D^L6E<^w7~gq>q&7y5*WsrI!{TCD65 zj#L}&ASG4sQ|-+1kUs>MGtSF(U}q@|HS~Z!gz;;SjXAhTuQOT~FLUUvp3QphHj9cF zWj*@G8$VL+qU8+2Kcmbius5 z;5py_S~}fM+bGg9Equw;xViYqa`~qNSMv-@ML<_fUz&kZP&jGe9lhR|c>_xXZHe!y zMOETkhZj}t@%!Pf9|&(o9C8tmlT!qC?aLOu#(X4d&vuE<=y%bYHedmZMe1Nd>*nS{nwa<=f$}jG%aLU`CYbWj-4Oq}M^RtY)*++wHJFC%0JQPoXnumR_x$`JQd5%2hH^OYg@g*3T3ce|$d<{)v zS1oxs2qOwjL`J_TP%veMarT$_y;=ZI`LYDTSKTt+U`Du`4-i|!c)w|7ke865$CrB>Z|6a)rx5mqUpP_4 z8vbRqV%W{_8%;_07rz>#$HpS`hS!BeYkMt)IE^~)Y~+DM@cV_L)d_Pwc5&zHfUBpA z0CIWL+jW@G^p$&(3!c7$nUxu+xFZ*TimanGIY3i4dmQLe2O6V9>f#$Fvvd`*jGC$s zXng1}SDA%dm^-pMVse#ZF!wXuS#Jq?&nwWgDYA!mf}M`x{r2Re>mgT0>f+a{vc(7i z*p53AItWK5*UKMy2V)HESdvfFgIz$hYNj;Td|cwD5ec)g_;^Q=&h^#P1yfs2s0GsZ zTA}#tRm1b;^RoTDeA@X*`@8!|ySom;cp3xUhVo(yNU;){n0fiu&Y|$0$Gn;l(yw!< zjER+T!xoL~(@*Q#qmz8sOOhf$%eXZ%)VRST?CLrIZ{5V4mkd&Q3RIC%0NDir@9OF@ zCmfOI1<+{}{mR9*AdxVO8pU)|8Q* zS!s($f5$kBEYI3`uBMMInSEQ&mmS=z!hx2yK;QAp>cAvSHk!r8NSWlhkhUTN>|9tQ!=}Dx6O8upNodroI zu;1EcCeZzGNZyZ%=s5BrJ+3!?OdT<)_{rT9D4$058XkexbQ7&BCu5Yb!$vQwhyX!2Okk`Z%O29jgflso;6F$p}tOxL&2@Z|%M_ z&PIN03n;~FHJ^Mn_hy^EM7{WUnsirPrte-(f!xd8Bg1bkbdIIX7yy*smCeVr#_Z;N z2ZWh8M$UIAXj33DHZa;G!Ic|dV#UGqsOPbpWo7emwWqPYG4t4d@}?w;41)yEUi0Nr zoTUQeO_-YOkNPJ4_nYNX=sK>b?IYXj(gl}u(2VO`=2yVK>yKU*+MiTboCSwnx7Zq6 zU=VTwamsWD^4Yp~$B6LLpb6y6+kR|yT|khR-NdPxIPJF|jAMH#7U1S_e9&oxonydp%ID3yPJx&e(S$0NgS0xCr7IEsft=EN2G zNon213)NMG(YFyZ_ibXZg3+%XMg# z>W6mg2$Ot5xa0hY2bWoFjRxmTk;y@FC7(iZKgV!3fYU_n!{MVX%X}5Oi>U1DO9a{% z0r}tJ9YLHYBfS(!1|Sd7Wb9wPT`EPE@_u^U#BEBD1(1FmV9~%W0Q9X5Msxwk3u%`T zn1K&%{5%-!?}2kC1c_(o zFmzzlQ)e}l2yQ%e@A0Ip+070-jeM}^liyT`0wBc*2QZZof&Jtm!-EQsg}gt|p(V+i zCTW+qwru6n7Hxgr8_dSQn?Dh&5XzvZZ(BzjnyH|)ZvP@2nNg2O$Etfwq$mhADVO$` zzT#9+L0&pGcE)eN>{WF{r4s28e6irIO?t0mE{e2J`W0y!mk;?v(;xrzBO2f1t+=kA zsz5}64ueqmJ(|gfQ}8=*f(;Kq3&Bxfa&+P2a6#qJb)YcSW>)n$*oOZv`I!FzNhIuoAEmgSIX| zD`zkHK*IYmE%>!}*Ye{J`460%MQ7!(=;3}UEorfVqx6*$sP?gV_X6)n;}1d-r9y4t zG!ut9Y6M9*i4HY&^g#KfJ|FraEfF(scSLCSf z?@S;Cia?2^BLHHp4F>kih%f~Va*oIXPplQSLu4}f<+kqVt55e?qV($t|X%q>Fm0aWSmP}8Ec)U1w{oe|C}yhNc4W< zO~hxqSI)TpRYi^@Ny-48igK7|A9$}TIvCpvw)YJNXG0`WC=?h!D>4lFNW*gyqUTA; z#*ifakqzE<$#Uo;j&FOB%%L8)MRa&0FsD@1+g#~mi;8bxP`%&zvSBp#kkI&{N|@(c zpT5>Q%NPk)t4YuDRbu9DzvycV?d#fF{_p@Q^mZuyZ&A7PMi{^(b!`dVp2&N9<5gB( zI7&Wi_NO0P!K0CwmW^yDxk69VJ=3{c9}5zOi7-kyHo+_?VP#Gk2F!zTiFpfA5ece$ z{%T`th3yRzldiwnx_yo26TLxGCq2*oY*CzJ4u4MoUgDv?O4{>?M->`jU>}e8{vM;& z0p~rD!w4O3I!iF%Fl@3o2%-)$z@OjAo!7$vs=7;i!p>F48&7rqnnre0oX>paejP)o zdVFqWdbak#z6ku_f5v{&O34U!$=L_X&P!K5?j+0Nk6zq zzn~iz_`dTlA3k@c@$*%7MPXtB%Fa7UiZB-uHPN84~|V#uZKJ&}zPRTVoRB=}i$*x){w zvtYDuGgqxmhe*3Q{jK4ZDEmjsQ$a0&l0z=|#Ue&}0YnGJppbZ22HHFH~Xy9vee^F7Xe7jh>0 zf}7#6@QI=jBv+De>5cCg5mw^VyKH)P00(d1*;ws5(Thpm)M^$1k&SKF^5qFDiSB;? z>nVMq+ix$=*XF9|9n^=0Ys0?*HMBjtQO!;JR%+G9PYaiGtL+^`sv{1OLBblFJQKzo z<1y9WBm z`RwpGizoRbP}gQfX6Efoz;fS3M{u2IgHGf4B}ISjgQ)Yup&-9S(OIHC{?Lx#{)C(m zSkXJ{Bte=|M(rd;?N#CegM`s@I+BB(^v4>dL4t~j=Sy9L@bO4F;c#58R^O0r_DD4( zH%96(K{`vlHqUCzp~@wnfS|hZ#>W6qwhc3$PQLDGV=jaJn zrze5~!k7LGFh4_1GF+2VnszYCK1i4BH?Ea}!b{*1E259N0C|1-zOxU*m*sc~2%+FD z7`Wp_7pS>poEKrxtcRW6k8no*BkLPXZA|^YQs1B^R8{al19hdoJ0CegxJ4D(?2I= z$>!cnXH)*5PCI1!K5c?cTks=q9aRYozkBVx^F&>VTwcATIr=7(d14@qnpt^giQ0~k zcFu_D0}V(2p&AeGVjOI10X(17gd7Dlpa{-ZYW?e2+L?`ZtxXoQrQ)lA+da-1)3`P1I!de#}VdbF(%!(%S>KcKYYys-deRQvtV3+Jt2t zJ#hoZaQ?Ny%C7|Pm{F&JYMBOGfW&px5jcaPRf!PnRCq;%0YmOkI*4Z;)?MQ*D(+h1D1#_cNi)yQ_Q-GMquUpZ*qIV< zl#J%>?r&*AjD=aRdnhvb&Is5|d^(iqrAtCrpKE^g7}g4moAc=MqTBllo#ZIe%K?4- zk}n*u)ljConRN5jRI3?^5Brz7hJ1F|BxZFbI~7vRO`N24LY_(qcmHNS=>~;i6>R1y z;=kB%^P>Dr|F{-mWZfo|nJ~PACFQQBxo1q3ST&N=IN3e25Q{Rg54V?anpOs5=g7df z>BY%O9?6M9uH6~LCc8HSobuiUEob$mnso6E`SY=nhg2x>@u&~m`d=p@8^0{$-30-D^EqLJoPRb7uaWy4wyI3gmKJUJ8WQB-v2tWe7l-? z+4cP=&;(rbH@jN4zrN#SDQwe>OL@3uC zzeuKkZDgP;h#OHF=E#xo9l_V-98!#+C42gR&<`~&9OX;!#oWK#gYr1%`r;i;E9Zm; zMs2($F#xLHC_=ZYf9SQmm)&I7G1O;620`7|>K2>4m+w!$j-}#^j{*29`%$i<9N6Af z(`>Kjr57u@AEc8+_ipf}k$jNqTX^7o%R_~lJ~{RcKne}(f31kV1aoY4K1jb=?x>uM z##-by_)rq+3611V7aby zwS2UMJhLux=oUxXdzan{JBO>}#uU}Z&2O$dYdYNX6-|;zsffVlxt6LJB1kJ)jcFYv zz~qjiJHkuy_lhwh?1(6IT%@UP=BBVigONsvsL}LuLd}0eiy^C^yL95Q$$`3UrssH6 zx`vYbk9Z&tc9{d9klWzX>sUXYLF3-~FY&1X+A}V)pIRQ3`PM90Jo2gyC^L8r5k~$n zzG~8&lQK`~_25qT)i3Q>ef-|p_!`r_jt)Vo1vYSgJr z{fhWjcLFG^q3{5^^uBy0{URm(cg(au{EUQWUjbu1g_uy~539xO8YWU%dT6^Wv|4W| zC8QZ;85kV}<}AA@-!8DlIWZO`{7@elb7SG3W9mIkQ_lpRGUDeK1Tmn(s#$CB1Qr_E zHF_3(jv8fCfT2pi@Y5XuMDydO5=ewPr_v0y^q?giz}uYg%~FdZkQOvME3*m+mM?Ne ztVE$~KlcQe+FC~)#!3gabiCq_Bh8^=NEW`} z52f84@EuP7SeLHfU$)zX_n?|^8Y2u7P6*Lp4D zW|H8@>}gg8r*&}d-8Ez z9bFjZ>Ju1385U>NR#wx*?bsx(!Py_~bX}lYIiNnGEZ**35J9{S0}^r4TVK z)#j!MpaX5ZCfa(S z;TSDD7tL~#!Qyl+w^KRRkhmu-_Vz$%SPyGLVEqNvGoeagQ2(VZDN^J-#<$|?o-|22 zB5V5B;vWYrvwG3}M4N}&&Igkr)i&C50f$AcEt8Q(CQ9qW?bo&dmS%YH=;ILRaIGS-u;>ktwe zO$LeW07S*c3cF`VXJM~eeQ3j88ExsHopojzs;FC_^DL2M%fd;eNP|M1I9ef$pNDjnnHp~J^b zoDuTJl5Hma|0bx6P&MoM0Dv6g1oA~OGHn=9Ak!ZH62&7VIO2-I>hB57Xo1m*zLH@I z?PV`vwP3L$+%^B;o%fen{urxkZ-Pen(Vg+=7B&#NUo4}!TuUya`;9+6NBce;m?#e_&+Jn>1Q09 zF}CMy%64Sha0)XKStznE68+iE5vk%j)IJ)uA7-Xr@kw86<~4<>)l@?x_{?bXBWmjp z&!@RGFP$n2c5Co9rUHZ@1nfYtx+z)7uQfz+pYk1ls0`Jy4baZ(KQh1nb!3kY zf3-7@eq^{PkqpX9Ae+RL6n`5EXi!hQEV&f+Gyiqu2s z4&OawYNF0@KzOIlGnoZolIce}qc_iy`wGP>wA$p3M(^)Mmwc3Uk)~n8Huld`i|IdHPt`2}DF~utR(a)`*JGMV;~K<~(wO3i9FtF+VRag#V7M#eFma3XvaghM z)S-h2Te62#WR?bi%-Nj?MJZVhAr-mOa0To^gUOF(kVDR>?eNm|%XLH7fS%O7W~DMV;?Pk_$*&>rTm4?3@m%%)`Z>yAWkQZnD!mzQV~Qnp zef6v}apDf9s7kg^rlSJk0Iph2rI3iewnBtudzv(I_?hth>{E`l&*wb%UM2^0A?mp8 zeVt{s>*?aZ*3gXJ)JrkmeX3oYCyg9Mg^ME*`F2pss_6YQi{1|X-IN$|QbUyUCiatv zuPl?#mP$#mKa%!NCsx>$>mX%IL}+z`T3|GkBB>-%>2I=K0;DLl zh@W-2KRe2F{UcBqq`)L-Z4#&q&jJjNi_8-4f*&$j@%ISK|F0ymxMQcKfUP0Y)oRQr z5FFDJt|V!mz=oHhj75Y8o#!p8lDTM2_=iB1>lo})GSvub!+GThPmBbL*|x5wxnwty z)Qo$VwzZb_^uFAj~p}twmv#?8~Mu~xObn}KjqVIkt zy;!q7=5?Q0D*+xeP|GCwtLrmTB|jsTf-lP|9Cgx)|US@fo04#>5?r?|Vx7zo0EhjG7)DEldi(fF1$x4=+H2^McO)Yx`@u88y=K>aBLPv~Jj2caC zPIe>X$`dH*A>mRk;Eg%~X&% z^MA2uojL;zz@njQ^vrfX^=081Ql#CgJCf@YFLm{|iko-1xcEufjkO=AnT#L(Uc?fl@lc z!vi?&+J7t~^DRSUs@8z7R??L-5v?5@bM2ptM75IYf*Zk+#B{E%ly=O`g10GL>;0ll zWk8ZiiEgKj2oi5QGn)o7)s%iV_XY|Rm^pnrGR@WYf{X;|a_It6SPk4JOE7J`aKEWS zGV6baSH&d}LvfR_DOe#soI0!Wx~Ih5HNanV{+5XuMbz9y{r}+}NnSj|l6?KP?<~O^ z-ANm`@PNJ-9?D+SQ9gL}i!w0jm(i(0Ui$v7N8^<#e4S3S`i`AGti8_>RM6KoD9Hn_ z-s59%r|rbD-_3mq#ZTa5N3m5`G{r%C}Xa5kADdzfIp!(^gfh^0H6_sK%}WNBl;n5(tLxl<1iQCWDZlhJ*D z)VAL-CB^W^v85P#bq$?vD|9VwB3&dp7YtaUmQs%rvA-73x4tIaMdRl;*1r9!3fYNw z^Vg-B!2%0{2)|{L@h09T0>G5_tWCRC9y7BCMQ3O$N1`IsN;>1ES;QS_t8r)x;wphH zJBi@m7LLzZLEQiFnf6s!_Gt?pavf1h%6&{fl1~x|9Z}HViDDOZS&E?5dJPN5YUoz~ zcIF2JHNhD`SJ=T}n|{280I43;v^n$7m7N-ns-6ayYWIlg?q%W_J7cmUdu-JR1PZ{S z?V=G*3c#vR?ugj})r!?x{ScYsdOuh+0P)Yx;=1ze5rM9yuV;TX{!6k2NAc_=r2Q~2 zBE|qy5<~aTAOs-U@T6D*ivl?D=TS&Ty}tmAS^v@KsCv|V);i}x_>n$wOu^0vHG3#T z&sjlhE)al#?*o&tFYScq)Cd-(NrwnNi)%rm#J8EY5_nfDo)7qr_Pa@{YzEhQI0>Zs z={U^w?*?UcH}f30OZWG@+xLmQFXAU_zNz=*4^w&$hUi&Gl_W0&f3&%eJe$y8QSmAG zEI(M&{Ue*pUXCmT@ly|;Ei*11$odf~iy`%!vCJSnQz`x*9CuZJt&?c)y3h#D;CyoPUMg*ru4)=fauFB! zoN?oMjdt4C4){KL2H0jZ4v5`M1%4tFenAAm)-t8@^#Pk_n^20yz~+0*RJoLC@ABwd z#e-7Tn2U;853U(#BrssFx~A$NB1S~;5i@;y{eV~?D6OriM$kOe?h zDjUyNW4w|1g~h~7E?wuo<@=vRwJ#hH+QI~2E3$yycH&opOy`1Ug{@yRk7DF`f^S*V zM!eZXpiel!a(AKJC3e>ou`ra)%Ku>Cbc}fih~na;1rU@NW4cWV<@EiQt0GJUfr`kW znoHR(FV5o)I~u|HXCJ|DxjJElL;dX^q*|mpn!#HZfjSN}i}czEt>vI8zS?lop5cC| z5$SIDbn;FY>z3_SOcMWF&qRt)D&5?)11;cd1h!P`uK}|X3akyG`&@i=5v;gPf@J}2EDpKsO3Q#new}lKpz}4 ztF)wV>uohCQGO>!(e%@8^-DCW5J1aFd&cTfdK)DEi`QSPgsx+tUMeK zI(9(aPk=IRI{0z=oB0B;t^Yd%>w~NPcgo!e#-p`-vsgXLi_7LPvx~xP6(|GfEv?^) zt}*;*2D0f90DTQo7I7E0^?_*@4O9AFR6z#jRrL!7lHPmIxx8XH1V-XO-kl67*|T*M zOZNRNc6>$}qt(pbX=vSM(|U8xwqo6Il&OwbbkCqW8TZu&5QS4?AO3&Pu6;zBs(ZYj zJW^0@YCT0MrKD9wm!GFva>l;zq6CDbzwMdK0>Y}IQh-yo7WY!~OsGe#+UL{V(bz=l zU=+oc>>B>nlB&}u4xWZejvncx@d#O`cTwxFl*xHk7Ca}qGWD%$1Wr;iswZg2Qz5zT zpt(WHx;&pS<}{YC6#(2y%vsz2kL411{$A$Jl6FYy^W5NXs0)!a;QY^)8?sb)XJ47z zix5W9h9y##qf>>3&ycU%>=7JRH8e$bIvi}+MY$bn1gZY*1-9xyc`Q+!U=;%geC1_mTYEkuPa#@g(Vaa_+IjWcVgsGJl!UnOb; z(SPfcPx&WN%OQKq0#v7%JURih#fQK4_4L(W`zmL-MyMlMI=q#el!Aw&Kf%CVRe@SI zBenf05>m0p+WK6kvYVeif0-DI5Qi^2@}0+*tU|qP*PiC?PF_{#Hqq8m zBY2RhydX@t1-SL4dz@E}DytS(rT4MyxOYxP?MQ3eOe2kO!IXRqJ4r*||DILjoXU-= zDv7G}iQlnd517|_0ZpFYQicj7Dk_nAi2o+KiGXPN7SxhL7W_kX;8psRsu1ZA>vvNt zu5-AqpL=_w_sW8JRSixSRx8hEw=(TKDl#qOq4T(ER_lnT^(opbzv679wM%%vjaR}cJP7FG7tT4TLX5qmpbZd=cjYxA{M0`T*RH)!TFT3 zfqbVA3F!MkrISouhdd)-PUK4$4Rj2)sCF+1#!?*y+0s8luIJMw_Fqu~PINj=Q1t{g zp3>ssAKAAp7#6gxzJ}a*z_fJ(q`3(es<-K?YZ)>f_EEhB3$N4@Bny>Sd6^p_4qjKd zcp4bJ(j*5BCu9I8t?^ze#{jJFC2gRhGZ5?H+scgSOaxS6oT9m9y7Gc_hF?vgdK&ii zX(+tIB_S=m24XV7yyF&P8}Qph&?Yd*yPbSw2=+#+DKd_RjC^Wpsmv6g(xerKIhR&p z|HGPL7%TJpVMNF-fO2YFd2MWE^{sL{u^6d(Q36;wPi``h*ju3A^W}o}K|xJ_p|!PqHeWe;K`C(LoNSp zcBkKvkDJq%YVrWWE?yv)=yGPfFk|z)!qt9IbrRKDQBc*?HUWl_Ga3Kgfo=yG2~WQg z$l4>V9-y|7Agljusgs4}(XAr(fWIA8@FnWARowG6@A>P|Gr-1QXxfKBsu~1F7W%E| zJ-J$cyej+BrZu3~!SAV#ariHK&!_@-4ufC%bCEb2oYi;nv=g|mj#|H|{CvU6D2>{q z`v`Z{ zOk_t&Z~#318gaBQbs&LBy891@s>X()Mj7QY#_MGK1U5vtIn-&1I=djJ7q#Ctw|+hc z%+vpHw6wvn45@CMT#ytgc*N4flSakd((K$sgth6<)3(Fe=2r(1UR7uvCvb#Sob9)M za>?M;u4&NTTJzHE2&lqfq_q(kCP;PsFH>4L+R7KqgT=0KjV&BTFH0Xj;R%>Kz^k~i z3QWS*hNtjF{db75!med&9LMhlB$^kHfavJtOthR*JwwdCvYR2ALwJS;`<`}Z;n$}t zb2`UnZD4k!zOLO=Gy>Wi)8?@eaTtrxXP;;<__bO}UTOTFcs4!OfHm3R-S=1{o|8;l zt3@E3oqV1mBrYWa+3bL!ei80jqW@etXh`z1Ox{8f?%UKyAol>)K2V<5KF&h_+ra^@ zr&)wwef18QX?_GmuRvE#g^Sh)nGC$(YqeM=5Iw$?hTkNTGhbDeYtJ)J#+-* z{ih*S(Eq1zOI7m7SQIbUMWsTCQHc46!vHBDiwC}1FqwNtti?eEj&7c@t@fl@m{L6* zR@i=1V{c0)NW9p=(6M6&D{Q$kn?qP9#caZ<7U1uk8V4qyHjxchA9t1g=fn3ofnQAbiW{w1!Lvc#z{UA$ zR&_jeh^{hiC@?naG30N$RbnFp#It{>AwfdbVsLr2fYaR${fjQ-4ka7kjJEqV7`i_t zZMz_t9O*`hY1p|tI3(p6YFR}3^fxSOB)TF_tu-ZhS$Yw*PSFa;&~PYUhkc>`+b6cY zfcRGM?BWzXvMu(csXV8$Bd(g*{y&Vp^;?u-7d1Kq!!R@oNOy|}B3%O((g;d7QqtYb z5E7DtbSsF2gmkBbba!{BG&ASH_kGW~uJir!{RcDmv+upvUTf`rrx)Ejlqp&~MH}cp zHCHx1rjNKUF-JRBsOx4UG`5#zSBr0dwq5VBm-KYK*@~jy&BB59{kYB zqR-f7(ymi9*1e`!S{>tycXo^-MuG9K4UKzQa}_oWffdaqDiIT}IipOzJjs6Ml#x$I$mT^6=JN>-27Nbpl%6n)+*Wi@h6Et3G&ci%}BaoFeU z#|TfJDyuGL2aPA(c(s&<;)hKb$x3Vn6+Y8dy_11wAbWhOFEj>QmD;E})0s z@A8h0YiVCjTpu}&K3)#BNN>?8wGWWIU#2U+fTxq3ynSp0@zSY_xs%{F%>QBSpqq=U zdigr-iI#MsFT>7X54mUgPWM=yD4&L=H-wL%S!GTzbEJaXTBIz2-~04a56Jv+UrO$R zxXJeiGQBMculiW#w=1SoJpc4FxFA&@b|?JbwTmTKIpRgbwI8~5=5hbT`E^|xfeU(d zsLfv^_J#$6Cv)sJm@X}s-RnHXYA`jD0Rk~RY3CtuxW4~r**gXippk!k&$aplAqN)C zq=W$jlq3>PYsodAJYEcb1PbDvmVk77Y%@K)9OJPar?yU2TSGt|p5)~*8m^*T@rV8l zhQF1uxBH2%xJz}M~ef-(}Lu*{+B3A1f0O5s-YF@n-H+UzXEoKXM&<1*A?Ndk+aOwTbZlVx2xdYWkf)Vj=XkH4msq2#MWf+bU@QOsZFRqB;+?7dN6-_?kUt z$HU|?W)3X|_JxYjc_4h|!>IUQDR|V6RvvJF0;aKYM2$J++u#2XtbF}2s})K!!x$&r zo3L7-N%Agj#!UU~?t^9#9@4?u|1^zbeqFWwNghQ-Q}N`YahhAqwqT$u$6>~N#4R%a z*fBNKA^|w(UY4c$2$9 zFamYf@jJ5BC{agQI@3y8y)%c8KF>W=(yKO3_gM+|IIWa8E1GX8Ua4rI4i_o2=?)+6 zLd4-oSV~E-xukdmZ#arPc=-9vPM4bFtEDiqU+?YrXFeyi)9eH?Kj2Qeuh<)y`}X*a z5lAjBWY1khYyhx$l6}%Ku)gL{miHL7yyewTa#O!tc%`((F61O~$33&y=H9hyUpl@3 zN(f5|eylJ?{M5yjT;b^K@aBN0?q)6eiFf)l8Dz%O+w6hGX*Ag^rXgZ}t>b5>jpmA= z?6q4&%3$gCDZW%^wr*48gTc=dm&crCAwA%7l|lI7aY#~QIO;z9Vn@q@*bx5v$bZys z(zg0;I>Gt_@i$q$+SRb67sUc>9^V#w6D7N&H+hs{|0WU{cs(Kv?@ZzKYdyqBFJ<~yF+Y6_}-677#VWyQzP zr>O|=+o6MGZZI_Me?PqVmJSUrXOt0LYT+aX3 zaPGWB<{LIqMOJY7zYCT1)%&I@t3cLR4dE7vxc&t6@dST=*LLaNrRWzX@20W=Qaa9c znW6A>E=-)@#vuu@sslrr4}Pe{2W>u#yct+Jg3ImjUEJNz!}C(g-##Grdp8+v4IpP_ z5cjF@;`&I(;2Kitc6yE>6D1dy#x$0}%Na)jux8cVN3(zd5y!q4ht{PecAOh1U611G z+eI-UnS?x9He@q89=eZ;q@Q$=&gkl7<@>9x|5WVGdcaq>+U_YwSf+DN<`*-*{He*) z#~Q|3^V(ysWWg14DXT7}SI=q!9xB(n`{ZygB7m(dWb|-lijKpxp{AZ{Y~*k)ej|p9 zoy5hA_!phm2T=FpxL7TjS8mtC1b#E~lv|TGAbuD{+icd-18H_aQOZ&~(w1t(9nzEbV5H}Za_B=%3VI5yZ?XK@;$TA`!t*yTpSk%qgjf0y$)I0C zgdIMolMkC}l5)7BORUchZ8j({PKXwSt&~=Rq;Q9gvScte^eIcDVEP9+$W4-^3@Df% zzl5=1^Np?`Hno$MbbRTU2uo*KDk)99X~#*#z_UX3*YzboZMpwVe5pbW?%IU$UvGV+ zNduI+8Lfh9zRPi&jFAeKW}lMaX+8s5`+NaoKoVAYSDy8Om9QjyB_+Y=e&_YEbKM_n z7|_Rb_?$jclt6JJw&{=Hskh(g{A_PV>HxH6?x_l(HBYANX$?65?}{L%`td0qW2$g* zY$`9}v|uK(Sl~}U!~|dr3*RUp;ZrAJmd$Tm(nq08oxcIwEbbbic(N?c|F3%$V$xc5 zulb!fs`a|``SCmGb)`rlh83Xt>rk*=RkXjoug1O0qBQFYDF3BMRu;m7s3#`}U)sUr zzaBdR0+2yYhGWyA>tB9F_;Uq_ncY=eD#OiYMkLPWG_Zf`V2e)s#)6ty#P9xgN#UIb4M3&s%T`t!*H%N@+LLMEK#D(`SKO_zjNsk9Fvv2$uL?MTkf^5 zF8lJ<9=`^DA~F9fS8Z6wZbav6hA0ZC(}m*V^DDe~{?b;Sm9ULU;;t8y^Jcm_N=?jl z#9^MCnT`XpwSzbu-~*7Rf~zvipq;RfzS9@si>&Z<0`D6a#Qa$-`ROTq!h~X26hsl! zuv_)n|FEnY-+q1h-SsBjf-@ClS6tHAbumXe&DQ`lN5mUM`D*i*)X>KhwpGjVVmZB9 zF0*XtfJi`obUm4c&6g_fb%;R;+2C40jQQw0`FUeZW8*uAC^?>?%bc$rV3<1j8Tyad zW48s%zXmwpCr)}mEX2Efe)^A$d`$yh+1t~if;Zf&U+n~bsOe#DDR}Nyc>wK(BF;3| zDI}}Py7Pb#TdHt4Z=?6h5_-b`-|L&28@1dr5cQF@eJ(aQn~?z2A=9z97$7=Q|J(&I znxB;KxW^n5YeIZ~%XzYvJ@vv0klHFqkrxIjdV-7Ei zPE7vp2&6H?b9H>q$%)hqBE}Ed?+JYOD2H!6JF({pwgv^|OwNCbw$TZsyczN;Cv1&Z z>-B@T;0$ZH@aIN2{m{b&6vVRJSDyoN;5P6_M0M^;EOyAFEJW3aw`j?|F-LMpF{{; z?;3T5;rd_9Gx|KIGcVg-p`a~jT9)AfZY#=$zX!dnsi=ciJBfnVPj{36jisqbMj%h9S#Q2P+4Id%Q%)2~5Gi(aJQt@CAU5sQjJ&V3jU z`}Ll*-N-|>x0Hc|AYSps&etmdTsW#5fz;`48k+u4E(vHtX|43EoorqfIgYMVsIUJt zxKeX8z6;=>C*Ku2;@y>c)$#)GS##h1V;3MFj5Hh|b(O-61hYQMBc$b90Snm#ho|B2 zwJjm}rnv*=Bc{qiU>9Os^NGfTzTY0HsNm4YZ8^=&_4pRmt}}maxb>i!vVQv_^$kP! zu-ubbqiyk1GjvZ+=sRr3^E?(U~g?Ez{GVZNV7gx4OiuTSJmg zbY*zPiH`}xgUs~!+&@}Ge5q@)Xv64g6B$q3UcW}h21RYdle!- z2Jj5Mr{Td#N|Y%MApCemM@2Ch?2l@_fI%_jeR-uYG4Kg_r5LQi3x&MAnzp=BzFRS% zik)d=uP7$dw6Z%1+BSj)pD%rYMM3U!S)k6uk#Yq43qVG5*|o5+lUC3-VJ;#1r!AD~ z<*$CZ>@xQ?=Eg4*kGEST9y(F~9}uT1=~X5Pwjp|B!MP{bM_Ukp*droDadeGrVEBvM z4(1rzVTxmU8bVLpCtIW_NTX@d3^V1uBU^~t>x_$9{4{{Zr>ND7Ec1hv+od+X*Z0}* zuUTnUce3&Gj10n}4cNPAogUeY@Hg3XXDhz&!a@f7wJ>2F;|TG(JK1*YPKKa<>`Hvx z-re>4MzX#=EKpYcWqMVCng&K&;WS6AV3TzoS8Rc2T%h`i0hsy4*3b+Hh6Rg4o`dr- z>i()(m%S&)u=z0oNGFzR>)k~|-)Z5%=ZkQelGC#U=)%^s_A@Lg0D|4{qFch9wc{}z z$#!$DzK#&GZ2q`~V4z|x=3aVlzD|dNokx}71RwYvo-KYC@KGQ9HnN2-t3E+9&D|2I z=>;kKnJN(yWH7Y-`h7C-U~dF8ic{#iWR7_;B-zIy>|@z(Eh1u^!I9Xk1zD~v8)Wt4 z#K&NJbyUrOue-5}f?g+@q}jJ)-+9Z}$)Hj&+MhGjvsyFJI6L`4qMJx z?{tlj(-&{kU2?s+(oiA!z8@iT`S#Vs3FlxiSSqE7Ta&aWM8vNfaih~3#@9y}aQDrE z8Xj3Qr`aN|CM}&%$KD!B=&3UB<`2O`cHBT$e(BS|W)>*MTApm)%Foj8pq4w#9`qf6 z?5YG9Ww5t^$C<^>#pzG8E#*u5e4hZo^sO< zXo6Zc0UViD`(9tnq4dZSIsIV`mPVP$j_cYGnupZ5u=!Sl@m+>}?~(tzzDANFQR_f| zDz1Q#lX!r{6$VJscU+MFE4$O;yvCMM4t ziTdc$>Voosy%qsXI@y37=SQ17jF(J|@{Qd83)4TNX)0plQ{ngczpvRX4dD1OKV{TR*CeJfjkMSc$2 z=4-#J!y#^xdUgn^SPZ@D;j|u*v^KD_ONfBZ9jAq6LU5*mo?&t%`7zGm6v*?pz2~9AW$gI9rP@zMwUCYsBe^+=hpP6(ovmh zX!0WC=Fc4?6BRfgW;1XL7qdS9ZSr%@eoM#w6Rmz;^zn42DLfZ!p_-|4X0z}4eA5@n zy}5X19MochJtUyfaB{2&lwD^LBotHQ!Nx7hmYqU)Ay1C;B@M7UW|R$wtAR~0r=F)p z1(|&+w;h$Z(3NOTV-Tq5mh<^vNAbGR@V{Di0UH}@Qj-oIyxoow>!T$&BBjIJOD!l zs3M=3Co6s**&L2JBR(SG&!Vn;h%$CAB+6%0u0D<^Wac{D8UX9}2pSz99($@AdRs$C zFw%tVfQ}ux--+PDYS3A1?qNR5d0tRMN_^^^0QtVF=x;hwR`TZWP0RC|{z6?wYb@u> z%vDO4v6!k~w4~QoJ!??N_}3Dy(qXIKr*$EhX8%#GOB6#B+l_#)T~yExesJsfJU#w@ zM1ua9OHSUoM1=aF3*zSB%mp!{exvfZpJ(zCJz-dn&^!L&aDMj`$+>|3xq$Ao;6qXg zg26+_OeH%I)~#$Wllx)Lz2~t|y#WT~^ebQhvg7*-S5R5x+`n-}`Bu(^c(zoEyhl0r z^*aeg+~Zm#&)2k>9Jg_4rqC6>EJ~rJ-i0YfKTmaK&!3?Ne1e)u)p>37H z9OYkJbJiorvYPmffA{$^FT7aI^|c zeB*Lq^~<)PlQf`{0USnkF#cNAOL}Jr^`L9^hHO_J-h#*-znG2^_gh3mX^L805FZ3@hdB;2TIbNf0^w|+gE{pw?C5;e*Hevu z$fi%C;07xmdq#peGy`?3_whN$3_KfNG^J9;=?z!RDr*aRKg4LN`cU)1TYu^Mtzm9S zfTq+Uc1*VUIL~FUU3ow=Dt7&jVygqaeERkDbw?IFa@W)lX=;YtO}=SX z6`tGG7zy^L=s68#7Z-J@yrI*W%-iC~M6LQY>zt=pawyn0p!KqsHN+RfjZgEjVde#m z8GVIG&A+kT16ZUN-6@0jIS_zk?m@#qjh8EXw~^QBgROJdSw)!=V9)XgcjSfPd0s7W79;LD!BZH<5yv{a&-FLE zqoPfg+98ee%4lgm;d9fg!={i^D%9YQd@PG+WoTgCI0TUBh`&%A3+O@Jx6+)}e*%4V zN&EyjQw+<;({d1{FAjDknNS zV}&0ypHS(YN6jFwc#N)^P7=|G2*i9TPO>KAt{&d*#io~sT;<{=6i=0bPQPYW=x?5) z&)jb014MOig&*VeM)qA$L)zkTTA-g_N%`=0x2n(@E()5fNK{lMW2)Z^c_h>NY~;Bf zpkNy7RchsB7jnh(!b`QIeuk)xn)}4vwfe?-dyXu0+dVf(wGaEfau2d-wGVIUJRYDj zI?aCuzyEG|2;CnWw3OQGsGq0t@NE`OUq#QPACKn(hf;q zti{?>Hf?FQ+w;xfm&&(WTj-tX zFicDF*Oi9&ns0d>q!wj zF#)cR@b3n;f(^ULTjtw~?&7WO@*mZnlTidkTqQ3Z$NS9FpI4t6gQS$t^wSN1Y{EZd zPAor>kDQ16SO9+qNxX7H9oJuizeD$bn}R;zV%=KKGQ8v=_)3XcS31bwO5G`#t%1+X z1h@&YK8fqDihL*;Z3SK_Rk)Y>f$KQq&KF^d_irNak6?hqizbL1O_acusVgP6tJb_m z@_%5rfk9~8b&Jr<#Pkvw*34l{hT7{1lYT-Cj=FDVsg3-0Ma!p69SE&&MlZci<96$3 zFN_kJ{`PC|LFi@@Zy`WhO<_+y1jvY+omiz)pxQF^WG-QhAt}@uP-`bc*Od>$ACy7 zrJzc$ySu`}Ih7&Dv(-XO$DGF-`sjMP*w`zCLNj^hWMU&!xZS`JtZn<^-D(wB-$xQj znYrIsTd}HiaYhlqyoC6h2fRV%*jCfAoMiO~sd4C-Bh?%bc?}En-N`?1LiPBcJS}q6 zb?PhIz29|W4u_k}|17}IxHvp>|LOU@yKzI99M8fu@#gR4>TjIZ<3GpU0m4%|{EC8v z%xp$Jzh#GWEWQ1H!flMY>h@}%*QPJ8j!Gyb*u-YaC1fNsd#{oSlUnb7*Gectjz{@B;sIlAkekuq?@+c;Q z=!{hvWYPG4MO!%XMG))BVZ<;7{Luu5#rj6)b)~^rMc$GcOj5#|I&xnjnb_vO8Zr$D zo9b!Vxy^>>F)U@N;!gg}2_jiZC8-pgL)xs~sSJAh$&n7V**)vA`Wrd2t;53VA{UmL z`l$@X{5Y+{{P?@9O8WKIk{%A;XFrD>X96sbX~dM>dGYRx6ZO7v2v*4p*wmvGsxG3h z^hGG?!22}BA4w>6E>$zH>;G*RhX?Q9U6zkAL!vJZ8qW@j zE6M)~OoXi}%kX8ig}F6`YET)J+p~DmZ-Sd6W`0 zHgdziU`_SvHSh=HIZjWOSJ!^;@@-8Qc@S;=GMd_%6;=->TnqaBeS<~bT4jw;D)mN_ z=;q6+>>174%nvw==l2!qgwwh4-|ndUrL^8R!NOV<9cmL>AwZ3EqgHp1c;3KcT z@R$<*h=KaQ>;yewvE&`Q{--q_%z8Shyp3razh(cFah&q3wmC~4wx;oFU5h0B`qX&Z zG+62y>&)Vn9y1ytyLIA;5kgl{y(qj~Y;ux;xqFZ`gvN3w1wq)#cg_|BDO&y>;T^!y zmU1mPj@NNP*QO@k1>mdSGcdRnbdfNi$t#Qu%jcBv@x6Kxw>D+sY}|@9ZOT>q{+Gkj z{*R_M$TFze)uaT|n)<^?0!>GwAIWhTrf!jqDdJYr*MIG3Nt10A7OU81 z41AC;6T_?TolKy#G%3_W@(A#y(}1s2YJ2BW3=U|Vdlv<{+O4}TxfS|>%4ojIlUtEt zpaL7-sC2x;Y!pU#SM4Z0!71tDA;N`Jm401Z@|5QqROUJ=@UL38Ykm}cqkthPJg{MG z7(eMVi!Y+5@KMvlfzN1Tt+L-6h;9E8YppGp`|J&be9CRd%c~ztucSS_;;l7jT<$k< z<9k%;JylZG*Ew5y87!QeJKA@?H8|?Kj?x)54u5)nUFc%#eAc%2;x>a?+D|P?iXQ_c zdj0XdW3+0Y43tT^$l3>5>(CU$s(m)b!(!T*39`4ULeoM;*cZod@^zB6s$VO>x?O8` z1-N4Ok|mYC*O`sDgp*|Vd_OX+z+E*s7#5Zmt#fNpUUpWDjc8N&<#B$WT{a;;EM;KbE^4AY`7v@w0${#0@V9H&5$v5AK1J z!LCoR7uPx((a}m#-E2>(^n+5_59elLbn?%NaWWsekQG2|`->NUe}E^s4$IPuKM$%& znu_gRUj^Fk~c?eslmz`g`F)At5DmfJ0)X6k)NVImL zndLaD=Q0jeZw)%J8_o*enjWSlA|Kmvz}_P!o_9MdB)wqTiNtB>3R|1L*lSC^CZmT@ zO;mw{2%ZRli3H_PBWEW zQXV$|TURE)4F59z0fVgA)W&aV&1AboUh#pj*jVxtO3gL9wV{19)hZglhG3HZQGx8y z6ujodpyEm6UOhV&-q<%WjL*&<=;(w-joRl~bsjL96-=h61?!Euht?DVjSG!!9dMc3 z0c;q3vcZFT3B(Au85?ZfrM*yUab-1HI}y<-EL~gw%bV`*8kVG1p0EHpPCwb2#td1) zuNZBl0~Djwl1hHs~Zw#vPAfiB7xfng`VAw zK%4%EZ>W$VxLZ6;3DK$As!RKt@)v_!G2|6ICS%8?S+K&ioe8v>ZK~<~8IRfh#O~(4_1?p#KTRp8U$_cFC z<$n2Wkp_$d+R)rA#KaiI5I0x|)_y|wRO;e~E}8uaX#1KAMRn1i<|m7!#-8)n0#OqO zjP@_IqaK}@NjA;ozqtz<>*Q0d<7J4^-NE#+hsNrgGarlcC=6mK2OBMG3;P zwM9VkmTk4zIDd#19Cde+lKCZLDz^}DQ}cuMkB~w4-rLA0GR9KjU#z!@L$=H_ZQ-r^ zVk>%V{y-Z3&ld9!;n>akQL#C3V+%n>#bani! zNRpFRr(<~bt5=Eh%n@FIFOJT${$i35pX>A_q!bgd*B_t9M#Pb);E#ib0tjnzq!*SC!Z`D;^TF z!`8*<(>wj8HFL`uy}sYw;Lxd(RZ+#*7uI$C{{obDST7`2R>lS0-HoGa5R#;5R>D z-8%Uu?JEfqHX+?#Z~au)*3jewB+>ek>{d$v?8KBe_Vn};+`V$o@i%W%XM(jcs(6Th`>$@K+$I z=Tl;=XM)r-CwHYoc- z`t9O%kK19=k}00#Ii~U6WZnsIZ#R-E`iH%1+hs>s9ESPp{Q&sD;&_+t>q}UEMqJ6T z)qkUvhd(d5Kx)6TI9E12eTXPV1>-QZZi$>F9?Kw+5ac~1l~a7g3duRMI%d1@Jzi^} z`)G|sVt4ZbR)`8mNx*s0LyBmn(518-0lNo_C@^gW7>BXU*Cvo!AHc~#a-BZz?&#LP z?h+AU(ATkui5yZ(h-jmh4DY!P;@{rj$iQBBFMi>p79%mRkQt=xaOm3}xt_hpH-@(1 zzJElrGZgeuia!IhL-OzTZQC}P-tkh;`c|i&MxM}I>+J5>`TGOa@oKwL8K3h9286SZ zYt}IYwY0>Y{=R&~*!c?4hZU{KKw45Xjw!KlIz(;P%TsqbB~qm!=KRjvERiDjv3p*w z7W6K^Wm~1P=uxq!u%%EIpA0*5G;tfWTh=KZuR~tPAc7wqe;CAl`g(Ilx&Ykd-t#SJ z*l>r3cJT(bF}r}klU&i%PVHE~uux1?)-FRc`wmW0**;5WrviiA*Bk8EAnWe=PlR+F z=_`BonR`L#^&J5>s-r^Y$Yb*-JT^8H!r2v~$fexoEN~VB89oDPc_3!*`_AG&!1Iqe zhK8-u`pOLN9I2PEd#dK7SGx8ldi1G@zsribHKzhaLZIhQP|gI(EGRp+er7q)gT=DG z6TTk(IHkijN0O-=7?ZTvVNY;i83>m>Ol|IrI**<`BEVKYo*fN-q1+T{Z)Sr%qJ}30 zHKG{bm7$6SA(&21yZ^Av+ITXBT{(3b@6`@%HaY(ecR`ukZVD zG34AgPYf`9eTr%p4>h^T*Y*d0#PCl3Tkw?UQDC60N7BJb=Zv?_J$lmCeZRU2%n9-J z_#$wC3nQY3{d7bWdu#am-+gThnG%*=mS17eqX-s6sVSZAL>p+ci!Cv0^p_aGZ}=Sf zz5dEQ)sb7UAzhVl337DD9+w&D_BP3Oq=)!gfr?&Hi*X z6|ARmd)?&dR5m&LeS+NUJp<)daMU}DC)|c9T66^}G6}?%HM)*=sD7P`wjZr=Wo;GF`GA@s%SS=|i<0I0x5VKPN4dSqk?=Vc$v|!`~nltgdBG zS$z9v5ALx)5c?VV5D|$gg(svEQ^5QZea*<{WiT1PzK~!(BsBx!V%WHDr>iNyc>x3Q z6Wr``_kv5z)fhI%J|EXa9JY;l0m+phQb@G$_%q#2D|KT zX&tdW+-uoKn}c3H+rj*nejN4a{?btjx*5g84uQUu!f4LyQuGj=MlE=YIA$Cs876@r zAKQD7S1btkZST~r9g{$)Rhc6VfZo}S zkS39kKMQoYfd-x*brCK1`ewT5Y@H<&Zub^f;EvB0AHv#CV|yA?iu}Wd&yYY^H`&nD z_W2}(TT3m7LUYA&D?kOow@DcIe6G&@b=8>Q#ic&4Nuf*u>B0RGWdy#*Y6d}%{xv?| z`;7KdK8}#+3vAU(9X9W4`>IA6{;Re;ybAm3RUX6#{aXXEp-uGtLgzX$jQgP4m2Y)= z#EaLpsfplns!TkUB%M?u-r$xQH#@oc19MfwL^4@ltQi=>>x4^zSl17?@ntsWW$@=c zarv_;tB#&%pJaX!S@tWvH9X&4@kEk0%jTYS)-HyoJ#ZOh>zVCW6r5-*iFlLVFA;)7 zW$1GT`QLp|8u)z{R;dH~VbT(R9J?kjp%Isq#U961;WXfB`PPSi*FyX5eNv*X1I%vk z6Um6Di%y0i7+{$j%)a9RkLclZ1~gA~R+S2&Xj*{wHlh!85FWjM`bkOw-cWh2Sq$Qs zA0s|ALk?iu?qsq(mC8_2g;m$B&0_A}Yd(=qbGezh__ZixWpHU4P&180cKK*c?PO!b z8&Cv`T?wRIEYC8O-INi%s)O60%5SZP4IZQwO_2VwO1y0{7+Z(y`3?7_6IZud<>uZL zKUJOa+7ZP|U;p)^TcW*Z)7+cUUVY><_BO5o;!#b?b$}u+KHh2U3bfBaMmAv}y4;34!quU&m$#jwj2!!+aX1r^4Y?b|%lyGui$k>_RxlO<^yX zL5AfqjRTK?_v+{$V~pO=zfIYn>Xb+VLj&WTKo>)q!8-xm1if7G@K7vo?TZ;5gBmX( zTV~wjegL5oAjExb;JddEgm?}b+YY^Rav2c&e|hQ<3x`j@V`wiGv`}Q*I28vM>wOHm zf22}`FPX{iC`%aoLFJLr{oRfJp>*8^m_X~=5RMGx8rq5?yYLZEUFR3h0P-s`KRgR)DS8@Z%?@p}x!Wp+l5K>QRxH7m$ zV=2(woEbo4iLrGu!>f+~cs$4Gvl zgJ(IR`4RVj`vY)PQ`-?EJs^LLU6Y_WLhiJPzFrzQ^9rAh#tzpO4L!2xpI8E5XEzPI znqm$5YPCn~)U|&Wa9^??(qC%HAa_wge~^uRDEeQ~f`&F<9uY!}?}L^tK4ZYK`anE_ zz>WgTxr5gli!zj6W1JB23th+aMng$Z*9acQ^Dp@i`;rR=1n1J~s^%i%;+7vdUL%OEZ4Rl49iJI$-bKn{FyoNBW`l9FH*gptAF30mvH39ZLyQu` zC6eh0Y8&(%@ic|t@-CvS95jcEf55oVh7UNeX>%|1n_C>F=-Guc{01v4E7xu3YS#w5 z_%L~y>Abz{-w(7&KI$hLb+AJyU0u^*qvUBJ_(h8f0F_0E4AfJC;p2g?rwF;4O4kBp z6}-o~A}GQ6X!@o(SM-uy6P1(VCj;?N+5qQsA)cCtnXAcok0rLl%!?WWU6zECPt5h@ z4gJ9xvm`H%!!ie@?pvk`&Mi6o78p333&I!Y>glodwe6>+p{wHEF%x+@mlH)#$)@_1K?xM#(Mf$37H4<+I-ch%7(Z$p?Pnday zbdE{D=Y~*C9es-hq*<>E*ZU(J948l$5S}(hH($tGLn3;lfPV{<;B?y?oghPA z>?Ls1@weDyu@5m>Cn8DAXb~95?O@h5IPMb{ef*pjEptDc9A;huVe}yk{)E_9?l`e< zQ|LTPrMWFIa8RaXb`hqsG7ySTMH5A4sk*)b(p2V&+ID{hW3zIctqJF1aR$*G{AQ^! z*^UgZ9UAys1Mk6F7i`4&)i%Qt?pl{00k{og}HVmD&Z7te2IWk-#m zTD|Lr_mSF0TfyJOG>dD@D5o(xgksEZ9<_{+Hb1xBge6`zjSmXWJjd3c>^LbKqd|6W z_1&m+uLxcArp;C`O0_C0f`oG!Dl61nl@asy*aJj!H;Cfuq}+oT@sgqm{}f|1 zPENW3GJ}p@asuTd)y&6sMELFXLg7JmmBCp_3y~xte5v{cdPKYJ~8`ZTa_d3Ap~M(2k#H8P+_{P*?5L}GibACdK*$+bZrs!4J- zH!r)sw3%O`DcuDEQvprR7S}Ea67iPletCis;#9^m9z0g-b@QRE0ToV!C<zYLQmr zW?H^D+4ijIi{6&RF0?zOvD`PD*8--Gym5MUF2vX_p{ds{*g7f9QS}}x5(6wa$0&%- zvp7R@EWS&ZVqhacQaSq*TX;Tgj=a8JSjfoK{;fquP3t==YG2HITx@VoH%4AR#=>{J%xN*bW|H1`$*t+!O z^B%s}CJuQC5?VLm)NqbUWzE6=6i;3RYsnd7yHZf8%L*A>v3QZ4GUN%@*e|xKH zeq1GL|0Rm&PoKAe)kl_Ta9!Gl8q83uN+S~o$t!izAF6=ip+dkKuK0}66Pq5LHT}{F z6oj#(!WSB9@*aaiEn?*%7Ssx*1oL6OZ=r0h*svx(o_{lTd$<#H4Ljc8xQF&~ zdUYl85Z%L%Nby&LqChi8C9cc)1cj}e6^g+I1Zxx`0AymaTXc||e=^Pk z_biL}ydrFM&tQthGZMDY+>hQIW4Aj$04heyO6=*z6Z%CuHrRaY4M?yU;%9^z@s4H< z%H}{QX0Rp%fqdQCFHHmm4SxS)JPET`oi2;g42Yy$?KuH*8>WG=HaMLJZd=nQp|Bpn zYoE4iH_$A^AoG28*OD$7DbY(H+!5~5?HM$v{++R z`zRzq18sGZfkLY7`Voh`!UTQ2SB5`|^(w`J5OQ`;XV`Z#o-&g@n^X8`p_2zYakDBt z1_Ad7XMT(b3`=C_lE73a-{^ev@mWHsS*IjkZqaP?ZPOruR`eekW)LalAg|Eu_M*KQ znz%NjxUUKkZnZPCL6^Cu2sbX$%$z=eiQ$3^5BB1)+%`JC!V{c*Ez7Q9q*s-ZC7u3_ zw)516E1_hHZt*yV;jCRhxsVh}R$bX_Ht@pD=OuGD}$}Yo1rh|80s^Ev|+_vcc8R=-dWaGI-uD&Iq^J z_hj3lX3npDfOVl5?qCCNTqFlyo=A(l{Uw1p9?vVyi?bq8;|k%}d3QLqz-(QiYhP1* z+u|%x3VU%Px~|?#687t1(B22-*_Eo9L~c681ZwR9$FBxt)A#xh(JnqS1&A=oW1^NN zqS*GPO7VlH$HGyPneDGmJ>=^9y8TEww&p^n_(&h+ zll|qU$cU~C>VvOuQ`pqvh{zwt?vsn=irdNlgtFUQBVeoKKpEVA0OYGRc!nRc-Z#CK zB^H=rc(V+@<@)v-JCKc?qG`zjnlc0fZPEefwd(wLX*{qJJ#q24+1#7WeL5s+U}xg2 zY0i0|w6V#LhgFFh>d_DysGJqz1)XguU9BQb7i0uQEhOI({cY+s@o0Get2G8^8X-$&83Pm;5#hd~OeR;d#&F`7jlkocUfT+=o^;&r{h+7-Vb zxj2pLZ1QoGnZyr1L)3UfUSBzBGP#o*JXlQ!JC88H>?h!PXdfr?HD!KXl31ve6A1V4 z67yyW7{=@ij&-tcr2Yky+m}+?=r^u; zS?jd1l_CJy$4wLQR&aD))F|m_O}`@nEJDfKq3Czq_5@)7J!SUpI)7*!l%!ON-8rl_I>I^8mz9pT0TSz z>#|)euK&_W4Kp7A;qnXQqYIL6;jc3&G?Ejm+2Vg9MR=RN3!d6M5i2rynXyJ^$fcPK zZfq!1-BiwS{7N63L*KYtr?DfZIq!BqLS@7}Hj-Zyu{g^c!sZw7frP%-($hxjGui81 zF|iBU(c758&#k$=!2pATA;r&zOr$am@o_OOTgJP^ktBQ(Cn8cF68X)yXb*ikXI{96 z#)u69{`4=}Df8FKRX6G2FwG|_yEcI$hEpP<9coA2tDpQqR}9qLxa48bW+SH{Gg59o zR-zUhWRvf-n+`9gxTcOHJ67_xqQ}P<Blx$=-e9v_R-+B|4q(V(*$j z^nO1Wc^Q!L_ovYTTA~=m^8=zg*yuF&;kkWSkkzLmaOURjZ*zV*e$}aWVixy?O%B)J z)eIbdlu|l<_=)*wW*Wt=JHvBKFF$f5Ga!Lk{np-wk_6(Qm+inm@o~5Hi}cUWx~0UR z34Yx1IZ+HsIB^zmh-k-08%$K)&SFK} zbS`88e|%E9!a8(4xLUQQ7!2+Q^>>56|$6y%ayArq;;V+yF z)^Eixanf|l!5PO;(Ubp*E_d3jI=TI&aW?UiSQ~ektmv&GYBOQIB*I?$4BrruZ*tZ& ziq`mWR?4cx4>I9GBw~@j1M%Wt%;XYA_>vGX`y?}}fihB?G;FH@807nGf<=95{0!}1 zaSLy{#INIyw^l4E;R~hEmq`P{x-RvAlDCEg0%(E-{7?Me78mSnQnx*3rnDv$n<-t@ zcn?N4U7i|#YnG($gA$l*5twkCl5V3r?JA#yl923lsQ6Yf+1Q|0G^n*_76^Xe)K%5; zAMCfWlz@JM42WYl`e*+i_TIv+$uIsJzZbyhQPL$yC=!A+Y=ne#h?oq6kQPLGBLq~y zz(5*NK}A3s1vWwviBZxq8YHD-@!aU=`#gWc?{|H@F0O5E=iKLAuXo(%K5vH$ziBNV zFUM|4vZ8&|6Ez*qQe_Yi0ZT#f96z|w>E8w&*uaYEtKLgrM^Ci@y@{5%(vYx$k-I1o zbHrEUb81@RchZ0$sXC@~rm}b)m`H*Am$U(Nytgep(w^d^k(ol({llv1%wrD6nJ(C# z*U$SQ0O+a<)~5ktK(DVz`#Ov6h>H4$fxAh+S6*)E7&~J|I{LOz;a?Xr@%=+ozslz9 znT>B~J@4*q7O;RPN0}1B{n^>p>&jROkd~I7PCb+x&U$}xkDm_L#DYB2Xx?Ms%LAN; zCHQlIJ2AP9Dt1n+goZV_!nmj=V!I0Gec_-Jwt%L~@YSh*-*5y?RAeO1?znnvVdnH~ zwX3?uBk7l~nE=C&tmG!R7%L%Yzw7zgnIBmKIGq%TNkjj#$!6ZXWf5vw^V2fkd(+rS z#EE-E88?g=7nsU~EDeop-{s{3+S^q2^UgCi8UoW;{X)}q6}D5@lrvY%uAVspu!u#i zX1{npWqb@mu$Fynb)b0{PTuONFZW?;DyQ%iEMwPy+j)4QdXZB|eWu>a-##5j1hI0# zR}|30 z=@-QUKeDtNH2^6f1zVob{Ors8IMnUnSdQQE>kT&Fu5v)U8OW{sb>qVstyNWKq9rpH6Ckt%jYLuMWSLh7WUcv!GC zx+aka%1!Ua$v4aJK_iIq%Ihx~fOzDTVK72)eaVIO%hum{|^v#%D2|WR)rj-Di z((+B6zK4ff0Um_b2CF}#Y`?#Ctjp(mlqh%tyr+j3_K}6s^R8X}i6+8<+RM>(@*2n@ zc{XvJgMhRxjkm_t`LXFqf9`4GJDC!IuwTgtX>M*la1)Dd<>8K;{c}Gw#v-y%Q{OeD zqhi(&-|RSfH|A|bFZ>5%q&H%1ODQhm_Nq~dEAXZ5oRciKVI^43UyTj5S6~8Dsb}p)%_GrStjVQf>!MI=bQA7-`TvQQy^6p z_{*QZ^6o~B9|_!^%8M;JZp}@H`sHtez?D_I5*mceq0eJIe3z-fxt3Le8({0s0wst`mbq=a%Lfnzir0n0y_TMhe*KhNXS`%zzJBb$ z{eTqag<2OR)#_lDpUX+X)k}(XVBQ23#rWJ#UwbXTnJ>MxXp)+{d-l!QZgDQ4A#(oB zx$?aICNbbmBJR5eF2+He;|C8fBq^mz02mo9mH4A(WjV6J?leN&eQfo~uYlt`abEVC zG<^4V&cZy~HcM4v^y-n=t(D@|x2m)S;X=lw`}N0UW>ZSz6a8yuHL5d7sdo#9FxR~1 z$b}n8r=j6RblHqRVgN#%lrsyx>}tn?FUUeIRoz}5IO%(;@0gJzzJiuN8xi?4tM#C& zyf}t1QQ1;9SH*M^IM29LFKOL;`FZQK-55&u-adY0D2H4Up}o9oxxW#8={Sim4Q_H0 z{X!B#tX_>E3`Kaa z8LYqLg65)ex81(D0x#Zo-w9nTJz-11!u1?RZ*!l#9UdIgP8_^Yezm9vj!C~RfLr|d z%E*NbFQHx7>QX&Y0CQ3QLYF26laK~wyy=40xd=TxM@b#xJq%I`pdGfeR$;;OiAwiQ2 zZO!VbiAGaB-QC?catnv9JOG?ZQ26mX&qI3J{nPrISO`zJoeI)NY3oxC9>JopnHa)p z2|_J&_Wa9H5r=ep@#5Ip$8+|yRWyZICwD#!o=v62!Ah2gYMK_` z$=fKPI!eO2vBUi`t76>!^OuK-?O~bZ?G{Q!-P8`lB614>-F3R&e=bAl``C`UZp`sjx5gyi2zShb&f7Q`-qmZ{^_w_j4Ymb+k<){!8@j(|ayo zujbQvch7%Z36-V>i36py_Ankwv@sdiJL|jWbQBoCD67EM7MS1DfvAMZF4b!=$^q6Q z)n**VJZue`UTAw>h2Z(p*zQ3=${=6j=d{W|MJpgN|<(9RRgl z8{~`N0m4mgEm{v2=eYR&0GCr|xc3~bAua}wAygp=BPDlb(gbF$DL&CezjKQ}XO4`f zoLyIg;9^!^x?xIVt*i%yY?(ln*GD4|5OGBrD4%)eHS0Z*g%*Vzr1iiPyfeo`0CS7~ ztm_D?FGHLvyu{&!0z3wXL_%TWfV@T&fH4CoVqE-#^Y~7E^bI)NJg z{PO1}hZBR#0g%~8UPYU>otQeZ@%|P6oKS!O!$et1;SVUl;R;IuxAM@cX2G+wJ~DMcV_4%DD4gl3=O$G&>I2 z!ua=6Xgs2m`bguZ&*LPRnTTp93mpLG?fC}!$cuo&TR#J_6iDC0LAej*AQd3t30#qe z6g<1vx^Zt~FD(Lh--8SIe&O4Rm%_;a@)jWT{+C#_4*rz>&%ZnpgTLd1ut<*k*=mSn zLeau;a2(@rMwieWmG9AsH$J3FNmi;^PZW1e|78+5{pLNuyag`?(P;XSCDd5|Zjk18 z@iK23J&x6Yv0d$m3LyBHr0Xt%_S4G0B@YKj@i9R8_Rg!v8xDS#Rb?R1rx(0n)epX^ z&KnJ&I|mUt^aRNYrWl;-JrAC9j{mVJ7GOxUry0NqCvm3aY3_ZY8)snvQmW;Z=66sW zGV;Tu+XUXn(*r@b|4CX59-|f&}j5qsLEupElhYC;3fbFh6T2Izh3>eb{Y*!NpuzfKHM^)+K;Pz z`1SkY^XJbSU%h&jR`ue4H1qL8K#c}*_3d;M($9hd!QWH>^^+{P2@7` z8ht8mryFW+WbppTAxk4o4RFb{pEK*kd|qR61bBae8}KYP#jnX9Va1(>3w|pDJkiM{ z8F==URuq~U<2S<$IW!9#at-0@i?I8nz$bHiU9<(C5Iv$N|CteI!(n0$1q8n*`HJ99 zzZvHO51?_LuHG3I-DHdqUr1J)ABLlu0HMq4@!!FnIgYt0pmMof1XNw_ z1q!D?>nh{d?;Tn!xK7_q_F1$DICxPzHjq?bJL^vkD3klzB=n&BY2{HlnjlfZ|NugI#bEs;B0l+4Zyn9*E7> zO`U{8!m<_M>WHSA;yFG_+H#f))Q1vg-1on*c=;Cc)1d;S#&=h`LVrYuW zy0`$9=C{tk^`DjyHpK|_q7`Hc1Xj`9LEQApiJ^#nV8D9D6WafduNKd#a^jVPrI zH)CI)xuH5F{dE_^9T>FsYL9=KPUv9@!en73z|Zdw`nBp^FrrC2sdlw10iCb8cYyveQ_KVpo`QPh-Q2F;sq7Q)}Z)T zM{vk^_DJOfGEHqZf)Z;y<(AcC1;IrDbHD(cFmFr?awuZU6D^fkh&wLC1Y8iua8!t? z?lP)Cu&^xm{~>}CuD(C_#;5Jz)Gz!K6dfQ)8ScYZY$`m>BVrTWy8ynNp2}!qN3;Oo z!-kV3xkW@our|YD{>NRw5s%3^Goz8eX`)=eP>iTDioZNyOiQN_>oH}@v_GEj(~Q6x zjFmV+1)?h6nFcME=oKjg@M#$twNGBZ;reICGN+)4wY9a_Cb36aRGVKu(^e;OpWvkV zaeJD2;<&{_fbc@)BrYQjiDr?32U!5TbZA@chA`Z?!L1ur|Cu#6Hw4@rHAZ_hK-rQN ziEJgfuZ}l3f5682&7yW5-VYf?WfrwtfHyFfJnU|o{W(o~fI?%8R{yZqzC!Fmouf%&PW~dUD>aG5VIp(U+ktOy(8V!;t&p5tB&G|jPO8+p_ z?(`o8&vJKeAYs=)D9Ae*YUdBU`OfbKt#D@Ay_gIw^a$G3YYPK<<*r7)TKz3N_$c;*LrE=RYD_41uDh z>jhgh{J_f2`Ci4cgS6jSvp8QO_RlMF>o}lI+0|nrYHm^REWggL@6bu+3o;?iA zU&Y{rB>OmIF;NF92KQ~r{!hT>Y*0xW>UzsKm;L=|QOo_QHY^kD!dF^v5;gLk=uCH( zgYoOtH`^1n*2t7-r1d?dFCb3RLO_NjFc(7;GcSRT=Dt4eW4Y3AY)m*+H0PQm_QRf6 zbygzPs}j|*KPNqzmrDLawo?F*u%|T|T=Wga6)lnXk!TBoS~r}}^PeM_(!)(ZdP0*J zpqNrG_cim=Cf?wED^V+du1))-I>9aS(p>McQ}EI)-e(h$xQ85^$m>W zkOVXF?4RKvJG6U$@9X#_&5z4IZESAPbO>p zWZ&7Ho6nlI`ie59mL2#{qqYLEn8%uofGjsv@&r6Z40BxKtBW??9%%+o8}Qgy4rIPO z)s>l1_z&B_eHtJxgwL>HFVE5F_nwX>arrD7tmQeJOCodk1$7e=t z7jwTyqR-ivU!=fK5`qi{#PB^BWT?4hN)a?4y?>l?iU%45u^>prMADl>ot$rH7ax=PYuS`lfZVY5tGP`GZ7Qg5pmWJt4ffjcf5(#2t_V*UuAE?;ysATyRVMo#jO4W z9Sb&5J1keE|B z0atwB4sENvkzm%$1Lbjpxp6x7y@X%No-g|AensJ&&uJnrcKu4mPrNzyzNgSC4XeJM z2VhjOObiLUgyW1@hf_CF!9wkPZz1lK%Lczvz3 z7zDKcDQO0W*q_FX3Cf;;)8@hXA&Q@c?c98(q1C$d!Un1OMjmQ+exWent3C01lnq`o?W%qnnvroJO zY5pUDOxjoKT75L&bW_->ou6D$?&G$q_8esaspx$)tx%3a&o_e)EiioQGek)QN`y#6 z><6Ye5GGSpF==3F-kSg|gLuqu>ek?o%TydPQA`BBD=mBey_F0+!%+Je#| zHF#BDJ)D{OEbmkb5(C;?=V8@x=?4#73=ooXi99~XfId+i^qKG8-&9~yUrap0^69K3 z9z6XcPI_7rl{Rm@P8>QgrLw4n;@dYXcTRY}-6#r@?}vdx3IP_L%5X!M5XLmH=1L6J zR&$OV%(UL3D+T5lRK(#UWqXjS*?+MkdV)jILs}(Ux_A}O@P|fcc5rnv^;X1(HI3KO z^&3Avkb~Q8P*X$Z*gCU4%ksMlmNWa{?p`!Lex|w7(rwy13eb{ueY;-Fmc|1m8}X9! zt&bvl#StSov0@A=m3)-vI2^R{lrZ?Nh*{mMbx7JC8F&tEz??P7Y_ht8BASxz_x#_0 zJ-FYkyE;0|_^euot{#0bB8D&l_~7La-Sv|Jt(!84;?ngWOi(FFMfsj*iofH8NOX~R z$mrup=n+3TGbEsSXs_J#kh;>K&hP9y^ju_-V-gXZwHzqpP05{tFIf`Q8XHs(|K61S zacq5;IVr7w3A-19Im%hFwwmcmk z*kt95YwCESW4xzrLJjnmh+tymKGGI&cCtTTR4k!^-zM>>&&=Q{1|-8%HFhS56youS zMt7bfx`h26By$6!J_VxDX(lNaIRgn_hA0Azi(U+=h+UOL@^KylQWX=_Xg4emx7jKg zj-=8Xk%upLC)Bj7T20P@jIR2$AiXy{pXnFut$RN!Dv|?lk)k@Ob`HN6e_TxtE(x$O zw{-0BeSgQgHk~+Mw&5Q@Y@9{JM4@Hi)+Pu#$h*_dQF|Se48QO4#)y&Vpec;r{?Hsa zG+P2pztBVk-=MOd{_eTmn;*75dj>N%>l*D#-mz;RcFUUfQjHj!(~DAD&+}n09iLQW zWuLts$5FRebxh3@o{%1!QMCRw9_K8~1bxS<*n@MYlR=~?V&*nw;Wy|YOfYS8Wzt6T zqo5~(EY2&0lR|g>A|65^4KGP{2-6G|I$3T_4yau;+GI1#t&YK0#jTa0UNma{u6iUp zRlLx2+GtnurbIXx0W=Y9!rbOu1-Jlnicb?U$cXH6N>K~{r0$RdOz?zfMo%i&YvOSe z7UNu}!<1S1o}?M!41N6R%n^u28M&Bu5GIayJlTqb+)qMobT5*xF!)GP1#U$02#xq- zbTu9?y-t4Y%fWRI%(TylnXNeM=&ea!O{2u>&~2rl>_AK1x}oG4Do?jO61yCytp6pc#QwVmG&WRFZ>oYr{T@V_eIS0k z4ao5kfbG9Kqo@}Zd@nW9S`1UotvzobpE45xGrPY^O!m0N{9^4?4vs*sQt9##;1tc& zVOseKdKceigrvj{*KXdfPKfB-<|Dsez0ErxmL@jxkM*;BoT%f_Vazl%cbzlpRMmIA zEF$S!`X`xex};iiD7PIs`5vX(c3$KkSc0EsZEf>?>Yk)t1&saaDfXuY9P0N+e~cPY zjL6(<=IG*3ZXr1f85TuUYyVFraa^6o$A`b-Cep7fGB{({>M}yJCQMVVl(J-oI!Edj zhHJEFXe*rVG;96#Fn;077sZJF_%E{X?+<}5{yVibT`x@|-aioRqTJ~-PPdDl5X*68 z=ae-ai;EsuCaY`Ag-iIS@B5!ft`jzD+DzPB5WFDhHw>>nSF21W2g^D9Om+vK z)Fo~w?wbBKqD} z{dXRKjJ>xbm$=t{-kde!}8t(1pjKKqlA}LBR7s>ytp_|-hb6E?w;Ia zkb|4HimH5q4f1O8aemPk)(T$jD;pi~(Mq+g(5B32AFc=Pz~z8&%3wn`7ZP^8Bz@p! zC>G^zwQ)pNf1t=yaBN?tP58WWco5%2+?~?>qqi>J&1|&*pi9=blM~ScM&f${6R|gh zIfG@m1GFktHWSwtHd1fK>}?GFym@75DD0z?qpM-^p))W+@YSdJ-0*oL7z>&JQAFHt zBR$5ajuAd_n)r%g^(x$ilZ3Dop+a8DJl8=_Br_`dsVt^Y`MHkuEft7(o7j`>x^9(u>~TH7UG3%?Qq-|O;i;85T>nPW<*J~&Yq-STV?yF|J9|$|jL%-G==UiN z;nl-rT=6nI!7(ua{K~azG}q^b*G_xon)Y~uT;ZN`oda9A|3vk@mMmZR=IhKg`ro-B z&Xw=^bFSiTdSfuTz`Eg*ranpCw7=pAUeiOYzzwQ z__8s=b~WvK%H>E7QtL%|OL&QwN0aBoXklaMp#SUVtjtBM7O{U9Bm~zlVQm>eq`!3x z>YLN~p4&RcT-10FXgMx4_47;&NFK-O*<+8M_V+pS5lj2qN-kB65;ypzK-4CzaVQ?` z*f^Q+c1R;!ix?Z%udT-Wy;5=Si8!)uO(Il9$5Ip#u*fhN3JPTT%oEnt+@Lc&U@eOQ zzEtUAQ`*l$jJ;U|67+JkGU#7O{4dq{TQ&S!&HGo6{jaq5zaRdq&;QHb{(tx(Qb}Ts z28mN`obnb(2>`%E0X{d_!Xh3K*6_A06`DF zYzFo2gD((&TVs8outR78{9t&~(bUDl0yqx7M*xsGcL09)6Yx&}`~v`3HUxlyuaLw4 z%ZC2Xw@@G(_W#}={*ZaM{2lT=!u}VltaK&8LS!r#y(Vf_^uZUp}XK#t9UY)AiF;-ocx6Ahl;6GJV=a1UOyALn}uEho#^f zI*8xHT`iri3Nc0YlHWRN5WjzUcF=2%yxffmkJf2+f|x=Z(vYzl~AO229##D zEqb5z-Z*>TrU6s*hgOpwVB(agj74;+|86>b`$+MWiKvsKqk8Q4HsiF_+y%;#f#XtVI$B?DJqz?eVIIzmu0=F;anj*M^eZz`Q@ z>27iluy(he?PqhnAbl3ZlgM)?(D+{tX1gb+D$hb_EV_ArH0}{k5HcI*7)UiZ;-Y%; z+E0I?PJhQMj+DjjRi-lP?B+oq>-mF^IPCO~C!dm&lPj-HW7*$SiJ~?`*CbH5P49vQ zle&w9_lk#bh4JMk_@IXQeomc&vhFtWN9XD73QptWN?aq>qD5h1=t zy;S$#<#j~;St=#kW-9!uXHe+f+&>5Q$yvNRu2L>{@Ofg0!iv~tk0$f=fm6Yt)EkI( z=h10%zrRSD%8kylJtEvZ7`2+~w5Rioa!~3@)b(2$Q2O#mQSJhb8-1n7ita=0SKTy? z%}+lg?725Nrcv;7x{rvIUGZ+#(7-O7=(A4n1VE;Mi;;?A+2z>V69*Q%kb!iw!7~ zU$STzM3rw>l?@#soEfpWke3zlBDjqW!&lfMasnA;{wOWymoi<6D0AWB`2EFwC!@W& zKL%ejB0S6HpVU3qqvb;c3yEJ_?6N z|0?_Q}fAvSsrvRD~~_ zjxxahdH);>oVx0o89bXpcSnqLbAI$J15n$aX;eR@K1t(_ki#bs$Ln36LGk~@%<=#n zN&ra53TXWDtYp!Y#I=)nFL5^ayvpxdS_u62Uu<{o==mSq#4o%{(~YU#q=|Fyy`iV( zP_NE^{|JQ@0jyBBdPqkp9}8LG2hBg7Bm4cs(ul67n^yfih}U;(kA4!PMNJh_FLCVB z1j#^M0(|}P;?xG&7*x* zm#cguuLcWUof6*fa$&z%a-7b7;9N^Z0Z0Gyr2V@SW|VR9dC-291$JNlvSI|yMDYew zwhzlCvT@3}HBs7H9EyAb;4r+Xzzn~qS=Ofv2y;HIW^{5qxcSW@0u|*!p-IyNSq7vm zjJCxG=wCDAX2OX1O6WfF8Ly-KZfk41yH*z&Y`y7;&asm&5qG@P&|iFjxY!-?hY$%< z7F6}zE%Lp!i2wfbg4GWDL8U>Z6Bw}!If#K>-@!-4Bl+oQW;NGI!F)YpV(D~<#ftFb zpB>Odpf|OEz9ov5V-V+UxJElCv_%uD`|+ne?4w_S>fdFse1y&$_%v5wnOtF+sNi+U zS|>CS9odKGF8Sont?wzlOLo@7v@MY&P-7j|*`|$PQkfMgtOh(7IHDKTA zSH9u|yEUh(iC96lDW&Jw$VEz4mmT#4m+j!+!39Q;-yVKa7-@P)R>P})(R9r_4?5rp zQ@Jl;4l-b4f+1cxW@<_+?L|vV3ys04VaI*V$I0;KUk&JFq*Fu;sOaiaWz!jhAg<6l z<}4Ki!fd|C;A(#q*m?nJ$dSEvv&5KMHMlBM7er$+rn0-O@%GyOBmT5C5RG#eiV5e8 z2K%QYcG*5mE1ldN_UL>{Er@#fE9+7x#UW1N2`iO>U8IKjZVnZ{+dZ0Fk3X37VEw!J zGFxy0)6M3rPCfLA=hI*x;U8b%DsXJKJICFF@gzYi^;n(fU5_<(&V`GA**$g~r$>PA z&ph%TESWho!UY!78n0iPZw?*)amP`M*Iih(D9?h)NkDVmsbMKXt*xVjdLpc4rH3@$ zLwWuD|>le2fK=xi?fp+9(^j0ovL%y-~K+v2q3|Ijr z`AI*#6KGhg9AtwF*F4Vt%OWtN$8n_<2TJE#A)Dj)y9?cKJ?e5a5tvYO2EdesX?;`N z;)qh~c;4E~p3{@e2IK$Ik4RlRP$IeN%!*NIP#WBV{UtO2N!rO}1JaS&y9&#rUw2eT z{?M=!D%~PFTjQ?yAg{u|3%v^YjsJ6lV32ZZS3_H5mz#v#2tX2=_+}N%)oZPs5^P46 z@%(BOiVvSg-KA)nBKBm!FldJuBsU>)gl~T%&ewV;&$S~xCk^_ZmW! z=!LK5jWb3t!*>d31l264p4jX+H&26mUc*(`sGa$%N;wV$c4(64K=FjY3d62)ydFh2 zgO2aF8KhAgK;)uAe`AT6I{v%2$pVSoar-Y{3l@R$_&~rN$5AN}>2$l7E;9wOrI|zn z7h|_?qmhIbMQyC?ZVzV?wb^%>hX3|*DnE1oZo|COU&W!ffQ+y!J~V^SrnZ-dcdiZR z)AFp;`Gm^auLOq1Zm|H%Ye@1-Hc?$NpUB>UDEJ(Osltg&&i^ujeKUyA{PGzrK0Xgh zg`doGR4jW5jr9YNMxkZPQ^OTr!_mfuZXcXLkvQS6dwu#Vl8?3kg>oh6I0-drOK9~) zGekQhi;rU|w+sb!&6(-27aO|pgP*TA6Q?w~LbrPHTt>9NsGvB{M6xBgTxQyBt`fy4 zSNs}dh`pn(F0|SHXjsh?1`Mt7k>_S-$L^76$^zu69ld`j#!>_Aoy%RN0U`%fPSrq; zEJXz0uGgHPx;YG|_=OpUdvyrmF;7Tr!oXTl%)7rR%*RA$v^KDVYPvv@7`XU=d6Ur( z+EVWndlNufQhJ8Qc6>>MNOtErtWh+Lgl)OQ{CAWLPj;Q?4s;~`d>xJZr_7OV3zETU z@O(K4uzY|dH~VfhO9@eEyo7)TQQQ9#>sWgTND$K)?RMOFm;f9+hAQ&>4L*9n*|oc3 zC>MKqCpe*yHs3`fVDWcge=p)@PspMhxEI}!MRz|6Tg8i;G>R8a+@@SrWC3|#Tqdy$ zU|d{W1T85vp+X8fxnp@pA+O@lCE7=pTLTO+P9u0MO(*~1B$O2zdsLTY9}rtI9J17E zVSh)a$P^yTr%}b?{|kh^LaA~D2O*&dx{EQTfHM+#?Fag&$~<@>cV|PjSmByGVPWLF z&3UXCzV(aEKj6Oz0kx34GWvDlD)8XpMCmOo1*850WcKY`ud;qB=+5>cSOAf34;p3d zSFh?{*%qrZ?Un+=N=nFfX7rz^4!HGFwmRoE#;4KcEb9Pn+g^L$ID}pqMY&S2{1!Og z``1(sAq>Gpq81achwK{s=n=!|!h|IlLZ-+arCJ<$fo^?x_TOD7`>y|fEoO(i29swO z9V)rmDs6-Qti%G)Z_5#~*?kDu{4d*2fd<);rc$I`n8m=u z+Lh>~HS$5)4ZzM@O{fE~IV@Ln>2QGAe?QjzghMg=s=Z@~Pz#GQ`@wD2pm#Ix0_I@Zi| zEqa&Tkmz9}SUP`t_nur|4A>UnZ~$mWW%hvow=l#GR0xn2GGV(x<|~7s2TYyw5+}Ow zm;5%yqfhQs9QiMx%7xrfca_2agqm%qw3Lcf<`_{gUdz`!hNJ(mAZ6mFLw z=^7F2E23H~um{Q9b`AWvNDXF16BtnEzqFB7mAv61L>d3JEpq<#UmW;A=sZDEfOTGi zisJYtaJL^%z8!6juF#v^vA-LLH6A_c6q?@jo&PH}hXP;}UFX~wJF55w0(>)zco+PQ+LuwLNem171 zsrigW=hD9Rr;&z4{=y)baTKA01c1^X-Gb1@`kQ?40vfAZ?!D+!K(@tOVZ&hq7|teP z$h#T96pc~XPY3*WEyvH$6mB=D!jn*57U@3|t`LVM4;=xPNdOnv4Y#C?$7LA1U-?fx zz?@eWcL0b)IR^akBpF6jA$;v2N6^QSrDea&}Yu`Wo#~9r4;#+jSQhQRD=?54X zCRmhN0XIgEmy3<~XhqNmd@lC?t{4fsLc`~<@5HHI_|LOu$e>?7_LCr4YTE>IrE*_m z`E;PWo7@Br$zDc81EC=pffr#(M4d?5VhA%p{)hWw)lK6=PRw*Qu8 zf8>lhrG@uuyt8ptS z;&x^A!S$5!8#kK5R@on=<<-VK|z zJN7(!TV$29bpYV%jLNv?uNSprq?f-Mgn{M zlFClRtw)2wjRkvscwHCEy1>B%i+M4z_wU|2pT~7bNxbu{=f}Z1Cr!iXXLW(0#}QZa z^O%fjDoz`{eV;C^eiu%2G7Le|t5Irv%9a-mA)2-LvZ@>2Aw<;;YLSqBah8$_AENc> z&&s7&4cVB|>f3(x=^@Kcbqe+JcxApg(8XSakdGK0;pNTJ4BcMhLS7rg9w}Z~eCQgD zl-v`;u;ks^o>YFj1F&qZo>Ln;QH)C?zf((kb)Z|@KeA61cDWG{Lq~fXM14vRS5A{| zlRyOnuo%|a`{SD4@)=)UzJI@i%yyDGhvO!UoKm}Xt^;9$daRNvbxrgns5WBNjTeB_ zia#cz3q=;^=)U9vUnH5RUwwu!V;j5agW*$h>kngBAiidf5_8E9)y*(QFWS6B2b=?i zQkBN{x316bZh-x6At9%-wZ59xZa;#m7i%Z<#pVGrvoT$M<7`kjiTyj`T?+=oNCsfQ zG;aS+blKo?_mZ5S8H@1EOtIC8iJ#Zs6wWS1;HV&3_VAunnq%?mYr(c5b zB7a(bFzq;QpoDSf<+3*D#gshHsIR@@>#b}RV+D+qG>4;XhE%^$i*|s`<-4|mbxd^g zKQb&=MXNkYgXbUmyDU=OLw{yju5J)Sz@9d4_X(m`IDv@W$WJbdBMEzHMYYaXL)Q!V z>vN+*OffuKrWX4S5K%kT2~%9g078U(5CWhDyH}Z)fv*N@Y&c<)9$A4aAjvj3C`#wc zU7G9mD|X5ft4r&cY^XriwGffMEArRw9KH4Z71(TZW5slXnx*TuLRk_~PWsYa3-2=I z`anRRz$pXC>OV$e6W;`JiO?-AdWpm+1AeX;CekN$nBqrcS~w-6_j?fp>VV zs3?(`FfT5cXSvU%d{$y`e}F2BDR13s8IM50kTI36kYfJ|XCS+ub}-$V*M2zzx-v-h z3;ju}SQ?~qm)Sk|?OQEmrtWjENp9IKTdCH4q-IBYXIKN4N%m^v_jQp`&@s(>`0JFIsdYo$j`%01473(Sall*t@pMy6tzti*cH+Xpht7 zW#c^AX>Ffbxz$=*f$(Z~Zta6R6S)dn2tvo;Sdn1vvIE^kS#&>1mNd5Y1cO8Gu&pPh&RyxyImGpQH`e1RjG0zNT;b+Le-;XNIu8!JR&wd;W& zB}*P=lB3z2svpvEG(*zm#%GeKl9YV;&9Ga&HH-eC?MV`XZI$jFxXOSIK9*4?Ge?hb=Rjt)dZ=h|hk z4I03WPI%RD0e8XkENH2_T-R*Iy`1qhmFV4V7TmZ}cqLa^{RcFN1xBJ-LZ{iu(m@q8 z*ELVnSf4R*YA%iVkI;pom)W%<>4DANae62irR-mbT=S$E zJQ7v$H~!;^l83A<{+8$K7r!v#Vw9{OXy&xki=Y1rL>HZ@k$j#+UTxBWXeD9j>z%N6 zb1X7t-JUdA^hPU*1+btugaHpUCIFR!!J<#7ilQt~W?jny{WP`Cjs^C?^5-AWpFryJ z(Ad_-;dbeWy(5BT-n9{(tbwz?oo~ydg@!WsT~zBIeAA9R7WY`-gtb*Rk5@nUEKAVN z%bi!kDSB&P@f#J@K}`Pi7)@so3L@-)dK4J1@#7+f69&d2gn^h`><}{T`xedp%@zxK zq&#(=fo>k1R`_JK_D*i_>J-D;@39L9oLR@x8Icm7SiW0qeO4=du3s^ACa|oM5xeR1 z@@!_whig)U4fL+C`ST$g2bB{~>fJY%vEd^^SkSnCoaw)elEFa;hz?b|f}E-zqHE`; z5lS7zSxiyC|E62OcdM7%jl5@v)oYkK7pOL5=qh#Id*icHRm)O?L_k2V557@0>SE-B z0x(lB`yStRBk&u)EbEDZ_gI$_^OE}W>kqt6%?il7dVULK9^2VrPu0?`xH8A$Xmsx6 z)hs3qfRu+c?MA<_{j&NZLI;M{XZYiejOGGz12)Hr!v@1|S|7_CGMUIbKN!*W-m1TC zxs($BELi*Ip#5W8$&}Waltzz&BaV~3jy+wd>m@g?JT#d1Y~FpQv;(fQR9Fq6ySG)@ zUR`;o11D9>?evnVlOK4XB~JgT)u^H;KY~5QnIU0v9{aSTqpY;+rwv`mTR@nxXMtj{ z<6U1$@pl=n^->%E+Ooe3+(+=uARDOueWlQNWxj^*B++Y~A>OyI5ATPAp3UVhV3`)ntD@pv za3mjEYmZA>lXm^yR+}|&ePw^mdjz4DN{1D6u^YKz!K3G}lAIuQ&+`!345r>=TkMpo z*5s32WzRg<(0-4=@fGOiuE`)03R%`#=r?BRBKV~u+%XrReZYJ{@pz;xre z%&S)IUV|?Vb<5_wW6x>uF8$_=rCW7eSuu?V-IPnhQ%DK5T|O(8+QW~+sGcYj@l*RJ z{PDC}cn9u@I^|WjQC`{p z{=9tJXFu|@t^V|wp572!Tn8{jQ*aAYVx;_5Zt$evSEyPF#oZo>@L zkONN?bm`(|b?YYXB zJ+PFJK|-D=bRPw|PQ@y^dWUiQ(-V@)dd~1T`Y`P(1I|G4SZd|#?af7gpzaCx7RP~c z02@&F;9Y1TAiU)>pzSTPyOdtN%u(}R;0nFP^GgaAk@6F=XZqxMKc~nN@%r09L()GQU0THuJv^}2(!)s)t}qE|K<%DDG$ zTJB_{)nVw*ar97C`cKh;pQ;!S6D$s}37tS}Y_ls(8jzw%g9TS`1>N>Z|gD%LC?$9T#hDR6X(v@*gzO32sWdo;h%wCn} z3A)3NeBOmt&|FG49*0&qX{L<|k^MD>neq{38VWn&vkpO#9y$FogGO~Teae8w zD`|VLJC+3|&RbiA8E8)5wit~eS#1ED2NebHeJ@^^#bVb9GV7q-*A6Yo_%=8M~vx<|cv`|XVK z-BqK~o8yO2K3qf1*x2@p4k7wPCotVT2NCs<+1|XfCjDez!!1@A!vOh~U+~chxKju= zH*6@S?1bOWs$P@;gRo7`tarEv=<`iqj(UvF@wI-wF@tzfs4g+{>$C_Z?$hypi#pCs z+m?z3z~q7Y)s7DTptGdo-uWX+&SM+D!e^^dIyW<^(aA2aQ2BZ*h#ii zq&ms^cwzWjQR@vS{)=kh9seh7Km9N#vL|26{HhY7ye*Xr=*12jkETPcFdeua1t-5~ zK$t;giTU2wYyzQ8eVC{Q>5eEjra2CW0$+-Af4x1d@8A8X__@A6M{#ZP{KH@qzP(oe9D2r= zDPUu}g29Lm+?mpiS^f=~S(d5^7#J*eJKA}?LF1d-W$f^okZgjeZkbX5JAsh!b8@V! z>5Cnx-*~#!O4oR@Ew}G&wjlF zcalM>-OfoXQUfOcJ00SF*X@HA6H6PAJ+3C1PDjZ0^fkIUy4Ufc|JQL$)^fAL%-Eyl z)1tI{mzJ7zRdPj9rgc1kq+{=Sn9;X2@OogOJ5hQ?XCni zvD>qJMo8ZsJd+g4MnENZ$9#~H?l4coaq|%)GZt&c1OOFiDAG(3g6Kk<=|exPCkIVG zTldW#N9By|m}~t)pfi?d>~qo&@K$5|BEF3KX4Td>J3h^jKkG8i!IR!lDO5*51fph# z`tJkW+ES>Gf7L~ejD#Bwt_X`&b=e4Wo3>urD}Y7t`-Q37hIBsi_sZ9az|a5QK>Rk0&j8&h4ytE?f|~^@^MU z#+A-8aRRfx}G&V5kc)PKuuN&!66X*uO9BxBH`Je z!8na%T>QH(gf2Lia;P=l9k0}iSV{`nHHoJ@0~j65sO8C5F>4I`EHgXIldag(N-(doeK! z8R15uObd?iDG!!yU)X-#U$!azxyemzu5>O_paKa;gkYL*3Wd>gy|mNl<)MsS^B8$- zU3{AG&j#@oji+CN@;ZGvE!k0txrP?<5!*+_AHMMBsR_(2#_@`G>yXG@pE>N<+i_%RMNo*3)7+(jEakT!l57)x5v~{fU2aZR2ddAewag&qxT3M%X!3jVMITs9wEIn z%T>$&)>SF=2fQYj5mhw8rt7KgpOIjW7Ao8%s{IYKUW>weq-}45@#|@QxP0{FSkFQ>AE^5Jk|J!2BKTht?!#Qi zB0ejV_rh+XC^_A{-OEJhvv)i%Vpk<8zfdUlvS|@O0lu}zje>GLmT@0K>V4ZRz?+s` z5s3t|w)>zt|Aojzz$Vq-yZ6Ke4W_E4&gOx|XQ_+kt*vD7?g~wIg>@_qaiMqEOMfYZ z6yd*4`mE4d;En;Af!7FELOJ`!9Ue8eXLS0yDUZZfxvQQB_GNwvj`I~G)*#OM9pkjCa;Pd%r_h=>d}U7 zOuSg}XUPQv(w9T)zZrcj7_Fs=+gucDnV|S+lD$X-8kkH@2%-vz#9*%|OvyV+-H`Js zrUbq4ZvS!Huke<8HSf3t87It7tuZAgV3@H?63a1$@LKTAaNKS%L^|XHiZ*1RwNi+j zn{-cG-w+{(jDo{_OK@hCtf*y*Vp0>uVJL<(R|>qO{HL)q4N*$0ZBd@)`pO&zRR$%J ziUz=1GZON#DM=C{f@^pN;^=Z-3rsd$Kz{9t(e7lypBEe8>@~5-nbe@%~Hjnw{W0)ye1!W~3|IBt^61^FH8UCK(nJa;R zd7d+Qf-2~zVq8{VcfD7Xoy;7CTDtp!zQUw1sg#)CD>L=^$e?nQ;ABh7MUyA|kqcUP z&v5D{i)((I$fX&(M!I()IIWV`)lla10gK4kxMI8QY3*>#9*Nmaxd`vKZX?|lwh?lk zCiBi8XOQ&G9>W1{ZPUZW`kI*YYPdNB~v0>y0{i=z9~Y&z#fAtxE7)EnctS3RireUYwY! zx^Zkr{~H2jjDM9bg%uYMQBsNOQnoB?>V2izm4cd=Ha4l(djx?BDTh`>6L|VEXMP@% zvCeRFndFsTs!x|qL`K@p8_hgf>^F3U;!LT{qbi(7?zcMi+JGARemz2Z-0Q|Z$AEHD zp1ouq3Syl9p!}O-=&p(^&_$}&HeaUwZo88Bv=^ZTeobxVD5NPxnc}Ux%wvY{f<+s* z^r`VOtw2+BWGhXIakp0LQXlO^(}zjm(=4y1L28ScARne}4wP_jjmpo698?9WN|?+0 zBWj*Kb0AT(*X2~8dmeE(nc(2`J<6U(8&^JM>yC1mv^?JlE#A+%)t&%e&O%YX4Lnu$ zt)eYfjF%>7Hh0AyCw);#$nMSGTf2iYw#>qYq)B4n%toWvhX|Ej+h;&N!{|8^icD*g z^ogo*EWZ^q&~coix~S651Cq^$_8+H*?BoX9Juk+MgfG86 zJz1VEXLQmW7_18W25M-1_-Dm)`d89RK7P6c+-uFRVbZO!511tF@Sdb2S>fKsnBq6o z3~i8%(b_jq4ao>I6B23cr~cT=ALe4^a497hWZU!mD(QJfZ}tY3F4lu+BS%7!eI@2yKtb3nMCfXB@`M zP|Io@CTTo_%rZ%u!qt&Xyo5h?n6;8LEWA3^;UWh^B_xBfS-RZ=249CNU|G@9d+{JoBGJl+iV&)c=`sr@T^?3Yi20zEmg$L-D!DYKHML@fm-Y*>s z9NW3Ey)uJ!>5G`?|CIIjy`K9lojpodRf)u0`BWxrBpbD%e_qBBw&56V;Yd0Ca^fp> zKW)k$OVNZm9zC=_dj(4c5q0z2b>oh<45OrCUVHduI_qd}3N5SZ#yqV93H`Jw%Ue3G zo;?j-zPVWBCMYVu^r$@5!k0Xh57H}cBPqxBdR6urtX`ISgc=XwizK>-YAccJ)FgI;DB}Xyv=sO zKGi2ZwOnN-1Mi0m!94NAVm6|@wk_#7g>>kzKhl`hd~#)6lqi5*ltQuxmWehs*a5el zPENe$_>`&2;k)Z`*Rq)T()Ea`@cRZmJoHz)A6ppV#Z}2rO{s?1IA$N3v48g*5W|@l z#*4q=y`zU+dMjjWKY$@_pozvEv``L#;U%LgqxG6R55YM4&YiInP4V3+QUkPwRc?CO z%U@(_m1Snfxp+ZU=cK!Ftb+#2e0llp^IXL+Wf~dQ>8Vce9xjd}bjZPi>}SDn_U87Q zHq1pLO9#ldhySQ72Q6pfF4P6p{Og8q!VBMKVDzi@ULJ>|6i%@GzVF zP$|pd7-s*qD-AK}jXQSc(o+qY{EH{>WqT4_s=A3eT5|(#&2l=CI!!ve-bR!t`(wM6 zFXkSSZjWF=*^#sYO?V2KMbIDs3aJbPI{pqqprEYz)^xY`Db3a3uao32(oBWJK+$-q zghSfL|GW=HKY_vH7NX!`7LQ&oQZNoE={J)IqlDaz=$#DZ&;!zcJir z)a#Oo@3#Fs_RHAkJA4&rC{)K^M-hNLd(@X1t8^si?B%^$rXSmGqdzW}(ofrqcEBd! znYq~1{qU;@KSODq4%~oX%{rF@z)M9^w6#M|c(-;OS*srr-?`Bf$H0Y3GV6ex%6OxN z41d=!=HPd=UIQsgmX8Gkbd8$gOJr9$<*cG=5(KdEcd6g4x)&FR9m&pwlYV`|Rp)7y zJr`bu`2D5>?95C8^<(Q&_|DXEEP>|{V>9-}+?`nzE~vPLfxo4DXYOFFnWZC@>Q=|Z z0F50|8RW{8^c^BH;2uy8r>DI8zs3(OJqmd6{_pg^vj_8b+T-zSx+d;XE!?U&8#2&T zzfpy(RrDJ+zL8ty)HgBWz(82|v|K$`@BQQY&}}3c|N98&uX2j=r6nM}tD)Ue!^bF| zciqPzh2vf8O(%6D-93B5_llPWD|Kw-8|V~TcK=clBT3eniW~%^2KoIGN;+?)&~scx zC~mCPqoluTzVR28@_5nIkLD8yl4_VMxDT*x+2*Kv!c1*jUzoK2usRg`WHXB+qr##UCK(CTg&(V16;bqUs3$ zEcJWQ>;dwS*urgRtX z0(kL?od7~^Moq0|S3F*bedRyTuK}=UY(x*O0v79~WspR~Su-dzR9sPJfl&Smii^Wy zFPW8~wogks0>B79VM0`rP2^Pqzsj2Sq+ES1W1oL>^K51Awr|0^R)2ihrC9Tf?9yNq z6xKl47l+jD92KKH6{FXz^!QiqlZ{^Y6}6-cvm6t% zv*5H%2lcC2j#wAwy!d|2-jR=N0@EyAM=6@=;8%w4=%9&%3aenRBov&jF-%)=RRuwmHnYqUt8lk54D{Q~u+>8AJOtf^uNmp(Kv@2pC2A|Sz_GI> zG~e7b>@-r?yKZ1L=C@agn#3CjsW3s0Ok=(nloM&_)vD%KS&6V(5J$K6MSNS>!G+rD z@(*=s|E;s$EV8wrjiNQgR9wMLZhz-Oj>^j>!oUVJ;jcUe@T7PSxo_+_5ex8L^i$Dk(~@2c0U-gXnVscr~*8FY&^DDo0~_oOq$Pna^C$5MeM{*TG%8 zguE|5RrlkOJbQ5L-6PJHfQ8Ub_V|FBzclcHDv3*bKenVPFtV9nNoab}C2WW;8=Xri z_S$50;rNL)3^bqgCBiD~^`-@p>vL0n=Y!q5ouBqAoFFs%dTu{%^Ct%d99+kSmQKK$ zM&jd?24tBxNZu8Qqh%XD4+#-U!kZ)HQR@E2lG;y-!i%D^R65V-M&@)c@YGp2?tLH< z)5PH-#kZy0As(^888WbS%BO>=TW;0TZ^aM#Y@GBmj%`Ae4WRmPh$L4WMj=fR7~;a!1S_i>;w;g|(T4+0HQlNm>~CZC3v zGkUR+tH1haL-!ml$Mr4Ks*nhT{tA!fcwm0v`2`aD99mI1`*$>rX27G2kHNNv;#{!p zQ|ATf%0||OrIo+;WOLB)BIO-I*E3ZA7UnKvU^bKAu-3}{VXfN5J zx$0$bldddX>mw@TsQt2-@so=RVBCDhMpHYsX`{{GeROQc^|uT|k?AOQ%dt(~6hpCK zoyF+C0V-20?HYd2AP2aCz9>$qha^^^96G2I$tx^0BNCd(4i#aX~^ z&E`O|Y4z4S`+l{9EgOc3B;3Kc^{~UQPJFy&QcwSRU z>k%n~Nvx!lAsD{sjK>@M6v`_Lj!{@WNRu_ncSgFCFQ_bR$7|~lk4?uEv6}iZZu?E& zerif$NV~v%j%A(<>x8r`h&Mp~raMGuRUt0e_2T$>+sAqQdB~Acm(gft>pbG4BQ#tC zU(NX$nPfS{kG0-rGyn*~5S@sZR;9`WbO*?WVcTD*qPTT zB20`_7V|04=JYmU!ZNox1b-+qWRj!!Vj+}1m~oGuJqk5P#?%w0SgS_8Ua3%G9f;3|a1Y?&=p=+MTy#C!_TFZNQsAl|dT{tLaF44pn; zXX{!zTr@FZ5ayGP%AR@9%jtrWD8t8(A!XCCivD5^k>i;UCxehTte{}OU4T;6N z>N5$rgWk6!Sc34QK2#SbOA#ea!5oU^X->gxU|ZMhXRObRQsi}z@tpiD!Vs+^Y;%vL zo;K47?X@QFSO(}x7K#j}FX58=BGuADh4~eo;r)j$P?U3*q2s_a^Uv0ZZjZcbPntWlH5?-J2n4%?Yk2`WbtZ?OY#l+*FsC!VPCXQ2xyxh_b zMsaiW=6LX{9-qDEap(wX``@XPN;ogk42c)V9F6!)4&?KoHupzxjo!;N@AuIbX444! z!O>Bg@l4Wavfn?$vY#AvN&V@6?gHp&+6YfDEe=>OG9#pHtBd!*{wp15QX)qR2B*?% z1A2bLCP^ingpOrfwUHsG)i?V1Q3t7#aD~tqPChrIn%6Ya%s7@H zYh{Tn<9KH=EEop*s?B&3x!4Qa2gn?EW2R0!@$lb1?py_5=jk`HnFxQyoY%e2Gi*1# z-2Inp8*so)f!{lh^zyT1Fd}IP@FgQ&^-0_2y@5~B%Ypl(#DI&&19@*UKgqbVVui2L zN%4;RGTy0VN~`j%6dVw=rjHBOxV44oY6&!k9cv6 zv+rT*uyAhUq5D@_r=^xT9b62`f*%c9V3sx|7e9mf(8&_orV43W<+dB zJhfw|e}VRldj$G4o^-whrjY4}c-@WsVT_lN_Od`4-q1^~={6)Wo;NvJ6Xa5a;>?B$ zE~#54ZqP;AzUsiMrTb0(ZpWs5I&~YZ17{N|)?l>s*(gI~q2P<{D{$->4xcW0gE2?n zi?!4oWPN|a>Gm0HtSHyDxmyigIRf3(E*cX27%Ft7ke*P#e}trh=PFYK<#F-`y)~m; zO_ge`%{BJoRzdO9r9h{wEaTKCurcDJC-cmTG}9j=sd#Tds4-CP0W_NNKr7b&nxfU3 zOmtw1hG;OdI{4HShownVbf|Agt&Bc3Fj~uR*x=z2Am=pGyPaS$yY*9`ft}5j=eZzo z?kywCY}K1z+4lm4DboC^BA5JT+u5j&SY1L-@z)U01Sh|VB-T|Fu>NF%cuKPV(^mgX z=LFW-7#5w%{b0wvBKw+)=^B1B~PyxLV8iJ(qv?sMZT!p?$;#uhtt1kvhCN++O z-U9@Q=3~nVps6PHaD1wzGDn&=a-h^+YJsN3%aq7KDUh`Y>TM9|<3xHaloHwhV_xMK zgb&1y#Uwon_TkoFQZP6o>!=J=W&4)?Fp{*Po%a8OJ(53$wAuCmj+|@C}Cf^ zAA72Prj7I=teKNVNSbr6#y8MFFQ_cVtm;ou7o3@>$_yxCy? zzDQfUl$nmllH~T{tu|Q9HFF;s3}(NxRy~~mIcvt#)acq~2tZT@q54=<_aBY**DMLq z0Z^kqYl;Ti>PaWZTY&h!=d^w8Kckg}5u`fcqD z*9v0)51#2*{>U+9wpG40Qbo0!r9bfyLgI{x^I8lquiZuzyXy0xNQ{<2#cx*u0H}$N z!MMT;3SIT%GXX<&zp~j?V7mCoWLW(us8Fv{++Zsm-^}$PJBH_4rC@*{GWI4m$(SIr z8uf0x|!V%S?GQoCpkqA(&O+EKuV$+ z{xL$pkPSJawB+H)t;bG0lnT!?g*|2>hvS{eWTMfqrn4znZ?sNcX z0Y$xI$WI+arZtG?WGDuR-%IGiA|=*Y*5dh=O5qE9MPFo|ESJW!5=IWIxH|~-xKB`4 zbg)h%wj2DO`B*fXts`-`?3;Z3L1Zi z{B&qGP=L1Y_y_a+uACo14b)TE67zTvPndtgW3&^# z4&Xd`8lwGbERfwS1b5JKbsiufSFxn>cZ1~F>H^hVK*ROJWcj2C?~e^=G?Yc5Q2n*z+Vb#1`9&j(pL4eM5N)<4II-wTi9Drkl*^*12>)g#SrtK7S_(T z)&gI0!S7_|G6pVdG0zhK8a=hDM8~k1T!xLDvC(pXH+0lGj_nUx7HCz;9K3F3ghMm4 zbC7f%1J~{!*E^(DkDbt(Lj{pbGeEYT?3obDiO_LwlUw?Jv;uF?C40({H;3rL9X<$m z=gObrwU3jDEO6NR@Ae*zux=bmO@%qS1H4{lkR>(t1gLHecxe z5dJ@T1iv2@@k3miul}G{p;y!OU9t&Qb75PjR)y>S2ps3H3Zv*8>{&3S*bJLU+-PTC z`>+uY{6M>;|4*oC<1&hH`^=>AUF zwVp)syC%ulZ`;cLH2jIPX!&IC^+UyrAlrc^en4BZXduGsVeHo0X#8gFv2-lewK`k$ z?1_fcnLvV-u#dNa62#y?ky~u+wa%$_4Ht5qOfWU5V`IX$X0fu1oY(Q)SopcX=s=S2 z$@B?;SJV>#!6q{pArsloZ@BCIXWG6>#Y7oqee5{>6IFF|&L;w7&D6Qf+sVDX2o-Th zh0&Y|Mh$B|{k8+~hLbh)uNg)2U1+0D*C#e&i`#C;pIm3v1LkMWzzg3-b%Y&Bj9MCQ zKCjF7I%!RzD%FKTDn|*dN`GiFfKxebOx~=w5qOfdEn1 z$W*p?cUf^iZ|Uu7x??z($L~b|$HEfOACfriVlUuS8vU&C9upDLw)Yr^!uBR@(qsfVXsiLp-7du#M$X!@>CK zqt1Jt+uV&vKWCEE{kR;~JELv{?M-;&J?;LCX%AT9K<`e36>NR@D4yv1Ui{#gB3ieB zv(3b=*}m!Ggk#aJc0XP7LEbf!!C0)DJphH{BX9qIj9tg@6!lMWehTP;S;;k2h162E zrR{!R8i{G!zH^c=@}6e+F#8f(9+?DI*-qlKhF5$IR{1u+!G_icqbuf}Bk zZVAMR1*zeE{S;ndyVQ1Cqs3x*uQDiH(Uq-f(jaz`mQ_E*J{S+q`h+muM_rxm6Uv&x z=2i;Yt>mNZf#5*1s<+C8FpxuW0r zg$Q$BZ=%=2lu{Y4ti@Hs&69O>BG7S#XioCl?*I8zjddw8EiWT2HzIq@ zf!AwQ^%-pPoW^1-ps1)tWIy;<(S-oe@-472i882Pz4uw_xVkXK0DF;zEzb!``_9(Z z@U^NCVR@~Kwe2E&?G`6oheao3J@s#wEUTOeb>BsO&M7?}YHAnRGAXENT%*mE#Ht+H zG%#jo>F2I}hCA>_q%QPSD2{}U*4uY{J^fp7PVhQQpH6AGU}ar1A)h+QW~-?FDuKq% z9K7eV?1p;Wdk2KTJ9tZClZ|t~tpOeFqK!P;*l~r-J)(9%3BS_VJsDRuR%r7fM=0to zbCIoXRbU3>M4@obSl@V!X7excNU~EON9udn`An+h_A_e0L?=>&)DN-ZsI71Qfqm;j zp@Hj~%NTXLENd4)&5gHKzf4tMPLpkQjO>EWKGTes%2i$BW37WZeLf=~)WYqOA>XY% zqy#f*neS9J8chAqGRA89y)mA?O{@=GNkJ9HDqGp4s?N%!`Bmm>rr@0)g`hf}lF*}S zJxC;;abA1S3~mp>9>smq;o>7pyfs`wm9{@%>Qh-oW2twfpQ#y&yEwBQ zN{ZnQkW<~_3o~2WZ)NKcXzW7wF$n@w^L_HUybR$;)890@fYc-*n@`-Ls$V^Le(A`lspj_L6`c!N~PByuH`DvuNRfdd`7d6!TYB!YTVC2;w*&2PZxql-?( zlP@Igd_b3IcV+%-$>DWIpm(G0BBr~jq^_-J4$R2OwExaP*8@#OCY}jqY*AG7(%MT> z*1WgT&v@k7p(cJqv>pl7iF|Jx3;)bp{)UW9$Z>zQ9fM)zwSq&lJ+_QqJWc)2iglWG zdktF!yfkt5{_WnA>foCr5f$#tkwqb9bpO5E2-w!ds##Tqe_&P2I=$g-?)Hi;2hx=i zmeU?F>zR?}PgM;Rc$W_woNBf!YfZgXNV;+CJ83%Tx$iN3l~pl%MMku{QfM08kH$!E ztL8T{)_wWLQvR)|@3BuSL|?q0f23mok;V_2lU^_b5<_55qX^MWyvKB4{~wLyx)09b zD4wQiXicVq)2bbKo{3k!{7`kvPnT3Y2#phWEmi^33v8&)2%~JKpVkxEk#z*b?3X-0 z?z1BUs=084-upT$I9$7$QAQJF6DXg^YEsi{?xUtcqFDy~7+-OgGeRzUQt2Nmh1LT* zYoN8$^L=^9T+$(tUlxMlv12t%yN-@wdIN9TzuQnlFU*D*SZrF3&ARIZ`_-BuUBA}o zlc_?GS^vQper1DE?HKHz8fx?I^t*TN!aiQMC0=D|yX+!lY7d@{f=k8Yh%-b2LWB$L za@R^6dDs8uv;tNsDIZ<@YQoV>++V&Dmo9+AY$Vo~b(+GX;OCIrNkCt8L~Bye7gVBU zvM68L0AlmhPyJ5ib+rR^vZ}?*=ZQq|91l>Yu_h#|9Rgh*R~2cm?Z5@*iGOCajPal> zg+Z)*pfm;Ikd3Dootl-6<%zi{d;Oogb*JN1Hz!d(^@VC~u!QB@Eth`siHJ+>69`+& z4f9h&2&KkDO?ALbkZ%1Ss3=UKK~(ccKme(LMtOL|;F1wDE`r-eR^@Kbl}Wg{5h6Zt1nBeYpL)_nc08j+0+;w5gbGOr^65iq)v+!td&u|jVl7(1H?Ws;jxe7kpH%Dfb}$p3TQ4}0Z&sA zfL_7&%2IdTx3Xz|<5jG>cVbtaehuI9?mpE%Y}Bm)iTjcdQP&GY2;P5clZE_ud>hLX zhejg#c>b0t6_^HFz1{mF4YYW0v|udjicFV_1~IB(@cDIUQymY&%>OT{<%?&Q5d-fbVU&*Uh)7ahhY)bum@&H3w%0NI& zaB=3*^~9w6H+o3UQR|D!_EtL+-oF6Q_co-%Qtgw3s^HC9J!nneQ7!ifcK1 za&k(@Hqo_?@ad_Y(}H+B7+P z7hTy2jRRTv=z2!!<3p*^m6JPtJsxvKqa&&aJkdqU#bO;dTmGs2Ec-_652wGHJ@%7% zHruU9M%*kMC{3p2H`8;=7|b7OIG4seFnj4bM_~gqMb+$`UV+CkN}usge`{&o&swap zefp)Qz5HFo%v+`?<9Mz-Y5mX76-t==Au9T&YuH`A!PdKZb_A*<7fgiD62UBqv!Mq5 zC3ekg^1}CUbEDK$klvx0L$R)CFsl18=kIKDOYds2%kEggW4zzDQo?^x#J``unPHhm z6rZ8{t&4hs`JF*u*tE8}!Nj#SpbBB8p-#6?&Beahjc{;pli#Iuh4|jc#G%aI9B}zC z#Jn&3)78sV1TMgSBLkj+G7BvX!Uu8J2Z+1m#cUpZ<1}%{;B*gXh27CJ2U^$+@eQYf zO=urA15x>x?ikMw+G83AA5#i7c{$89M<2-_<5ArqFFvJbzF76JxvC1=oFr_%Q&03& zTM?4z6t|u@BbQ1<)N|iW6Lk2^J%SXTGFMe!Ism2Pc7v6WYT`Pv6kYQp9JuE1>AguI++zT{s$qdEUVj1|y zD(r?RpR}7|;TGJM`90PSygt8dmTl6S>B!)ZA9wzC$o7N#WS`E7lqV#kC4AzJPU;vL zDjw9;A#MqNFHh(Yf;szeYTmMlaW0V*tRwNwFzwQ>8t{_h;K)-KA0_62#QzyxEJuZk zl#VxA4QMZ64nlnUFAWikVA|y7Z@u@&`GRJl_FL2!maFbfAB7u`)l-0g7@oY#3>NqE zy(|k?5P^)GpPOeLib4!7nn?`<2PpAHoiE*lp*>S7SYXL6kSi z8XItk!;?RGOoqQxtQn*_MaSOxy1y6tq^w_Q-)IZ?2|6YPI{_u#`I^)3&f_$%00=LZ zSN@dz4|Dv3KT<3y0@uS;`yvBRiMrPzH90GXK9tt1g0N<~pt+t#QnST!PntfpRT=f} zKdxhMK-;9n-iKYkr7+k)2s>fjXU4tIdWiKG^@ROTThCW~3;u7N1z;X|i|(b^)x88@ zQmwx4w(n4DWh-ipyWQi*beJsC!lA|Y-~6R_1qj$QhfxWYsaWK!Yea0J;4~Ig$cRa{ z+a#Z__APT(CmO}VGl&2zalJ?bUp0NfOig8vs9lhQV6^cc(m3VY-#C)&QCd2eKr95& z*rBmQ1gso`2AP#jaEaY9EyN-btTT*uN;0z>G2!B8?=vy0wJ*1|nbYDG^= zms-l!YC6cn1*>ca!pHl)Ay}f8;-d5}sUAUFj)FwYG4FT#l^x&w3?r;G`S5!oCb64h z_X`aXTPmJB>`R`?(R)J#8A$NHyP)uQ5b-1o)z-JUVpNp%n6$j%)Jt|#yI6jsu*1Ob zBna1@Y3=ghbS=I&9ylSisn~G>NC|W2momkZKgPGef7ID-yyNoBcqQfYmFFhs`^x+s z;SXd3@1>32l0I9FH9qMpw}{lyieD$#;(a+fZIMKyF`^eoOjW@{h}G{##SV{xlUu`Y z@x;z|H7y<)i1qyOo3x*`tB0RXu(5iSEv?(Q9+v#NjF;Xcdj*#CVjy~(LlOP{MB-CU zj|c;JD7<;2Br|RH{m-UoHp!&~B8^^#HWW^U6Gp?Odb+*^$;i5jsV~)lX&&;370Y)J z)`+fkBLN{54r4&qQ&T>OoxJ)gG1}+i?O%5@u9#H2wT_x{rJraB7x^X16Z;sY zoz`iJ(8v|-cY8vIjig6Wpc^k>~{C22Xr)_N2+!HBC+z~gcL_B8hE zz+3cdW7WAFAWB1ASDzN2EQX2TTstItuWO^ojAb^PR z0fs^8QXV(W21XYvq`6xN86^{!Bg>X>LEA%zwe%%dKf!{Ic2=RE@@9$rKo${q@p|`1 z8Qd!E$5NFv*s>?aJnZU5noC+!u7p7~i>d1_6_>9X{FxP--MyepE8gH%7D6g9MUvjb z)3LUp`UgJajo7V7F7~453nKB_Zx7Jik05cHQg7U@#&LWXmPmJIuMwW3lXU&a`bqSX zflr9E!k(a`-W#s;{wMMtRubHbO#P>_*X&EYbis;}1?gL)Uv*E0QK3%|M?XS@i}(+v!dR~UF^9oUd7!)821z*muTf{&%~3Q+q(UL#-$zf-NwvZBK+ z4_`}7G{oz>I&y9rv_S_vrq}C)RbKR#$@>@8AF;HX>?gi<*Aqw|lV~llXHbNB(Vs47lc0pp9fw)Azjw))M5FHO0kK)k z4P22FX^j7iz4B9QuDdt%6ijNqt&liUMZ2mMEJm^huD>=3{bfzJ3Z63C9<9O;SK$0t z_|Vbf7DaI#AH;VfPFJ!!0s}}N`qi=R(DmYYjaK%#q6#D53G+F#kQ5 z9>F+U%b9{s>0-yW@y+G8tc^$I_YxoEf93nSMR=*#ra`Ur~;IRi=G zY*&hp1sv0_TTYkB=r*#L<)D2R^e>EVCbY1PuVdRp(Jv<)-SCe!pH!7yGD0>of+s|Y zu?$=mKih-A)Nx73y|Bk%hba3NXue!dx}X*kTw-|c#mKi5h->@%nq*=vkDJ%~zz;P& z%&S0Lyixpq=XU!j(_Filur3Rp0eNKkj?wwo-F{UfEDYpc-#1(<=T#w)|FQXy_W&{H z84g0PRPX+ZztBXpgbN7d7m+=y*7=9ylJ$2yy%#YE1ZCZq637&TfqwOCc5HOXFR3{< zAs!iNh-nWm6-tsfmb*SH&!{zq8#E_rZAPrUQPKa$)pV(I3>{4DZsvYkvp8<;-VxM-{q%Zt=ko5H*BIC6v(K)F zJ&_rnZVIiY$~Ti3Q7t1mGnc z`A)xv3I+FgzKQl`glns>U!UgOc+ol7`xV38&f<$-hQrOozYTe+f7K}Y*r9oD&XPJ! z>!IaaLtnOsds#kG_@QjigiT;o1bGHd@*(;AIHb7$;HWS*!zgHS?TIldF&=U2jE_n( zU-wWi=<<|q9TZuwO@=tV_C_qLZ)t^0?lniE=}SgGOXYqvC$bhov9P3)ElY5XTix)aAyTD`^(=I8GKMyWZTQb=*HQPRHai;8E+Ho{y+rxab--U2k7tKTWcO%X5O zc`#%UWy&JO-NyPXT1|&lnhhW`ryR?|zFm!ipTg zt|(k*YYYyV!bZ2z9YQIO#b4;#qu$iUhg+U8_;v8EcifZIPzwC6p~n8?|9HxOPF-m! zsd-Aef{;XRbRMnmjJ$Vf(m{BAqX(b14(KXZyBBVprm zia)hMTrTG1)Ue|e|GX26P_sL4^eKwkG`$LLLEA-8U~^}Dr|e_18Pk+My!RF=bPLzxFZ#_t8?j+mi6q#sKN>q$`wtK&D~BoK2iZP+Z^5)L zGfY|J@4f#J_m;7LVhhPn#BLO7V2><<`q2RG*nPG`oP;2n^6u$EHEd$XyYcGlc_Vf@ zknu?xbt9{M(W~v#T21u4w)`5_+SQzFeLqG9qEhiFsy=>xU5=arowCMh@sXTu))}PC0?VjK zDfB5g^ZP(u!#@Z;NC-s&u@I^0ud+>*2{Dpw{tU2AER{D0O2=|3#lYu_aG8_QwFGiu z=V|vTsyKjP*PM49;m%qK$c}`2(93TVxU}2(Hz-KAaTbe`p4)FT&=8#`6~%C@o;$o+ z$>sG@8_k~BxvQ!_zN22ed8n)#sOW2|fQ$3>X;)>>@eI-a1i~anvFlHB=R%@$9>3o^6k?7z2seg*Cv$c1Nc9mqD{-GPGz+tf)o-Jb_L=#7+ zELkA`p}7!^v0E4@wC$sXMs3~W{M3H`gH1^QB{rLlSGP(GFoRfT^&Vz||EXYlTxiae zV%F5dKEV)pE8OIOM)^QwBjUq%`zMI0%6>h$V0nG1{tiF)cZAf1%A1*ErqLio@zgdp z4T7OyLEi!IYwgZ3?qOVi_%{n|VYQ)2xAVT7wRE~Y@>Ey2o2x?H&gVO1!2II+{N>4) zc3L!~jRNU{wUUZlgbp}n56lIST?q%N3S9+coN4V{oCe$NNF_fMdijHto0;vPh+rb8 zHj}OQ+isNrXsmeYJ~v#CH%QsgO#P;AKzc6+{F}_ENGavmk!qpw4qNd?Dy2Vc_$|)C zZ}}wV4hFUUc)~HnLjW%O4m9GQA(u`>j7)Dd(Gzy4_+#laZ?%D*PMXPp>Dm%f;G=$S zo85qvE5)$S>Zdx~ieD;Z^Tm&h#XtBs0f@K)KqOUVLHYL*1ZxiQ;15PFE1Bj*Y>FylA-b4miZk2s1=h+o9dr(^sO@ZG9{R<-@M6qdFYME zW>@djhR-{e+!mH}wVZLQ|CjWOLiKB}6D40l*MSzB!s$gz)KYfVB!Fh)bJI3f7!pEKu8oyd)x3Aff!C};(H0)D8P_Ts^a0lLrtrULBD(f7Q&BtK) zlB5_er>w8nl=CI>sdRJp$!VNS;7T2B?BAd5Aat_f;bCcFLshc;UC5&O#nmD!Bk9S; z713wLdhTlJAuONOn<_cy!7C3J_KuXcHp-jTIMAHx@!t% zANM(N(A0R)L2R#=ejmrrMP28CvNngLw~v>9DoH7TUrl}8opm!=8Xm(4I7OoZ4gQ4O-A}jY4>&%#4+IpF zGY4HuvWeARP4a6+n#B#hxs2Ps^gH5GqxP8IiJ2M}3JrPFEQWtYB`xDn7-zw;(@ngAP zt3N<^3Hhnx=20w|_U&{?O<79H-{+w1GZ{bfn90*cF^Wu@yG8r(q!Mt0JQ_-o?sjZD zHakYR@zZy%vjgUOv5rcG1DFEaKw8s_Z-E-0Eg?E<$ z6LOh(4mbMX`aPUhN9LoCY6hM*DELU}{Puvc`~2?nWHaZW_Z z-IMn-ov12*|M$8>qG6<1=a~(v^F`)5smoMsT_q*KmG#gD8p`yya+Zp5YZy;c@P*kw zu64P5NK&^U_}ZmK-H1dtjuIIO{}B@Ord*%}XQB~mqb}ap2d6IH3u@Qz*he^KFYaax zn!R<8@*U3LPm!GQ>YefGyb3xb5XBiibj(z+M?k%ihEcm8LLW=Sq3M2q>NEca9DwZ2 zU1JHXid?)ut|;5dl$b!DMpD2E#nbD<6?BJmyg1v?qIUd+Nila<^O;s5q{6c})o_|} zh~F`I+k*uteHJvp<-*a}%nRwN4Pva0cZF_7PGz+{H|iPwOtbuYJ1@92&O`p$4iTJMEp;44}xOiLf-HbCH ze(hYm0~ZMA00wjN$)LCI&G1k?_949*;?!fu4MG&@ zetT-~)f?ON|7$3wPx=vQ!OMSyDK9JKQehW#u>bJmTfhJ!@n8O0>swF}Z`4tIpI1mf zf#V;QJYC9b^&>UDyjexRq#~UV3vI~@r+9OUy-|d`yb12xA?UNH4}BN`lZFexZiIhy z4Zr_qCP+9cg{*e?{LL0Av}VBUDq|+0qu_)$KrV(b7=@TdbFEqH#_St&X`6H?Y*s63 zQQDy5vYG4uGV3iaLaIpP%giB9$;VhiAnPTeB%nAV=-D4R;=`+g2RSWeO=>JI={HU- zW~LH9pdW6d!}ghRz2EYQZIny!gmL2#LW&Odk#Setl*DT#-EHv);a**wFbv9)CH8w` z+lw>kFL*2aQ})|HYRdx3h!V2AnER_FSLP<6#X|;M^!&`~&1Jyq1}t9XQyIOu#AWS? zZ_sT1hbLnM1Nm)vhog=}UN==#D`@f;!6hPO;|M83a}GPR|KPi?uaxHSkQcs%*<&_6 ziXKOu3R|3__mc3)NaWN{vJ^w)B_CdF#ATF*o#s*`7LAqw`=7GP%$E1@ho0x^K{6&6 zQn$%Eqq>e+AWaEm4J02PD*B1EH|nq&l)7hhsS2Rq7hBHpvF*LCm8mSA&V>*;jp#Mj^!R0LL`GWfo9u=(isZP0Hi6a4sD?4d-gPz_+MODCR~zxwp( z==5Z*T+(d+6TIj=yza2^NzF#jrXxE0k?vEqn|XREYVT$>o-a$^90m(1|B>~V3YNDT}_)<{x4iBz+>E9jZy{ipL4ATvJ&6jRn_ zeRbKq*6|@@5cdr7Adm0{nQK#3%WSgUEtJ8w>lZ~#P}CVT*nchm2#xCq+`C)oYU0*a zvVOH~&l(Q5ocd8fnR&c@==I~xi}v~@DFzCgmr3UbC-c9_8h8Kfwg*X#u8C<%P;>KH z`2AMv|6=PK@B{8((N?ojeXlxWc79M^Bheu)>rrB63g@d}Z4Pcfq;tF}J+Fm%ct5qT zORB_Sx~k|6w#;(Pd`-*r65c9&IF+e?JvsvJ${W0z2!yr5-MCdtY|j&gsb zmNC4`K<<{jR_jb9_oCTBvv~Al1H2`pijdx74xuR5ErCtd6{dI{)-Ck#bS~2b1kmmg3 zyxJk_!uU5h&zrAvN{RcEL%+RnSAVkko`*^qm9;#fjhA@`ZRP}ywsFk%Ta z+D~N=0Mmgl=m@_SsF zSY%J_0Y6nocpO8^df6(r>jtB?9}x^G&Z6wj0w$jLxK!f4#5YNB92p zho;ThAsbN7ROrNCRG zOZ1s6{7)1m($U@Fw81Y2atuA*#eF54eKGLGrJtoiEw)fd%gjvG)s{8KE<8@N={ey* zJ`VTKzi_i8VPj2Y=~G97PTYF@-O`#{XJW~KA)$2~nd}^sQLax{dubN#wWqvaNPYWT8PrS z!t}0|u9@-S%!5#=+}y#gqm|x4|3#ef;PZ&PM`z#N9bX?dZQQ@eWKjxu60Il(24v3O z9<>ZsY|;Z+Ow+twz(R|m1aYc&u^$VG&u@O?UbNh+}ur^zT>jWRHF|4!L_FRq3L_ zT70*L7!{K^V3=>HNzA>A2z)#yEN*Relew!(c<#8oZ51c`ALuT`FN@lyHg(A zcTD_?$w|u+_UU6<8S-3zI#U3{y>6g=hcs)1Sxzow0(zymViX!)S^j-P-r0e;#S8}L zI6dCbYU6n8_oDlX;dhJF8A)DYO3K|p^N?%P5*pyWyCsG*?muf5u4*gCkeEnB_3*NU zeh4bgm@|pJ{lg`uqR8M-rj1{7j@t^yvoc07^|vn)Ta>EHrIJ6LzcgblAljn(v}@hM z;}cVlEllh-o5iyEEa1zmvACy2Ik+xJygDSlFAJu@46*>t-_@x6u&2S2R` zmpRELUAT7q$?0fOD+==M{}op(rsev1Lq?doacuUrswbD0R<)K(;dzY;T0p+B4bS~Z zpmH*ETld1em9*}#K=0*tdJ^iklK}Jua`;~RYmMal*Vm%R>e|8=#*R0dQqJf(pv=P+ zpb)A%;vgD{N4S9|Iw*;YP#or0CJTn1$XhtLws<{N>dfqLIJv@I;#O+N!4@Nbn_%kg zI>OYIVG!p(#NXmnlO0|9tz?+ulq4e86CM}Gu*YP$;Iz=UiD#b2Q&v%paNH_T+Zlyd z?U|R|so$vOC#LxOSr+`^wKqN{vEHEiUUrQ)ly#2idNkI4Y4WB$qj){GzNrPSa?wKy zPr}w12mABk2+e5~h4J zwPE~SlESyt2F0~vZ?o{%l?Tq`T8xTC(0FWcDl@%}F^ zfv;_A=XqH8eI~EL+xa60JO)JVCmq5Evd`_)%I&N|Uyl}Vr#z#JI@Rt`9J}ZIg(IfN z`UPFUBz79%q!4_H4rBvD2TFvjPwrfYMFOZ6GA#X_^1mY)?Wv$CN0lSxY?$+wdb|Ab z1Re*Bwjh?~BpT_!`ka)#C)CMYO|``xZCLBYyK-BzqrUyJO5UTz>+UH|&ashkW~2)h zZ@FqWWNyG&hCJ9lCT3ipd@D6mg4!>#->PV|IYJWe;^}!G-pBoDZK`E+#JPbqe7sJS zr&&Q{z>?wecOvj0Sk4?9`s@z-UB%;XCiKpG!0MSGj`_GdJwOe`LOK~}1hBw@2D`D% zxLbQx3h43t$CpZDpR#YXI}5S66igTYyErpz?p+ySt-S+SxLZyADg7Mei4OH+| zjv?zjHC$@jekcX6m7PwdUDRtlWtcz20Q&<*1&42Qb=9---2xR?{!3ulx)0f>It*p+LwUZRecHtA3 z5)S`vNl`XwH9V99*SC((@M#`YiBJQQPFq-cfz}cI{hicw0hMb!7rYvfvrPvi2i0nb zWL^vl$)u^to-8yLa_{>764Pu)$lIMC-=katYk#fuuW?2#T(U-vEhATIVWN~;bs+(~ z)f#P|7V% zvk6^&;7H4w`uP-39X7*tNO&dgTGn4K3p31Sz|pm#i3!R>%4J0x1;+#%GFRBp&!DAC z#dU~ZjB8%;V*8lcq9$rr)4do+Ao~F>QweI7GyYtDPW*a0Pmq#)snd#d8(w@jvU?M& zYbBP43k6`PI$Txgo2|Cp>)T4DE9DOG065f_|yOl`u76 zd9RPY&K#05Vzz%ppGfgqSpQ>SS|vz|1CdAW$~x;7i?iB(*-)ciQS5}#+=lT(7iBpz zS_72~b4$5=zFu1JyV2r#8O^x+_BGxA6qK#sG%QX{#U0viG;z4QQ5Mwb$;`tSX9`+CN|c#QqR+16`$>yJ#shVA)t6~RF?Np zf@OTKO9?OxWtzUw;(c}(XjqU04;^mzaV8Ok9Y>c z?c|x46beT~&o@T$_6T;nlw8)`?_1rhHo)W3FIF2sa36SFFX6tnnCD02sS021XEt^C zz2alkvxhcE+KBy|$Rb=Q8E4~)^kLGj3JL{5T|qIsB}C5A9&zgvHA`I)v6sAxH%Nkc zSr_m^bOefnjtXxw#%R5pN&h15bZr_3Vk-lYah3Qx1hbeC{A2>Sj_tO$w40r^NlSB@ z8Qa80^*u?9Y+_M}=r{`zTU`>!B*ngvJNA1LE8m073ek4i_HT|_%-InAi+2#ZxR~X2?e(GzL#F%^AD=*iG6^-@Bcq|F+Cq6<$?gxYo*rZxH~W@3kmRC{+~F!a7QI?CC~fpp*rg(Cd9N<8w5 z|7w)j8GKfsee&~f9w366cVQw#k;)3yy5%u}^>-@}k{hDfyrRf1s`7ksGeD5@%T?>7 zpqHxqIE>so%k%tafuOfw-;{%byTZ4EgB`t-xAt1ceI2fF&^2xb)RYko%Pl4pf`Z(+ zkRAN{12Uv5{b&E|wJB^=N^Mqsj?ga$@@t58cE2f6Q+RRv8j7?^7YTD0)Z~T=_glj z3%fqiwh+&72#SnEj3`Yp(vN~KxM-&#+YByoKV_jX;K*R}m@JU4vT-wgV|m4WN}H2R zeXjJcSXJBC4t2s+B?r2eAUu3<^Z?gMn-uRK2-&8V?wL!uN0$QDfMN1X0s)acF|D7V z-k<)Vt8_$M1jj_AcR;WY1076h-E1Fc^JP^vT^5DifcYNe%{y9(a=i^SOS`f*HkF@{tj!G{3)! zgsZOQTjot9tDo#IChHSC8Lx}(mo~gBG~jWW_bT-l9GX?|_i~t`MjUYQ?eo?L6&0J; zbE1$+28{NW=j0a0#J+m%?QrQ}Z`k@mPIVc-#f zrYp>T$XJ<4Q+BPX{T4G7!2D%nQk>Q6qq-yndaP|oZP@w?<6QQvLTtc4nijN$^w^xT z+S&stcaC&16x;STu^dfIE{H>cUcO_b+V|Q_rK>{+q_K(AM&)0W2u|<^Zm zJlOorTN7l%m9FtNcE{HWi5DBwb8uJX>6_5z{kV?$)WX2<{zDW&t(DY1~&xmA}xKi)tfHsoSdp?t@!|aSw^hIX#Pf>xe zm}64i6JtK#Gv|tW6|vK%Jc=^s%6VbrE3*r8*}i4`)$gxsWWiT~>bcJ~rm*R=>a-+K zDRUNuPO@9Z%O0W%38uYZtp<0SOMhMfB0s9j4( zj9-eFv>N}Z=>moSx?B?N<5}xBHTLd^mmP2wRO6xJCmfvSZ!&C|BOnQa!eyG>2noDA zP*(U=9$v*Q=YG24Kr$z^kK}-R>U4+eyi_{jY5B&R0cVn%UR{bZ9zEQiFOukCV}*PD z5HLvPoTzKJ$2+F~h!ZbdnO&hnEt4MfY!%ss+lq+U+<&jA0k11RGAsfFrvAdVtx!Fr zj%!(bcNH^rbfJ|s3ll`^S1@}@>F(#_$G@f}?aWVJ233urP;GujqiZ?f1arnv*;Db< zbLJIBBlCqJ;K!fN7eJ!>^Rp2W3P_~`jE(>q^H+Xk1W`eG!vJ~KKOX-XV$e? zI)CE$d(otO61RQP8do{RVSX3~^qQMn*uQ%2fxtW2b>K=KlXFd9I*z?g&o$ChXaVF~ zCd66s?YkJjL9ySw@qXpjRA*)KkX${;)mFQaqp9l)%Vv!+&E%g(-Lu5wtG~37BBORW z3-{qgyXZgn(Z!GG?2qk{4IJyo{543ZmeUel;o8X2Q5N3;(kbl5&`aojZU9jlTiBvpK+fDFL zsgQF14)FDhYm&lS^-bHpUvfE-n+L)*NbL3<&|kcd4qh^{a;Wi(x2XP zeUK-df2(}Q;_CWRcVC7H7OF$L{r4eRAm;B9k>QLOS&USr^0a3K`>|&5w})D$&fOAn zEfs9vb~*C7XV5Ci@EIK}BjdC3h4Mi>(OhmY!==P3k6)8lNkD3u5Ocj7thVA9OjZ!9 zEqEFSbi~itf(z2fzN#kcR3Kkct@H}$3-Sii!IxZODeBPuU98SC!e*v)}87UZ?6ka!C4a!b&2eI+f?CIg{6W=s;W&Q9NFW?O-lB#)zY5ISd;r6Vy8vMc!i)mj$t_ib zX%!})zc(Oq$0n{v^#}!^ZGZxN?_>VWu9SiviOKYuipj|M_!)WOwKgIwlTclY%~Ot( zD$#O@GbGKK!!`@dRT*J;sVUQ;Tzy`KbS3#T;*p)6`tu&EZ5k^*!#N-c=~xe%nL;rq8Ok_nWL z=Ri+-;AfoB%x;?R&E?*)1-(9!6t23NHJ?IpTYfd%y0AXjeMTx}&)Q5K5eUkhpnb3# zp4F>()izCHa>nJic+d7^2&J50M~9=oV>cTsd-WUnzG*|AQ;%MVX6%wl1G8w$5#^)O zEsoh*Fgo^lVwp6Tfm}Jp1PvWo#}B=a@_!;&4_gkt4BfKF6tPs@;TDlJ-lYW{AxI33 z&1|qJt4?>p7dvERZtj32#WQfbXqX;?8AXe5Y*Y#gY`88l*7Vpo1R+8?148e9j!Th! z6`Q>$oy?7wh60{j6CR=Q?*%y$CXLv*yZIgwNX&9|Qdp=e z-pF4au=T>OUug8$RoN*aY8vbCHEIsSx>WR6xxja9fqjbP80tSo;QEmfPePi(*fb@P zX_|TbZ8I|B;tczJtFprUu(*Lnff{lbRnUY_%op@i@W{m%&v)xL%$6_FysL{K3qUqh zu$n2WD(>G6?wXz7T#YsUro@5@>X7?=HawivTpd7TzhQZ1-i1ReOyuU})c(YX$)hve zAy6G)WTL1VD?uYtV)F40ILxhtXDIYI`SUULq7=tN^heVb z`E`=^u=-N}2oW2o8S32a;gKX)v%H~cvqa;Flt9G`;wUgkVv&*mvRh;c$A3I7iPq@H%DR`}OJ=q3@>b-5 zFRU^U66Umn-9FM$IcO2dwN0!^qe>=op-JdAr>79q>i{k;%-3eBo_F)bGg#OIKx$4@ zvHQiQE!$pc=_X@l91(tLt;WpV*V01E3U#H7_+3V@_N+N}QSU<6dbdKx`D3*3pDtf> zySF@JpqlihCs2e@g+Ue>NU-mInP#Xt)dHh5!DYJcH0`1=b!SKz{-z}I!<>}e74uP^ zH&IY5rW?Qfm*44Vi@+ESb8-7icQ~fm`Jhvc=YT{JB*20w@+s>A37-qKtoec@1oCNR z^Jfwe();z#^AV`C?pR5*VNev)e8(OE+lb7cKLce5!yT)-iLalQUJ`R>cZ^|=h$z_B-E+z%Z2 zxQAuIdM6yyHRQrsyIA?}Z-g*XdbqlwBp;GnS?MQ2P{-%oKZ^|JpY4Pt8sP2rGI6LU z>jAK^heW`qD1sYWM+ z1HQyFRjPiPb$tICRF)j{eQP70*mJ4n6Fl*?zir7Gl+4;4%5dyP%f0Q$uKyl_978Ei=fDXa%BM@_P#Z!+{a*&t9- zf9BLP?ar!xMw?GDob*N$juH_#&0f!|(;!DmPU0#AdY9=kRUqb<3FA|qmVt#?Ns$Eh zeN7(jvn07?=~zBL<;Ye|WltFl+8UH4zu$U1LXmjnGOtagsPP%6MWt~gotb8n$(fYn z+I8yK2@ttItsb6ZC+bCwI_mDDdv1@E4TQ)coaXt&b5d5r4 z8qCMfh@P<_N$rCI%j^Vs&A&dF8451P4 zc-OrP*3J&|A(^QAIbT7SA;ASiV^c0@4$z&J-gPg2HO7)HJ$^kv$dzi`i~~fB&~X%H z*|Dj#lm5OHcf|`p$DKT<=}qs?*gxZXCnblaAJEXXOB;Rw#zcYw{ajT>C_@jC zRlX3_Q#ZrwUJT~f<}*OA2RNb5!|*)3pBux1wwMWR9EqYEK)$`VV}&Kr5O_@*2N^YZ{<+=l0RK%#ufFLa;RRbVpo!!l|K#_efWhzQ z{R4aBK#>ARrEsV?Tqa}x^iUT*c1TlPo|(DnS0i0#cjnXZDn`nL?|6FgmvLISbq_!; zCZ3NkNV$NkW-=P2BvtYy{6I;IV0;ViI^2;hG=Gq}z>W|!Oaax`mFO;OXS#mk2>rrQ zzg}aoCTlq5c{NgJz&b8UOa?hUAri(H5cG{4&hX{dvhO#RZuH_5FBh>C&*M&;ErQ_>)QN?;Chf7)2}kr#-`}u78XG0K?TL`CTS82KUmJK3lzH&OVhb-{ zgcJS-G3l*$`}^vib9ji|`$ACG^7?OUu}5OMqfcdRuJl`OFFvj6*?z02wSV(H_s;kj zj^AWlc$Y(cU`M4#o~TmK*@1}`;$oWPA~yVXz44>cj}In4VZg8$`Ou^c*yJ?`MPr5b zp}f9PI=8pl0^w4;?x%4VkWju62%R224BAFEQ{v5sD=sF8BhOp0*@TUcC>5*vkRZxq z#}#JfNa{*jutBGVAYzn&$zx&w_i7z+&l$}5SM*f;N@So^3dpRHve?bg^9$J!SjSZT z6TZ}Kxnl3)m%-ui1K2b2e)>W^P26(gVsWIi(jg@RnQwWB9>g2GI{eA2B?eduB9n+2 zo&qA2$K$!wk^Zz)+9XiAZmR-QnCzLfp3HF-B#iH*R z%~a9}YF(4I#UxTi3nAnL3&idO=oJ;LEweP;=4P^|mL2~&Z}0+yLZ9p-K4TPEx=5%j zSEwun_UTsftxn~4-qF&owdnX)Tz7E5&l#{h8^=<8Bde*X7Telv;wc7x0Oqi`rS1uR zqg8ynUEqj!IH1KsnBxaw6<@vAjr1s=H*;LCnH22=1|zjlC2xF3 zCOqNO6#g|TPbFo!+*1KBzIyUB^3pJQBKGgMt5Zi5!yI)X0eTna(iurOg&isx3wjL* zN^M2gKsmRK8Bma7X-EY)fk9{Xu3bun2{Ebs5znaA&BOzF6$`{f_s}8SuVy6!Jv@eU zsc6kw*!!)hD(Ru>44H)5o{jnur#s%6Tko;0z&S|6J@^kkp~x;n*9a}CIv?gR5kCLc8U{o^Lo zi;5Pq6=~Z8UZAx^9Cxtjt7 zpvktEyN1gCe&iOpDnESa_6d8f{9vKXtaEq>VtFG>m^=mijmpp{t;Qn`GxV6~Zo);< z%UeA~%0(eSTc%u-G{6TWAj+8?8G{+(lez#Q4+aL@0|LtFnKOSS zTc?xPyK!e+2l#efhCg$>@7`*AgsvVHO3gI8XAiBK};t- zybc6U-2GE{x1UrvAAkiaZ`bjJgh)f3mTL{6&U!d#Dmb$7!~DsVAKn`)-`Jwy0|Sd) zWg+hW{U`4ISK@Uh=S&|G#l8EI`4uPE4sq}gr)WdA`R5J=Nlp)`P&-k-8J|}CJXe`* zMSwZC<&N)UU3|#O(I_GYy#i%eietF+ehH3XUK&~fQKxnXmhkpy3FM}P6F<05IkfR}D%{3#kXnQg~=r%g^7vMIH8lo{Z3J|I4sGj;112?ix8 z8$mFojhFI6;g_WV?|tgLr9NI9=Dp(WbFvKNAWVR_w{t*7Yu<_PJ64`)#+*qePVSy> z6GU{InhT}R%&&`QsI|6;Q4?~boA3E5@C(44R*->OTU*a;I72$<=>isa-uVP;1{BID zTfgZpU%ZcQHJtSf{^CCfX(J18gl-?)3H5i~dicv4sP24feuM6Q1tFZj72@-FQ;DcJ zym~Ba;JEetlib5+J)cqlWsLVGujq)?AE_lXK*-YUG)d+LIhgl*2lDXeOA^BtYR8}? zP-QvVN~&ZACMHX`cOsrXcW^u1q>`{mx7tPNs*LFr&%!ap zbBCq#!o4VQuwM~z78wxv8p<`Dy$Ck3enN@j-o7DhJ0>Toc4w4Lw`0$H`2pq|$PgJ{E%Z$2-ml-_QJ3@Qmf3sP}Ch!?WW@mr> zu3GP`N>ZVJZ9?8x8`b!Vp9?j+A~A>`JLiwm0y}b@xK$VxxoG)HAaie{jN>+*i-5dl zNjqDmQX(JD4fOZ-zxZ4@X8Im5{{=^5-Q(X3bh)JtHB(|f(3yWvo5b(V>Dzg85G@2_ z#JMqb;6;PPNp}4-J+9)Ax|k(B{FdTHPyHvuIQAl8|xZ34Z zm~)^S^LX0#$0OGs|CR^4mGB<1vmE(0zXwt&+pZ`O9Q#Is2U8SRcJZbvzcqpkRPSZg zr%muOJ+JU|<=#^=abAQ=0=f)rPl%|h44o;60atZZoNXLURi#~f;Ww}%RHlaz$+DA3 zm1Puig#%TA+&WF&wkA{}q3;Y##(TPaGtLj&FUsrZj!0wvpr@%=)en>o&!Y%@wzpiM zr|&XYREM{;ML<1l^lShdv+gKdJDiJyel-)Ca57<$Hc7F5^$@)Uv+0uAO{dxupQ9G{9W#1e5;ddx)d@hG+3o&BI&T z*izrLFq>*D(E-99Qs}$@VR(d3`QrN&22il#JCc$|K4~5FjD&j5%WEBezXx3%m=fwq-;JbY3FM?FgQG<)lC+63I5yV|FqU{xmJYoVWA+k!88Xsl@ z+X0db?;`>8uM>d>aQwEJ$KZ3xg~7iWIb4s$Gpt%`fAx4V7Xl(wfOM-W_PkNvxsMnC zYi*A*`qKk`s;|~`MvJqpT-peqQ-A*?UQwEh6B3N6gkdIrc?zU5FX~)41@*W*UHh|e zZ8GW6t|SN*yp?E!C<)Ql8D-TaC#bx#A`}YZ5)lA1YGgduIbmOE7tV(1jC?>_TnPsd zub3WLPf$6NhDtzw=_d$7f>AISn1~CwEgc9T)PW05TBgxubg#1F2V#I|N(``v#q~1x z0<}!v%HmZo7bbiT0L!|T9HiDMICpI?FPUkH^))l;OleYK8yL7^O_W3^<-u1j5@u)p z&_AQfPi>ThWjxUO@24%nz&gH2M+|<~iAW=M0{!~le0;5UIIp8>*OT<&oufri3IZ{?Q}c0t&9 z1Z}|m3p9tn<(W1(K{>g|F^7Sf+k_YRe zE6tt(R%8o>pvYFqtbDS?T7n{9l6B1$h{kMQdJDL+D-Q1SR+SKrM z6z}rPgdvp#MRkv?OY)i+z!Ed9V9T!PBKz&tZ3y94jHF=369cKR`=c#4BuFtNY2Op5 zl)cTQ7=i%B-ce{a35KVfJQ!vD){g$E;r~Pw0+2@P;qMV)givbd^K_@IFCGyAFuwL= znHIuv2;3f@cEvEn%#V1v{&&)xkT=S}#17q+0x7vfOfDaa51^Kykl$6gs0&vjLQpjj zloAmqK4L><0}OGs`+a1>R8-*pJ4p;L6hS5~>e>K!iyU5dv7+ z1|_EaiD)ql_DjdgR;XD|L-~Nq!$s&pW4H-02L?@x|KW@tVFfbcjP3WM2WD#s6|3CH zR8mBTgh16(=fRute`r-b^k&-rQy~Z@o5qc5upkO*R)KP@#7QoXLlnsYR+HVZ7D8o?;pZfRa#=kC zB&^O2uzN zP?_1p$G3z~4ZP<6KqwuGIW%uw#%p`MVG)QuFZqmQF?mR=8v#bC*?L={xFtlFz*!CW z@ARO@2QTF#E;p7X43UC1*A_1^UZiehvJ(DF9GG|#VsbV8l%t0(oeb6UqPr6)zY(M6 zX^(KE+-Fr+n0iEkaBgR~6p97|*zm}_1+gvc^(yMPor<9P#(3rvm0aSm$3z(Rw7=ja zCRC(PlGrX6GY;>vOG^7cEy3(_Vj4Kg&o4kk4ODXCDi?gf$U4&al!0x>Sg`dbFtYb^ zrjj2}n@aFVf&pFH8&nB?a&gx%)3N_BG>ijOzgIOwLlR(er0vKl%*vxs1}Z-x46l;N zYPL0TWQG6|oV?N!qP()Sz&EmQ?5wQkWUsP_$Uuts{$ZmCF;sES=Z%+_DwW95`O-30 ztZ5!vaxvbD{Pf_3#2A0gF@ny+YdiUbX`YxhJPE40viaJ@Wrt>4U26V6c;Y04ghzr7 zkC-4R`0N$6^YQkd6q(=vTR;N{8-l=U07S5(zW~trIr|3WRs)G%2(XQCblkjMxbVN< zD??OqgU6ejn>fjc3yV3O?(pgVE-~h=C-ydQ-v0+=J|;06H~2z3#)O9NPcvSU?mUSyxwwZ03xX#XbJ( zG_Ng$`T?0<#_HjvBC(p`047092$h}+Q=}AtcxeI*#EQCdjlL-Cy;mjzk4$Y&nW8BBqT5nr`z9>fd)xp*1c~b5(KOR zGH%_vbuKQWomZv{o5`yr}zv#g2mzu^v2rYtQSj~=YpMkre&W7_%7M-d8-{Q zSc7)D3?xF#ilHnCwb|1Le~RZp-ahNl zI;Y@|ob#x49D|^Wc{LeDxKRG7FjRL5p^WGDA!#KW-(b~$n4=*9o>-^xz~e!Yx5=>U zxa?Kz7D>3z6Y(8k`!ZWPAmaEQtCwRO_=y{k)52r}F1H{*eijEVab9`$4-b-|)03t_ zWBAN%94?GGc3FB>a+hZKJk$rLz1S2{tUn+Cz`C`RQMRBkifHfGId=(5WRp$*LGkka zk3Rz1?*m2amfsKX=#kAMgF%tfv((khMU-<7(s&Opo{zMvvZV*sPf8Ss0ro*~jxwO9 zX2|Hgo=Do=*=duDx%I00+CQKm0b4J)Cf~&&AG)jVmj5mK0AB))d?!@2ISj$Ia(gsO z2o3|tet?vsh7*E`(hG$Z_sD}eA;afi6kd7s=*IN z;R4=rEc9pUt9;tZIh?7}fh7gObl$?80j_CTC}@lu@ysVO!OCv{Uig0+Gfi1TE#kuM<9}2=5#t zc}lL0AiD~5v%GC~WNY-D>LLdd+|nkQ0bv+;^B&Yty62JgCHsQ7M93k8x5Z4@ZsL&v zlX!--xO3@0G)Kov%bD}qHE6zO+{J;Z=h>;-!ZqVU)1R+VBh@0FsR&r=Pm^vhN z#P0O22ZhO8Uq*W2f7nK-(;(o2-vTw#WC>4l5<3xj?l7;N*@zN4;y$sPr-A*_0xMqo zknkQGmf?R`OqT~!e5zOW>;fW$VU?h)Cd?JQU-o~@JN=5VmW9V;^ArwGisPTRdWAGz z!xLK&u6kOEQghg;1%m?B`*{Q>L((dP4jHfYK40Y>XtkCqgpV#(5N1jKX?^&I@!T1?C zIYFEN_mY?(MC83KxU=F#gnBDInBIoS=BnXPBE&drx#%DLazL)ejF4)+y+v-sg?a|aO8pED1d6(L-h}81>gls= z+a)nLYs-={%l)UM=~p;+n$l-3PMFR2&kV*cTr_o;mNV{a@A9wgO;-sW+)YL5Z}fQn zGNC3c>Z;gIM*bF9)8K;yD{PJWVZ-~pJx2nv$qawH+~s#7{?DreV4NrSg5Cr_#+y5z zfBbgdHHCe5uDQ_@eHM0pwybs)cta{k2Ub1hd2?D`+w4n#Mp%+kXrX01M&8Bp1PQ`2FwR z=iWc=eLni=4%zJ4Gi%=Uu6NeVo}J_(LczkOu4mIR&?gv<)co@T82 z@Ei9^>#dTRPw|f4Ww!-B*S?i?oN3}lqG3W(mx=o(7;v(Mfkk}Rw}%4Xj-o}wjc)%V zQ3rPah{lXbU{fb`7u$L_^=K@rA$6_G6=YeD?4^Q0g)TbKNx`tG+tPaxGmN?r$ho$K zXsFr|d(uy@(3ij_7!Hae`%Bm1FaP_fL#xJ6b{RDKOE%rV|jC&svc=_E>N*8>2Q0w+=Vba@V?S)09_9SH z&tvco?k9^*rWeOO=TC>si~ZG)W;_YwLzcz398H3$J4!PamMk_gA*3Xf6gvJ^`W`Jb zOi%Qq;P$a2c)aBF*@dy>F#M=k6H8NjlJ8 zBwu_N3mazr`r9@*moS5&x5;rFg2WB~pyzS-K08jn<*2t@eO;oE6j+<11jYjJ27P+? zgGgv%9>>1s50s<&T}F={wmsyrZHBZzMgT7iaF%y%xAllv9(w&?ojwcyjI7rU)_%^Z zf+&rdijL-}>sgOmyEk|+TaA02>zbXK_;b6lY>kf`N|sZpXq-EY9?n@Ew`wSkvm>!` zA*7{G`;gWQCYByvgxLtb(rtxN3LbKsmZ37GSP_z2l2PXtp~eTd;TO9QaQHLBl&3c@ zcCXQ)F#os4c7(Sd6E$_$HBQp}J;cMAcJJl+;Ct#@*HaWa_6`4;I5E%ho&yZ*47XL; znBY>33H^sZ=P2Qd*{;LWmu%HW0UI4+l46lNCVrJ6f<(u)3(c;CpO;|g0Ut|S~oKyh#Yl>cU8WrUZ`8Y zu}@VFDFI_)ZvME=*YIS|!dTp7>5`9cTm3(b*CKGz$kQho>ETZf*+r~>{KoJ-fcF}D zm>zL=w}%hnArg@(4u)U2Hb%Z4W$mMD7uFn~trWQhF*j=IY)JT>YwdH+7;{$%ADqyR zP+iRNWY8P>rnHCcANy#I+Vz)`s&2^G2T^GSi`6#>cEU`ECwr7S2rl%}jCYl6CzfT9 z4)ky)It>Hm;ibm-z(he7yE&4)P=Hr>3 zOvu>3brLHUU)>;#7!GkA3RT|2mzb(UIPU2~Z@|b{FD-ZN37m=J33sLvDJxINnLatm ziosKYCg4Ei7V7u0)jFIo^C&6dDhJnfFw#DO)}OP})LxJ<=M3Yjn-;jRrp9`5aY0t? zWUs`^={*^`8#==}6qRq7X2B6k;%T&POKW^YOq0_E%)X7pr;%do{B#PdQO7T{|k>wOR_SdxXvj3oq^*T~h(|D7%E}>mc z$9DSw+wB(|YBvX(26U)8l)bD5xWbUVLUM-EEK1mxuJ`*z3AKlx$9#<*d0?-^U}wu( zn--EWqIcJ-ge5)1j;NI%cDVVlhT@@i{g!tzH>Rqpl)}4iRLLM4VqjH1D{m_YxQ$}wvIwPWKA@wjf*bh$H`5ht^?uac(-lw1?4n*p-+G9@SBhkAIlpz2rY9H*=O#u-_=3J3aM*WC#ud* zyem`Muzq>+TTdB>*U7b>#M-#kxHY}+I<%3XH#V3K067ItPsvnweLAF_NeP*xw!QYvLCvTFfPUytQXlfd|BGmXGFH6jSj$f zrkS_XO`SzEti&a8TEcRW(dUoi;kT>ezEAa)?Pf^6RXg5V_2a=xAze-`jO0_fUyPLU zvfn|>)}4z}{^q?dQJ18*#~u>Kj3X)%@qzAdJ?)Y^LtDVzs#)n_&y7_+J}ayM!lFAIqZrS#^jpqsw39Hn_I4;!N12a#$eWNVH$chS(MJfbxm@`OvudjN=!R5Gxsipgl*VdPM-#c2(^oBmSF?Bkgv=t0a zGV;An^>pxjEEok`piM;7Yw5u?&)VI{kwb0w80N1B-CTyBJd3y#sOI)@Vba4t3 zUr!*#jkU6MT)XDEejn8^dRTaVYcQ^4iHtRk^984zUJhh=I=GWtD;6|{-pJ*z+tprV z4f7ePyxyE~6}kKb2sCIY}3Hb3C)JAmO0)V%ax4H1N*SmK{3(fl4t2J(hTGY^pZVB- zQ+xmW;otoH&(YieAO1kxFTMc5qgCohJp}Ih!oVNB6Gq4LkJ?7K*ee}^!5CV!&*~l> zg)z{n*!cto1&0tr8A5`C0(~#3A#cDCs`a!u2tj*yBnk#solAt^^GB}1;0#Sk5c~sU zBzRQ#4g|x3;}vl90LO4}i~z@L;1~stQU7lqum5}9f93pljQsa|0ysv3^1Z;(4;=Nt z(J&GQ=jaDD8{`6YwAci-rC5V{UmlN#;1e!4V07Z{_n+O&qkER>DLD!ogVD?G+b1o( zFH&aL)?uFv4m5b5j7;1+MPU%q7&N$}c(fFq z1O_7!Ed`SRHx|QOm!ua{a=xr6c3l#V5#RnQQgXM1=&_(k{8cS6iAeCZDEL}bDnf!; z6k~opA~Yz#$4E*nLIN%YN`#{&iQ;=uhrDkR!-4~Ru6mj)iW0@)lHe3ST2fqG6s;e5 zGa@`B(AUe|#o6YFsF=9;_DK;5G0}bYF*hRdp+SD$9#@>~ZLM@LqGA#v;7SoOQDMFK z=xcgl&IMDNl~=Ofm_#!;lTmESKVA3?X1nsO{7t1(SJ{(gk$rf zLjwG~+^<}|WMg4&rH?}YJSmJuq4;b_&u#~MdANcbTAEu}s|le{XklL<_i;0Z|w(BKYsA-8^=+6%|$a z*B#6W76@xK0m8p0`GpBWZ2SjHURIVQ1o*pInOm4i2@-_h!dqAJ2@wTZ1d{4!a4I%{LLj2$iFF1qTYnSHdP4q$f`Z%fa5y2Tg(1F|$0(AT+4CWf>VqyLW0odM1 z7=n#gfXENyi{_2sM+oqOM}`ory!`Qe2!3960Ej)@d~y5;K5mwqFa!%5$4zzw2OIO) z|0j?nj*$j{`i&%%%B@=}=s%Dojyn6ax+?n&TKo?rp{4y_zAP;%EAf&>|A8c7&AT;V z@5th!!lKwCB0G_UMp+fT0;fuf3t!|udvFdd`Ws1Tg@~HUpXc(PJ$dvn!&B}zk_64` zUsjZZ>x%N9=RD4Qa6ijP_&1Ui9#?H&Sn%R$_QSN)bb{1xB%y_`+%2mBH_Xd@@+dtc z*;Yej2a*I&UQ2pbRs?RCm6?9c5-TtA6G?lwkR&9BkBE3u`XcA?!?aLsthP8o1pW(2 ztU@kP5s?W+FCO19Ii`K=@J=YP@tul}jEqRUgTJVC^yqPU!9SoRAV;`%o#=4-^r>Uo zN43TN50qSQ1X-LpckcWdonwdpfD(`1)w8FGXOL&X4$S;Jpaka^zAlCk7Z(B(2LA#n zf<&od^;WRl8t%?u?PXp3YIMhUz}Com?b!!rbKi8Dj%I-D5b7 z!-o#Yii`c?B9w}S$%V7VhNpCoYik}+IiR#3gZ_mel=Q`a?{`#7T~%30NkQyikcfz) z(Wp}v7mdLUk6|^`4(?Y{klD#aLTXm#CPq3xZ>XpsFS!#z!cwN@=4Qs*H&m3DmqY(X z5WtJRCBdBTthUO2qC8AyCw>?OK%s!U3=R_I5egDQJK@752+FiDJ)x#{P=O#1LlJ)w zQG_7U!0McqDnX4-MM+-f5Ag7*8mOs6sv-`E3;co(z>Cmz5d;b)xDz=rzL@_P2*|~L zzY+{ylymd6k?tNQ0VWXT_voEGeijBpf{#cT<1TQ(3(Er5gTZ}3yocp~6q*7*9CtL; zyKHC(+Xp@~!r;*^+ZVvWPXgc{3`Um;htYu}9Q=i4BK|vzgk{qG{=D^z9RuHmVK5wQ zYvYhW_{>1i-;!Nc!|SQ{6v;0>ym;~ch5Yjat|rZ$wWh`XL5ZsTEw-`yt-A#ZU1Hr2 zsnlOJYFln^XZXkxy&kExhfcYnJ5Z@waMJGXVZ+G%yI%RF7R5QXl*uE4;QInXMc>i%gcU9fr2l52vm!>Zm@OtE2 z+I;razlrv}TxBI8%%q^I=kk%;MX{%Bm{X_fU7t4^#y4L^3HPY-A73!A*)p?m^OV6MojI&MK z-h)Aw<#*h4ziVFh%O0r6j+-mkuu8}gVaplkRj>?n3k{`A#qcU>dHwU@o(~(AQrKD6 zSd#p-Q0}?KMQb~*e6FzbJ*Pz?x2lMdrf6P!PT1>Gg{F-%PL@k~hHN^SR0lP^?bo*XsGI&sl+%-M5 zva-_q<;xfAph)l2gLmax1e0qPqa5SqQXEq^CGvt=iocMovGnO<&i}GA=?QXqA9Kx> zV;GiNnYMx!sKh2^ynYcsYvLF|wX6I->pIbv0euf650DQvSKtPdZ{HbnF-L`)iRY{h z`G4mjAKi^$T#f~s)7LredWQqXlP5l#D6&yZv9`8OH#avwBKDekCdkLjRHsm?jc#$* zC_#>`*(Y8@-Ak|Bx1J8zWy?56HpGYaDIo^9&XWTij9;f_VEVdV_! zj8om`eg@_l_uQWFDl_@2r{;>icMXv2-6HV88P9lS3vJshgyY0h8;_}tvmW;=Y&;Bn zvyq!7O$~~7CL~*P!gJDpDPdbgFg~KKX^fOJzRn*szu>hV{JIfKu59Zu+%NyKBD;L4 zqcgrvPL@1+qoiG5G9d{2jT1+u7E| zZT}gLz56MNH+uv#w(b=aKGTfJpme9yE=$(%61C(=Z3=?f>-aqD+G|3f_9}2@v2yoF zH%2(p8VpRwqSLB+nFVdBk~PWGcUXRQ38=yn!Y1iClBQEdEeyRr zTrj>5o`!DEFe!y$t7GFx-`~D(ItC-k?=2j@#H^~_#fVPeaP}KCyQeTcMcpJDgNl#t zsuY?xYOX$!-u2!`=RZdP#`j=;kPHt!E=5Nx36=hllCl25NlS8CVAvud!>Xad)$fwy zm?#0qKrh!lX`AAE9fsf_KJsZ@jLZ(jLnrethCJLwUCfwb&c;&BG)4ZSSX=?KhMWl_ z#pxdV8h%U*0i{qhMyRdutwJx88{e0#zYqt7BIPD0aVEY*7=oYpXsKn<&&;?bV9n{^ zaLS5F)6zKSa)HCfCGLNL$v&w|YXYBB`s=2RR|;)^WHSwOws--=jl;nMZl+heU7rqIoXw8^SYi)S_ zGJ|6hJoH4SmCJXw8UnrSF*Z01!A*Z@B6fW0^XIZ8jPanPHbmAR)E_%+R)l{h`kw}< zms_}DfDFaW@h=E^(`j#>fUgub6CP#ON#pOz_XGe?^1K*hY69rMq5bon(>2Q$Zy>49 zy>CL(W9y0zVTQ^C9NM^Z>znmW05bG{B5xy+`*k@>tR6PsM|Fs_4?~sN@tR4U~1w@*6%LNyVbu`W%mi7U0EuOaEmAIPx39$x&>WJocVo z1NBd6<oYS$X>>aH3(l01=;q+=^hFL#&!eVS7ahcx?*G~Q${a;hDwr6_?!6?-skE|I?28}Qzc28pk*WynN zLlH$qMRF zp?(dggI=v_+eIrIZH%uoP*{QZT|)exXg>eXhhc~snT1%E={Cm$!xLId_u43tkNvDr z2FQa#YXMF4mx%2NNrjtqa_k{Bo5Z>&y5l(%Yw?tIC{{y}-|P@nXv+8jc8+AJ*Iop{h2NZ@j46HL|IUV0$h)GN*0yc{C4NG?Ij7bg=2GBtes( zvNn9ev6z&Nq1H+z#^?uO+p=Nnzf<(-34-*r_mw^q;ZrR(v_-Sr*5f!Ce(qg*XhDfR zpKFU0>tzzc3?~rK*RNkIdwO~TP7dP>*bP}RTp@A)@dHa4>6rHkQd{_HNk&+K^rQp< zpG4};fr?%|+~qK_c@8Zow=!X^bOrg%q-5>&mT^DQhGd48pa4!vLBubbK`SjIAhZp0 z;3jYF!u9ySj{9?>d6pX&qeM6eG!9!tCjJ(;cpURv>-ruNeyqF5-lS!m29@}F*}oF> z=J?g7sPn`8l>);7wJUj^HJd=?P^$QpIFueMUbE|-HC;H!ve~M|8iiY3nP#9>MQ1Uv z3U!O7Y(k}>-=U;pEfv1%R_ot%;A@VPf`4pUs!Pe(Lm7JPNi~O&872=Np!!*%1#y01 zI2IU@33PZKiV}YcCGK4bwMZ^Nj+On8?RBl(i2p2vCJ>VJ2z9krN8VVBxxzEJS@U-d zAKHH!QG~ma>7S6sFAe(qD;s5csy>=+UJ!+Xf_BqvR8NPref(V)w-izZppl{Xh6sqa zXP7=H*${cvlQ4VafSc1f0KY5egg^5UcW+DlVA5+}*yID94y@JNl2jx0x@~*Xt&oU--zGstb%dOBnmhPMAZZv`(7$@hS z^^A@9_rkRK+v*f3qRz|8&uzt0s3{l!3ifi%1nJ&2RS@b41 zVP@lEd_0ZAS%#gt)LbQ#ohT3Vt<)jK&0J{DVlL!)inhTM9}RY`{pFrbOqAEa9|pZ4 zDjB-p9^dUUdYmqV)yQ)0LI)lwrTP6pqzWQCY?6^}IdpTD0`L zSyo&LHoF6H`MUz^L2zG>Z$kSba2XpQG&PuSD00Db>2tw*_WWF`-FVdh%2b zlqD-Gt8QXqfN-8K;*=9G*B`XHJk^0$6zn4BZQQ7- zD5C_^)3h@!5UXOdovS?xpoW>$*UKqcy0?TX{V!emR&mW$_`olch9iUZK;_oB)=!b* zL#Om@j>wPh+dM^JO?wUxo`SJ{GF{v2cZ1cRZSIQMLO}*4K26Q*Ti#+AL_Qg8!Zt^D z?C39x@p;36!;P)2tp#jpGtZ9?4ll(?MTcc_cQn_s-TErZM!dzm@Mc;~M)>01rrOmR zw#N$7DHKHo1*6%Xk_nSBw(o-bR`m|;s9TlH)r)h?3-yb#aJ+P6;U;1-hf3=}p-i^A zp~8|N3oCzccaH;7uAN*WKUpz8NuzF>Bqt|pNxy~$!cTnu-3^S(g`{i!yu)7?wft8T zbw$>nGmU;B*J*6&Rw-C{-2FW_ajR^WAV9W!PoWeP6xbYAR$g-Aq}W9LQY=7egh`=2 z?R7o+YeL8}>*LSr)PS@K@}%^T>&+hr6@?b?>miSblbG|AZ!MoyNC?a8K^?KWGHhcb zq8Yq=2v`lTh(DOqQvG<>uf7BkNfxcjK>2dcSaYR!b6hm5G=pKejx~YGg!8Wn2)LEF zl8sZ5o^DCAZP!#x2uqu)(wrV5r>uMeAr5-^;Wq8nlaTOuKJ<Wo zB6l%Sl4oy)VV_xtse4UV0V@$A#tv#Q>s!4)JR8o zor1ikRKSK!a%$G3@#9y{_NfoKceE-@TxlOFxaPbRyupEUxIj7UV=cb(o2m5^{^yLP z7*JA(^%n)+CfY;rw018y%-+@2b=t?r2QTGl@^^sVo*l5-brkmxOvhue%(d&b0fLe} zlZem7uZES*U6u1hqq}d;F)G2^g~{3jS612nrl(aIL_o))S%tw12R0MNgoV#Dt}_1@ z#etj@gWp5GyeI{~^s%3(M+hRwxYAxG<_)QAR-%K0Q;Ns%)6`dF@QFC>U07t(DOMT00&IfvkY*p|upOhq`LX{$8>F6geYWDHEaV zKhy11W&ook21w$bTea4*d0t34faC;T$H~O$&g1}19*@MYe}u+6Iy&&j7lxiFeEm(s zQKxW&iqjG##{=*M%;JYZF7)NZFs#V)VZlq3QLxZ&# zZy#;eK3;Pb_qeuJ)rk|0ICjw*7pW$~xOeyYJ+vYD*AN@G-%+g{S2DVtvb_=cw&5i2_1e@L}();B@o6)tK9> z(*eP9%hA6pLn>r800O&Tl1#rw#f&GuzEQ&)Kkj(H=!hHeJeEF=g7Uf^ERVplYdR2k zc_28*7Ej}!(p98ZfV|iIh@WziM(f1> z$6G~C5j~6gda#^4YkkGEW+*)9!-o%)L?omyzwrnpP=4F{h%|b>t_vWNXKVrveV*>j zU|cBH6Wi)`_F$Gx^E<Mw#OmnP}N})l;{mb+De}$d>HQ?Fm z`y+Db`NsMFBKt<&HW1s#Bl4+wa40NX+pjoU8QUYg~;oVVJ z@xCqLRi1gum)}AGaHc#bA5g-)x8mcW*IdRLKk}MXmTNSUU>tYtK3+oD-i;5hnh#$n zs9ephQ&=&BmZ9O-k!>KAL}6?7Gh6tr(m+ujyPHhFdNAtxSMPHqHXRQpKEaEtVfYuU zL3DB?BngJ<|EoHgDxNvbvxa$H7dnAAQ`o0 z(<~#XR=O${3mJQ332mxJIUwwHwPH!b#o^pqrUQ$7mq8}>xOm9e#)%T9U(do}3;k}EdtVBXBI*=uYfd9OS z-+RyVUaHl){TnhZdj8NGk>BWAzDIcFIiyIY z<4fFxhnhxtkm(8?GiLYH4}hmCVC6^4$eYU;*gILab4Woetw=+ z{P#c@^d@)0CfpLMsYx}7$I{l06`T8_VD*cmh!*?9B!s})4?YDrBteeEZ!X)U*iTPS z2LMCeA9-ZwxzkV~#QULphE2w=4AS>-Z4!)!$eYG^40VmvhS4M-Ea6H7hO&1G>zE3N zAyZ^ozxuuw;7Zo_~7b=aNZHKYgCl!e&Dw%92z%v7oC%=jfe6qm4B0I|wVXGe$ zlJA2@;3C7VDvW9F{#U<&kg<`3&k6QeX8FT)_iX`FAthrMrId1TIP5b?3hp-5NZ)JY zU-Mbz=@&dgfbxTn6%IY_EY+kMLyhr5hZm`wl%f4X5;KxJ=#EY{6T4?EMjZrF$&jZe z8@&v89$r%oTUfH^v>d}}AcA)-Wa{`gWADu#@5-i!HjcFGLnh;!N9#nVnopt}er3o2 z_u^2yn;1e$7N5iP7cUU%672eNpz>19=6nmS7Wm#_CIVdJsbsYS7fgljY0E|cG_EBI z8GT-ooR!hp=_y(qe1d|TgFqAwDAa!?Q&!7t;^1-$?b(`?((xi8H0b|bleRoFf8jYd zF_j`An{=50)H>Z-jIW6{%HcPJK!xy3u?B{Q)Ci3UhqVow%_uD7yNFqAri%!jLZ%{2 zBvRg%3+8{Rui4`@-msRpG-Tpq8dR+`iK71_z$@Vppgn1evNmN~zFUx5ot;8;0?g|d zgW7eHV6`8OGCDA{v2_mK%`y^Cr&UTmn?x z*-R-DIOWw$&ovEsk;2`u0lOIfgRS;B`eo7*p8-7ebqSYQO7N@M!wOR}>&sBr_-(Wn z74@MD76S6o|Ak4M7?eXe5?mK0yH1?NFmqIYUp< zg)Vo04@8rD`LmlG9nuq8}T}G@N((uD<~GwFna7|KE%zD{u7$r z8oTsKY&2?8x6#4|nKu4-sO;s-HP-`hC3Vy4w!t0p5@ZMoub_-18zi6WAbWd0Y$2uh zOFnYJKIV}^^Sn4bA2yj4Sw?)=yW5ou9{kEr%r?}~aooy#QPDHg`2HUSp_rR4z_4;Q z^jb32r|S-wCJTr=zsNL>6PCM`;E>SiClUq-9EU=^YHBaRJVUKnEZC@D9VTVl+uM8I zD8=m1;K1BAwwC#fG_#)8jjykZOHE%#;4N5d-zLn0ZLK1GNcOMm(q6tEhk(X}(n2>1 zg8q+b=*Ln>*62D$|CpJo2Kpl$nMDt^6Toy=jRp8t;H6 zT5_O(hSbEE-(3Ym)Qe@JSl-uuFNkz`Ve1vCKn*;4aheT(l(cBNdXFGtDMD2o&WNQo zf6sWkvYP-lHo<>|s#`>M+JtnOe(c$B3!CtfP(X8YSo83VLdcF}7nt2-Phevjl4|3hYSGg8 zlKqamY8lnzR~PFW4>V`{D8o~Sz@DTwtlmm@?{2w?r7;WrjYoVty(sFO?GPB#+hD5a z@O#keyA$Fxqs~2RBj4Xm(83a0N4Ao*@(W##?qOk6_2tj$ue&d-B6b#VL|#cti)P>Y z97fTD#qm;2>s6iyX>6PZaz#tSxmDE3_=x)cmAl_dL?|uZM@6umZuNg$&)x(GGir0} z#}%0-BiLR;#MlNpi@b#hs{MJDqs$aPmo2pu+nfg3?87YGEH;wX-N}?AJPFTXgJOuJ z#bpd!Zt%2_VPCzmg-DACFUVvzvYmdtP>e}U`@5=i*v7cmON*+=E+M$H{s{PbGuVyn zrNx0m$qN|x!G;CTAaj@OhUMk&zp>5JGj;$9!Qh36Eb<|@9Wn5kYN%{#Y6`V)GV=Et+NNnzuhPl%Y|hOOFVy+c)&J1p0NAy^GQ_Aec0OuE9X!xzUegzfxbOu4d~T{j^xH--7Q;wqC4kh+M@^CT*kYBfh$8cn^D7G z34f}yC_-pLV8`O(qGa)tAJ?@Gd3@=9lgtx+k|#c#6WB82)+3aOlEaZ&<{Hbh(;mUcZI562OZB}e1z_SPO!!HI*w30UY}Mp-9l{0CjYh~@MR4Gp!&q~QMg^&8Q##b3HVX%Uv~6U#TBXp@nknDnq}i&dwu z6fUFqhg#E^CzP3?7oi(aufKZEA0rvGEm&VHeokLse@J|qrikd4AfQrBxst=sTg)f) z_yaq2*rr2oL2;_LHf+;+ALjYTFF$t5UpP`l4vkU>uQ-^V;Iw*~Ks!zVGfLj|eOV)& zz{d(#8q&HW@CVGyQ&u&FQjt%a^)~Jsxg}`*9$|pU@REKk@*UkKdA?3>uN#8&jVWBZv6tAe z?m==PCxMfR9O0us_QO~Vo7bqYny2q5TPS0L6nU zKQdu3|C#i*rlp<_=PphMRh6!t=+Fo@AQ;Dl%n;7=-AX^b`eoE418Hhz7W6Pw z*QxSo$j(d(Q1N%@LCmy~zq4}eWHl3RlzsE+=Sy4&=>4OC=S@f)dFHuW5BBEF+v23w ze8kg?8(SoLek$R}f8l#OI)KE1cScbb$P`wd>NRD#y@%E}b*sv1F92=Kn!8&sh*TR% zur1F^TO4>WQSk|NVTY^*Bb@hJwn@=~Kpc=Yt zv8rPy3yv&5j+;|04}oaZF1-mA_%L26cYlnQTaW4JP&!DW zWKOW^wZMDyp0sR8kox=k|9TMHRqA#^^p}v?vP7XCF*jF+DnJP1%hiw5pP|#o=UQAk zfa4CHd5y%y3n%<@m6G(%NJ72R*QM<~WqGK@0f95t`a|duNXfgK!Wyk5>KK*e1q=0H zF=ym1_Y}K#csU`L2|p&S;OdEPF2uU@tHD!o)-=4yJZnbcP6bTLWNc!HS0AI2#;>U;_L&L69AiO`YMSXE8@~g;h{_yYr!*9pcZ3qOCA?po#?3ZiF%@bk?`;*em zwycnM+azB9zJAOFIsZ>mJRQ#Wb>p|vr{WA^2UygD+;SUF86U9f9Q#LbV?gl#bS0R^ zh-->hP*BKk)|q93#hjD#Z{7vYe{7U`=WG_oGEXd78&3X}SXO7`icZ zovvPLr+;ROqPbuu`tR#FGK~F8!-;^?VWr7ck77%wcpi|PLa z5{2C#n(s8HG?&lrgJJzYiS|ek6jtn78U!H!6QGd%!W54$ShJbq&+zoP%e_y=p9q>H zuj1(cYydO;*!wZT$D6?q#Pnk<4kn9!>_19w^=&2g#9B}E_PX6gt!=uf-)jI9Bl!vQ zka2)V$1->dY6nS`Dxy(F)|k9yCRm(_uNdhY4>q~TRQInSUkgwR4;1m+?DWk`n1t46 zQ)H-;Qr8{Jxw8u0Qt*4Qzg z9bxq;?P1aHH3Hk}g8k`if91J(I@=$Ag%Mne3{Ky>Gq86(MyqDa7`tR;A5oA?`9-EWn z4yy5b!AHOI@*4!phZIY**cN3T%JW3jP-jkm%MRF_wiqgrnK z5%2HP-$$J{pSk1}s~}%4x;CH4dqZTe$N+C4*Bh)hg)2uzRW(459No;g^C}qeG)@Y3 zOaGQNe0Ycftq!*>x}{RSrrV)Weg-t!{jEaJ-)ZbX$YR$;;zFT}-2_hDU)zrIZ1;!x zd%nyeInORU`Iko}5#Hh?zkA5L$yf9!VlFYRjH~*T@)A=gmq1?=5*x)&f);#$LrF#D zNy(LeU8ehQQsT6K)pV_EonRtlr$xiq_+(I@>#BT3#r^b0c4F_hZ{I#eqr%tr>shd5 z>4%wy+D|!6S5i8-fnlrOSPwb7Nf{V(wpJuHTy9_9MKFN*&YKoF0xW0F(>LTa^!pX` z%(L2*+?{DDD;S(#6-xC(t)~@KEg4%IEb9FnsI})4C=Tl~E=#+E$~OdHukqD|!uf4T zT(q|8r?l4CbuQQuP;q7DlDB(cs-pVmK;gDc^!~#4{Z(szB=i-9h>8Drp&q#Y%HoLH ztQKXEpl-l5kS!{1M;nDbVxn@UK%-|}%iXTC=2Nn`(p zUyPAd>Oj3s;AhQLo(qFK+}nnWvL5c)%9{ImsWA7?Usr-xI`vdNK2-anQK`7JTW3@t^#7Ct;BXfHa<+r2Haw>_Y@BcCd`tpP4HU7tD{(!6U9N3!J z&(%bJxz9t{dW)Heb4&&w+uG)nM7xm$0P;_X>n>IkpXj$>Ku}2i!i!(E!(3C4Un@`? zcRJKre8=qPs0}TvbWzZ&Yp|qk;D`9M$Juo40%}XMxUr7SJfSB)CxJF$x2OG?@7(quXPT@1>^piK{x^mzllJ zw9S*pWUr1@rOi3cYg}ZbJr^4r^cz1=n&XsaB=>3!!&c2zp>SdLe(=v<-3MZsYw*c`XZp9qdg8x^jn4vf{2rmVt|}j}+NHiU-3IoQOWb@anewoPhXE>YP5i_W z@crX}$NJ=p&4rNKZy(cZZBNMkbXUu*w$y<(Vx?KaA7t$Va(qG;!U%Z3ksk=#b1=wP zmxLdgW}LZpG6LT$nAGCD8sHS@w#_e@DaeZ%>hsn==D0o$*R2hl^V0jIOOZE63YmL$ z^_GS-*XZ^?e9jYb;NrH`qD*YBTsc)>TV4R4JUc2B=Osr=bXX`b|^|23VEco=4tr(8})ptv(M|gxDyyTYszZ< z+?ady=UjJLhwTYZrM-yO0woJKdz8mzI(!T<>doU6|J%T)$j)!AC;iU?j#TuC#5=U8o3OdBh?qkk=J$S|;(7_eE)4j?2=)JG0reJJ&;2tH$k> z=d$MU?P>VMOXW<{+fGa;hgRKhlQ;lUJyzzi_EYe|&B38VfcQdUmmS)3v}UfguWT49 z-^Uo&B;dEZ86;21y|LBfhOV#MHs7sQsZm(<-;|je-#4LlK#xQyp?&{+cU)S$LvSG{ zlEZ;xEjdy5J>CZ6E>m1BqVU|eyEnn354~U{qF&XN+?k!tV46ktH)LaqpXDv&eN{8* z?t;+^lSyg2N=KT-sI0)1&m#E*LQ;!EB$P-5i!#H9z~Y+^`^ms zjYh<`03AEMLY^0EVZ1zRcU}e9=S}PC*Vq*Gl&Q?lRgrI~`>q^q$I5Q^Y9t1YrRS?= zBcRA&h{e0*e4C%Byw%y%q=z7PSvdAw*pP#ZmxmJn-cfn!vkI-g zjr&M8(-wK+JelNX?s-?#2DQ%iV=tg=*YYuzt^sV`c|QET_AA3!_w*;d!slMlfGh#n z;wNuugpZ*smM@rswwoL#*Zu(3uGU!hrl-@dO>Wos8N5=MTe;P-E@0vijLM?&8MbU{GM$SHN^n0&JG`TXgJm7MZ% zp&J=_750vAY8Kq1r^yV%Jg4`YEp7=18h1j-z1hD%^+l)pOA=7?%kv*s)|&uz9@aDkCE2#NpqT2BOp4YxjO!3P0EMtp2yn`zi|=gaoK|l#xu5AbZai<9j?cB zXwIjO<$)~N5A5;Gap1CfeP^Xwzo)eNI{E$#9kv~atSs@lV%~z6Epxr22WPQw2%45l z)7&{0r8jx}wASK1o&=KUCF87@hjN7v1x(keP14ru9{6Xhje5RxQuM#fOfM+4o!qu% zrhm1YU^v9X?V-MQl9kimqAn z(a(oD!+t1kp~({wtzdIC0Qz#%dBMid<`LY##>fY1UJ5XjhZ5g#{s<~#Inv?6_=&;ff z;{3(=f@kQJF9QoY0Yu>{{7zdu_R;jg^V=Dh3H&I zN+S70xl${V#PcoLH&4+w%ya=IH_il`yY^*a#AfUM6ZAY}qe_dx(qzgj8#mI3dg|5I z0;s+p>_G5yFMztl+m9*?--WGPED!mWd2nnw6KXZx%5|A@R@UM|XIH+(RejfUD+S00 zLBp1hcbRQFbB#3m#|Kt^6ZX--=_3xZ&o^u$%14Uy;Ax2?pM>EduN#xIyy^iP)>0xl)#IhW~2b#{?hi|=GEHx0}F)K%iq`2JeR z>%Pu@NyYB*F{tso@M*4Xx$TN5GdCRNkMWb6EA^w@4VY@WN{>q!PE9T2t;LIdQB1N! zWL7gonriCp6dAKe>BIuX!%- zooySkmC!U#vbhlWuGr$R)~MD-ZFGNSW5%s7V%zz)OcSI;`X|}@_BQZBzNaQpN6wEH z-n;v-cS@w&jCsvZ+8;6b*@(RcVf%!kp_zT#H^mSk@okkRwB_bgukm~{QLc&k7MI7( zCk~Vb{p3t@R@-yeoxh911-JUn*P^HV%g zZZ;)pPz(%v${EY!JXYvEjonqAVeg5ycv+2XDJFEgIzoK0?jLT&fobct&Mc41MU*z* zt=rP9-5*Q%*x^1~dD-aQ2^PysdGbaeuFQEVN{br5BGTsSrd%~19J%G{Eo^yJvhzLS zkjrB2{``{)Tg6(zZ@MhY!dBrq@6^k`yk5tXsJ;OVTEi9-var4oLW!cAMOU`gG}i&| zMgy(R?!nPFTjNOLSz=}O?tmr1&hOayl+Xrip)8(v6P2@j12}$)M@JwYbG?uB_C-ZN zI>w)9kFsQ*PB~&_Ay<)V$DF=0ty$XKzfRxFb#EJa102rb**)KcV*K1{)c+sK-a4x4 zuH72mbSa7mDzFJDk(881K~e;yySt>j1VQPLmXhx7loaW13F+>RckPY$^PKm*=X~dU z|J~yrj^T3s=9<@>^SajB-a5HKp16MIct0Cf&DEpN`)MJ|gksllz^$Tq2`bW6)q1S! zEzR-s8!%e;Jc?SDGfj9aQ8u7io*02$Np{LeB=hP=uea!z~hRqa%C~_ z;uNiMK9TR{?L^aUw{cv@Yq>AZG%o^#>lVrf|HM!(@rT|X+!(@3)GVi%o_<2G0fklH zriBI?hpG6n1co6I_QSUlbae|^; zRj~;cP186{#~7;8*}$l{3$e9fgENCs2fWfZ9b>W6QAq_69FuIIKt&RLeSSS?@2GCeX{k4R~@X;(4yYx!=pN zPCUpI^33$xO%w}ipLgxUN&9$^0?5A*R@1a-)-!W>?!7%{^9V7FqP4dpKGyl#L`=WY zgdzc^K7~7fKIQdM1_BgsG^;`3w_vKUQ++!-mTBcCsYX^cxo)qz^O!~D#ctWf#fe)_ zg3IOFI=6@sr0fBX)|14b?~@Rt!vg$f&|vJ!9L8NC7xhkV((< z=dg2v;pIkcU!~Y^p+|9Qc}yI_5h3Z;WMSjfvcJbmJS3_E0#J-t$8l<_4{3Sqd#aT?7W7>3**?pTSH2~ueI@345yr~Z9Sfp zw)DpE+=jYyKe0uQ8vT=k3#pS``)#G&1x9rB!1<3p@sHlNV|4Ap;V5Myp19cWFRc?! zomz0dm8UJVbi1@pHfS(;O8OrxmHUJ=vzu%uqE^uRS3ztx(0#bkS#M8i&Ty7Vc3 zX^Ts@eF*juFbH0f=);S@4C=#zul9Nv%1yhbS`CaQgCUv-Mk2X3c2|#TfXVmxwZwc4 z&E%?-cZTPr=*Y`aRMYg9YBsbY3kL+K|zt%{bb ztLynfTfk&~8&6clc;--iTH^ySxbh5PvRQ{4!&dobQ8Owkak_bHkPUWTJ<;>jt5-Gl zWU1MqmwsM`Rig~H1$GRAE_uY4nLR^twYoVPjm1GZs?YNh)<{cL++K?Hl=*$~j@a&X zAhz}^NRSf0%?`~HJTGfv+u86pr>r;FMf=jh_>Q!A871CfTj4!b6Rud~P+ABTt2aEz z6Iekp`IEZ6uDA_^fGWc#&)p|jxx-k^&hv4z*5s5ZB({(AFXFob^Bp%-UZ~WhNZr^7 zKI=Auv2wCxgD{^6VXMs_%JNPbhpm%sJ6dXkX==XHyC;Wdh9AvK;4PGXPJ}O}W{3jZ zIa?spWw5#KVN*>h}OwVAeAsny?p_p{r@j%NP!?b*Yfv{{(XvxbG zGaOrADE>d(todJZvrDJJy)z>E&nwFuarv2w2%CJ&TYS$!zIByubB21GZ<(nGY?~Wc z1+{UzCl&KFegi*9y^H5bFF?OqvWjBysK_K2)H+ZztMVbCXb&h^_vQ2vTz;a%}jP!FZFqQ6d~+1 z5;owNH{r&7i>z&Gybe`$vp73xh!A)o<_LA_EPAPSkb~!wuaa6NS(gP^JOJmkRyy3` z@O^n<^JFYGb1tRsXS___v}`5bYGV286<)hBhRThfUoZS-uj9DE1)iRqg~-lZ^&^q& zgd(snZ)ltcPBOpw(VL}SbRb&{iE@v_-HHvz%1L@kCG>NSC3xY)^SP-3mwVcw?m{np zu5qA_POaN_yX^?h>5)INBlXe&Q1isfYz339JDfvS*eagPSof1^k|(9!U8sJgxJ29Y z&emD~>@=|U;5S?4%J5q!ZWDMEBljQ_15#Tuu|TXoDSJ_LDSjnmd;t2+X1|MOh;}5n53m@UH$FMpxr%dHh+kp3dwb!@_e9}Z>Az8;(@`E3d;jDt0r&+zaeVvW_&if`OgaY#R!CsWM zRn>CkxTx5`+c#F4*Z5abt*VuuqlEdc?z!|8usMG@j?90)A#bX<#aQyiK!K2#HMb&8 zLs>1s)}ihyc}Np$4zB5nUU)4_Lat~bbotN+mzIHcviShXvaV25GhBqD^b_^iU~|u4 znB01WpA{5RW2#X-n^0r!P!d1V@?$@4iH#thzTgo|aEUX!iams76Dt2L_uCx&F;-@z zdHdiDJB|}ZH%A-CRlL!?RZ^GR95bIRZM>Ua(_zB?vMSHtNKHD>n-)W?Yx5_Tk`^7X ziJO;e8QjAWAY*3l5k&*_hz_w9ySHlUcir}K^wIMcVQEF;NjBSXDbHN?ERFzJ3GfCb zz%-Ej#*(?jbpX}+`@d^|+P`YR7<*h)1`TZhWA5nCC|bezJa|=E6C)IOl7<_#I>FnE zqB@Aq_aH%*bp4fO`KE;Y`5QbH(oIR9CYpQt?cRW~>$k$*9`8Q-Yr$^a&*SwcS=JsN z>z|ZVCXEly@!xsaQf_}_^M8jKN9S;14wq~r>n&m?8Q`#|F}-D_mBV{?;WFB@nFp*@ z%{qhAUihUg>5ZgcQ63D^V4lFqCcymgUgk|82dP6B3iw`KU$8jlk|3Sj7@T68nar4k z@4+8Lzf;>ZtkyGgdCpdOPITdJPmm*DoUOv0_o(1(YC=@(2ilB#CH>GEJ2>sH{+rXD zFY)oNll;`Z>!HNMF-nGN(@t=CbnAN0P19zDc!d@r8ygmylQPcDOi$~O!6G zoL~kea-4ZG3fiHuc~rNI2cajFg@sMvU6iExyr0CJKoYg}E%GI5^JD47uC6Tw=jJ_}OFtqfB+WAEz7oPqA} zEFOQ0>%0ehRsz5V7Tq5rtu&8QXFo3dgBP*+6~qMz;hWO@)+msUG{Goa@W`h(WpYx3 z#@S%~b^!_hvH|{nrFf^G@!plx>&=|I>oaBYoR#w?`4M4DlZGsV*Mo8u?`+)&Ki)Ls z;Nh=|{`s`0HdR{2Dygh&Fp(#%!Z?&?q^4r7W6?dc>wPh!U#McAs#|tfCDd><4F)wmCp4OTt*M6n0f zY!m2fIjHtx^%P-i1=R%jex~jLL0?;SXL`|Y+H?<2lIdnv(@iu#m9s$g6uMAxXrx?3 zWy4J3M)IWPtIGI2SoZl(!c_662hYDXCwLNLPa}9h)uU#%&Q&slxP?vxL@FPQrG^7}Oef=-LGxs;io!Vw3r6%7?9) zO*6wpOqx+?&m}kN9Y`9Kx{Qn$Z=#IqmHhDb3EU!2VhY3h2720TXz#QZ4sPn{0;hsr zLKy&3b#qJpx$7ws{)O$Esg6sWP+0omt0J%v_uY{NpYU$^i2au90K!DU*1bRWn{A26 zSH#Vt0THda!Z26z*wxGG?C$Z#wz3_yF7SSV zaOPRz*lKx^eHXS0{&~yMpm99w65d6p!KXpz9(Ln;)+L8Au7rvSkG5~BN+X5Ht}_XM zm^KW)!e)e!80Kb>`}vTeyS&?1Gk<(-raS`a=1!y#B{sq_Jh%9%U}>_mkbsBA)!9ibif1A}64#qCPHgfdrK0RjUplIcN?=9^ zH6Srk=iwDNjpPJL#;M*r&&8wGtXqu2mZ}b^-`Uun{^Ls7bV32Ht+tSnYFaM0^bm6o zgNgVABpsj`hrug*+;6pKk|Q6qiEDepEfe4rn-%&mi^eHHJ7M{{zWl_z;5+X%`Tg33QH zvHdF#7#a7K*dHVwlzSCn@F}O+*uyv{c}3KIoJtKVSE@Uxn$Z12W9??*j78b<%?Zh< zF6<)LV^)v@Iq!Uqs-Ie~$GJE9IlM{JUV>WJHla%ob>nd<2Rf-VDN`luyoJYscBQpr zg+{781n+>5P5S%ezi%Wh(&F)Sd81NUDZTk`*p0+5d{l zf0Y>IF#SYqjpwzu1yOq~ zS}PS~^A9AJ0?Yk*YHg}6tg+REk3rO6lIb#1D^(7?RQys(AZRgaHjw;Arf96~2=@6_ zn^m(35}hSgDE_T2Z7)#R;J_lg1(tB`$dqXQ0(%?f@6)vRPW5H&mkk&WT~pXq6ZzVi!7$^K?Afaf?i&gY^gHEBK!v=* z2_5&n_swNf>w%fYEJ88qY=uz-UvZ*q(kN?*&* z4_;%k5B=;d>&y=va7I zx^leeo673H+Qq(o#cqx=o_Eovj5N`dP*P&(Yh+s#;U4{Vbye$Bh1~PPu`C%4sq;ue z?yc}g$3O0Z*E7|$?1CC?_hxRtuWam$cNNGA#Y#} z4od-7aj$cn2cX=@pX9#ZrAVM8(}I_qTrR5IXjr+4>-?FgJkJaa3np#!!>=d(;BPD} zPHCQ`=QdJSDnbL*W~9c%xZdEPOe603DrY#f0%Ppyh|N@Eq6cY<_{aS_M?j}oH!$bf^_jw0M4JdNkDWuw@jS2)ylqZ=}c zAh$7S<{`F45tcuobT6QVt+7p5+@BAdVY_Sg`;D>n2L8a{&&(U`or!W`#({g0mvn=%DHzPSiwDE+h8FmJ2>~;23?rO`YlHI{I8y;GP7lX6B+k96dAyom7o|`lA z-GM1}F9s}FV1G|Q@8B&&NV+DrjTcgr-B*DlpjC=!Q%f}swd7lf8Z(SnU-FK24Q?C( zQtoW2*w(3MpCHIRVB-_z50#>Ykiw#-!o^SZFek(EBh{YhzbrgC|2IGE6O5+5Jy&H` z`_(!zdH2A=*zMQVAD71YhNJQWZBAHnAdrOqbGG_p2;#XO8tR7_SCr%q?3`~DL=nW? zRCmx2kaIAIYTctbvaesK!Bz!D-mf!#Ow)R*)DkSO~LDd$k8YL372n(cB94rnL z-BseN5DwaR4wAtirKRh)-)nX(F#eYV9dwR0c=Kn_q|MEVGW>AVx}JlXfbG|Ym}c0Y z=q+1|9iQ4W>f_oLj+LF1u>3A~KQtAwm7UjK3sj~KZ1fYzYB5st2S9RiTCUc^yf-eR z(*vf_bv5#@Nei8Mm3bWp*Tp`omt@1_O>nXqFYw1Wfq8k{jLCuaU&}1j*9O0> zm2%(Jc9<8lxW9@KCIs;!egq6X_{1Sla!{c*J>ma(AN<$^B0Wmfn8A#>CEw<*&RQpq%u;vo)vgx9Y zt1BJ`s&ed@gvfHQ@NNx3zi2w!W$^{>?U8wN`-IBliAn4 zfsc*_=G5wNBh+T@FEMDfhW&Mnwoseb0Li86B>GLgt)!$tX}ndIPJBOm&HfhkvQ zT4h=bnewzMjjH13N6*K;5E)KQey)>vdHwxOrLE(j+v@_WoLi)*9qii^QR8CSwCMps3%2$JMl&HDha6uIt)wBbBH&sY;{lLo=A= zt#g;yS_nYwsy-5)tWmUZqClU(E+tzW1>~#NuA_*c&5b2$3^1sv^JUAiUGrFctm>jVS~;G_@q&B3!0o{QV!b2r z!_Co&RBgaYQnAVHVAB21uxtMMYZ@7KufUTlGk*-b&`Iy(C@IC-I$Sp4RGN-lq`i1_ zvdAp{F|*7)&fjdhOPtRzf3nFmUh+0=o6s=heG;p0X7XglfPyIyv`VKzrC>Drs<{V? z+??UTtjd_Naa>8sxHWl08}FlN$EhgsrK5j1WyL%{F>g3zH1N=FZk?0EL!jZoZi8h_ z2MJfwRvf%x9S_@B$*Z3gXP5%W>G8_b$VKtzW}wkug4mH%*;T);9*z3LgYP=)L5-e^ zMV_L0tdHld(&hWOFb!9_lEMnpGNly%RsPmrQe?ZBo*$1VzQI=cbyPwY#rw0KA7B>I zqiJ?-X=)E0l!Vca;qSe{ynnXaNoH3@Efeoe7T?|x@MHu>w?X(`S}bJM%ex6QSet^R zf~w;+Tz*39Eol0Gq6wc7nS;==KqXp*UX`jm!Y9(F3l`&=TBM~J`T1Y$SFXC0fgZOx z&fzE3;&LNKg&0+KljusC!hzMADh(^KAIZ<93gxyqH5en}m3xBn6J^{MsE@z674%hv zSQYp=x5Gw04!095qNhoBWl3CH)lbR)5q%L(d|R^~FHa8KkE*g>e;BS22g+$*7u^PKs1B9X80j!ZKjL}l{l|Vd{39z> z6i)m?;e{tGFX6&>^EhWTcT`o0R_<O>JQ$9DL$mrb;2IZflL^BGE@OY?_QNiQIoI zG!kI%RgLpH-^jYN-9`(73{8rFl~Ld{DDW5?QILZ4Z8F5+fghmr|Y7F zePI2#^6~g?oMS>~)v&b6WyN3Vxx=|cmmW`A{5ItE! zs=9Lz$T?W*ylZR|0(FjsXuSto6r<&p)??(1m5!NwOC^Wg%!OH!UNoMwZ`PT@@j3Jn z_iNK$fp%+T2u)rXOii74{%md!D~Gi`<#LG}+b1)C`YikX{u4a2H%5z;HNFS@mDRsh zF}A9o>6^Q`!FgG3RE^?Vn(4vzgxQ%p&w?92{Gf-%+9q9sDUj+81_RV4SeMVUsR-k~ z+3|0j6|j*zF-%X9QB~$Vk*^A!PqIgUc-)7g(0-BR8S__#p->?oI*K)x$EXe<_=dc3 zquI8mhZQfEh&~9}X27`^n|6`y{Q1rcn9GU}SW$TVSu=e?Y~()ov_TdA+Gt<6ZHBu6 zn{0Kpik%)oK(vEW`a4&mu<7tEurBM$I=7@Lhw2jv;xLP!urP>=o|MGG8smMY495hC zBE;-L(NGUK$C+=|z_6P?a=Qun_PL9~Otz|`+JHyj5mN?FRHv_OtYii>E&FZcv`MMK z5o$FtT`F8HmyS;8ACWRmF;@@+x`_!G#o{spIf!8p=55&b?AC+XtBiGxRel?1-UMje z0EANxb7DYgfgjjy5Vt(y3LEr<_gd9qbJ-4nWAT( zlTV9RDNGlL*Z;D6+A%R}U7z&a?ov3+mYKM)5J|DG1_ksSsb_CKyIiImgelxM9q*KY zmgj15Y8ULD8xS^45KJPE-wY=?4(cidm()K5bwc~Z{4sI2%Ee5mzc_aMli;AFy7Q~z z0Ixp#=V9~pIAY@VB=(V_=h zZ6Ib(gK_VBtsOMwz9lnk}BiWC=dc(80<1F-Nvq7^yt_Y%a$R_>Xh%) z|9TicDg`QrsR4CTse*a`_u2&pkQC%+wdm=+!JGUQ0<_rfn++6I69CgktiK8!p^lvf z^{|$hSjRTVaJsH<12BvOmv~{yT+$iV3C=lJ&LMeoClv(0&~?wV(lm zIhd$8o!C=8reW*V=Jv_&u3Is=@X(dU0O!kjkE+vKaK|`Q#su4YP2X#Aq=1KcQ5KW`$Yj)0KSBAZBY%lY@ zL!FH`o*xycD2ho8$#H#j%&9YQ3KH&v1w~r)N_!$oN^^?)%h)PoAyLAJC(OZ=9K?kr-WRFFmNxez^Cm`WGy4ol;=(FWC3hR82#!I8Gry%5AdOJ@GdGi|_ zjhcCHsE=#hqWgqYZr@Mn-+@UYT@hevZmH=o7EFF-)n3czZV##|e$%)v3~494UNrY0 zXF0Yk5NAkzy*s@6a+bN`6;u_TAXzL#{;8fM1*5tPfA+yljP}7<7$nctEQkOovliMs zMWy7e1<lMI z@qpq8S9!LK!`X&?^$d09H2>AW6Nfz>4)nlK_{tB{L2HHAetdBV8RP(Pm$UlF>l3f+ z_PgqV*@BV7h}y-w9-Z$NHep!3raBz}WO+H44 zr&We+vYwuwJA7fTP6^^>loqj)c(z;)Q56H+HgzA$d6kMss)YoJDp#XjtL)@nDrFk| zcCg{3WEuB1SQi9*7FZ^wo_X6s4;p@MA*;4vuKGN3%oH#jwcDHEq{?#VlK1>7rD=uE z{POF1Xrand?L*%S73_pf$+yE5>1q;0sAe)fZe)NzZ+H+&W&pc5vPuYxjyI*jvf(by zix?kF9@{xIoQWQ+Ts3>K|L&%?Tx)slL9a*|EkovLVjE+*X8BB#`fr~^0`!x9mG|l! z@pt|d=avNE7!*eNr%f_0puH6ZZN>UiT|o|aBHbm*DP2v2=k{l1YtcsaFmh8RlItun;#(R?56x;( zA(m}r22@cx_^8ko?EQxjC?)I^!or%>7hV%>7!H1}>y0Ndt|T1$qO)1B1GY>ZS{%+) z_?qzuHGkvG?Y1(HIAc!1Z`!Dq+rpX$D#BoDOTJ7auLaX7sNt7OH^@|1mlvw>P!|-% zxWpc~8#6#hF~=@{{Rfg$S{uCi`J&>uBG0loO(T+PR5M+_^LX7&E_-M?=DPW33Zdal z^AD?&?0etrXZhTU2br=!=Fmc&Oro;8N{?WHIIh|u=w4y!13Q~B;A}d-(c0R7bFvOP zFifBWqx`o(BTE@yu|@`yM!}6M>8Kb>AzVt*t~32JCK8COIN7$&qo5-RhIKTCOL;VG`r_gO zdg7!^=+Qqur#NdX9x;2@5~!h|npM`C9;sZVPF3BcZXbK9FIn3;;Z?j~m>O?qM-fI6 zeGPt+F@?-h3)@e34$Q2Un~OE{@-eXQ;#;y9vmc5`Y=cHHsw3oNTCNs%(Gtz3`g^WZvpEWo352Nt3HKVtcrD6VKLYc3+=V;&k zc|wt?SH*~Spwj<>=(k#y`YVH2Z(Ms%miH)#Mw@Hyl9IzXv~NYGo|KV#2J>Gkia!%5 z%>RrpOA$&n7(}yAf9f;& zlxXj=jJ`bbxn9z4IFBETkuTK;V>86pa{|-Lope@JPdOLIr%W+e{7LWlrc?!wBSY!0 z(!RaDE9;1waCRpdHLLIFOIB6U>^aFJQMIA%&wcfn(M<9}Uuasgom>UX zJA7RoyNnSwR=Qo`H1?_kWP18qcKiIj@4A0`84~TV7qph6`-0zQnrC;r<-7U?+V$ah z-ajb85?heVQda^hQ7j}*Qd|9AzpBh`yO-kyJbEnVRta)@m zjEmqTw$$mNO&c3Tz2MoOQT-PhSbDx>G4kt7r1PzJtdeLMF^~d2Tfx7-pku4_U@<~_ zi0&xIShwoU2!hS(b;5Gv&9PK>{K{b;a%;o7JbcY`uh9;Rs?i+~@?LphkQ(`7mm|@R zdrbxJK-~vs*TVd6+*A8oYFaiv%f0y*O0VOXWaJeQx-ui@b#$V@kEJxVqDkejchreY zi3vPHY-7dxETT*n60-mi<(?kVHFJHe_~(h@GxZ7SJ|fpJYd|;IlJf;nTd8#6QnuvnMDWzbqo*>bW_l)j9l!^$M=|nVt(f#Hfz%xcG za6ua#8|!W%sc*R)Zfxkvv;28o%gmp(f>SOX{RTZQ?Ex3T(3V-+g1opccHj4?`gfhI z{>iM|y@^gjKlGwrW`z+^KKtyH-=G5!YAPavg5@Q8Ka9Azm^u-sgo!l}t04yeLyWkh zLEnPgD-6Lx@6VW*DvappZT3c}jX5kn3VQaNQ%pPOmof*_n%`bXUCx^>$GNizTr?Od za2N4%-@nmpKAxU=ur00$q5Ca4G>cH=cp{{j2CXW9j=LE7o$aoW+`n|FJ$a;$*%t>hIqdwHRe=?(9|HciWWjZhEToZFFdtuOm5%l@X28Z{Xz&KgC^Hp9GO+2$`#7Wi81 zxS;Szf5e+1FkD@J=< z_}wcxuTHm8FT9AG2}jrzMF^J2*b1lyj0L#oSQCHv)5I>g#vK1JeI?>ZVXjzKsuYpr zgqMvk0dV`Z(vSt<_VG)|;25oXy_TTOo3fZe+OGv>Sq9h*qKeJQAKTMXA}gr0Rq&ZQ zj3^nq#2;s2w2S)nPfk}Gyt|1PT>hzQA8UE@J9l|KULHkyDCIAAsfA##VtH;1$dBBv zJ_wtrPB;2&_Ynz`$oAw=@~WJ>S}(c3EkMGrNf;8AP<7oN;5t&tHnRchl9pueU2+nV z4_9886SDrAOuAft?$0J8k_Uz2(IGz-qqwV8L7<`P1FH2e1yKG>GHoFPB#MEjF^(MDt}7O$HwBU>8U871QFWqxAHtZ!B8k8yCX#9k2)TPektRC#3{%Fy(~+UZ@q6FI zfmzZzuC^qU9#pSQb^lxikXv&vTRob4%sK2_qJQKU=dE-Q4Glb*+%(~e_=ZLd|}1%ENf9_3<+0o@HE{G zWQ_WtlVR0gLHmL0fv;vkSK1M4nedCdjcK}1+AUdIPRSDHDjTW|e(xV#^bE|4AYNZs zHkvRrq%@L-8YlUd!#1&LnhPwLsZt>)6pn{v?hh;8l`)!^jqm#AccnI3vRr-&OHCSu zOaEmpwgKb&ET{fMj;Nh9Pqw>$)ZyFS@#A=8q8O^YJm&+==&Pe%3lsw4VH}ZbvX_+n ze^p#zYlU{*cjsN+_2%w&xw(?DIsK6k7;w8rPkSv5X#Wl*Mv+=x1dfG=P$f-GmYvJ- zgyn%GZi7w7~{Ix0)w*i(_;D z?0HxBAX~9Gw(z|G?wDIdXxtUTvSoCn7QLFvsNih2N@W*n2mqZOrlJHFd zaZMuBAYEYZFooRgU$wg*d5qq>X-Xo#aH_`4M*P?k5nV`YJauTNVC)r&e|8gl`G|7$#E`#(?5JR>>)Y@RjA2bc>2>dbnT{I-Au@(;!sqI zZ!u)sHcOP+S~xG>Z5SH`>7a)^7UyDbIt?=T?r8OfD^OF%_ayD)@=(KOE|LDA=jKA* z<{IxR%=3E!*9d&wEDi!5$Xd(zIWQO}Lz|#4uY(1bac@?#If<`}`Pd}x_0uSey!VHm z5*X`q={R71Am|%UXZiwT*5)#1VM;$6C*mbD;_}j1(~vrUxqx&mFP%vZ#MmB8C!vpP z$P8N_6hn&lwAF$P)lzKr1$k|D-~58M=ZiHf4WLFJvomJQN7tnVE`7e2rhCvjN&Uim z__`M8tM+~2Vvx@T$5Q2KVlGDEnU-@x|2T+@k1fA*x!fz&gMBo zp0WMUwN}A;Jf+p=#f}&k`KP!n@(R!3r`gvOpG8B)$8r0TpCr(HZ+kepQjBh0wW@ma z#pPSQa4?0+17Q=|0iBqf0Az=m$F+@fmDM&q-_F~RmUla$%iN7OHPzPH){*vtoNzu! zhC-nIk08lU{+0$=?hm`c>dWMf`<|SxW_@xP83kAU8zttgYZYPNddLkpX^2FfUgglC z)gkxJDQ7=e8T@0sIHfo!YfYM?R}X`25ad3jMOs?K>|?NS$hV%?<$#r^w~vNL^fSz2E`EEodIR1yjtawbqoecVj$8yz_8$)42^-s?%i%)kixW4kB<1*y9a;L(Tv_0smmnXf{T{wEc z?*aK#@63DKjN}ssJ~@iZC4WnU9+E3&O8}u^YBlCTQnDJ3&|m$2MsnRL&w@r>WzBQ^ zJPjXpCJ+W#@n=V`Ve&8zE)TKTSrvr>XzE|D{qi;Gyoz8te7$ZYNNX8gg!YHP;Xv1OwLL*f8Gu0WZjARMa6Q!wOO1U$SI zHFjv*JWc^;!iv?{%cR7Lp^rQ# zy!(M4ZFnj&%8SAg#k?`r*j{3|fk2g{x2KnMgaQ((I^kzHM>6)=GHuL&`a5?U} zyy?5(Um|lwJ4AuZ686Ak<0Qhcmz$qM%ssbjcbg8R%H-2Js%tVLUjK+eAW?q4e)=r` zt;ps(1mG66XYa_5$Tvl0MzWRjJi&SYjOxu;!$<1#Z>7YFg99kn1YJ1y3mtQ^4;SYQRv$)`hX%bH_iu0wm-x>>y&jxP!tb*kydh(xpH<1r8`%P&9onfSXvDH7G zroI1_sk4?P)JQDG_wFQ4DqEcPWoWQIdZXB#dBwH+?vy}((H|sg@|$ft>OD%yBKu2b z&tU3C+FoeoALg|I%)9ar^NyJ+@lEQGM4OWrBnK^;5o;$ok5XUwC@yh+d&R=)m8*+; z--PHhj<8L53wG2K19$b3kqI4NWNoH97ItYV4^8$z74$ms$GzTTb45MHmQLEhm65SN zjn3lqJyrY6cl_J{UftGCtKVY?I);*ciSi+9`=XT+d3;v}gJqYHd#qb9X?n+$?>!2# zA9dr$nsAj^-eKhTDsy^9F~Oq^%nbQ?_*&%5OL7`Sd}eiUW>Z%RmfX!Xit>!Ctgh>1 zSgC|uM-<-INBz9wDXH-k8=vJ%lW~ZH9rPmp2OpL{by?q~ywzoxb^@A^y#Aiid=miV672CtmaEaE z>XFMs#^0u!4iL4zko6xiFLJB7n!kQ5DhyM|Ah*wiWbWOG;qVP2V2)H6?LW*`OB9il z;g*n>eo^>zL`9eR5;APY2{^bYYRrquTB&)+|4}}GE(PgXRq4LtxJ1rRhk@pWlG;cG7B7L zt^W0zg9gW=*(#O(zod~n<+X8-?ikTb7~6M%+GwAIFdStR@!RTj)m@C; ztDe0poo1W!FHl1vv@RQVf534R*z~+I8Av7JHXy#L)-`Z*JH5Uk+?^*;s_sxI@&sxV zVsS4$a_#dBO_MQ?X<=Hg0${t*w8#;}Hx$t;GG3oE-B18QTXqh@J<*?0>wjy_`53h~ zQwW31AgaAs2Qk%eO1H6~LZ9L)HpkR-PG{Z<|<9f z{BG(H_XpV1)hmKtR>1g=63d_;@L%-uE&oaA;oe8w!Jg!ajU=)UovQkBLXi+A4MHAO z0gns9~&qhPeLD5Fzf?OLfh{NagZG+5r}_2Cg=x z=+^d#H0*j7j}czYldxReiw9*q%{2NS@_Sp8@e@|X!;STe1M8Rfd35IAeTOr4kW$V4 zXBi3d@7F*Tf5_2_NV@f5w2NFg7X1$a5Bcu>aO=8{eJ>Mwe@J)Rb`Qo8x~BrKKUjR{ z4@aDKS!gfMlh?WXan!hF7H${VHeJQu%TtY4zD`$XUF)!g#{KVkENFrH)i(bbQFh9!tJQfd;MFv%lYqTV?(H#I?%ew;sW=Z~K)9l)~=dZW1CZhZGc_q2EOl?QXaP8}=v z;#RIVzd3D7^~tC*%GuqrNmL{3FrMwCY{Tr|s;$;ogXBT!%{E+2L-(CCCq&*GZkCu+ z*lfNp6(BDDAbQxzH=pi^ZP(mtnO#K;*{jEQyj7vb z&pySursUfE4#|*qy_5?upalkq+J9#ZfB1nvR2+zaHIK^`djol;xW>Zy`Z zx|^U}vw-{K2(Bmu72U@didx$jbBXhhkS*e<-FE`3PuoYDWioF+fQy}g2gclSO;QaZ zkSocnO~sYl97*ScYw^yfC-+h}$&EFvKFJ|m&vWEf$*3zswBum;kWrQdrJkC~lll+L z&t>1G&o$lGg%e@n&%fEWN?~1({u#rg^r6VYPytpW4)yeTg>tmmf5qKh>wcBYc{vx%m5p=jUxHxKOHq(T zhQ%AO5}CAWxV8u=44dm^%$FA*`Za=;2#>rkUwdLRszL%6n<$u11r)p zya?6>=J)}Lzu%+!3W<_W2JbLX~Qhx#|~wxIZT z-i;sJDPIO{VsLC%6o2^w3ETH;>QESy+>6iY@H|nVmWT=(uXQQRk}eiQ4sEx5Ab2Vd z5+(Uxhsg^R2*Rei9dVpzg~xG%Czi3)H*St*@TkqMQj9fMD6s+LD1|+BA$! zRPRuDw!2GzgJz)`SU4<7f@4|`Jf7G;FrNRP3Gq%PnFbbDvwIosPr8HT5a?WYZm7zA zEL~V8N|Q2-6(o(Gb$H}27rX3l^djAX5_ECst34k0YKTH3jo2P{9^Ri*hwVa)Y#W>z~UIUEdEfeS}a1MlQj!P02w%Trin>kYYnoTqc1`aP8gRLToD&o0siwV$oerk^?g4k~qsnAC!C!eVGH^*Pb=9P0ni%v>YMTF5V+ zR=09sSAsV25oq>myl=M-4{4v2t@aW70B@)^p&p^O&M*N%GtYZa&tE%r^|1-g=C&Ei z$~9{+0S=ZfgmxGWVQ4k)e*<&!x?CT|R8Qii+|~|15?^0%XOlenANqbmDgS@$`=p{Z(MQBKhYZtP0?6Q}mzEp$)LqSPvhZJSU@gb; z^BESbYhuxDhrWX;mp5CU4WOsVP43*UT*S6v4VrJ?kt&HMjQRH4(7Mg3k7v+pzYq`W z>O@(Cme3q~c&XD@-c=d!%k&ztD<4pM`jHDlB?Uxzs75IK5`O)l4RgjbMY@C0to8We zx~L${@FJFa!ht*MZDSDnw=vj=`=6x%f%^mBPzyf-_iX8AiYk_8WrK-Yu|8gxOjLGs zNVm{Ox{T{Cn$+EySc5bE3Sj(_1{BEF2n_5+07QUyoONxLVeV+V>1v0|jBi(>qom10Vo`P-*rcdHWBtc|& z!%!n^Y1}72HNv(`!zsRNU!6fblU9?gXIG8c3JHVS0GR$6;O4yEw7$&GOkwjUkX%amc|+Z z>A$`@V92Y@_5U#Um0?+>`@1xPNJ>kWbcsl#C@m^Th%{0n9a7Q~f}}`ENl1ruH%NE4 zbR#YGo@c!{v-j+o*?V9A6W947`iQ5yPB@MZWNXAuRk|kx`62hpPIXWxoCU_@OveuiXs6Vk=TOs9LEIYdf*CagVav-U`*&BxYiggw-(e@z!pBO0Ij+bzMCr$ea00nxX?CCz@oobq%35qqEP~KI(apS*ZT;vIlQLqh~9#1bPdZ$dwjjJi%@N z*e?&(Ke6LSKEw&_l9H*-1{}fnMLE`AsyU<9Q-mylAnJ?0=DKk7B%9+Q?-K&tQ-vtU zkGeun@gCx+-<5wSbLUX~@K)cN&}UXucRK8%FBj8X`A%6`%X9t%0h|kf;k&8~`X{b9>jmxlx(|l%;`lW_qsVT&D_JsW$dJ*6-szrWX zZC3kpFj9fNZ?1Nx@T9&pNR3dC>;UwUG@E`Npf=ng1T3jgY zKPY5{m!ePSJwxxalVy9QwoQIcYVEo!FLzz=5TYwSqb%i}8k1>(Gtat4rcyoWyvzUb zG<)${u%aGCMw zy7BVX=M9dH7aCHYBpgN)*SPix@3k&>DxaHr%yz?JL|GqnlJZx~uR-cS3ihP+i!X3{ z04S!3)C&;Z!LeVQrzlth>*uV|f<{%9NIVK|9MYipWgH?RQq=5k&+Pnw0M`OI+Gr&H zqZUA3%oGxE8XJuf^N^wg%hV`( z?))%rd(33xHHypf>+TpEShfY?Y)*dx*a4RF(4cOvkvAj*2X}Ch%i~c3S8QX#$@7mm zhxNIm;$1_bpS`0_fYj-n`;1B`tjK;1FW9-|%~c!l|TxJ`u<_=-7X&aiDzFT6*| z&GRSwn}Alm(I`nV(U`0mWLuK zxD&z!PBsMu!Gh}9=IwD^^mLrdqE#q0J1 za3|!yk5z{5gtT5-yQMY+_F>a_`QEQu(4Ej!JGtq4cXj~w<5RgyQaRG|j9)Z~IkcE; zOj9u-=v0k8=`^FY7s@GWd^l54=*wYTa~`q;9LF};nrLU8%q7_)g~hjdox(rMB*r~P?}c z$!J#8<)u8wq0h3a0+dE~CtaVoh=+PkW=Ji;$Ey zV?#E1yoT92yOGzI=6bexXJBinduIvSw?=a9hy0Y`cD7#ZZ$e;GEL1Bhg6E6s^_VJs z72PWK8n&~1jxC>F^OK=pssKo-=TngS|Ggg=TTFzLS=)`1edIAH{k$H43!;Cjd*WH9 z=eyq>OFUamU%{?m0K1|rt9A64EBE84tc|#a{{hhWP|oic$(8ONjkVpp?Fdr<)}TkO zKeNkieWqyW5_RE&CiQmR?%5&x&-f3JkBKPd_iMt;tI_RLw-Z<|E9uNq;!-V4YS9QB z(D+hGo02e|hQ9~Cg(neUPwX)M6UZ123c@5p3ZNRcNHBMZ%#|G=q+hMw9=Oh7_8KQfDY zeYVEF)Z50MPyP#Q{DiIxotpEfeBeQJR@~2T!=%LQJFC{0gRjk8)Vj><23zQz_H|tn z6wI#>zuFmi_?QTy$X+!jC0>NY);sk@JlG_JOd|CjH19$+xU=4xfh9{EHM9+|HyrrwJZCgWDw}6b zIAZ}(G$C5tTD5-^*~trA^eo?H>_mF(3%><*^q{krgcWoB7 zL~iBNKY@K?aM;NVVWFNbDB__g5JCmC{`Uu{`I)&$MDncb?SR=@BjOg^=4IIei z_ax5_A~SG)kr^t#lNsN2qZdMwUA`BSJitY3q}GDg*6{lO4N#JyA6;+3(MergQf?UJ zW*d2m>M?WPxVHpUVr#WjElAwoM3@4PB!2WD8Cr4q$feVzBQ6U8I5Fp?Th~?CZZA>g zpx~$hYU~8ZJ&DkcVJdlFnwi~_=a#&b8DjS=X*I`ly7@Av8%)LTkl*D{3(zn6i~7D> zlYy_bn{kk(d0)AG@ny}u52V!b0UQ9ndl6Z|KFd}K#$ofTwvh3k_1DnLPoh{o6ueg3 zhc!39(BYuJi4pNjyi3y$@4tKXfRmw7yy1RPHv6)d%0S01ev4k}ZEjzfId-kxz(%!A zp5t1_au<2edN2y^WDX-dRuK_TRG9o8mbgdob#A|4I-PT+IJ8~t=|T4JrK|E)N8T>dZ+J_6+t zQYgBe9Gh+u=xTTwp-|pa3{u@8{X&5K@&>En9S__`rsl`*^|CpVBW#!2xYWOy{sE)h z6=T2(`E(n_HQU`=H^2&sa>I(T=ceKEg`LgX%obk{?B|0$uRKkjk=(CSZTAE(mEi~A z7!dEng_FnU*c;Qe2BQ1rr(IJPV!ghR-rs_sINoQUGQjg>%5>8Thp%O$Y7ON6<+Eg>M20fNo;oN3dc}?$T@4|8!qY-x&6Aof|rQS8a{Uc|i&&bVn6U)bcvq1Oj6#xe~_ z0?~9AbL9y&IqLiCVGEbKpEdCIf9K9%6xeu*t>r(MrADqJ4famrW8geYV$@z)`*f7d zd|kWI$bB2(pj;G>kipl@KStlL=k1Dqo*o(z-aHti61-_rCX#%#Sw38@E!6PCsSrcb zL_PxKs%I1(D7Deivm=8Tt~R=p{gtwfSKY7eeaqY8m6|RgR7oekydc*BOXZ&ZNY(^h zr3RV2k(2f$!|hF}TN22c`so)Fa*OTiV9}qk3fc{x-sq1)}>rNo89@!u6hgeD5Hy&&*i&mO&-RcJ`fucu?7w48523NL)h}`b@dak zql3@dy_UXBIj3Q;Sw`$zWh|{pyT#T>sz;aIN~8dm#g%tFRn)J*a0%nV ze8}YrtQ`M(AgArifJ=k`NbSUM{I!?kja|twB*ZE+^h&ljY~GdN;*hEaCMbx!nPNn(X&Qg&#QYjQ6h6yi@*xMI^)P zCqaRfI{)UUm#YB zTT0X5dhP;<5yod420fv+di+D+~%bFZOOX2l}#Ol^N7A0!%Ot3gQY-IV22u6^PV z;%Vi%f{1IS;|$X_-#YokI#8zH$We~7Y1`O&FQt|Jj9Cald(0YaMb^XfV3Wr1Cz;-S zZu%3v2Ul}JhAI{3M_V6IVf@>Wnc0Ur+82$z?}?*IVH?pZr4~WvBe? zbxPpZ2_(ZrCy@8-J3Oya9nTmUX7c!|`xRgT*ZEp}ZL#X!({%!W`Yr9w=B)c&+!XBG z1n-?kf)ZFGxkM78B7t=-9XwQKbcfs}?dP85QxHa?aX9aC% zw@RtFs7@x&TWU~7x2P_3<#xU)gSB=xtD@SksSy03ABjn#(RPpsXp?>GU-@`~vRbVa zv+g90Vund50+dqYw>XMY`_2$U4Up8aq1IuTmw_yUXSa6mCqUGSZ;Frqz^bKrX^r-*iFPWDPv-X#Ah6II7=$Hr^i^|nCq zw_PM>Q#x5}LV3}kwKoN`ovu3D?C9rNgidF(ad5chKeWg;mlL91t!o*saNHm`ic+Gg zni{o$&1_MHq4P|5K~tsH&wmgt2P58)r(8`m2aA}X5E$4{Zhr|m3K1-@rFksRl+DMw zfJa9Fxa&eKO78aiBqOt=9=;(T<_TR|R&B9;^wRV{4o`wmk(!t!D{x-|@5Tt+yAj6o z5t%9_sLRV$mV{cs?8=!MB1R+8yf=s>>E^G!DtsSjy0pN#!HrH;_9Fgga==o1?5kza z-TM+c4CbKWnCOCN6{#3`UL~eJPOrdo%fJb5gBtNVA8b zm}&}A?VF3CA!|AwY3!zPO30nd&GP|xH}QjI_}X;8SntTDXf=-`NO*FQpuH9Be~iiZ z^PcK*71cvHi|qnfY>-d9DR3q?ctgU4<35!$A#c7P$(Ms$XY^P!Q`mfGib1sKnzfL**YD7%J%WD~^XUJ&B_CpsHMJ;<;0ISOT@LdP7iQk=UOAdqIUN)X>;T? zfOigz%K3V-KCXI%!no-38_`mh53MGbQvo-hEN9RXoNX|Au5Vo)Rln zJgwlVBnD=oB;v5oZK*(@l4(Dn20|eas4~nnP=G)cPSTLq5BzYvZ>iui4}bw*c90ny zzD??5VHg3+0$ouovHi+In&ZON)6NU$eN)f9wTv~uxKtyb9xJC3g-O25epAovOZ6Z6o}xHqpJCH^k637VBL($I@}$gj!}%|Pib6NG=g zsY*q=fN{;QI62w#t@v-bj4RJb$XZavb{;uAy+`#6i zU97LSNf>@R2Ay8X_@Uv~_vE31w~-;~lPU}RxTIpb-jWto2P=KkafWl6SVfWUtP!hrz5 zKF-tYN^fYhGm;l=>9IyR_~hYno}1yga)S2LJ97B=hVkknZ8)1kX4p6}@@gZU^7*gU zyj{ED&+{?#P$U~BsXaVA(phi>+zs1PT#w%TtO9pU4Qg56k@z_fP%^;{IxA_X{s#QL z4EadJx>xvO?6j-9I_qc8q*w?BWe2bW_-%jsuUMJH=#AW}=M?T7#GiN~r8;T-xN_@x zk@AF>>uj-w%pZbS+PjXFYQ8_l(th)hctcao8^2Y;@5{}&rc$C-wEJ{N6nqWJ*7=cK zZ#N#sZV8p>{&uz0gR6z#Vq9<#;$X^LDb%{c>krnyu$ZEoJHg|X`XKG_>11}Z#q|`% z;wnu!{z@B+$h|iTaNfDmok$u}SNs*@iif^MEK^~V7grliYXN~I9^l3OM(|=2lMkF?G-T86 zvLnz&HSoH(4d4E-hxsJu5q(&h8#i$tK3}VCQIq_wElnZUT#bf(7dmec{r9fT~jjzQ-OY}=Q59$xkz>6MrGrLbRDYAVJd4! z8LDO>GzUQHFYd#>5PKa`A=A+KloLbA9IZ24q0!a8=Khgs-;;qqkFvWxhP^>KH5GY_wI(96%p z0-#m|2Wn##(ic-jYs56Arz<`J2jwD5<#`&nI4`;w##)GV=PJ;kNeyyH=sZux*sQa* z)oIW2ce2;L$lgqCv6BIWQv4I-qX^+48e%8Ree>XX>trL0MGP z;;t}lzQEfm-$8#UbtI^p17&-oFQX-XPZ%2jt9eD(JLN_m0#BS*(q0&sco@Ts+u9ozJaj);&ZP4^nP0R%3OEl%!uJ8`Qly+s%i~}n?HLVT zXvn!yW-%aqPSqXTK|ESXCbtjSKG`hMhlw4`DsF+NN*wT*P}txF!IJ@}W2%!6R8qTs zFB3XwQ64H{{4%Suq;mTjJ?YT;@`CKa;}{B~9^ubAJaWQp;eSl512HN3uf!yZQy?Zi zJ3>RM-4&k*f3-v^2AnF5QPd#$sUA*pFRMSANy#`=Ywv|S(K>iIewSNzH>i(ab5GXh z2H8xw!mQx8j_TnxH)io#*Ru1i+%4k>!9Zxr_^%e(%3?lGUgi*($(*Z z5#gB>%)ig1G*7g=||_uL0ST(+&1fPi*b_Ho?p zSnhnAu=3p9p!{`(!Y0@@7gyX}DUH!TrXw>=JC2@xw-Cwpb+hcD$9X}NS1yS3B032K zSr|F489xK2C^c>diB~Qm*Z;>5M^TDg-Gl)@O+_ITX!yYG&zLcf2#0E;EnO@W4Hi5~ z$Dx96XXLSV^zAUk9(($BinN4SG6GM+y-I_+Ly8HmU>B0!0pc zAE9p6;lpF6!iWCOpz5JQA4A1?{$mss_MV^h>&v9PklfF-0qQt#LCEOpJe{IpHmvC#sM0>A3qDJEvw=CU2k5ycv*QgC%*HE2ys z1#aji-1@p@l_6xg(me&GQxgTON8(ebTMwRB&FUuK%M@R(XWw%ioPGj{PGr_lI+c_u zN~Hxc`8(Vc>q+nX>2FN25l_m(^5%HOb=^-sF`Y~%KA04nUGkx&kh6z!FU-V`JK@>S zsFv|?1+Tjurr9R9n7|t#7!EpoPZZDekTKiTka3jHuHgdyJQN+$r`_{^oeo$ig{UrylL5iS>I0k?=dE8)qO-M? zoSMsjM>l;+PEfCnvMkV?Kw{tzn$zE-n?YE9Nc!_|@4Lze^qV;VNN2Ds(yxwQ0YF-2 z&B5K41VrgBxn_vA!QrmHcUA>xn{P}C-mVKZ$&zKVfU`*pnff>|1yWUD#JObD=GKV~ zEEGx%m~55CJ%Ftc(3=~l$yv|)us6m_4d4#EDI5y5KJ21Wc1PnvOkfQ+#y%O~3vr*n z{{^vgiCba*Sz5YC6Pvcgt4TsWme0oZ#jM}57wTgz>56fenc~+6-XN{vNoo*~1Lth} zD^u3Y0On%h(`WPzuL>3}WJsK7?4AMu@N)6`U1y^rNnRNWoRFZ0cwj7eHE;MQ)ycvT z?U>KE={(B;Ly6FArtV}ZM&P1PbEy|Oc8eIL#xd8iw8VnW)@a-V6<8#nS$S9lahTZ& z<=faaPM+TuIV%-I9>*aBk=IY~*#67BOuHAs5sK_^*(rR*I1fNs+b-BLbA&jJk4Mw1 z?WKIW3y8FR1;wIs}dTk4q(p zlR-}6cw9A}-*h8fc}d0K!}Cz7!D-FE$`uMS4BYhW-?-zvgohgb3=%H?IaUbDDV}y< z(f&CcyD>@(!m$Ke(<@cnHCO79>A%}{Weune#ky6 zuN65ZewmW}3q%4sAaHmFcjT)6o@~iU0~%QAer~Lt##nHNScPGCb*>wxrGXft?fg}E z_G+h8nz*c;!eK5pFK_p7VER1yn;Ap$u@0e)(pZ zigu7fcozjk#a{hCLmtY}wHMy5KlGHhinnY8;7fb2W;Iha1zGzvoGSa9!cQ|<%5Sgi zfJi^hVCeF`h{wtV*X}o?U{edaOg*v{7pP!D8M)pH%9)&0e+$eW65U+@<}SRbYpCMm zbGelI^NkPexKrg)Sqq)Hdv3N3VG$_U0Jykh~83>6*=tSdP$Tk(DJ({0%x0tHh{@ykxE zKcZ_kHUwi`AI8n{6C3uft$Fk1>@Kc#jC4j5x|%;*bk1|uj)%I07RyyD`UyAd0%p_? zYSF0SQ(mv5Wrqu8vDjQc&13c6koiBDRsxXU5z-4REWp)w4A5^;Hv~HELjPT23O^y#n z9Kj?8w1J?er;fEpfa^evjec#IUiY!Gh>Wu-XlQvE0ADX4g!M1$1+b)>67qStVw^r| zLc9J1{xfx9r$J<7x+lc`A1QjSc#(c=C+U15?u!4(2*ihAJ#kv7px=pK0HPsx$ zuG-&JJ~Kt=k`i}g1g)TZsc`e3XcyFwNBOHEF9$$@A8bNRAU+H6gV+K=aa>SC-rXul zCCnM2o4*Vfu9N?Y6l6sKZUZ;rYG12X^JNU5LQIuK7aWT8;Ii~~7$@i?R1YUj7YLLA zmhj&ho8M$~w47W8wQ#LK$9b9aP1uPei2nLOi`$?~uA(JkyKV%w-yE^(;WmmdYz;av zaK*}oB5W%MMxKE&%*SUnoxP4!IC;dt^u-NTPCvjhr<}$EVdk&n1Mq8~;Qz+8|FWF8 z=8efvN%rXwHk$ziVHBrtYwl;-%{tFGcfG2ianyLW)h+#6&BiWe21A=JM?N?X-uC$C z9=-cGEl(+e9D_@ga&@4LwISkUNSUn;pWZaUKWNDGWw#zoQq2)|v0 zfN<3Fq{h1_k-v2TTm>LlXs7-4v@x<=d>2l%<9)NXzfoY`)86a~Dm0>e!UfjO$A96L zH|2{vsd+ATw3(9-s!M=`87dwl4chgBK)YUewllJawqxR^%ER zSe7zbqj>qG2f#mqsGc49pce{`mezSd=Ss7DZV!+(KP= zx|Tyh(&th8bB*PUMEtY>G9|7X!#&_re&V+=$(ZT8feYM78c7~vC7g)RubV1Z>nMk= z*cA9dX1aLj~K4JymzxQcptT}SFamV93b1yHe0Ng!o2K(?K{60`i-x?akjM@_n7ox zs@WqbbTo7p{cx;_ai{{cNK1!ZksMZ=yt$hY^N{Os9cy#tr6TCc0MZHw9R9zSRy2ox z7SEAi*LKwcR!ZgV+AZgw@H2foe)XMd8FHSeo&Ch7)~>iAzb3TQKUwhslioNY2nwjk zx8ah%o|-LAq}zC2`8lpR=Ir+VAZ9U|@xfluO4f7Kiyg!@zBWddG-aEBh? z7)<|%9D{O0_gX0|uYYs6+T05|T+A#9?5<`Fc+|qigG+`C_ZoF6pOF6-&3kvF{)la% znv5`MKzQ@DT#vsS1wt~q>h}}xlN|Z0(ZdZOp1=UIZ)Jiwe;lOH{{gh{Vc3H>s%Plr zwJ>Ht`!<(AF`2(R6D`rT&X~>*-6rwkOwfrcA=jNB^(-y?xuzy!;7tQTa$};Zp7cAgZ!%PVgJUjs&vx9x<@)IBL1cu_(0>2 zPM#)QF4g=*D!8=W;bHR(andX|{!6m~cave<(}@G2>g3HYr_%BMHemXXH+ zU8iLq6Ze&v6mN-+ma7UTyL-GeAPsy}Mpnwd^n$Vb=0j&B5AN+9U}Spw<)w-QKU!UNh)qJOwjp%sr;^csjD;e+s~;T->; zqlTAyn>$f{F2oT~1CGFi+I}|GLNidJ8VWGT^+RJFJ-=*Z28AERv9Cqdi`$${V?5fB z7qxCca&igYm750-*8Z~>)$;MHzaa&J17qPK>}(776O9DFcR(HlshB_L(6(n98QwI) z-$DSA3Hg3MzgPCuC;eZOh5sTGI?)JP&c!&cl7eObnHOa9*leN8^xh1m<|q5at-7JC zmQUx?KLrcu@%}&vtQx{QvHo14i;nIieIz0R@>GARJ41fcOJ>&B?YpQ1|MF$EpQ-s@ zRQn}(Grw2_)U32PR02wGtnBQ`fX}!CEqnDpEc;G7=xOB^*6|c&JUX6UwU@+hrfzQi% zw@t@|*5wKJ+FzeoB|STdqY+I2of@E@Op{O}mG=?IzEp$S!Ups#{XHm6g`YP&vLD7% z|A=!y$ns!2FNR7aixVO3^<&mzWd1xzdR;ouiYrDA7t*0xsy!m+jFsSg^s!9l?WPnz z0Y-rovI z%VQ`S)SJWr^#zT+t(M$0gGwIVRi`t!HrLlJO$d}*vO&dabqzDLnu+%ATXp>Lj$@Mt z8x>=Ax<6o+-m_2oExtM-Wh0$=nf0Yrd_`!u^~=*=#6tz~t2qx@=ilCA1XV+&;LEFX z$?#Sd)jCkJ!QM$s3>#xZOdvHg$Gs*V$$F-wOK5=#r3vjWofm>a|G{|?9L}gWd4b!$ zkHFmzIWAFZGx-+<(Q?YUyv$WwdFY0S@0-OyayI#s-L^9I=kb)Cq}^UiIgZ@_tRe~Jw(FkmGG z$*L#jT*FM<9!5M28c;#%f3RNoryzE!=*!QMdjH-(e)A770{Q1w+5GYvgZ#)%gnxz+ z7yk}MtYBxKcj3a?c50V%>mGq6mJS_D`EcCnY-+1J?%|4F^P`g$TkyC&M&P7huLcEI z#9H~0!EbhpBTmTF^RHyWe@ehWh$4Uoxyi=r{XhKt?`+v7`>a81CpqGhOE?Sb4h&?M zqIzG~$OJ z?f|sH$>HjE)oSFKi@B_&GX|KALu>qV-pU>h1*tRSXRv~7U~#ck?Qh>79fPX5!gHCe zmF)=gU#LH0)6WCILai)(1zrVA_wgg9?QrbCGp=B3qW4I^MEYcD_sS|C6BL2D2O=v}?EhdAog%^j^c={rUeiO<28L2<0ft7Ap(Pv91-fhMAmrgcT%856 z4=#@b{;uKZFWCoa>grm@-5>#d$l|+2e8b;Tg6i7ph6ttXVqFN(@cl4-LcQ6 zj&^5|CatWxCQqjx{4|(c_AM+ifZowQfd z`die)CfNH84w|HjbWDdl98ytx(>B*$^s$pU(3&b*rek7>`+SpSCzy&g8F-` zo8+JI(RLf71`Du2q0L`0ZmVxs+|A<8+hObF0h~CsE^1yU_1I1YSP-;U$A_@7C2A}CnPh5%iPndrV&{lCa>-yme_&NLFwQMJJ z4$T_9ngcJ@C*)fWu3a0>6mQLsUv%x{A@0uK*HEGGC%stvM+T-P?`=0^vlvc|=%b~M z>DarHGLRqlP|avoIYk|ceh+k$rQ3oN`W?C6KXQpJyt$VQm!7W`CVmS?X?At(S2Wc{~PLJTL*3h>id^pKF)Mb3Du!+qR@UDjbU^F5TO6R=I7 z6XQVU(Q^19aC))2R*amB>_&V+hVZKOO=B?9tLiqY1{&w;mJ0?1EiY$#aU5LZu#fko z^ef`tKdq8A>708u_;rLiS`zPrVe1%g@XaWYg6W`_!obwi%F0tCuxlFcds2Uo8vlNZ z)T<4DBX2gWYxAC*X{a~SXulPNLF4Jj-TOc`eisIP9wNk;TYl()XQg@Il){`Uz+lWp z(%}0BXCu!5?N)iW&WGGM1EtuBb>7GBLcqMx~>h- zb|?@oQGgqCeeTXkOnKPXX;irkoek_%BD4pK7>xv;Te&|tw&X<0H2TUyeMk7`)L_Iu z$KQWh;NMgqcTR`@paN&L1K)oYSA+@_<#AXP@Igx}iGQR4u=~&CYc;4bt2G8PaCSv0 zUYo!^YuvGcaSq41njH_gMIM~O4s7lV<+DS~M@h5U;6r2dz@L>y+B6fE69J}6by)#B z_&Ud;?!~$kCd>!OXyu64Z>WZ^+!EI_@f>jYn5fIeAVCb}d94mR>B`dXF#hUIAbejJ z0fBtX>nV*8$Tx$7d{punP$~OC3u^N&{-qrQDdfM!Ht4`JLoM*;n@bJ#UErCuMH0$C zY&138e7;X{{v&#B0`ouujJKmi6r+G0U$2wP_itACD_!I|!KNv>I&RBT@I!lC(`xN8dB$QQ8Ow$M)X{CE(buD^GHE z^LP8erFaM%sd72AKA0Kf&VxQ1pi>Sng3~yXQ_Z5#5wZlz@?ISA8WnOr>bmMk=bJ}4 zm8`6mUId3nNOh`&o9Ly$pMB^Fr<(u0*YrR1GiHe54G%)7r^P(EKJ@^8;^mQ&J`WNl zGkx^wU)!2M6lf|f$N&vwXjE7nN;tyY_;XGoy2or_s=gdJwP(R64(AQZU_Q>|CL6$J zGl8~L*z_IE5_|Wxx*6AP1C|XvQ0Ts5)5H7fB#nM}*k>C(UyO+QEJPSZJ>wVpA>d%v z1?WedRnBgbI&vAW_3%5qjc*$sV%%g8|7P@7n3jtiQ@r8`iwOD@f?lp$GaqO!llwPa z3%^2I>JdReiJvy-uigaYM9|TvPx`OzWzi&w&1;>%WfwQlK{1&MfgO50 z#a4sTe(A;Y(UPNxxe3_i9ulQ67O}HP&|FghU;XL#C2BYZN`6$+UIvda?pAD|{U7FCxP&>f-^7WXO}yjh(Bh8FHIDS(GLaU_FMd&t9MVGmUh%Al3A0Q@P%#yU_kg^JZTf4tD5%ft_p($4_4(n%!qX z;p4r3U-$^`bPn?;m%hU-F=@221U%W7jn1H`_b(fr+c$7PKI@O&3mQ@Nm09|vZ5MuT zjEz_7cChjfhvqyI_omJdBVns>GNu}SWRIK=P8p+j&R~n7^*Y}iuXxc}4W>7@i^EEU z;i+Jh<5saYiNxy7UwPl#dtY#vBQLXN_X&#&pP~}({j&cv8%+8eycO1yg=$X!@Y#jx z1!`V&Q1YC__qwIT7bKB*x|0c5*N+dVbgQl`?O=^V>h=F9#W>-tYea)b@Trpc`(A2h z=wHAandr}aDGEs-9{U&Oo_@J0k7B0Wm|~+7ilUhdw;$FCSroTELxxyu=-8s2q0o%A z2L%Q-KiFU+2#E_6J$cd97Hms$2$c$_sz@8KeVC%SgLrhK4JPz8B(zG*R?rZcJ=Y31 z!-3+n7l7IOj}4Jf4hwy9@^OlOThK>fzFb6l=Lj!*Qd%h-@X-d(_0Jv9MG3^+b0+jzz^MA3Jt#(&8-SSATcxtZnG_))K~9O# zw%0elgJn50U}~7P$Lnsr{4;#(2&qgPN4hp$T56+v4@+TwY*bklfM=Gp2)jklieVS< zZ_tdqebLPTz|lm!ikAaldHdPA0kn86NTUO}3eN?y>u{SOVJ%P^zuhtBlneRkZE zhMPk}&ZgvM%cJO82#s7WW*X$T&-$o1XK{!xqAzr`8-1>6OptX6klB^qiXrWi&e>Xf zxVEKUQIcK#^{gheAOwr&`}@vs@{&suNrW1dqT3z5&!X?5we5_uK0PpgTqnaruYn)f zM5OQ6F=k#}gF>LB%%i)Lx312wFai6Vm-m51?1)o;N}vTroDq%ov5wF6I+QvrJH{9! zR7F(#pzgtdxHU6`z~y?_@-2+IaKt1uQDXNQe6Bm>)JHrv0s#vKPMvJGm0Uk%H@#k7 zFa5l?Cego3*4;nO{{xwbDRNci)9z<>B-T}(%=XOs5OGuq?3`=LhX<~`1s}Cl6s4A| zGK`)|^t<89n^y@l&au=>$5`kHYZPY{LhmsA(hgHHc zA{dg%H$>M~YLTK*l+wKe+|D$xvqs@h&mmpEJ=;hPas6ch4?>6jME=qtRbm2B+XmXh~cmLwhhZ~m$sma0xZ_;u6ponbaX8pfiefNqmr|X_$Pd2P@p^m^4U2iuU+(Js zI>M9pyexzSsVH0bUYpCtMJ^Q-b-s!CzB_(F_8A}Pr&UG-;%Bnf`RbQX`DlM+c(N{q z>XeZ(lyxSgaWNe7JXge;r%!4+%C0~O_h6;P=fIHU)i+LFZ0++leIeiQyuFd+>&O+K z4E$IF;CrbQ#itz_A@Sv*PTzFiKsqHvZwJf#W43H*sXG+v>A(7Q0B$L9V+wS z;~b~W55rewm+uZ~Ws{DPM^JogQzQIQQCYi7(6C+m`MW({ELpwycOFS3O1f-&*=f`aAQj;h)jMv~kCT3?0 zSVB?7ln<|H3#gKFuI0>~e~ z&yq|D4Eqv=UqUi}%$8z*%+j(FapOm40;aQ-GDeGTV>H8Dn9iq#Ao&1R(^BjR9b4h% zIN`v)vR}KqI+WYLS~0#!i;m=b=7Nk%Lu$HsQ{L%BX5K9JA&j{BKKAt}8q{5(CDNDI z!8w&!CyqFcv}3rPQgY*H`oY?ky)z?Xna7lZ6nk$h(!2UMd-L59!zEU`Om&VAGtgzO znbn0@KZP$O#AM=S_E{%rL-g%gYba>@%ViX1)%p}Q1$P8C;bABQTJ0B_vxA+mFB{~r z^aw+hJ+M(SCgO*sPpg)hysWwAHOgQSjF!(4Od^}SSLjKS?}5a9gbwDWeXok1UltzL zSnh&uV9J{yd+LDrvs&u6Zke@#36Cz8`4Ona5pQx;VwP=vG%77NCcPddfm-9N?M?s+e-o<3r~A=#i>aj9v^+YY){cEd z9ua^-RQ|-fz)c*qi|_t)PF6HH`fkay=eM^)oLbDdf5zqBa&OE2U^&!qhr;gWQbFX{ zcNTnTJWP7fO#hYWS2&y7uogK{(MKegYOAn6FxAfI`vNQWwsax(^mJip2f*l+qlE=;rKT$3D zJZW-eZ84>b92Y#RN9PcSQzo9 zu3Fm51d)dcY2-!qOb$poH0n2xS8Ew~B1k9W2Ybe$O7(rFB)zDzWwS7T;IM}3t-UYa zSwt-cO%bYkoL+KVq-C_UU|);|`g9Z(bn(CK+i*1F^Oq@;HIw1?&H~FQjZlg18=+rK z>f-6CbybjXZH0=I5qs>x2)X;6>+VRjs9cEDVHngmd(BaO1hBZwe&ke3i#SYl%OJSQ z)#({Mv6EFnPT3sHdrLle4=s7o3kD9=fz2U*)yqRwnX}?Uo#smS!{?FkyP<;7UG|gm z!`nsqtkEi+AKrHj;C~`f;vV&Pq>q{uF=D&?^-T;yqJLQS>*w8W(eIi4@$1I^@gL#G zg*O^3c8KiMxYX)6;St$_gh#X@I9hp!;*7DbTe5O(+;;!4IUgCSE+(tAS+P6Vj8x6w z4*O{laK=z0%fN@_VXZ;Qiy+hRO#PJ6weI_ZHOavrfkx11`iHUih1NMKmCA}e=_M&J zdXiGQW(^Eg49R{MP++9XwD>{Bft%aID_$)CJZ9I##lbjKpsJ0$esDje5LN{B5wzEQH)&3>Zvt^>1BI#j%a`-!2fSDQ=nnyb%;9I{Xviu1^isj3*y12bmv z^%WbHN5GM4hmqEaf%uCCrG;@bQ|a5v`(mF(HoM zaM0}$hoCpfm@!)V7V8ScPvZ~U3UHGFxr zTtDeDdXlcdr1u}GqzH0DxiKu5arD1|J0VT>)kjTwH-3YI<&wbWb7HcxBS|Hl%GWd} zTV!3BFC`Ff;(8*jYFb{YsYAbsfZCE?muxgaS2%O$@mli8A;EsA36nxy9;Qa3KZ28C z*ciuTA`uS>OBsy<$=;eH8?OqlViy;92sEE+p#A?Pt2NahPv$>-;I9AcX8lwp=Yf8NQ$6?P)B z48y{RM4>BqK+;yH<%t&)E2Oqh$A0d%dG=wrU$2zEVb??bBZ3doW1Vq;TXn`o5KbTK z+`|-hbWX=s;E*gioXZOxPxL~Zpsc7OqyCJHR5*m79w28gJ9W(bt{UQf((ZmV zi>mY6aUZWa;63S_!`zzO#cvEj%Mm-~HNky>kzw?;KFGrIQ*;(VsEZao zNybB*NSb7n`2wRIu9kYf>n*x@b@TcBw_3g~FBELN_wFmJ5`L(MpQ{KC*dQmbrt3h$&|;s`v#{~XKvdm&CX}?$|0Uej}xIy@$`zf-c2r6%pHP{V$~#Opl5X>7+ zMm>ml?Pl_xD{nsx!ATCm3?mUd$gEj-DRq`Q)AtuPlUIbSe3Aebja1pCE`G%x^Ynj_ z_SRuhu4~`$Fu(vqDT30Vbc0eNNGV;4gmg%^(mkXgA&qo*DJc?z2#6@%ASr?f(jD_& zgRZ^T-p_va`yJo=$6D)He{sz{*L|M9I?rMM?ufV}R2ye#)?lo5E^;0_&0{YiP(c}U zQz64dVcaBQvHIoYd?|UZ*3%ES{1n#Yd5KoH%SgN)-^XLd)Vjx6QKsxEfG5+XKHc3; z{=dHx{MgzxVxHS50m5^tOOExSZLI~|Ici4V$yi$$y}qYLz@!jI8gm2HG<&mWvUE|L z5IWeQs^5|ZB&JdO;i~hJPI@J)OYhc*sMwoqyvv(Z(8HI=ofe1Sdyy>2SA7B-pK36q z6(0P)ToDFo_5MHK;(vX6zpuyXwht3`es9G&(7z@IHAL z8_E~Ee|wTo=&`r!#$`4SiNG_Ts3Gy0pRKAMgcaknfb&6$!y(w;WzZ!|St~bNO`r36;%|s;q{>VvK z>Lw=U;fKrLlg|g}4F~AOR|oc94vKyhr}5*cJk~xJ_f0UyfCuGEJj?a3n#6XD|##c(bOu+*CkQJJ}6x{y-3Czsqsbo9_h33vOD!5(v*L& zOrx1wSb`1H-m!-sR`@)C#W&Dg_xoWJDZ3o6o_v4}9Fz;S$v-_vHrzPvYMQ8C#nD|7 z8570o*Wr#Z>Hh8~;Z9aUc5`F^&A zD|kwv0P?Z{EWpc_Z;S)S%0Ze=!g#~*{NH@%1_`g=7jor0gqH{#ha8ntw>?IX6N5&I z7!E9R-3b2lg-)OKL05kjH~Dl!{UYDM`r=+H0|0gEdj8HJDG^td9XqJG9hVD+J*;RI zd-TRGUQ3cMf`}3)%zFY+4b*3ak6+~8pq0_QKs)RH3B9Lm^CiKr5BHvQ0{p-KM&utg zUNCn)LhuJS{{`TY=L_@87sb=Mfc^mTXe=z)>J2iTbl*I4pJ=IlB97|7&+VR)Z7Fv! zKE&Yx-=?JjxW4?AY~ zRmoV;e~p>8CK?lNk=Omj-N}K$UD+d~``zH%>-jPT`wSexyu_xJhfufdJ3@9=1}R;G zH-Dx;*qt;5!bF1EM;-53s|Q>$WryA2i^fv7O^b;;Qh%?3Z(5Za7710l3MyCCA|&B;hCe_sy=o~nt8CY6C|h# zsbTHUkq|j+Jv}JD{{>MBfHp1}63btW6`6%BV|A3TJUN>0@Y*vx!#U6$$>aW-TShCp z%j4!X|17^wysTyZv(0ccVI7vU<&C-b9&u^9eWu(Ty$`#;yp199d21( zU@?0sOd3i?aN8vJp}A1+r`p(Gn(|_~ld$mMqkBSK3B3&vxGyXNMp z{}cDTz!z~w{GaA?qs}>A%JT**V3><$W1#8-t4*Z5atvk5^+irA#W{Q)7S1E35Vy4; zl^AI|#E=y6Ot|-fw*@ohdAH)E46^V>^zF?=f3ryvq?Xt>p-~Sd9@Ry_6_k^%B*hLl z5&gv#T$it=vv-&UKhBfOT1sZUmEZQH<$;U1^e4^8k1wOn6?+CtgSa{$Ifn5S&zIN) zH#an5shhEIB>^JCmg(wZZRTHT_CFl!xA zPvCu&C%udp(aIEv3B9%`4gD#)_;Qu2wD*DgZ=owaEXpf?IU&%kX z0{^XGK`S#W^s&?OMI)}yQe7N-?Z(kNwouk+)0@~O6%oZ}A)?>0%o(uSUNSI$JlakUortj;}=DhV(Q>k!B?T>KCsN!NH7KsEDUcd4yZ55 zu~&m1+@qoPk!J9_JL~|15}#Yi(nA5mc~*pc@+)OoO`D$r9#nxWv&A$@cj%W?qH9*c z`L?LlBJIGf{fo37YdkUiKr=S=S!$7tr_qqrgb~{fAXK!a>l&Dl4`%J*ps<-4+SgM- zRVEDJyuKIFWd)VOW3JY}90RThnfV(N;-mpc7*Djh!xJTw%6OL7K&xv2AUaHU-e4w%Qv7r82Gha%$>P2P zCcyzFW?z=XI9fVgJ4d*w8QH=&DIk6)E`GMBL-gmTkMk0H(dW1O#)VP()odgir2sNs za!v_CW!mM3qy=w#8D8;*25VTbH_EFYClnb|jWNtQcnsxJV!nTQ! ze!VJKQG|Hznn;Di`vU=Qi8**WewADt26YOb_+L_rQhuq^7mDrVLQm)=P#gpPuBFIa zWeg%TdZUR}QsONWw|G1MjrsmvtG>`tP!xaHQ36FBA@;$0y8Ng)!M>gA<@wa5thwS;X(PyjD!F{v)7_a|+vbfYx;X-8e7 zT1#D{EbFK*9(8e4EQLwcR==G-JlmbJwZokAZANiFbaREbZ-zO0G%{sTF&S2l z&IP6yweZo>mpG9r58~EXJ}gdFU)MEYiB348mwrV606j@0LSm!=Kz>=hsPmz+jt7^C z88{kDz}Z}p3Kr++hpm0;d2@08GXV88cm?tm*VicQK2Zxzfn(cbCdr1&p8|H45 zZ{rX8#Zdl3)bhmSLFQ`HQ2V@J@zn+?ga`Uj?@aDk3X#f@x;MSO^=t~#U`5mDcCuY4 z=Thig*^Y_o=nc>@06dDpi<16wt0Tp9fTI0}whN{S)?oJ z`Xz-MM1kAIY!-=q@e2u%Ap(8RJ4gjLCSP5NACaEQEI4|!GH5~h>V=@pC8F`a2_V>* z6o087%Ow?L`wKuTNJy{Id(YIC=RG-6{JKMcU#7bK0gI~MOVHNeJvE5!;*3b->|k8~ zErA#lOxR$xKMMPzX-NB!W2U|l+h94mw_^B+N!k8Fk{v@+?}~SHk)cpbY+A9YjK^&D z8!}t3AVaqFs*;>Tx=>9^U5Ep*V`y9X&PSpC-+s!f*#%NO00*}@v3ah5`>iWq6ZYJi zIy$uE0bdSuj5Q?nfeFu!c7QySlisZi62Rh!?(j9I8!mXk5{zy--oFPv!lUHw@GIQX zt!2Dna=dKA6*h%orNdVjnaD#XrlV@UZFD~1Dk@uH6FV#PtEQ~;5h<3*= zoDu}aCU>p0nh^38XpemC_U3-Ah2OqLTM+?lmC&B5k$-9LW4rNaY?uSO4+3kk-1e&6 zFGWW3I057^?cPlLgs(`8kFyG$)b!h0|7?en3-eWk)k}&=%&;ylTF8%KpWNwD`pikck2C+ zPHjDv3}!ycq%qC7;d1E$=bejZg*pWhZ&tLlTeMj7kkcjONEa@X`fI`oKM_$GuGibj1A zflq<{#06_v~V6i)}$W^ARO+|J7d_TSJlUUdat z7Kw$%&QS-ic+z8&0yyjq0lM@ZGN2{l&v)E@m%dIr=H4&v!b@lo45&%&YlP^g^Tcb? zV(z~gBv=W1Zq`2(tn6Eq>c%7SOi44yC=6A#9XHHbRKICs;+Q#gD#Gf2Eme!O3sSyGc|u{ zH<}oECodxg8_yUsxr_`gt1zXQiAHPz;ITD+3ur1K?}gX?T!j5J(ZuE$-b68n!684G zK>#0nJR@+^mBPa4!(CVUTgAWZxA={|b(_&*xmG>fOMh1#fa@9w0IqAY;^uCUCdWgg zh1{6UtV>+)`b|D)VxrHh8h@NsjiI?94Yg~9VG?ZcG6Y@&qA%>-vs;vp8L!6>y;e>& zR}fm^ehqYM1M%n=5`22wr0PN4kx5;Qm@If9<#LY7#-jUhg*$6xyhOoieGZk_SVL**7{>m zjNLu%!ulpWxi5`UzEQ@B<$isjrznMz;d!i1~7wr``ta0waIvY0?g&SIj$7Rp94lDfM&mKbd8gTawi!BMOF_O8nJ zw~6V=g2Z&zxd+hfk@^yo&|;MCQ(4VV^Y4Or%5%Q%CoO(dfEImVB3fz?O+3*dyM=Ds z`WJ2cN$3B)Z5yiwIH~nii+WOGkwmn+1u`gKN~%`2@_4Tvc0KKh5l`DnL0|+bY}lRU z0i)c8&Fv0FRJ0X71bubY=+WJ~n501p$kH6VJfL;ICZ4&sx_`3Cj4@u{p(5m~Z3SKUu*o#@h4{$lu62^U3>Jg>cM_*;?+l8C$`SZBKimAb8bn(3xv zRQrq?u!Et|?KA!4JsELbZ~j7Y^1PHwn-64BWFHxmrSg$Cee-=#eyN!lTY^L={54Em zw=Co?Sy4-MT1)gM1Qanm_D%O^ZULhO4y!{JF*-!f6s!(-+J%_Vq@4RY_*vL?_pCr(pMFbeVP*GDtU^xgm-v6|%%cXL00voD-~UNaEqos;r1{Kk^EBNRA?bvd z?te4=5m|~hAsYXetb?rHDCCw|e7U zZJrNI<-!Oh(T$LAfUl4KROH2L`OTvA$9#4_F!>2(lo&r5l18l@3=ck&w`q6uc*LV( zbtK-3fhHputXZH6l5ymIko75%xIm)&=>eW+Z94A|EA1`Z|2RkgpL@D_7=VbsPcy0UI6JkHa^FkmJ z5czwOkGA;3wj3!gEC+a99(ku#TX*|XN(M=U%x%3#)yqsTjhYg^K+*04-{W@0`_Y#4 zO+VM$HmU8A45DWa68F?35ESTy!>#dROA=CY4MbPp_eB@~MisX@1{XJuVGRhTss}c5 z_5ICqnD(f?>cTH7T{mv6tt8TqS1ixk{1DwA9leZ}bmRl+73IVhU#I!21$5hL-9wkn z;8$hi?PfJP0o}M?p0~B%C#uNiEqEoY@jKq*blW54gQcEx6^`EGi9y0Hk0T#Njn}|NS5=Iv3kZB z4@#qtXa8FLk8eQoMf_h0G{WJ(5a<}k$H`@1nT8D_It2hZT3<3&GWjQaW_E3kW!qx! z@1f;7Lz$l^wq@$?j|@<(J4u6=9s<_}B@0P{B@YZJ7~EUPA{66kDMMr5*|xkaSIu0u zd{P~jEoVii1HPk=d5^=y&Yuf+Rc0ALu32Hhs<{ zsn8*hoQg6R)tdv|)s#wp5Ek79YCv?eR2`toDt}rOUO~S-jvZ47;6pFh6=*xC?X4hx>sS-P;uNwIo5A3=nWCEP-#4efJCjqMV zxGXcqhHr;xsKA)jFq&a1>qK`Ic8_rXBY zO7;Yv{K$KK^@Fyav?>3c*S4A>iq`vSbqk+7>hqHs7q5ldBB9D7A5M49D}IAC~4~Q4_&fG}$bmA)QH&^LfDSA!KnwkEM1a~d&{Vju4^Sv?Ww*kb>AP+>eA0%EB z!B|6#W65h2>%Ei~)^i<}7u<&X!rq-{W)|$>j%`hSbF9uYRiNoBBl(3zk-QqE;UpYT+I#sP)bRNCb~guRyXd< z|K_>!`5CVtU}R%0y5=QwfP5itlYq;vBZVl5(rYGgHQR5atI)rvss7-o+zF0Ik>g7L zBqMmj`%ll$B@}19@=HRN9NT9$w^2TvC@{Hw?nKJ zQy)j8s1Inh?ykeiTuJ6bPR;2X41$ zqR~Tn>amEz4+eG-?J7zR%SneBk{1f(A2oF$sHdNYL+J*I61?{3q}2j@ZHGHvh_PTj z1J}_zr$_iKKi6x+&qca^p2uY%f1=(W$-TeAdZ`4|dE8oyxof1j_rOqoU8SeRxa#4N zd0wK<@xvf)@SVSIkcq?ikqGGu<)F9@s*=-^w?s@Aby)@{XqelNvTgq~%veMUF-PYi zZtUsrp85Ib{3l?|yEhPkUPG;ijZQY!%zc}Gs;r8Rm2#I%?2yRu&V15D_?PYlqD&_b z(2j63jiuGi+-?q?+IqKu*`E~a2K@MN{sQngDdXPo<=jRcSu4q263Bzhs-13{_|C#! zej3mAD)Bpfn=5j*UyemygQ&ympZE8cFq#*@=&n}*-LfR=WcBfv5a;-u_otVKq*h)im|5OfNSdLJ$dE-c^Gq&PseJC}et$Qvcarb=+> zwBzBdoV!V&-5H(u?sA)-B8z2eyiZ|Q4=VKu)-~|bSEB_+^VeDf%&h%Y(CF`{)c%ip z6Rzan^%xk%^OxjE1O@a9w;kT|Myu-B(zS|#d#~8i`~QaT+FH@3%Jlsge8tZ)Y`&|8 zf!v$)Mj!X3I!=X5ZlmVQw=0{eK&Y@3SanM^a=aI z*!gu<-!r-kbeyFC8qgy){`sekh>ptfAc4@g*4XFAtIZQyf-o`|NQ*=O7k72j{| z%{ZTN-YWBf^gk$^NlsRzT?$$+1gR1-B6}f)2X$`q`Gzl1;`L30gvBD-=h3r2LcU=U zk|tXLFZGha!9o&e)7@8I(cC|3rTl!4e1EyfVIO|okt-j}7!nM9zK>DnoH$IFg8Nd% zB4U5($w*zSJr{Ohc1_NcOUyu<0*}lU9Utr>vak?!dM{^I8=)7i3S%y{l7W-sjH!_i zDW$P*I#!tdYX(VszlPm>co4~1_KJOeA&1iMu>hee5iQN2eSw35MrPVA3~f)7feL;-51Y|QOm-^{gwKtYRhS@E z?_n5VILb^x60fZ{FJ9rJON&_=k~`}XzqjhVr;}dZf26+EYU9#aHhjBdid1Ab(Vq(6 zW_4{925F~<~C33g!N$A~l~9q}$c#$oq+uuxk-M`?&heOc%|87%%y7V~ z|B5bOn9HxgM_6)}`7wTaMdQfZY|kW?)h54rpA^fUx$}iLpuBzIlN)naVWSp)yG{^uCr1qJ zDuV!RL}4+E*c6Cv|3h>6V^u|%SMpo$8QPA0;wTsYB}JQ65a{KZ;-`%1JJZz{=+pGx zfq$z$y6kc6z=|3eg+owip}EUUnw@r~2Fhl>1Te<*GR zOS;u#&xERu-(lpb@zwF1pmsczZGr89+}a7 zH!tmTL&Mv{0I-#U+712b)mXefBL-fC#XGAoIo;)p`)py zk0Qe_lQqcjO^1_sz3L)~kj)+sGWb6K$+m+m;*sW5dMa603={-Bi1$}Bc-A(vK$8S`uv+rIm`m+x;xY=pEb#t{o$ z8y&GSd@5lyX+1=DE5RgOi>*R?C`O%+H^L5ty{yGJcHV+4ZAiD69))Tgu$YY%L5_wD zE~;l=3+m~IM6*7tp7+`{dvTg4X+XoN&SAKF=<+lcYSlBzETTl#~(l^Utsoby{v|8N5!@wmIVnwB#IE ziUdyh!qZ9X3r88GA9K|{UBSfiyEg3ix&7u(jY{;Osf2D=)%vm=f-4Fg%10%!y+s>6yMD%Vpv#_&{{ctegY*u8)%0t|pE5ApZn( zIF&|!ky!J{EK-6{?EW?{^J_4$GS9wL9C$saZT3DBLv_^El{?JGVs_Y+J3)(^n%?%OV(|o-xQOb2y z+cR16rgH(%cGk_a9sQB6Z0UA6p*oR+y7DssqtHlY zV0w7tw=DN&`tv$q<;f9m$-CVfJ_0kiz>*n+ivN?4j4oyG5E=?X5a+d~mRe)>ngRHICgn2t$9Q)t*{5Wz5s?#B+R@*6aHiy+Y)wXJM5jv@iunk_h7_ z#`!sRR<1T-B~o{CQ;>pry>{D!(~H5F-^+mZ%go2*4+d=8NeQbk+JWHcB^GTG7AZh_SF@2|CP8Q^gR-jH{+w3vTr zEw`}vN7fETU*KxkCT%l%Mg?VmI<4p3Y7-YTifYN>x!N?@x5Jid0MyLXjQuJC?8ypH zAtz@7H|gL?@(9^JS8vl;HV>@oGVnVid3XIpUX?={7ZhvYbNlaMc6cwF_3NJU-uq^v z)Iom>J=_8N-!qmW@3mb`tYtI4f<$Df0*W;e&lap1w zHyf*kqCpi8y~6szQCX!tmT18J?9-@_Aq%33vcM?F<{95(mTeD$%= z!HvDS^=~W~hyKVribBK3?-jyeW|%akDNm3LAOSIx7o-@lMOnoxAgb3H?f+lmxoaLC z{9X*E{9dl{IA>r`f+JsZ%{nq~Q4E!;9Y6j&nJ|ue@CV95J7J)zBr6cQlM5c$_rl77 zhphb#&~aFQLtf-%-_x59e}=|oV$H$IT(pL{QgG++Fm8Rs;fpOyM-TG=Y~>rq=gA~1 zx>E>7+cW~4#vRS&K};LRW0bl1^8zJh?z4yJjP3uJZL8)>i^4$-yT0Shrr8K~b=Ti1 zM&U}b`?sS4)~*U;W9#Jl`?!r#c3F%M6rYavRb^OCl9RdbI_tIy(d9C!jcL&5IK=Y& z5cTh1D<{#d_r=#oT~zDZ0v(gTUST&Jmlh){fV zl+>0%esYfOwZ)@@!zV_0cxVf7$RPWx=NY)~%xzjG&uz#F#u_tr*-(wJ<(duDPYyEU z)5=nkN8Zbp3WjTv#SjbdO_FP(5mM>&;i|?APedPoa<+un#fW52dah@Je?~GC-#Pm5 zXy}K}7q>@w_S_Xi*Ab;osZ?iAOUcPQ>ln%R%dnD;rb$6Gz#L4T0izVKw_Z3lEq8ql zB_+davx#SuZ+NfTo#l<(WdT-cQ8Lew8>tE_$1!H)sn?a8uv@icE9nO#-Pq<9*7%`t zP!yiPl{R{LpJ80q$2i_Pm}y-R{*W!f3K#91f=K;<7t#r>kQvamX7p-8T%U7Qx+aa# zcz-(oUEkVN!8rcCl)QsB*(sgc4gj_Z|o&}c7fmLukEWV$7Zh(@)?3ra3Q|)g6iBr+D%#hV8JZy7Cj0vYN)g#}t zbla~$NCWmS_ex^uBSQP?Mp}=w3|AgM$0hdQ;#j(vq2`Wqf}Hl&^hj%S*H^N48?lDh7u?w=0Q_WZQex59{X@_)+z`f7Qaua(C z>Ph^4%zgsIF^_nyy3CG@IJ4IS0_dHd!^{bdjtk)pG@;hu}dFEVGhR^X$2 z?3;%aG!&7SYM@^^^3Uhx^9~)6R-tAh3UK?o*Cz21E;>0)G$D4F$XX?ZFW`G%(pU-^>|Gh;fO?P^r4!-Cb6SuYCpVc~ zx=QTC&igu=Bm!x`RY9(|B_3)RY@CX<$g2*Z4oGDDV~F5Sl|rjkVPdq0U8-jsZ8{8{ zui&hsH~TjeUu=I@JUCrgt{U zlbPRkKIyY`YdeyZVPoC`v#TcW;#mf&;q7tSTk3wr<{7R+DQDqL=f$6PWVixs<*-$L zd_&Dpa4kt)2>TI5{-`LMdnS+QbTCdyh`DIkRn#$TF_R09_iX)+MIUltd>P{eKBSjo zJt$CB(x<`Nd`2Xuwof_IeU1{rVg5v*(du|32ABPXKR#faHvn9`48S4CDrn0I^wh~i zH$u|+ufHi=C_Il%;AC4LuiS-=tr%8q_^pWuJu!MGyWw*G;z0aappsKwdMzK$3O5e5 zkK=-lHL?2JLDql#Ao{xFcl1nl>n$WZNQJamSCPuv)QRgZC-08p&4y3N-(d)oPV#Bb zhTnPnPGMlyJB@Q}g8%*-I4B~g-8J09JT_gd)7LLuIrw&^Gk&u0`r1nGCaA3`_k2 z1@W@i+DNCi;9y>xx(QE5mllw3WKz)Mkpk<02aTz&C76(S1J}hn*}YYq@I2KvVFb+TpK!u)$RNvcU5xd_ZI0OeesGy4~J$%?$W_XcZIamt|^m^G{2G zHp5s4)EV8YojK*1HIvJXnhuA$33grM6C!S>I_Wtbl1p`M~F#{~U0}SD@G^=B!%|G?MH%Kg~@TLZ|3KC9XV^&c`3s$ z16urt#B0I!R6n58UCKkvw%6L1R1` zv|Izoxfh1-b+KO_e_6)Z)P>sjV<4y(c)G(S69Ez5(yQkAhPFd#?*y*_dqgv!`{Hs3 zcTeg|t#?@JF&K}#zW+TUJyBP2a{4V3X61^pD2@9~YnPDl{Kq%HAXhe|Dk?Djk#$o1 z=2cL8?xFAU!2{gA{p+Ina}Qnup}dhM&(N_%4?%?!bG+%&5C>U1iwquV!$3rFVR-&z zG|Zupp?bbbOluU~9p*i-~hQ zA4#eyPA3X-h0Uj^bC>a+)!fz?ze<9>^~CEmMd)uTiw3hkOX9`FeMt{BTtVNL5w*Ex zv>!sckyvv5F>imgZ{+ZoGoxuIkvG$i)ef{AIA$o-C>3Bn6G>%YK!$PA7#-sO`+(-( zW2)$t3W;Ln;S{yU$(f(Ff1U;SiJU~~oEHv`rmo*(C&zX_LY?C!qHu<;J7U2UgmgfxPsw3T>z5Uke^CM!@4kan9OkcMGj@SH6@D z(5?LNTWv51ao>cm?N*(NAF2cds$~VEFHMyS{<*xAk!M>vFPZx(!4%~E+AHhIMrZQ5 zYpwlnPY}f9?$z%n--kDxcG)t%&DrH6x~dNO@kF z;Mkxb#dh-b{r*HS@dl>R*B{b$zHG(_$v8IDuQlCNC6a9|Y(s^~8a=_OL=vr|WPev; zw7HU^>FnI{>Ez{rvOfkqLJL7>xKTXwIIHVZbp4N8Z7rkft8v$iGjU$Rn~@+%XkY3g zLYKw<#IRygVz@#b-&AXK5EL2@P-rYqJ%@kK)6TJP5`+z$2`$i;cLq2e+KaRhV9Z}KY<2V9dMam}xh3G9Dv|j&BpR1(2 zrTuF*oC#$$<(IE{)?8!8MlvQrHlAOI+EoXGWdJi<>LJ4K96^j`SwK z5wlR_%uFLM&Hm%YFaL4lrCj7y&rWox_=Vm_>{n>)_BnGrV3$a)XfM@6lrwmWb6gN< zBLZ+uj0kc9E@A^<+(9edU~YFo(C=#Fu#e$+*`vbQ40(V1LTO#-uOdR7%P&3CBLfv+ zP9PnYp7M9;HO@$McVr@CpMs1DJ?npCp1nrC@w8l1^q|ec5aqK z4tF1m#M`Sr6TCV$tS>@EA0C_eHWpzicq!gU8%oT*Xhi}hOVN|E)l!Jip7MRd$e8gr zYq#wllfS6*9kez?_@_N5K7MCE8_=|Jz4p6L_Wh5Rub>;zJXYJ?Cu99@Mv?m9AX4h! zAlZAkrwHh+JY=pi05HM7nxwmKZ@z-q;rD#Kzc)WgQ#+_{-Md%P#-O`x#8|(BeIZ)4 zS3-fp_M*oMC~SHpO`>`kmICk)@E4mJ8A~1}4yRXD^mQ*6uBX=EDt_Q?n)o%p*~;q) zHTbhSV}8#De?#Q`mCLlj*+^oZnbKRfn7FyOWOf5);Z+I_IF`C|(ij^9rO*w|+0S)Y zD|R1S92H^SwY(x8kDERiXe$3H?O3L-x@%r<548zfL!=H|`E#1SS|EJIHFnI&1^NGeqKN7Ws`asnv`g!qW^5*nFAj zrB2A^zQ`M%lN1Lh-c8og)q6lZ`m5x=r=1by8=k4y%C7`*nospE=Ak-`L#qiggmYis zq6UMqA3qZbKl?IC67je_6AfCzuNCLbzDBRR**Nd-#r#1Jl7R*%ND|Rnrw6O zmh3@gV}bLi8F`~GruSq31<12JoSUzce8?D5o|8qg;v`))q2mFP^FK56)}}>L9I8iS&SAOYdm6iMYW7?S^Ds62y`F|@{_#{g(KF|r#oP_(a8w`U zaLQ&fE1dTh+a6UaE&ym^$w;ZxLK`2T4fVsk>gV-o-dX z3^Su1Wv69*7XbD1lwjC>)MGI4{FqC0Y4+vQRD+{~=9!zhq+wUtH(T5!L)CIrb;n&C zDd06||1oGx|L{9;rRvpw*{xH(^I!@f_}bh#*j z>g2lzWX!p0rsK^+3&pY3w9di8Qqn@36r#r|D#H&BG#!e`tdS>iV#{&1i};f}F8Wl9 zmpcFx1q5p`yRz36ET@PCnGR73k_s$!%lg1rtuDiZlXZ_~y4K^f=Q^XgKkpLmdi3&# zEt2`=bwq@gGo1f*Q_*t`os~}d_^~G`jcG_}L@@_)8FJc(S{E--k)5>@bE%(`XorI_ z#(ed2*)#8~IN8;dH|{s*oXvP|JGSsA(+lh8vef9)-1&NVya1;mI`mgr^sVeYIpTO5 z!DG>91m5tJ2S{NsbcW6$lY?Cp|5$y69w)MIoXaUk%0)bYNd!{h=ROx1|{Rv6!9{IE2 zXj;y0e*%{Ny;9g!kEe4w=>y@2XF8IOpGslwIsOZxPP{*=`hS!Vk8Tv7RZF|L_c91_ zX=AR`!T(+2*tC3xa|Lq9DNLrF zS6z8n4i$2L86VtMg0QkJ|Dw2p7Qwc1F-|wUn^>&(qVLz_u6_5b8!@OZZ* z{c7~3A?1!mIquOJtEiax#juiRg#lz(tRc|**pA1353YOz4}vko{(!J zg=I>BHuUS-zg{ZKeQ(jT(w4@iG~`?+?VyZd+_gKq#DRY07ME!5V;G$Kc_Nljxt7lx zf!sKZMh$zrly_1R(~f^Rb=p(Gve1d_U0W{_b0gCzQf-kJ!^^5&sArv+DBT{RVH}}f z9CsOz6lI7yQih`iHxo{@ECtNb2ry_C1Qy%?&6=BlZr9DjBfoKiN<;}2F&6@cSNxa|ut}#1Wyp9%0(@kCbZmx_wE8;xv@t&;qviWYMkkw(ny2R( z-dxU-sQ%rePJiv<1rgCaaq?TQYi#vQ1wo$=ffkOOM^Zc&E>cNgak-P#Y{}w&XYu3d`ZUmPaT+x9V$8=7dNs%h9dEZmi9T zwI3{!AXTK?O;DrfM4J_wepN!pXDaS9 zZ49ja{!n5kA_+|PfcN=G=vP(0t)Pr<)y-7r*Y6|%ST0TNVXC}Q1zY5v?EuuSx|k%w ztpD#zHir1l+`CcOZ*!KdgBG8^DX#pH*B$5dGVltV6%?-TLyFgLqHz!s{oYgODBZRR za1&CuXZze&4s|$C8Ftu+UamW{Jai}17JnFCkEwLCAERQmca_Ui9Z541CX(}N`h8Nf zsUt@l!B-WKga^w01+iuV$z9U7Ifdt1P2ckXq7frkDJ4wfLGqS|dpw49~HNf3`V^eyP#L;bunT}W+H<-tx z3sUcId=(#jt=k{}wxo!BsCAWC<>*>@em?>KWie*r^S8r8%w_(jy}*W2HrD7#vx)kS zUJ9WG-d=DunB>hmt>bCow-?JqdZ$Tt9 z{W9;`pQgEec%&gm;Z zX&qvdM#3!f74zsW*6!fk0(4kdc#r|)rrQfdAO43i}fMBT5>lAt1We{Kh{TPYYl zara_*=vJ@)VvA*{2INN%=Q{OW#P8Cp?X>d=ClZLIUyRi-h5Zp6!EGru{;%Oeem3%CK9xj zGGt+@Gc7XSP}IrPipzkoYFOX7ZuIucQP`xO6`0@|=)OYr5d=E*Z}W7UUX@WyG_E-D zacL_&9dao#&MZSYeY&fp0U^(fhc=y}=15OaEcrrf;Loa|)B5+73)0nHEBWnajp)Eb zg+$xYinkm4M~1_17D6xkjGk{TN3E(HwRpw!V$Pv^f{q*QB}-uk!t=;Cjc7Y8cY^Ee zwL-%uUhjT7;`5^#R59#W=^k3+h!>|)AAxkT6hJc0Rh6vC!FC@c+By_{Pmd1VbhB<= zx&n#2OsP4%@b+P*c^KpFpQ@gqn~Db`ilZdL?SEi*`4e}Tmea; z*D$95llI|VvcskI?8!R?F%(eJWPhm8LHzMtWF4^|RM3ZcG*NT7P)J;j;|0fExx@|X z3?0n%Y`#|sk{`O;@24~y^Z6KT`#EJFQrh%JSLd zZP5fHnC%2AUGz1gi80!21a|pL#|>CHeb5Y*>!DFBvkAn2*0Ifu8RA zt?C*Slt94HBYJHWP}Z`qDbYC!nXDp)sae5j)+luii88@`jJw1@0nKq&l)$~aRfQV; z6nSXtAN)j7g2C4+(3K|>Jy}WS-cVr$UtHLh+QO3HMO>x1Zx|{rl%^2{PE_GvF0=0MX(cH2S&=S9yq%k82u?euR{#e{U8sgQ&$cy6Q;Vka6 z_bD3vVTWPm*o&)+ew4*AzxS6K=o9elwTc!3nl+P>cr8v|#xJYQ&*5gB5jMOEnzSoa z`pD^auMNkd9(}0)wq#iKOFRK{tedUM=lK#UPMsGP?}~h7;~@2#g7G%t zH)tHGh-@&u7z|bActu(`?Va)qPjPN1`6!*7qa*_T3LVWN?@^GFD`Rk?=#<5W!5Xlv z$KWmKFUwG_njg)>BWoKF!LG2RLS`DQ)%7|EPy1qv@Neug(ZD~3VuoA=Rom~Z?$~`1 zu^!6wJ~FD8t!&vn zzaBYs6HUMU0qEq#P#bM*UYLLk6zzI`z13M ztNT8#Ck(Z94eXx&>u1-CZ&w{`TfW&1i%UA)8A7Q^pEkB~$=gWTVVqi5Lz_?l)`60B zDI9&x^I4vXJBG&EAjZ?!Hj|PzM%dEHqafG?*~n&~OunP}#R#c{9IlLF?irn_Z23#r z8EuTH1Prhzos%=^CGA>^)1DSCLo7!E!(@1hn8btc+`b&4=JO!^`e13 z7~DKobRmhiEy4n9SD3V+VYO6z2pyJ9PK0LK%UCDuhlf(SdoGNw!qN#h5jty}1k*LJ z{a(a7tajV0_D0BKapS|*ofkd4#K%L^k^hIRH;;$%d;fs%Sr|(U5wgn%NtTkxHdBg9 z_OfTIEJ?^V)|pAA5-OF-S`yjInr%k5O16aTgUY_|%<`P+^ZPzeeV+dEs+{}2uk*gP z_jR4?aLX^oSnzV)+{-VN3fJBkKlQJx12uO*LqAISNFx^<&u4J~gG%%OrB>&F&iqux zJ{|3-nQd5b2Yn=W|Nfk|xv*Da?O^{SVWZ!d3#Js^m}HNgE+k2kUm+6ae(oJfI*V%cWLd_ zH}FNd5N2a|c4QS_G|=QO!tb8W+@ai_iLO&4!{=CD?;d!j$bws7^jL)!b?q&F9l$Zr z7yT`=Y{Ud!SSX?}T`WxDE2%0x`|bUU)JLAepOR8jQKTOjJ_J#9Mg~o>!49JI0=D2&aYlyg9;BNF^OntOfgw^<&!Z zBZ`SGAI=+EoW7n}!V$&@Lq*A!l&-rr`2tJYEGJgd{!krv;?Nw)#G7qHtXkfQI3$!N zF)>ba?qFS2`XVE?66D*v6Sk3Vkdv2QF!4Ykkhi5(!o|?;1KvcdT7WiNeAca;3p^~G zsV|O2zCAvJkum+U8!xb?97##bhaoU+B+K}jg&PoDLuVlEY`$V%cH4Yd?#AcZ?R4{l z8{-K2s0@Q95cBH&3&ycXhQq)WDw^ZWIcCxo{F64FnC-2?4Nx1V(4JrUd~;5rSe)Yv zbN2J>mvf;V-+gwK`=)&}C>U&>(p#-CRDg8f(_VxW`X(GkeheQ!Ld?klB(!6(+b8^2 z_KvvX6^0tC{fh|M@s@&F=?E^C-@Le_Hh6g+hg8a(OqDLtz`_*jhh9r+o(b8tvCwnU ztG$-s#W6EG+m$#~R4I{jLQ|T|mJi7NX}?p=H08-hjP9F3Kd5L8ue<=-;dJih;NATO z8Z^9qrC`f-cpkGCmlz;51n04iA}7C*_wf&T5fKp?gfq%RIF+Z7e-C}`&OZfRt>>r~ zqeNGxW+kqa5;Qbe8R525ZHDx1ngI7p6Zc_8&kyz2Z75na!{j`X7OxNhGE$3qlm;NM z2zG$KD-U?)s&v))^&8zQs0?ylX8+MmgxfDWB1?!0y+OG+t>)(N*W|2p-HJ^~y5OP* zk$=>+ow0F;!6LR6G}y>{Xj3BGXq-Q(b={3G4{Zn&r3+f($sz!lXMl8^rilE&D#GN! z0GY!&53Y>3D@byUoG#+X?ST2Ewok+Qg>k#cg6k2PPdv_%)o5PHj_skJJ=*Y|bwMjX z@Hr(zb&I>tIf;P4!oBUKjj7Y&-_3Kh+a1QZaCqG7SLE7HH@AL@C6jMKEO2uQz5nFk zkjMN#Wb+`i@ZvC&fL1O4c1O+xYG&$9EB}dMlZ{Vjev;L8uz-n>UA-rw8TlXX^ExmC zP<^I-(D#jc)j0U&{)ZQRSN1Ki^+T>j7wm?z+YU>YTpH30fOKg zcn|+qjeKC&z@-jr35u&2T94x~?!7#(;1YCUd3w{x-Nx3;oE7gFtT(&)NU-Do*X0Mdo z6KNjWQfVgK_BqiFt|en5^p2lzHQ#HB+sS2NdqWNMO#?8$=v4eXPdHtg#re*wS z#6dtU@(+*UU-e$9D?QE)L$c^wr5Q9cObV%Zf2mp~h5VgKA%B<2_kw~jB{F(9gH0*C zUI_4a+AAwpN)NpBUXuw)Yh_$r!m4wcRb8gslVxp9$RF!MxOoOwIr^m!b*(<`tDNYl zTk0^8vp;WRW*E(R*?S&)-o9J=`#t=pjZZr0Ik1l_cXTt4q&9QbKC{6}AvkViLBOWtvEp+p~%s}0MOwdaVN_vvE%RoGlS}ssnys|cN^x; zzcVTC7?)g;-NVgt-O_2h@Yg>!?Ri8$un}M00l+=|Idq~wjl4r13?jF#?$t7r7<|pu z4N?YUe(P7Twb3I+tP3!l&Fyb$my+`xa#V*b3&ZkK@+Go|wl3XrKe)4F?PiRK`nOSE z12O8&QshkM8~Dmuj7w_aMbfD^wO!^c#;pZsN?&j0s#o`6UH9E|i z!Wp7>>Q%KymOXphU0HC3wNnRL;8mIyCX9KjsTq8)Su&|3g>is({WvKpM}*Onv9-`} z*LJfc{pITm}3joM_Ec^7gk6|j~rzIJNuNmvrk#_4NXjJ8~wK}d-y0Xus9Cw zh+93dTRniC73NrYAC~Anlrx_~&2Cki_=M33n+fyFu3(s$oN(i!-?O*RX=Q}wR**8+ zMZXeAhB>ggL2I+Jo+n*S@gEM^q7#{n6W}KiNi0* z8~nKUey@halCscOT|~$mXIcFwIFh~+7)>pTB|Yg!-|u=Un2>Gg{iS?szUSL&)Qa&3 zvkz;Jr|9^meVRWmY_}BFRHX?B?pdWz*q>jE{wXUg&z!o>Y!KqD{=cL7?E)-?pot)5 zYD|A$p*T_Nh5W*8Zp3kL?joDq?O+nVU1rr=?O;yC@N*1BPDS0qr?O9 z3r`(Tx54ODBj$Ngo#5(pdex@odHYi-c)6~(hqfO#Q;?z`w?B$b(gUKl5kd32d zkh>u^Y7{_fh##Dj{FT?#F(k!Lj;rH&8B@gZZvk!hYT_pJf3pBUG!M$}EqQt+e#ayE zkb70+I980?y2>!smda&o(`8Q+78wPH(uRMmuRe~#Z9~rKWvdCQ1)z5gC8BWoO?3a= zYmdz|4LQJB*SgNds_+X>{EvY#nK12v9W&Ad$0&r;P*0f3$j0q-n&6T65%f~iQLNHrE^|@WTX2HD-NDUcpH|C=j5!zJcge%u%8Zwz z_!LZ+8i$Yn_lUn2vr6tYrMSVtl7`JC*STq+Ac>p3mXy({)3k+|ysgECCzq$LJZ?Bk7*3U}u}1-A z@)9zhljlX90IGZA4O!V6-9=w~Ur};PF2;F{r%Lz)u{xb^wP)NevS&Ny*pIAc1bvRA zb(Y4sc0#_dXn6BUX{Ws!gUv7aMUL*PvVa-!N_skwe1h^o7W(9&yTbfbT#ME-gmmhlp@2hpvOm%B5-j| z6R$5^6>#GxL-p%p_eLatnqOjJbue7B=K!$;7x3-*4gvf>@g2J@U9q4^hh){{PZmP& zW5gK=ik^6(-a0#gp0aDluSvE-F*d|Dh(U#q!n>_Y0N5n~ZJo($tc-^AuI^ zN(dGeTC52(s?Iw{xqG#qzT#3=La$rgH2)3T!cBb|?dO_3nqK6Pc^(qUiC#>E$bBq5+)tdDn`o6pcSrI#_7^qk=Kz6931W(S0qL!NwTPy0N%x>u-rthf=(4f0cNij zVb?};^ZL{Z)jW^lzeou6h&Vqc!)P0>u}WM)S4z@S5xV>fLovI$(iL^Ko7^}sHzM*( z59+Ysx_ajh$U?7s5a6gq8kP_AP-FS_U1x#?g!M-=Ga2A;8Jnr(Ar_p=)MC72SCP69 z)o|N2xpf&=_%>UhPzJkiw-2aFq8Omv7z7Q#G8F-z>FzgQ8wA1^7v@RBTRTl9_p%Jl zu6~nX&`-~<8%HkXaJBCL+7H7Sl7%f-i(gd!`fLD>73Iw7UjU@sK7t~o+F#->#Q#)+ zB_b^nQdZi&i{b|6@j1uBC%Or~Ke@SQTkTCKlaI_*ZW@PIVRSQR`EXNxuWBgS1SMi_U@JmR+ainj_b4c+nHa!EKM9}aDevT9F;>q1=IG!of>qG;yp^o2dBh5h zkBF(ogR_UHb0C}=d8ZgZu;Z`aK%}j>z@&e@vn{hoY&(1tIAt)qiY$EvK}I>B=2)nk zw6fbiw1LmS?Sx*~uxw}(wnY@CetkACs&|TntMPx={4gBI^&YwjuYg;_(sFpQqIM8` z0@=($ePDfEk{z5q4J&)kC((Li{f^-`_-lC<-1MhmhboJH6t3FQ;&}yb&*uT_^RFI5 zVyh`bI|-5B?e|G}+k-yJUhX^y`@2W&%XP4{3TO50!ysiyGoGklm3YeqrQTrQR~Upl zhN4y1Q}EZHD=P<%l_7K6cfmN$oF-O}83qL399ND1DiR2`PKcSh3l-EjF^>XW=gIDKoZPxdo)_&K5Q+)ler4BH|_ztMMa4P z_rV*r&1jNQ#*OR2Kn=IMjY(JNeH5cUU}h6pG904G4q2)Ae(yVFK1E&}S7yaJ9x|*h zlY+Vr_qcUC>hggrtiWYf;FUx%1CG4B2Q$% z)m;;voaEH1Av63?n87w-QM|BSmu+Qh0H;MB3;>5KeUW?HU%{>E!>^u$qhzL%OtOaO znDXx%2!t&Nj;N1_!{EQ%fJga{Qq?il=E{?5cs6H+!ir*^0#<8VGK}cFezvlzzj2Z= zJEel-1bUVM!un}V!+-kOdt51 z`+EyJD<6>wU|L3+E$p6Gw%YT(a6X9dvE9WTTE-_1f*k1cu8k+9FyK1(G3^y-XHh6I z$)rhtDTPN5a)ZN+Z$uQLiv~@L^Lbeown+LHk?j0VOgwxa2Z#se#VfwHuamq5_P~NT zTW7I)Z9FTvwQl;Yf=@t~6{hC9Ileb6M@c?bMS^4D8KKNF;oJE>f%e+Vgw2T+DcfEx zl6lk0@7t*KLHKt?`ru)M>+FPI#aHgKJGrkr8rzL7>m0&v$KvYD!Gmgo&CsdfF(uWi1xZ*wF6x=B%|gYH8Z&YCslPD@Bv(&v(IE$S#e zX>Vb`jjp49U%T9Y{op5mcfDpCz26y*{ipK>Rs-pC&9?4q81oRtaz|UfC8*snF}O~u zG8-4fWh59%|MT=EwO}(;e#wOM*f)2%Fne2eVQ8Q&=X=5w&is4|h; zL01c>cU<~m>8&3#?+Ri8v8hI5F|bRA8V?f#c3DEmZYd38(fyXfN)Hd3>UhZF0Az6DQ!GeCA(zSTF-J3;y9t!hDd@xz~wN zn1_>7=xNm~pdfat-}fE*WJ#IIE})+_q2a^cX5?>a_IITx#|!f3oy@T~oc1px;k>R^#O2?Ul!}hImPK{Elix>p+rr-v^sEs{Fnm1u zYh+NhvrD4e%@n4)V{Wl1dH0pw&+EgtFMB%bP&cgtuElm%F5VG}gP+%wTF^Cu57^rX z$H5AX&x5=I?ah`PqE7zz>Neu40`n{g&o$J)k@9ocztwlQG?5rt&zkIzF-HWM( ze>wdE0_F3)JSTYR<+s|slFk7p&p04ztWn~VPsIxX{=Mf2sXUmW`h5iR2ezi)Gb0AV zmL0+w%_0F2L|TR7#Jgwy4+4={mFkw?l1nb#9E0@_H}o3_TA%nvzj~Y#ehyNAK1>TS zw?}S87<&cAmTN+3o!Lw9gZc&&n%o4ZcxPAf`h3u7=TG>`P`|@fj&=^F=&Si}8kDYP z_6?*n4~&GOYs#>&FWso_Q=G(2M34_Fq>EtA2B*?`bOLbKc<0B&5_x&zP7u{Aqoy*g z`f=66*56nr_j2qHlwNhZ@Ywi1#>ya5NyV$V zd}Reup6~gs!~f?y`!!j7Sh2YCw}2Da`oA32?H4Him%)l(o1o*`zLnua^M-0LTa5nD z&N+?znKSAqT_jJ&WYnHhD(jyOT+gJp56_v@U|v{-2K3hC_s?EC_2|dDS6{IY9%7fCkLo49La=`bqI*d3hqI$!v;y7ETg z&UO)+ILSI^PS<9n)_%6?_{!7{xcVqNQKDWEmiRW;zKn&Z&yceB34_!(DqOP8(NW|f zNkF89TRm_jD^|*eusQ8`^knt!9M&m>O;o%(pH9nsJjK{lmN02MWz_L(Mne4DFmF1CA*U6&;F;hMB`_EOV5!!X( z&5K(YI8su@A3orw>R&IY1u-5dHF{2zlt4}ss1b(1Hr4_-O(yg+@WLn)+TiohO@P|US+$&eC zQtKb$>luY?opZ%J?Rk&{$uGzDt{sA{PC6})sTr}4*0}#Gt-*y2LS5s^_;t$ zaE$S2BJP&o5d|p!*z<5H9~e9+0vAE-J3E_z)gCXX*xtCbni*=6L(?0&+NZ2O)p8i7 zsK3=^7u~g8CJ?-H;l3`1^5JLqM0eUgzll|Zi9#jTjnD5OoU}I-=|v?7$wu~qW}&uA zBUwc!Da&(SP!VpfPIG*A@HdLHqkP*iXblO0OMS_%@G=1taUKnc0NKgW_^p#A+H{mk{Jr=zHXW)#*h7IkC%5eH_*OD_6a^Ce(UXVlquomH<+g)%fZc@_24F;g|OtF2aYk4K&?lS?P`KlTNu&_MajzoeebYSjQ_|rFD`0&tztsjX^T}4)Q7NN08LdeEb6jyDbg|VQ7 z?1uLuV>6BQ659;|3J>Jyd#=Ix3?#3@oK874=L>Ta7VUXB=~ue=j1oQSPACc^+3wvY zZ?zs>tt(pP74r(=mQMYYUf#x7o=-hnxlJuhNl|EiR{Ve*V+~ty2D)OL3&ti81xd$j z>7*d?)T`+D3urd`7~q4!yicJ}pfIdm^!_j05Y1}p)$YAC#4A}O>j_G0wP57s%d&mF z7>z0I5ym#|E8{?poa^XwzlXDu3xV2vl(DiW*&-pApcmfoW|1?;*0`eY@(4)+iSOX~ zMR@)JrXqP?&|Nke(I=GAre%Atb4VsUs+w{LXjWyI=qKP(}y?+ z3y&6awmw^XKyX3n!guejXd5nLE?Of z>nc63;qEvhCEV*UNdJ|Z8EvMyF z#E%p{WYn#bKEc|wmyH7LCG0q2GmdPRGU2)boI+*stuQHfad-eSrC|PSoWnutmtPAL z?BLEqjWz^4;8beC@(M$K)7?)v{uH(h`awC>c2GI<4*D6yO^`xdb+2jd z|5??e0SRY4;3D$!l&dbc!Eonw?ru{t4Em{(%C_(#>5Y%RY-~G{2oAg(BX;UJ>tH%U z*FBP=PcR>fOvX4JVk7FZzZ!p@EJvOGysgdArF7b9?xmI~-U&%PS~jaT;6pl7ki-TI zr#9Tg!OIHMw3fK;%{4En=oK8qDBRl09PjFr_1@z(Df7~fr<{k_52St*r$PDLeKi1= zn<`SYE6`<<(JkK0aGTEdEA`rQ|0=9-Xi?D2qyO9Nn|V$UM+Mx`ij*Eo_+<2PwnAye zwUI3>kkN22+4o$vb^o091bPwUmyp7jat*%h*P{idDJXs!N@_c3!bn{*QP1oNgb$uF ziVhDbOAn|hogGZ=qmMxDBN;J24{jfD; zk$SG5T}KhEr&>i}I+6=kWr8{DV^0V+A-a?$O>d=V@6lRv1N-2gU}~vf-!-0zo#ngAknShQIkU7kK^wDTqE;gN8RJE0;@h1Qx(Mu+ey!&CKda3m(C4s( z3sJ`jyC6Myzgtm^#KxSCYWQJ2HGr*lq;BHUImpE7aJETJ=)Dbz6;#xkWIK?|;u%8R zkZtZz#gXBq6oUR-%q-1*l^sR+%hxoKp{|wO3+r%LBI67Q6L>zj-a7-v@ z`516V4}$B-`na7=Mfl+SMT_WdIT%K~l_HFd0n-rapT)CIx={lG$Ar|X4sA^|w>r<)$>En5)bQfBe)3RL1SK?|sMS0;o zD#Y!!o}2`WwL{cZ+|CUUYUQVp`c^z#q#)vMSsy>T{_0|-;p3S@H)-iTU0X_V=FeYy zetv&OmN=KwlIMLT{Gf63PW_YM8`zwiUVKL5o>_WpMoNi7BI&cj29Xf*AQ5-iVYPvl zaKzJPOfvyx;&?7~Fx(uF?q_C#+kJuj7LcHA1l*M_h&x?d@kGY$n8J+9mOgE@Z z^nr3upf&qmH^!FvsfPKBLSvO1^qxmv%HoE8Jxx7ou{&UFx5=3n3!;QU5PU34q?}!^ zgOSlLx;;DuEZ=YJgI&n}m2W~f(VJsg%N!yq`O8<$-oU-{VCyZ}YJh3#kG zUoQgJjV((Zuzfm^a9ph3&$v4fm*LTm@m%qiWt+K~(i@}NA}rTr$hJ@V9s$@1UaS)L z1I)lrNgvI|=C@D2@JK?$13Pcq`DQDNpBeIknmXny;~Xs=b#a!HO)5YR1S|=;LLjeu zD_hPtDZ=76!pMS&sE}B5Ql_B}kl{&WMf@}Id+*$6lD^eatG8TLLCo{4ablEyzklju zX3D)_9PT3rq2oL{7D?2H$FFIU^9E^C##KY}BL{Ws8>3Idkdi)1L^}}w&SqSeauCrt zMa*QFeC>K!(0L|pTU(VA?Ntmm}Ivkf2#I!b`p%e1-?>&?vdAJ7DxNKIq(bFNePM-LVb7$=g8hd|ObQe66 zO|$|1xpbJ-N2r-fy8Dxkr+e76p^tRock+NhHvbrie}tLA{8Syzg1kCC8(~C_ZV49; z40#;gQUlyQu^3JUBla{OOcpo4mEJ=@I2}*(a{-(H2?Ce|5V@gzTBA)4`R z%+Qyz(7o%6mg+&-n26=95OFkB8Vdo03J@DMMSy+Hxa>RESjg@BGtlvh$~OMe?L!a| z#(SInj@97nT)#(W?Q2W#1#xVu+JjHJ)vR9Tuzqx%u($b9>^QF4br)9lq$vEto`uzP ztFG3F%9MS1ZuUC2t+vA#>)N`8U^O2Ogdft_k956Dj=}AtM0pSA4s{~nc7eZa^Ge>` zM5J9xxH|EK#CNV*M#Erg&h`jIaqhXWf%Q4b_CqISKN%qi z4uD2?6w$h6X8f7W^hLX#Q>;E>kEMQ;j3Z_F$tPp0c;K$nbdVolre*N7 zR(8|OJAHn8EWXDFhIyJueb+USN5kB6t75F2=~7F)gFmr}i^iR60Q!5PJA(SS!s6*w-i zjcvXbue(z^b3Yi`sB`>P9UJ9V-V*mHydgSz}f^1K2ULA|wq@1~{IGb<4rI zMQ5m{TZz0$KMo9fCvruS9GO0LIRaT^k1{amO2ymeJLG$%&LQR5z>~aWKm`RC>QQ6+ zlq7Drw7ze!;?PW*bgmDBQFx$J7pZhyQWOfgb}gdx*;r{E<660CoLkO8ZbFBHclh_| zp`>)Gwxp)-5y*u-W;FMk&}_h{J5QEOVG-Usd{-Hax=z|GEDbY#mXoN})q_!54j~2#fJ@EOv37ADbkp(2DKr&URZ+4TlAMw)Km zuXAJvh?qp-J!3GUM57;UsItUbYxzJ{x>7+A6>qxV0|k%V!{ZIL?{l3_&h=iBdQ?CO zk&~x$Cxe9JR_w+X#0J9Kb5Xg!_e12`wX0X>XsUKEcmV!(6P3lQX0)$NHLg}PXeZ1! z&eb}vu110wtb6XKh_eY(w2IO`TH-ueQHcC2^XH-vDR1IVJ)B}`X4nNbfPK%NqEN6! zGCDeZi{{gNjfD7^$sc=|=wbYV&fcNI!BF?c9z&+1OWKpx57DXC-le>O|ETd znFFh!e|I}3@+XW^&Kfo903%02!(@G6fDN5Yy%Ft*cXPTCHsQgHUb0(bqv7pz5RdR^ z7*TA2>88&%Owr0^uw|$ndlpt0M@?UGvF&(*&HZxZ-uP3G*Y(dEg$+>G>@9023g1;Cj zDD6uIR$TuW#S%QwS6k9Y%96!iX5yK=@?o6L)H$Jd${@v-v{X)LX5{(idQWkCo$em3~r>ahUHF_%z5QGI35Kf@Q zrtOiXfqBaHlD1QKNz&55eO$~EL4V`jGPAYiiX9sqJU z%X>!WRDYDR{!+xuTf#^h(^YLr@dUxDst*kZTaYh_XIFfg!DAl1K@cDC-9|m_hne2R zdy1`|^8N4>WJf}1T=+yH=q-!8*5AIvKpW+x-E$}XSlR<}ayb{)ByCQ;s&C*9il&qA zkn6o($+;>CXsAFcU+YL0O{e1NmF$+!Dx9D7={x=2g zcyvO=P_LaU7+#)2fR^TktCp zEZF+(3JGKnc*tG~%6l+(C>n)aTgH)B2kS1cV=?hebJ)-3F9vn10g%&*TR-(R4F;uj zewW%whN&DcA3v0buE5O1Hi!7ETnUnIInrVV(0-3;4%^zae_X9PxUgJlk`265IoXA% z+W!jZ9f4fDvs?_#Mx6i=i&6@&kEOMP+ZtoP?xUkl_DGjYKg_CNSWIm0g?FW9W$)H} z`+`sN>0_CZs$>wlV`ap$rnG|iQd@^@Vf?Cy5HO}Ekh+!YFU`JV90!47t(ldSmh`_A z@7%@I-&t7|32#(85TW81wZKO`47HapAov9t9&Uo*+iNz>Vp_tlJ@fqDb?^4XxX~ye zYT0Hl+(iiEWb8)yvK8OOR@G2_{U`R8ro`UCb!+vqJJC9`{hr=z z1*o8%Yn2FWy0nZnt5V0g$hR~vWO>EZf?7I=>XUH@`JoaV)k6W_npTvuMtD`0;qyxN{UUNx2VGKa>jg zC}HgppJWPx>;-YDe7Z%}V4ncj+K?D#x9M8LnC$=aSp>2Z+e9!VQ4?MS=*eT5M^oq+ zq5NVv(V6G=DZ@P5C$B%fbo8ATZP=BhdulQ074o@vp|FR#>z0!^RQi)J`Kk`KfCDym z-t{@Z9GWPw6IWVlK_ihEuWj%2!vLJ1-`Q_P!(~qs&VSeeyg1pbh6V>8BgnS0c9WtR zyK;k#%}LaAy3Rq# z@4we&?t@bPe6mfPD`Fb^aHD<*)){c?Xxv!xP)HR%h&bE1XN`%;Y{^v_M29xt7n+Q~Xwx5eOcEBK+||7v$Q{XIxoKJwY1lK2mxq^|6T% zFGz5t;phE?@a@akc1g27M3(BZ#AGa2Ti;b}=jHL|r5~^VArSDR$~C6p=0WLX*i}r* zq6n*TtL-1RwVP4(FqN$w$MvIS6K#>Th+mIlP%!$TEhWF>|9J~<9$a-%QTq6!$J~&K zyY2Yi^O=!D?)UGFFm0H9jax|yiZB*Tu--@&J~QH68DyFM=;&Xc!6U>%v`(h(m))yA zX5Fon@%YJu#!n$li)Q{0F7N}C@?-Uraz%E0UM3%rp-VH|Jw2zD+F(M9=SF@05C$-O zB1o5OOHm)g^osnKUwPzu<%V9|b;ZP-Dz=*`6}>@Rnzqn>s$sv#*${3*N^$RZVc>=S zl7>E3(t#m|A`}~%6!>@Av|X&WSmQ9jp1SzaLln|u4KrS*lS55GnzKJxa&>f3lN>r; zYFM)vecVf1!aSx?;#8f+t zJ3YoYEHrf688@)D=aoKE{YBRiZyjmRYUv!O(1)Y* zCjnxGyIOJc6<_CY>6u7AG~rUL&sF8VWu!m4k#0$*JUoS_g>8-*0yF8}baNr? z{^~DcJgQ4x*)A8dj;$^G8f%dz`1jWo^Rc0@gPn{bS2KIt#){X)$6jfF5MtU{#EKvH zn_3D?IuRA0(EVNfCLd+muPEHkJ+mtXj>}c`uXQRraUdSoScl`yL|k$8kY;> z^AA)HU~6jMR-G{v-|_C}6I3kg6I9nx^`YJV2Ov__7h*5!KdYT!tB?FR-%z@1+d9=c z@jLH*wKH$LbHkOcot)x+3f6)ls$Kj*T(y8x3BKPOC<+2|icJ+4Wph_t^W0xw1|eJj z;W1(KOkJ4foqp6>e^K2rIAya;r^txIzz~SoJvcSsP-y*B5+tl0ObBlOGzMVkW|pzz zuQw7x5@~sPdEPIj3eJ=+w0W4g*izlR;#XTGxAa63fH$D*Y}YT0M%_c13NVD7g5R__ z?>K%HAyj)}gy+xKd3p}MxIn5L%w!lQTE|&a6-@dXx_E-5FD8K6+8f>DR0rZLQ}kU#YT#G}$g9LuJauS54grQQDoBCYE{_MN91V=X(E zn$o*ln5%~jRX(t?k(==JWIY!~^4Pf9WboJP(2(}LfF2*6$ZE>7zqZ;+rck1pL5k7ePc`v~yIRvgQZwv6V5OtG zjs8D;AM;zg{S!7;&T6riA4~~mAi5(L zeqfssob)~*DsMF^;7-8ZTsf7ZWFwrSj;AQ3SgkU6^(U|=Pf)2>yw@OT z=%K&ew3n<8_=AxZNHvhi(*b^z_P~u%6!9rDGwc{A(=^qdIC*}7{1MRvxQ-d*0tZ2x zEfjHpb*f0H5N%Njn3msx{QuNv#YobXF@OB0?4tLq)cJ>EkONEFm~}q@DmwtG;(H#o z!3DyDl8rKiB>H$mk#w3xX?1lT%6h`s;tj53Ne{57NDjoGI337ZDc<0z( zQ038x$5xqLkxP+Z3po3EbohCCrdq{MqfyegLvG^nA3A`8Qqk;AIPwq`+d%SWDcmTt zQi{f-TkFcb$4%aHI87f!xk~&6f2TFJ`W%KXn(M8I+tu?Az-HziI+cvT_BCkAzPxc5 zEZF`!XVKMB8+WA$k>(HoLF~QLL>`R(sg(=I#VZ{7pXh(%jhu*F>*?vqkXV|*J8UT@ zJIi6^=^5v8?FJ6ULhEEG-PX?DW+-sJN1${7#izk)A|L(3d&`f&Jw3d0;N6NWY{rq0Q!{h7CoWYYsYe z-$LZSs7Ttw6Mrj8L_{QoR`&XS5T1;j*bwY!u-OL8XsHRdSl4M)OMwx!!1_9 z$Q{N6TR9uo`HzT3UmNs0?FDV2OUK#Q>6}BQN##>6XgUlzom8oxst15w6oD8 z!S>jelqY+FVJPmPdH2iV!|y_Z@ABE|gB6>VP8tPGvBobgkvp-)R{7Fv(z7tne)%{U zJpi|1c{x_^Ka1)iUDgaMZbPd8-mYx?=o z%r^yyu!n#$@|7eLH_y5z~aKf1qtcYKLr6|r30|)wfU|bE4+lJlbo@Iiz8sD zU!OYqvv*%xD|7@^1;@t628>D=U>o9Z~W$8L-KL-dUMMz8~x8jK_?z zr?ttUU^;*AnUbEQICVX0&Xci}?d*Z@{<g6=t@WzJX)}h>DI)(J$@Uo#A zyyJVf(dL_Q!R|D{4zdk#Zk@DrpwDM8`WiM{o6BH(^Yel@vGrPIY@jQH_Wu3*o12@P zjt2kH*r$8o2YQ2ez!pY<8eQkZ_MU5a#pQ$+L&mPQSmF$_P@2s5!0fS#2Q%XGu$uA5 z>uhK*d*{@?$y=EVR_U*xN;+`jLxx(3pc&|J`(sa4-XL8bdLMCRFh~<5WQ{8=(UrbDLrR8HtS_$N%`wmk zk}qHaWcPZ!El?USw+i^U&vEU{qQuWFQTsUMsAQZp+0Ivs9Q8ojjm`J&_eeyUhPz`!U*3!%N7>QyiV z{w(F$mC=`#3s$3=_=|?a6}*#zrJ_)V+tO$|n-zjqVQpnbA7kVymC6ZdOoGVDM*# z@h;By;`5-wh4uaqOc}TFbbZ2{6NTdOT!teBssJ_JqmJD#F-^%q(lz@_K`F5Tr$BZ* zqbuI}mtz2p55HEp$IcDgZjpH1=0X{2W2t4cC&!;Lw72I&88UR4wdxa(R6c>&^2}0~ z)?_Hi7n&x`sajqJSd)b&-Mjs}+uv4JR#xnyVC+c|v?s#<30dQhN~UZvqO9o*$q>6% zy5Y)r1|yKonz}s%p5S;F@5+!HyI|UH$ZR;2RZ@+P2+++eTCf^$!En+=gEmWW=RO)% z?~+VGrCs|Q6`);YQH1XMh0ZYaw{WSsx;AVKVRo2~4jVE8=1KLW{Y4Czb03_WFj@Xm zfT}AJ9g)#oULW>;($8sRSRq}YM0X` zU*E+@5%sH8cA{~A$3yMn@B$EBF?{iOMzFuUGHv|~DS;l!Lil` zjz4Acm#;_mr6fh3AhmsbIqYFGZbNOJQl+#^+DGWr98YP;gBW+L5FAo}x#(GGo^>k2 z)p)E%5>c)a`IU*)jyE!E3_` zaP94a@Y9P0bJ80poIf+AUy+uf(KflCx@(@DdON`*TJ+LDdkm5>RUR}o{1ZFe(Q{u z;OQAf<2Qm7!xD3Jbl}o~-%h?xQ3ug^2AN1Yw!amt1NnaF%Pe>i<+;`+=W1z!w5&zhec}i)s3mut&!Kj*SMC6-mLh&EKP4J{bV0}Tkpcvp6H3G$Q1R~L(x37<* zny4(Jc4CUnt(zyeKnL@{N}`VPJ})P60}{W-0XjMc{e9y@8`__q-PO%+o!x?kG}U^& z-c*dCLU(3;I0fY$&wvE3NWcFT4nm^8ipIrPvAw*!u=jF}mpx`mcX5?Db@R^V(b_Xp zo!k*|$c4&n{_wQ-$u^RZO93)o=I?Tx4YejK=C0M*{sin-eg=#}p~TLZLhANfm_vxU z!T-kvJrI0BC0oVZ$BeqnBlS z0{!id`xQLqCpRT8rnV(IC)fk=!vnBJIb@24_^e9r_KcNmOkvP1W}%MlS(>t z)oxuTt^P>J+P5k{r1p$XYvuifydG1th71Y_q_yn^V`ru4VVv@|N#{!u+wph9sWHxI z6J{;z&GsKC-P`lVwYvrlJ{*Pa$jSJ{p@PFraR^fqX5V=ZPxxuoqdc*t9zgKn*Oq)>c+ecg!!O{zgy`mEMlEZ^D}a)|7t6yP8BgDbZE>@6 z(>swhEV1VHWX?@z#veA8aQy&YaJm!J#DVIvU)DQ1K{Sru?j-}HDI!Aogx?W0ubHVQ zSbH75%c@2vz*i~pWLH%VF{9qEPO>Yl1`S@_c zjiVV5-ZT42!t385pcZ_XIRBxksfi~f-{5{3U^ABcTHR0QjuvR?&g<1vdD?n31Eva(ykXmgYge8R zj4V-mSbF8n6sc{1*TdyBbXV zl~^ML3jwP02UdYAH(!20`q3@h^$LKFl4Uh!x?x=)wKjwB+PeSZ&t8QGtf1i`cqnRZ zbs-nvS^Bc{@8)*!7*ub}4`At#H;;nlOv;u!KLaNR$3EuE-p;dwexYG_pSc@f+6rkx z+<)94qf2ZD3y%g84cY*}7m5UrgTnWk@ZdQH3_v>Pt;wjq!Sya>9%FqU9&@xDhAU-s5!)9p4N+1D}&sQRM0@uJzxSD z5l`v_1oLAb|l*iM4;J!NNjZG}fID7ys z?&puU^%=)h#?lL3Iyj5w!!a7gm|AkNHH=h5%<|m`PVUqxIJjD{-h1+om^OUgI?cy( z>E;z`qJ^>j@3I=n{MdS?Gg27a-n_KN(g#>^dW}nXXqbiqE9<_t%|C$4jGuWCkPwdZ zu(y{mk7{4e0h@`!xt5YW3spkDE@_$qY$lnvS%sank^94w_q3A8hlp`XzeR2&7iaNh zSFnsZ%qQ4*EN!{>jFsA9HvQC*@U4=N@{s2A7SeWxn;lz$RtRkh4!|_ly~0zqtx5$p z$v2!Sf-e`^Y#l5s(`)_h+e~0#jn29gynSzdKM2|_gHtE)lsS5=Qe7H1e3VY*>CH{q zV!I_pc~3rU`hbLd9yIjR9MP6>8ci9dj}UHXQv!Qdr1qFc-Curd|J5XF8al1zO*(Y# z7cgzQBRXxyIsOR`dyoEBY+h_K7;^oL^IIo-1x{wkHeNjws9Ro_S{rib+oG+A(B>X- z19pT;t0VPCYe64c3qRMyap;Jr;ON1cF#MfugTx_tTIZJE^vzT$nk2P|jN@Sw+h2>V zNbsDRobU3KSH(Ns<)b~g=VsI%W)T4f8>Y*;_YFhRW<(IpKRlk5sNKy7Mlrj!$t~51 zMT~;Z_*mxA0Ws2&CeSrB(1^zB4ZkIY8lG?K>hG`HGt6^lyy8i4%glzI3;80*fi81Sg@K z7|Fj(72?Vx0dGG)Y#o8Hh@Q8l&QCxNHIfa<{Ny$XZV3SJYHPS<;sI&K)kTB0v>5=R zcJUw?$W@66kIPz?-&eUYNV}BnA=(s2tK0JPnzcD1OeT|Fhh%)vQT)wmUwI_>-mfqB z{{8#sk85a<t}wiZguVZa9-jhJC;E+`MC z7ZfIv9lR|&XZr38o8zq5L9@d{htwF|koD3@ztB@ruYYwS2jdaCKg0f*T;k=24lWdq z5j}nah_#kYR_3U1_1J12(NYj!DwSKlyfo$atO=PXC^{zE0BzVfqzjW6H(Qt=$p<*!Nh({tPp~$aO~?!aQzJxXN`$QdlC`=qzfx&eB;mus?KLu7o~RN;c3kC~T;E(7R+tSW zVAai0RVg-l(U7wd{&sLZF`?ULd;Ke{?uUa)pUR|k^BcDYm}8mcrONNvz`(g)%}K*sfF&s#0GP22Ode}HmEP3ySPn6`UQyj* zc>4ARm~C38#+3JGTlTGGP4n}SxJFLugdnb*Cft_O`{Y3*Ig&3t;ksJJs!tzP z4iWxWkDE7dqV%K>XF#StDDn9%3B~;qSRe*x z;fbz|KQ!Ka=Oq~#89l9|m2<^-V$)*0JU(gKGLs|;5#tsBvs>Bx8Hxaz{Nko zA;6c|E}!cH;GSW-Y|mjkVQts8oLQGf|8;3prQ3SdIm;s?7G16lm6E6?tTOFF-PlgM z&7wqp&UP9<=~FrM9{(>f(if5Fpi-Xv5mqUCN$;qde3biy-drgOH+$~qx$hodr52SmD@#)@Jc-1E;U~~=&>s!1MQO;Dxf`|y6xfR!5LjC=vf~+<&TdrIl)(5bG zHj`Jk8+7pwnnBOp`h}alzF)MQtLDQdP8IZ#GaA9okx|s#qM&s0A zY}ZOuuyX*YEQ8*Vn*wmv_38_^mv3lj_$mtaU9C)cR0`MZd`rt|(!Mu;S&aUrtQhb( zpBsB&0|_Q;i^T*z^Lcl?WaQjb-=WEVp+Ze4%o-Hy44N!=tw_Sw?oKE#v+uLMjNtNSYsrl9 zcNcV}r_r^A3rJAVYo*CIaPe23=gO#{B&FbMI3NVEarC=tF?S-J?J}6Q*o4q-5RbPtIA2Fuq0Q0_$;IO%r0PM8 z_|)y`(aFYulGv-A!-W4d1`5}m-2V$eJ8CFxbyq(rpUso8O3;tn*4Y{ml7I{cjIf3a>_7;NUl%jSiJh4Zd# z^o^&!s1>lNR=z@DCvP?{J}0*UAqxEYxQ8K1y*tL7DNh?B(qF z<1T&Ku}Bw80?3gyAdsy#`{1ivySeHW3%F~UmUv=WeH&pMKU|kh_iW! zVZSP#|C4Zc#CapQ^&IC?=x#q@l2i+1@AlBQapZrb;L3i%Thj)ud|a0gJdys+RpT_~ zF05eCi0y0p_u&NkuzmuT;s`JmT{WAa%)u!0RPW~uC_);=!^lSGA8~Fs{AE_0?fect z$b@i*1M#gbK-szbD__$*DV66FbtVYkY9w(7u2_o*-OF##zfa^>e!rNg_^Y!!coQMO zV=SvIxz@H$IWN?xwu+6cnjJdfi}W2*QyNl}@k^J2NN~DMIQGlEo)3E4jsz+5wxlfh zncK!nhU}Gqom$s~m!Y1uA~1x^unNK6j+~>$u=CK`*TFYQm*(;0Vmt#jT=tPRo}+F%FUfA4o;iaPSUPxB^`WoQ zkl##}2U}&foUZ|idsV-b;v>f6`n;m!?=mf31nxo#Y)U~qp58rv!c!9V2fT@v(;B9i zRm2FeQ8*u5QqvsUPWaL<@osd&{duJV%?jCxL+$as)IoWO;@D|IQhn+5YsB?DMQ^BQ z)Z(w|aW5KS`px_HAnz(D#XMm>f6j*4z4R0EV41cBA{Zi!ZXQ0@6)qRWAe4Cgsm5Bo zhRM+VdTk>@f~~c;W~(%~S!3SVZj6ZLet4veMdV%;?gZpDC{Rv+U(Fv83_qLU@ij{d z8Z6o+m|fB9A0Di3!^sMO-&)SHx3gO7qbr~x0x>u}J?&Q8GF2nW zGoTD9SDYJCJAyZ?_86BNsI#5!wVUoOz*B3vxDv%uGrCT@n z5;Sp0V^zh~h{{bW-vA){E;nA@Kr82I;aQ>1_LZ;9C?6?7wQZ&TaE>;CC+R1$tT{eX zZ}jLF4$0pVR!uhW{S>jid@V#|%a)E@J;8D1T5;mGhVoenmc*3U++F)`9$sIkbQ7@c z)_p#1Gg(QVMz`4FHS@4xa%{{;lAyHJ2MoVvdnNh%rUSN6>PJ9|8|v;~vmp zbO1n(Pl3LKr;^aR`mx0*o2}EG$nXaGdoQ>j<;|~jueG#j`C8%3mC=ny7+G1Peh+;% zHlW|qb9eyg#shTiH&e>^Zbk`lzE)>gu_M_i3gIP?yua`dsoVpi0JueQEU`Cw!lk*oM)1lxR5v zXD-g#3naq|Rn~I$fo9xl+RItl3h}b#)Gv#pyTXM-g~NnFeQ2^OqbjlIf*rfJDw~w# zn$G@;gq@bsJLZ2R1B$IAk(PV?H$-IDcKuZs|JO>zBjUKZqU`fvU>Cg4rzE;0+=!p3 zeP!yY29QKVLa=>&m>(*KeiVc{;m(z>NVmyJorM2?p3VmR~0xZHO6Af<4WEC=wac_Cy6x&h3+V0D-pDmQ!@K#Yp(}s$Ac&M-(yAiA08JwWN5jEeKt$_3JjHHJ%WMCk)mjq6^}&=^K52@T zXcg-*p^lol^nr-Jsn@QeH*5oWJGWaVyIOHcpWxd@nL84gYX1EqD1juiH%>de0S%qBa0_J-i+V-J^r=Y<5i}HTghlbi+XW>Muhyh9{nyCf}Vv>wca>3t;s=(m$~I7?%=Ctc+Xw85s=g+M2zZx2<7eLcl^N??3|uWe{ZA*iV;S((*(# zG~4Wwjs+}_=(47hTBy>Z%s-(=wAEHkefl5-l=+2C8@V zFd?MIt>71t%azWKT`F!Qh~@CyK2MY5!dK*K%WYDltUjLXnYlQx1g1(ju(8~qm`1aj zQ7^`1;Fhezed(08H+8Rgca@eMOrANvIDZ4Q5^Xz)$cceOd&*cWp(h2HntyuBRm`Pk zLW3b)CZ9plXIv6q;>QZUHgSD=Mwv^kCzUo?KS1G&PRdIWH+`lEMLRbNLACe>+Q3aJ z&HuG#4G4kvKe@qiYwcZ5jhlET-i2Xl&c}Qo7BMtcy|${bILfOjQx!#qRs7Y(jo}Jo z=e}l9EnlqF`T5#Yn$p=Gtooia3ztr;fMq=fO^A=z7t|JsjzO$c1^2{MAHch2t-FcC zw3Ci+^%7Q;SIb=+kSR<|@>E48q^WHtqhqEICuwbvx$moaABJY-CX}gLEG7_(pzIP= z;vZ-W1ccroTon7?h(+^p11s$l{jSd+?u&oCBNwoj5@Hv@{wm+S<(`XQQz?)l4qEr@ z*G&ja;3^NO46BT@Zjo7CVZ1&<5{^9>Il|Q`3HWlud$U-lkVH4Z zLkp%nDmOgmPC>LNF?uxoy1zb1dXfnlf4n+TuQeGMpM!Tr%w&|3PBMpLhPH?5SP9fh zZHdNDXm4-qR1+6Clob<88Gvw-PgXyo$w z>Bd|Z*qonzPl=Yk`^`ry{A~hQo?(13?j>!1{q_}g?OVqFx_UzStH%TgnR}J?lclgKwZu#7Irg~>HyT)AEoO`sY(nD1xq&L*hUTCelVqBZYy!G1Pu8-QSHK)$WJ0$x3c>x9fb!{< zlMW{xJ%xiQtkamxqafHJvq+f{%8gWgh2kr$x%n!5GM-aRrH;IpIs6r_3_G|gJEjX) zMPyGNE!FkAVd7#Zli*EJX>v>P7;^F`|7cy+>S0^?O1rGUdN0*e${M-!$%;7`dRhm` zRG0{?P?<}%vZ#ugFdh$Dy=T4R+#i5Gb=PD=8*zMPhRkjIqKxGh6>oG^46-qWag>WUc8EsCr^S8gH zL-S0)X6DZ(x5Ny%)GJKsek~2ycXWxmMq`eUymyVyHfcYXIXnY5UyI9HE*}Cdo(5oL z>St?T$x-AA#EGQTN(e5+#u{IAyN1!7x&?yMqI=pCRqTDMMBE%amo)4txg@5fg~LkE z_NLG$ov*$XGu1)C{bCEX@U5q`QB=!HSm#yI7mS@G33K=eJ+4Bd{Q|xztc9<$^UwVy z+QXKONzdbg$CbL#J5QR2sAE?cUd!iN@%`M={!>!IKWzX#zIkZUmp0W<7PrKKW$0uywf}0)3BU9k*8_F!8!G`UCN)M9w2g!#fdGCjtyRajq4yDsfjG|eYjpSVW-~Gq zZ`4`fI+bK$(Vhn^mJVQgS{}|tgALHeKSUY6re?IfwL|)erQhuhKJBGZOllJ=D#!)5 zQKXFw(AYgUcmY&OaF$77poJ4!)x7siij!VrO6iwtG$q#0zBR`24C@y(YKYd~OctnC zdwSaaiSr8#zD2FLiCSA-aqCS?Q&V$}?Mfo`xQ}gweX@sFG`_KBZu~vQ5#KY%t@5D+ z(!cqQ;F#jJ>nnU>K~OYJ3ub1};IRcRuR8~Z88cfe_<%vMy{1Il5~UsO3kBJ3RQe*Q z8_b)FbYp83>nmikM@j57b}6YT)7m4KWbL#g#EEcM>7^cj@8fTuoSEIn+o^2P*uMx1 zQ^jayO(|kC^SiD+PRr3F&}o--bzj++kSB&g5}k>KrbyvwZLf#Uc0kdH*`?)GwIvf} z`%DVy1pQd=T+$_@;~U#$5LesNbO)UBl2_Y@=J>5u8C&^gH;p{=+9_Dc3!}re4_tp zMC~u5btOO`*`>|XJ~N(?w_uyP0_q1cQl+i@@!krLG6^Yz!FnU;OZCIH5119VYFi~Z z1BobD(cSwSM1FQx4`lf8;7>o?nXP;E z2gOGvTs%m_+m zD#EI-1DOXf0VK~qq`IX;ORy!LLc2+|il!OEIBED#)D~msM~MDVB;I>h2;N)bOWv4j z8|tkix`3(PTU}e`Tf%UyN^Bd?oegjfd@^jtWMFcdUTHU3enu=t)dC3&%HfE-#SkP8fiBTb$3EyjcA2(A9 z6O?ExTlLzV^8%l+wCO(%rn9tE|Jy)fDg;ev)s|t^Y8TE0)(?x(Oq4WL3kNHjq;Psl zld}iCwwBH&eCU6%%E0t%q$o^QFi^(}7RI$z?Ke!UpF{O5!k_^}+@PcT*nN3nN2x|s z0RA*7rfAL$6bk^q^LT=CVi5Dx`%>?U=47uey5y2aZee~44fPElQ#|Je>N=|K{Vh@1 zo8cxGp7NKh%F7fz;78iZ=d*^<&J-U@Iof2gAa`zBi{&l`+WpgU2(Z^FRS-dG?${FW} zELNxZq|eHB^{0+k*E3L_%uHfw(yr#ko`p9ih1xA(vcXW!gPGZI8Y~eX%=qYkhN2l* zU&Od^Fki}m=6+;?RJ>@}_oKBVrEs6wLQ`3WD*Kf>b0jRWx7wVb7-*sYx3C-*gq!?% zx}x7!;^M5(-3jLZZt*WHgp;dCW>#QGJ|p*4kgnJ|QIEhsvq0=mDcTN$@SKD0w_r zS=uV3Mb29O#i$(i=qAe!K&+v#;9-@A?N z&@sxb+RW@$r2h$wqON`CCp5^MMazgyi%iJujd=l%^#wEj*bBy~RXiH#l*m+{YLg|* zMXr5AVtT7xwMqe`u69jXNcnG(^ugOvw~LEcftyC{??;uPbf;LP30u_V;Ep#ftnn-Z|GE+GyXf*`@t;RxKGp0MraD@BYgI1qWR!0<#8!KIv6LZ zeZqgsY}$611eq{NY;95UDaj3WL=TX&w0#*x)m5fQ{wa_2Ic@P9-0#(YG+qOJZH$8! z+_AkOSWS@j2Iwx5;#a~X`RG5L^JXgq_P08@J5Mlzn?ik){%i9u3S2wku!Dxyme2Q@ zLu;2-l(Z#dzXk_li2c_vpI-!k)M<3;){h1R`;I@V0pNCsX)yc{WUG-Ms2g&C$t1Co z(Qefxcve@nxhG9YjrAA^$~ue#n|YvPx<&}&&WTKJ>WW)+AF+f}t8K>q9;q4muzFx4 zxVnma5QCTY!6(MH!0((MRClfG%Qr17M4?xb@s7J!}Y6GhjB>0-TcpV)_0&Pj1bAFoN}X%i)OT2Ibvr2 z=2RK`{VzZyaZ62!Bbug}KpYq1{5@tBSlB#xfd*a#+qsprNd5r7oOZJ@b~iJFD1u`F2sG9c0$=|Kc0v;CIF7AF+UE}Xbw3v)Sj`$5-&h_u96jsO=HE)gg9Q1r5(nCHXmq9%wt$Ii?7 zw8{o^?oF_VBN%NXQHGSKI!8n5S6ZITVUE@}%+2mo%9gt6cMohO5RTU{NpCA* z9qhVMUE&oX&&u)AGi%fBL1)Ze_b6BSvbf}+x-|FgTk<_ux$)Dx?%lg*>OaJk?0epJ z%Etwt7<$6sUl0cO zqRc+g98DO_Nvbj$8E0-blm%KxeirWy-u@8hyC8iun?Wp1I8?{nQ?0&z5mVhjHBLd$w3P}epaato{Jp(=bJsxC_$BCt6L0x$^g0AtG}#L^E-7XIDRyA+eA9&< z)ya36uZEi*#Xt9G_=?_y*?)tG^f!{%JaT`Z!)0>$Yxq-@``H%JRA7SdgOmpm`G{aa)2;AX9N)jzKi-)9UNiq98D7~Hfzi8^!d zA?JPp|7?f!!pZ{7B=xrU1tY(&h=4=)7o0YSTiLp;m6L?y-6j`}6s7lD6ub0at+qE}$ep{dLMPk-QVK=V$Yg9@Eb z*kSW5xQsJw@EnpC5EEeR{C1jT@vbRY-0?+p-3`8Sto{eEzw}Ga%xm{*vy2HdnUuqq zKUnWRE=b*BBcD<4d?yO|Bl!XBm?G+hcAZ3sZcW8~-$?P%&fssw@+NS(RMCaJ8jHM( zNrdqm=HC(|T#|22orHf>-Df6t#C)grWV7FuQ4zrkq4FE9B9BBrDu$>%d*HY+;hs-# z%SQTbfeO3gjk^~0MmE?jAkfp#*H-pwfa`Tn z@Y`kCAU}T_SwEK>ZmvOrVi4$^Y-dps^GoO4&~DdG{QaS?0)m$V{QQEVp!vd^cI@1> zd#|OeFUs4yZSxip(QOje=9g5x+|Yq`;PvklS;@M3Ukwb**(w$Vi{i`KE*=Gs;*Z*J z+4riCo3p3{c*`xw^=i;nZ$A%}05^BH05@M(x67Wce!lDH+#@NGogKBIb)U4%e!IWU zwEmAae;oo=()`kCPuC!DAy)&?IWh-q%rBk)&jko<-7Tx2u3>C?;gbE;>;B;}w`1e) kB|dnBdtF&uSKrY1rF(SUD}e`e-3P5dWdC~k=jZJI19%x=ssI20 literal 272578 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ⅇ#FG~J7cDnHn%MJU+Pxt~` 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 Date: Thu, 5 May 2022 07:14:32 +0100 Subject: [PATCH 041/157] Improve mpticket file parsing code --- .../org/multimc/applet/LegacyFrame.java | 46 ++++++++----------- 1 file changed, 18 insertions(+), 28 deletions(-) diff --git a/libraries/launcher/org/multimc/applet/LegacyFrame.java b/libraries/launcher/org/multimc/applet/LegacyFrame.java index c50995f67..e3bd5047a 100644 --- a/libraries/launcher/org/multimc/applet/LegacyFrame.java +++ b/libraries/launcher/org/multimc/applet/LegacyFrame.java @@ -28,7 +28,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; -import java.util.Scanner; +import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; @@ -51,7 +51,7 @@ public LegacyFrame(String title, Applet mcApplet) { LOGGER.log(Level.WARNING, "Unable to read Minecraft icon!", e); } - this.addWindowListener(new ForceExitHandler()); + addWindowListener(new ForceExitHandler()); } public void start ( @@ -73,34 +73,24 @@ public void start ( Paths.get(System.getProperty("user.dir"), "..", "mpticket.corrupt"); if (Files.exists(mpticketFile)) { - try (Scanner fileScanner = new Scanner( - Files.newInputStream(mpticketFile), - StandardCharsets.US_ASCII.name() - )) { - String[] mpticketParams = new String[3]; - - for (int i = 0; i < mpticketParams.length; i++) { - if (fileScanner.hasNextLine()) { - mpticketParams[i] = fileScanner.nextLine(); - } else { - Files.move( - mpticketFile, - mpticketFileCorrupt, - StandardCopyOption.REPLACE_EXISTING - ); - - throw new IllegalArgumentException("Mpticket file is corrupted!"); - } + try { + List lines = Files.readAllLines(mpticketFile, StandardCharsets.UTF_8); + + if (lines.size() != 3) { + Files.move( + mpticketFile, + mpticketFileCorrupt, + StandardCopyOption.REPLACE_EXISTING + ); + + LOGGER.warning("Mpticket file is corrupted!"); + } else { + appletWrap.setParameter("server", lines.get(0)); + appletWrap.setParameter("port", lines.get(1)); + appletWrap.setParameter("mppass", lines.get(2)); } - - Files.delete(mpticketFile); - - // Assumes parameters are valid and in the correct order - appletWrap.setParameter("server", mpticketParams[0]); - appletWrap.setParameter("port", mpticketParams[1]); - appletWrap.setParameter("mppass", mpticketParams[2]); } catch (IOException e) { - LOGGER.log(Level.WARNING, "Unable to read mpticket file!", e); + LOGGER.log(Level.WARNING, "Unable to red mpticket file!", e); } } From 6bffa060637e3620739344925a4681ec494a725b Mon Sep 17 00:00:00 2001 From: icelimetea Date: Thu, 5 May 2022 07:16:16 +0100 Subject: [PATCH 042/157] Fix typo --- libraries/launcher/org/multimc/applet/LegacyFrame.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libraries/launcher/org/multimc/applet/LegacyFrame.java b/libraries/launcher/org/multimc/applet/LegacyFrame.java index e3bd5047a..0283f92cc 100644 --- a/libraries/launcher/org/multimc/applet/LegacyFrame.java +++ b/libraries/launcher/org/multimc/applet/LegacyFrame.java @@ -85,12 +85,13 @@ public void start ( LOGGER.warning("Mpticket file is corrupted!"); } else { + // Assumes parameters are valid and in the correct order appletWrap.setParameter("server", lines.get(0)); appletWrap.setParameter("port", lines.get(1)); appletWrap.setParameter("mppass", lines.get(2)); } } catch (IOException e) { - LOGGER.log(Level.WARNING, "Unable to red mpticket file!", e); + LOGGER.log(Level.WARNING, "Unable to read mpticket file!", e); } } From 113528e1f299de951a7223df033bbf390095dba3 Mon Sep 17 00:00:00 2001 From: icelimetea Date: Thu, 5 May 2022 07:20:33 +0100 Subject: [PATCH 043/157] Make line count check more lenient --- libraries/launcher/org/multimc/applet/LegacyFrame.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/launcher/org/multimc/applet/LegacyFrame.java b/libraries/launcher/org/multimc/applet/LegacyFrame.java index 0283f92cc..f82cb6057 100644 --- a/libraries/launcher/org/multimc/applet/LegacyFrame.java +++ b/libraries/launcher/org/multimc/applet/LegacyFrame.java @@ -76,7 +76,7 @@ public void start ( try { List lines = Files.readAllLines(mpticketFile, StandardCharsets.UTF_8); - if (lines.size() != 3) { + if (lines.size() < 3) { Files.move( mpticketFile, mpticketFileCorrupt, From 2fbb7be23bb31d0c5007a0500ad2a5d3a51f644e Mon Sep 17 00:00:00 2001 From: flow Date: Sat, 7 May 2022 20:16:55 -0300 Subject: [PATCH 044/157] fix: filter based on MIME type instead of plaintext suffix Suffixes are unreliable in different locales, while MIME types are more standarized. --- launcher/ui/dialogs/SkinUploadDialog.cpp | 3 ++- launcher/ui/pages/modplatform/ImportPage.cpp | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/launcher/ui/dialogs/SkinUploadDialog.cpp b/launcher/ui/dialogs/SkinUploadDialog.cpp index 6a5a324f5..8d137afce 100644 --- a/launcher/ui/dialogs/SkinUploadDialog.cpp +++ b/launcher/ui/dialogs/SkinUploadDialog.cpp @@ -100,7 +100,8 @@ void SkinUploadDialog::on_buttonBox_accepted() void SkinUploadDialog::on_skinBrowseBtn_clicked() { - QString raw_path = QFileDialog::getOpenFileName(this, tr("Select Skin Texture"), QString(), "*.png"); + auto filter = QMimeDatabase().mimeTypeForName("image/png").filterString(); + QString raw_path = QFileDialog::getOpenFileName(this, tr("Select Skin Texture"), QString(), filter); if (raw_path.isEmpty() || !QFileInfo::exists(raw_path)) { return; diff --git a/launcher/ui/pages/modplatform/ImportPage.cpp b/launcher/ui/pages/modplatform/ImportPage.cpp index 487bf77b1..1b53dd402 100644 --- a/launcher/ui/pages/modplatform/ImportPage.cpp +++ b/launcher/ui/pages/modplatform/ImportPage.cpp @@ -143,7 +143,8 @@ void ImportPage::setUrl(const QString& url) void ImportPage::on_modpackBtn_clicked() { - const QUrl url = QFileDialog::getOpenFileUrl(this, tr("Choose modpack"), modpackUrl(), tr("Zip (*.zip)")); + auto filter = QMimeDatabase().mimeTypeForName("application/zip").filterString(); + const QUrl url = QFileDialog::getOpenFileUrl(this, tr("Choose modpack"), modpackUrl(), filter); if (url.isValid()) { if (url.isLocalFile()) From 29a53d7e95508f6c7cd6c1945d2100cca98533c1 Mon Sep 17 00:00:00 2001 From: flow Date: Sat, 7 May 2022 20:42:19 -0300 Subject: [PATCH 045/157] fix: always have the instance toolbar be vertical This overrides the orientation set automatically by Qt when we start moving the toolbar around. --- launcher/ui/MainWindow.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index f34cf1ab9..44eba3694 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -746,6 +746,9 @@ class MainWindow::Ui // disabled until we have an instance selected instanceToolBar->setEnabled(false); instanceToolBar->setMovable(true); + // Qt doesn't like vertical moving toolbars, so we have to force them... + // See https://github.com/PolyMC/PolyMC/issues/493 + connect(instanceToolBar, &QToolBar::orientationChanged, [=](Qt::Orientation){ instanceToolBar->setOrientation(Qt::Vertical); }); instanceToolBar->setAllowedAreas(Qt::LeftToolBarArea | Qt::RightToolBarArea); instanceToolBar->setToolButtonStyle(Qt::ToolButtonTextOnly); instanceToolBar->setFloatable(false); From bdd2d57808004ccd12b2438c7af9d163fbe96c0d Mon Sep 17 00:00:00 2001 From: Ozynt <58683893+Ozynt@users.noreply.github.com> Date: Sun, 8 May 2022 11:19:53 +0200 Subject: [PATCH 046/157] This makes more sense --- launcher/LaunchController.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/LaunchController.cpp b/launcher/LaunchController.cpp index 4cb62e693..002c08b9c 100644 --- a/launcher/LaunchController.cpp +++ b/launcher/LaunchController.cpp @@ -93,7 +93,7 @@ void LaunchController::decideAccount() auto reply = CustomMessageBox::selectable( m_parentWidget, tr("No Accounts"), - tr("In order to play Minecraft, you must have at least one Mojang or Minecraft " + tr("In order to play Minecraft, you must have at least one Mojang or Microsoft " "account logged in. " "Would you like to open the account manager to add an account now?"), QMessageBox::Information, From ea9d61c21cd0b1419329371fb22e4c101e67dda0 Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Sun, 8 May 2022 23:19:23 -0400 Subject: [PATCH 047/157] Retranslate account actions after switching language --- launcher/ui/MainWindow.cpp | 38 +++++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index f34cf1ab9..e5c4708c3 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -238,6 +238,9 @@ class MainWindow::Ui TranslatedAction actionREDDIT; TranslatedAction actionAbout; + TranslatedAction actionNoAccountsAdded; + TranslatedAction actionNoDefaultAccount; + QVector all_toolbuttons; QWidget *centralWidget = nullptr; @@ -1252,10 +1255,14 @@ void MainWindow::repopulateAccountsMenu() if (accounts->count() <= 0) { - QAction *action = new QAction(tr("No accounts added!"), this); - action->setEnabled(false); - accountMenu->addAction(action); - ui->profileMenu->addAction(action); + ui->all_actions.removeAll(&ui->actionNoAccountsAdded); + ui->actionNoAccountsAdded = TranslatedAction(this); + ui->actionNoAccountsAdded->setObjectName(QStringLiteral("actionNoAccountsAdded")); + ui->actionNoAccountsAdded.setTextId(QT_TRANSLATE_NOOP("MainWindow", "No accounts added!")); + ui->actionNoAccountsAdded->setEnabled(false); + accountMenu->addAction(ui->actionNoAccountsAdded); + ui->profileMenu->addAction(ui->actionNoAccountsAdded); + ui->all_actions.append(&ui->actionNoAccountsAdded); } else { @@ -1295,18 +1302,23 @@ void MainWindow::repopulateAccountsMenu() accountMenu->addSeparator(); ui->profileMenu->addSeparator(); - QAction *action = new QAction(tr("No Default Account"), this); - action->setCheckable(true); - action->setIcon(APPLICATION->getThemedIcon("noaccount")); - action->setData(-1); - action->setShortcut(QKeySequence(tr("Ctrl+0"))); + ui->all_actions.removeAll(&ui->actionNoDefaultAccount); + ui->actionNoDefaultAccount = TranslatedAction(this); + ui->actionNoDefaultAccount->setObjectName(QStringLiteral("actionNoDefaultAccount")); + ui->actionNoDefaultAccount.setTextId(QT_TRANSLATE_NOOP("MainWindow", "No Default Account")); + ui->actionNoDefaultAccount->setCheckable(true); + ui->actionNoDefaultAccount->setIcon(APPLICATION->getThemedIcon("noaccount")); + ui->actionNoDefaultAccount->setData(-1); + ui->actionNoDefaultAccount->setShortcut(QKeySequence(tr("Ctrl+0"))); if (!defaultAccount) { - action->setChecked(true); + ui->actionNoDefaultAccount->setChecked(true); } - accountMenu->addAction(action); - ui->profileMenu->addAction(action); - connect(action, SIGNAL(triggered(bool)), SLOT(changeActiveAccount())); + accountMenu->addAction(ui->actionNoDefaultAccount); + ui->profileMenu->addAction(ui->actionNoDefaultAccount); + connect(ui->actionNoDefaultAccount, SIGNAL(triggered(bool)), SLOT(changeActiveAccount())); + ui->all_actions.append(&ui->actionNoDefaultAccount); + ui->actionNoDefaultAccount.retranslate(); accountMenu->addSeparator(); ui->profileMenu->addSeparator(); From 5171d99fe5c8d08014412e320679b32c73fd2789 Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Sun, 8 May 2022 23:42:37 -0400 Subject: [PATCH 048/157] Retranslate playtime text immediately when language is changed --- launcher/ui/MainWindow.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index e5c4708c3..85f4157b9 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -831,7 +831,7 @@ class MainWindow::Ui QMetaObject::connectSlotsByName(MainWindow); } // setupUi - void retranslateUi(QMainWindow *MainWindow) + void retranslateUi(MainWindow *MainWindow) { QString winTitle = tr("%1 - Version %2", "Launcher - Version X").arg(BuildConfig.LAUNCHER_DISPLAYNAME, BuildConfig.printableVersionString()); MainWindow->setWindowTitle(winTitle); @@ -851,6 +851,12 @@ class MainWindow::Ui // submenu buttons foldersMenuButton->setText(tr("Folders")); helpMenuButton->setText(tr("Help")); + + // playtime counter + if (MainWindow->m_statusCenter) + { + MainWindow->updateStatusCenter(); + } } // retranslateUi }; From 40e0252d7d5f994b8ff6764bcdb7d9416881ccfe Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Mon, 9 May 2022 00:54:47 -0400 Subject: [PATCH 049/157] Show "executable" screenshots in the screenshot manager Since the readable/writable filter was removed to do this, extra code was added to enable/disable certain buttons based on whether the screenshot is readable or writable. --- .../ui/pages/instance/ScreenshotsPage.cpp | 27 ++++++++++++++++++- launcher/ui/pages/instance/ScreenshotsPage.h | 1 + 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/launcher/ui/pages/instance/ScreenshotsPage.cpp b/launcher/ui/pages/instance/ScreenshotsPage.cpp index e694ebe3e..2cf17b32f 100644 --- a/launcher/ui/pages/instance/ScreenshotsPage.cpp +++ b/launcher/ui/pages/instance/ScreenshotsPage.cpp @@ -251,7 +251,7 @@ ScreenshotsPage::ScreenshotsPage(QString path, QWidget *parent) m_model.reset(new QFileSystemModel()); m_filterModel.reset(new FilterModel()); m_filterModel->setSourceModel(m_model.get()); - m_model->setFilter(QDir::Files | QDir::Writable | QDir::Readable); + m_model->setFilter(QDir::Files); m_model->setReadOnly(false); m_model->setNameFilters({"*.png"}); m_model->setNameFilterDisables(false); @@ -343,6 +343,29 @@ void ScreenshotsPage::onItemActivated(QModelIndex index) DesktopServices::openFile(info.absoluteFilePath()); } +void ScreenshotsPage::onCurrentSelectionChanged(const QItemSelection &selected) +{ + bool allReadable = !selected.isEmpty(); + bool allWritable = !selected.isEmpty(); + + for (auto index : selected.indexes()) + { + if (!index.isValid()) + break; + auto info = m_model->fileInfo(index); + if (!info.isReadable()) + allReadable = false; + if (!info.isWritable()) + allWritable = false; + } + + ui->actionUpload->setEnabled(allReadable); + ui->actionCopy_Image->setEnabled(allReadable); + ui->actionCopy_File_s->setEnabled(allReadable); + ui->actionDelete->setEnabled(allWritable); + ui->actionRename->setEnabled(allWritable); +} + void ScreenshotsPage::on_actionView_Folder_triggered() { DesktopServices::openDirectory(m_folder, true); @@ -503,6 +526,8 @@ void ScreenshotsPage::openedImpl() if(idx.isValid()) { ui->listView->setModel(m_filterModel.get()); + connect(ui->listView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &ScreenshotsPage::onCurrentSelectionChanged); + onCurrentSelectionChanged(ui->listView->selectionModel()->selection()); // set initial button enable states ui->listView->setRootIndex(m_filterModel->mapFromSource(idx)); } else diff --git a/launcher/ui/pages/instance/ScreenshotsPage.h b/launcher/ui/pages/instance/ScreenshotsPage.h index 50cf1a177..c22706af9 100644 --- a/launcher/ui/pages/instance/ScreenshotsPage.h +++ b/launcher/ui/pages/instance/ScreenshotsPage.h @@ -100,6 +100,7 @@ private slots: void on_actionRename_triggered(); void on_actionView_Folder_triggered(); void onItemActivated(QModelIndex); + void onCurrentSelectionChanged(const QItemSelection &selected); void ShowContextMenu(const QPoint &pos); private: From 96b2758169a7a003e2daba150bb897e702316c6f Mon Sep 17 00:00:00 2001 From: DioEgizio <83089242+DioEgizio@users.noreply.github.com> Date: Mon, 9 May 2022 17:42:17 +0200 Subject: [PATCH 050/157] fix websiteurl in curseforge modpacks --- launcher/modplatform/flame/FlamePackIndex.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/modplatform/flame/FlamePackIndex.cpp b/launcher/modplatform/flame/FlamePackIndex.cpp index 549cace65..ac24c6471 100644 --- a/launcher/modplatform/flame/FlamePackIndex.cpp +++ b/launcher/modplatform/flame/FlamePackIndex.cpp @@ -6,7 +6,7 @@ void Flame::loadIndexedPack(Flame::IndexedPack& pack, QJsonObject& obj) { pack.addonId = Json::requireInteger(obj, "id"); pack.name = Json::requireString(obj, "name"); - pack.websiteUrl = Json::ensureString(obj, "websiteUrl", ""); + pack.websiteUrl = Json::ensureString(Json::ensureObject(obj, "links"), "websiteUrl", ""); pack.description = Json::ensureString(obj, "summary", ""); auto logo = Json::requireObject(obj, "logo"); From 288e7bc9c5e1358d1ad78961cd1a771e6292014e Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Mon, 9 May 2022 03:20:53 -0400 Subject: [PATCH 051/157] Make profile menu scrollable --- launcher/ui/MainWindow.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index f34cf1ab9..9e1074f81 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -950,6 +950,8 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow ui->mainToolBar->addWidget(spacer); accountMenu = new QMenu(this); + // Use undocumented property... https://stackoverflow.com/questions/7121718/create-a-scrollbar-in-a-submenu-qt + accountMenu->setStyleSheet("QMenu { menu-scrollable: 1; }"); repopulateAccountsMenu(); From 527fa7ba9cb4e29277acdbedf4880f6fc2255a8b Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Mon, 9 May 2022 15:58:08 -0400 Subject: [PATCH 052/157] Hide temporary directory in instances folder --- launcher/InstanceList.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/launcher/InstanceList.cpp b/launcher/InstanceList.cpp index 6e37e3d80..847d897ef 100644 --- a/launcher/InstanceList.cpp +++ b/launcher/InstanceList.cpp @@ -38,6 +38,10 @@ #include "ExponentialSeries.h" #include "WatchLock.h" +#ifdef Q_OS_WIN32 +#include +#endif + const static int GROUP_FILE_FORMAT_VERSION = 1; InstanceList::InstanceList(SettingsObjectPtr settings, const QString & instDir, QObject *parent) @@ -851,13 +855,18 @@ Task * InstanceList::wrapInstanceTask(InstanceTask * task) QString InstanceList::getStagedInstancePath() { QString key = QUuid::createUuid().toString(); - QString relPath = FS::PathCombine("_LAUNCHER_TEMP/" , key); + QString tempDir = ".LAUNCHER_TEMP/"; + QString relPath = FS::PathCombine(tempDir, key); QDir rootPath(m_instDir); auto path = FS::PathCombine(m_instDir, relPath); if(!rootPath.mkpath(relPath)) { return QString(); } +#ifdef Q_OS_WIN32 + auto tempPath = FS::PathCombine(m_instDir, tempDir); + SetFileAttributesA(tempPath.toStdString().c_str(), FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED); +#endif return path; } From 512d7b07d01d82822a728b40dddb0efbf441dc00 Mon Sep 17 00:00:00 2001 From: DioEgizio <83089242+DioEgizio@users.noreply.github.com> Date: Wed, 11 May 2022 15:18:07 +0200 Subject: [PATCH 053/157] chore: add version of polymc area in bug report template --- .github/ISSUE_TEMPLATE/bug_report.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 1ede3f743..b387f46a2 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -23,6 +23,13 @@ body: - macOS - Linux - Other +- type: textarea + attributes: + label: Version of PolyMC + description: The version of PolyMC used in the bug report. + placeholder: PolyMC 1.2.2 + validations: + required: true - type: textarea attributes: label: Description of bug From 37e8f495b420be9c59e0349235bb5940249ed666 Mon Sep 17 00:00:00 2001 From: Ezekiel Smith Date: Thu, 12 May 2022 23:39:48 +1000 Subject: [PATCH 054/157] CurseForge API Key update to PolyMC key Use the key CurseForge provided me to use for PolyMC *pr done on mobile if someone could test that would be great* --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b052fa1e6..4d3683d7c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -91,7 +91,7 @@ set(Launcher_MSA_CLIENT_ID "549033b2-1532-4d4e-ae77-1bbaa46f9d74" CACHE STRING " # CurseForge API Key # CHANGE THIS IF YOU FORK THIS PROJECT! -set(Launcher_CURSEFORGE_API_KEY "$2a$10$iR1RdPDG95FWdILZbHuoMOlV4vL4eckBx7QPZR6SVZmliEb9ZQplu" CACHE STRING "CurseForge API Key") +set(Launcher_CURSEFORGE_API_KEY "$2a$10$1Oqr2MX3O4n/ilhFGc597u8tfI3L2Hyr9/rtWDAMRjghSQV2QUuxq" CACHE STRING "CurseForge API Key") # Bug tracker URL set(Launcher_BUG_TRACKER_URL "https://github.com/PolyMC/PolyMC/issues" CACHE STRING "URL for the bug tracker.") From 046f1e6e58b7a28f336bb2ed79995656fe66f0bf Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Thu, 12 May 2022 17:00:17 -0400 Subject: [PATCH 055/157] Add instance overrides for miscellaneous settings --- launcher/minecraft/MinecraftInstance.cpp | 7 ++++++- launcher/minecraft/launch/LauncherPartLaunch.cpp | 6 ++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index 3ba79178c..e20dc24c7 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -162,6 +162,11 @@ MinecraftInstance::MinecraftInstance(SettingsObjectPtr globalSettings, SettingsO m_settings->registerSetting("JoinServerOnLaunch", false); m_settings->registerSetting("JoinServerOnLaunchAddress", ""); + // Miscellaneous + auto miscellaneousOverride = m_settings->registerSetting("OverrideMiscellaneous", false); + m_settings->registerOverride(globalSettings->getSetting("CloseAfterLaunch"), miscellaneousOverride); + m_settings->registerOverride(globalSettings->getSetting("QuitAfterGameStop"), miscellaneousOverride); + m_components.reset(new PackProfile(this)); } @@ -984,7 +989,7 @@ shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPt { process->setCensorFilter(createCensorFilterFromSession(session)); } - if(APPLICATION->settings()->get("QuitAfterGameStop").toBool()) + if(m_settings->get("QuitAfterGameStop").toBool()) { auto step = new QuitAfterGameStop(pptr); process->appendStep(step); diff --git a/launcher/minecraft/launch/LauncherPartLaunch.cpp b/launcher/minecraft/launch/LauncherPartLaunch.cpp index 173f29b50..d7010355a 100644 --- a/launcher/minecraft/launch/LauncherPartLaunch.cpp +++ b/launcher/minecraft/launch/LauncherPartLaunch.cpp @@ -25,7 +25,8 @@ LauncherPartLaunch::LauncherPartLaunch(LaunchTask *parent) : LaunchStep(parent) { - if (APPLICATION->settings()->get("CloseAfterLaunch").toBool()) + auto instance = parent->instance(); + if (instance->settings()->get("CloseAfterLaunch").toBool()) { std::shared_ptr connection{new QMetaObject::Connection}; *connection = connect(&m_process, &LoggedProcess::log, this, [=](QStringList lines, MessageLevel::Enum level) { @@ -168,7 +169,8 @@ void LauncherPartLaunch::on_state(LoggedProcess::State state) } case LoggedProcess::Finished: { - if (APPLICATION->settings()->get("CloseAfterLaunch").toBool()) + auto instance = m_parent->instance(); + if (instance->settings()->get("CloseAfterLaunch").toBool()) APPLICATION->showMainWindow(); m_parent->setPid(-1); From 3aea639fe4359259a27e971ae357e308ae50e69d Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Thu, 12 May 2022 17:11:06 -0400 Subject: [PATCH 056/157] Add UI for miscellaneous instance setting overrides --- .../pages/instance/InstanceSettingsPage.cpp | 19 ++++++++++++ .../ui/pages/instance/InstanceSettingsPage.ui | 29 +++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.cpp b/launcher/ui/pages/instance/InstanceSettingsPage.cpp index a48c4d695..b45628432 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.cpp +++ b/launcher/ui/pages/instance/InstanceSettingsPage.cpp @@ -101,6 +101,20 @@ void InstanceSettingsPage::applySettings() { SettingsObject::Lock lock(m_settings); + // Miscellaneous + bool miscellaneous = ui->miscellaneousSettingsBox->isChecked(); + m_settings->set("OverrideMiscellaneous", miscellaneous); + if (miscellaneous) + { + m_settings->set("CloseAfterLaunch", ui->closeAfterLaunchCheck->isChecked()); + m_settings->set("QuitAfterGameStop", ui->quitAfterGameStopCheck->isChecked()); + } + else + { + m_settings->reset("CloseAfterLaunch"); + m_settings->reset("QuitAfterGameStop"); + } + // Console bool console = ui->consoleSettingsBox->isChecked(); m_settings->set("OverrideConsole", console); @@ -247,6 +261,11 @@ void InstanceSettingsPage::applySettings() void InstanceSettingsPage::loadSettings() { + // Miscellaneous + ui->miscellaneousSettingsBox->setChecked(m_settings->get("OverrideMiscellaneous").toBool()); + ui->closeAfterLaunchCheck->setChecked(m_settings->get("CloseAfterLaunch").toBool()); + ui->quitAfterGameStopCheck->setChecked(m_settings->get("QuitAfterGameStop").toBool()); + // Console ui->consoleSettingsBox->setChecked(m_settings->get("OverrideConsole").toBool()); ui->showConsoleCheck->setChecked(m_settings->get("ShowConsole").toBool()); diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.ui b/launcher/ui/pages/instance/InstanceSettingsPage.ui index 5db2d1473..cb66b3ce5 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.ui +++ b/launcher/ui/pages/instance/InstanceSettingsPage.ui @@ -349,6 +349,35 @@
+ + + + Miscellaneous + + + true + + + false + + + + + + Close the launcher after game window opens + + + + + + + Quit the launcher after game window closes + + + + + + From 8c8eabf7ac1920b47792b26790f3646cb6693ec0 Mon Sep 17 00:00:00 2001 From: flow Date: Thu, 21 Apr 2022 22:12:14 -0300 Subject: [PATCH 057/157] refactor: organize a little more the code in launcher/net/ This also reduces some code duplication by using some Task logic in NetAction. --- launcher/InstanceImportTask.cpp | 14 +- launcher/minecraft/AssetsUtils.cpp | 2 +- launcher/net/ByteArraySink.h | 67 +++-- launcher/net/Download.cpp | 60 ++--- launcher/net/Download.h | 14 +- launcher/net/FileSink.cpp | 50 ++-- launcher/net/FileSink.h | 36 +-- launcher/net/MetaCacheSink.cpp | 22 +- launcher/net/MetaCacheSink.h | 27 +- launcher/net/NetAction.h | 127 ++++----- launcher/net/NetJob.cpp | 270 ++++++++++---------- launcher/net/NetJob.h | 109 ++++---- launcher/net/Sink.h | 54 ++-- launcher/screenshots/ImgurAlbumCreation.cpp | 15 +- launcher/screenshots/ImgurAlbumCreation.h | 12 +- launcher/screenshots/ImgurUpload.cpp | 13 +- launcher/screenshots/ImgurUpload.h | 2 +- launcher/tasks/Task.h | 4 +- launcher/translations/TranslationsModel.cpp | 2 +- 19 files changed, 433 insertions(+), 467 deletions(-) diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index 1a13c9973..fc3432c19 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -14,27 +14,25 @@ */ #include "InstanceImportTask.h" +#include +#include "Application.h" #include "BaseInstance.h" #include "FileSystem.h" -#include "Application.h" #include "MMCZip.h" #include "NullInstance.h" -#include "settings/INISettingsObject.h" +#include "icons/IconList.h" #include "icons/IconUtils.h" -#include +#include "settings/INISettingsObject.h" // FIXME: this does not belong here, it's Minecraft/Flame specific +#include +#include "Json.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" #include "modplatform/flame/FileResolvingTask.h" #include "modplatform/flame/PackManifest.h" -#include "Json.h" -#include #include "modplatform/technic/TechnicPackProcessor.h" -#include "icons/IconList.h" -#include "Application.h" - InstanceImportTask::InstanceImportTask(const QUrl sourceUrl) { m_sourceUrl = sourceUrl; diff --git a/launcher/minecraft/AssetsUtils.cpp b/launcher/minecraft/AssetsUtils.cpp index 7290aeb4c..281f730f5 100644 --- a/launcher/minecraft/AssetsUtils.cpp +++ b/launcher/minecraft/AssetsUtils.cpp @@ -297,7 +297,7 @@ NetAction::Ptr AssetObject::getDownloadAction() auto rawHash = QByteArray::fromHex(hash.toLatin1()); objectDL->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, rawHash)); } - objectDL->m_total_progress = size; + objectDL->setProgress(objectDL->getProgress(), size); return objectDL; } return nullptr; diff --git a/launcher/net/ByteArraySink.h b/launcher/net/ByteArraySink.h index 20e6764c0..75a66574d 100644 --- a/launcher/net/ByteArraySink.h +++ b/launcher/net/ByteArraySink.h @@ -3,60 +3,59 @@ #include "Sink.h" namespace Net { + /* * Sink object for downloads that uses an external QByteArray it doesn't own as a target. */ -class ByteArraySink : public Sink -{ -public: - ByteArraySink(QByteArray *output) - :m_output(output) - { - // nil - }; +class ByteArraySink : public Sink { + public: + ByteArraySink(QByteArray* output) : m_output(output){}; - virtual ~ByteArraySink() - { - // nil - } + virtual ~ByteArraySink() = default; -public: - JobStatus init(QNetworkRequest & request) override + public: + auto init(QNetworkRequest& request) -> Task::State override { + if(!m_output) + return Task::State::Failed; + m_output->clear(); - if(initAllValidators(request)) - return Job_InProgress; - return Job_Failed; + if (initAllValidators(request)) + return Task::State::Running; + return Task::State::Failed; }; - JobStatus write(QByteArray & data) override + auto write(QByteArray& data) -> Task::State override { + if(!m_output) + return Task::State::Failed; + m_output->append(data); - if(writeAllValidators(data)) - return Job_InProgress; - return Job_Failed; + if (writeAllValidators(data)) + return Task::State::Running; + return Task::State::Failed; } - JobStatus abort() override + auto abort() -> Task::State override { + if(!m_output) + return Task::State::Failed; + m_output->clear(); failAllValidators(); - return Job_Failed; + return Task::State::Failed; } - JobStatus finalize(QNetworkReply &reply) override + auto finalize(QNetworkReply& reply) -> Task::State override { - if(finalizeAllValidators(reply)) - return Job_Finished; - return Job_Failed; + if (finalizeAllValidators(reply)) + return Task::State::Succeeded; + return Task::State::Failed; } - bool hasLocalData() override - { - return false; - } + auto hasLocalData() -> bool override { return false; } -private: - QByteArray * m_output; + private: + QByteArray* m_output; }; -} +} // namespace Net diff --git a/launcher/net/Download.cpp b/launcher/net/Download.cpp index 65cc8f67a..5b5a04db3 100644 --- a/launcher/net/Download.cpp +++ b/launcher/net/Download.cpp @@ -30,7 +30,7 @@ namespace Net { Download::Download() : NetAction() { - m_status = Job_NotStarted; + m_state = State::Inactive; } Download::Ptr Download::makeCached(QUrl url, MetaEntryPtr entry, Options options) @@ -68,29 +68,29 @@ void Download::addValidator(Validator* v) m_sink->addValidator(v); } -void Download::startImpl() +void Download::executeTask() { - if (m_status == Job_Aborted) { + if (getState() == Task::State::AbortedByUser) { qWarning() << "Attempt to start an aborted Download:" << m_url.toString(); emit aborted(m_index_within_job); return; } + QNetworkRequest request(m_url); - m_status = m_sink->init(request); - switch (m_status) { - case Job_Finished: + m_state = m_sink->init(request); + switch (m_state) { + case State::Succeeded: emit succeeded(m_index_within_job); qDebug() << "Download cache hit " << m_url.toString(); return; - case Job_InProgress: + case State::Running: qDebug() << "Downloading " << m_url.toString(); break; - case Job_Failed_Proceed: // this is meaningless in this context. We do need a sink. - case Job_NotStarted: - case Job_Failed: + case State::Inactive: + case State::Failed: emit failed(m_index_within_job); return; - case Job_Aborted: + case State::AbortedByUser: return; } @@ -111,8 +111,7 @@ void Download::startImpl() void Download::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) { - m_total_progress = bytesTotal; - m_progress = bytesReceived; + setProgress(bytesReceived, bytesTotal); emit netActionProgress(m_index_within_job, bytesReceived, bytesTotal); } @@ -120,17 +119,17 @@ void Download::downloadError(QNetworkReply::NetworkError error) { if (error == QNetworkReply::OperationCanceledError) { qCritical() << "Aborted " << m_url.toString(); - m_status = Job_Aborted; + m_state = State::AbortedByUser; } else { if (m_options & Option::AcceptLocalFiles) { if (m_sink->hasLocalData()) { - m_status = Job_Failed_Proceed; + m_state = State::Succeeded; return; } } // error happened during download. qCritical() << "Failed " << m_url.toString() << " with reason " << error; - m_status = Job_Failed; + m_state = State::Failed; } } @@ -194,7 +193,8 @@ bool Download::handleRedirect() m_url = QUrl(redirect.toString()); qDebug() << "Following redirect to " << m_url.toString(); - start(m_network); + startAction(m_network); + return true; } @@ -207,19 +207,20 @@ void Download::downloadFinished() } // if the download failed before this point ... - if (m_status == Job_Failed_Proceed) { + if (m_state == State::Succeeded) // pretend to succeed so we continue processing :) + { qDebug() << "Download failed but we are allowed to proceed:" << m_url.toString(); m_sink->abort(); m_reply.reset(); emit succeeded(m_index_within_job); return; - } else if (m_status == Job_Failed) { + } else if (m_state == State::Failed) { qDebug() << "Download failed in previous step:" << m_url.toString(); m_sink->abort(); m_reply.reset(); emit failed(m_index_within_job); return; - } else if (m_status == Job_Aborted) { + } else if (m_state == State::AbortedByUser) { qDebug() << "Download aborted in previous step:" << m_url.toString(); m_sink->abort(); m_reply.reset(); @@ -231,12 +232,12 @@ void Download::downloadFinished() auto data = m_reply->readAll(); if (data.size()) { qDebug() << "Writing extra" << data.size() << "bytes to" << m_target_path; - m_status = m_sink->write(data); + m_state = m_sink->write(data); } // otherwise, finalize the whole graph - m_status = m_sink->finalize(*m_reply.get()); - if (m_status != Job_Finished) { + m_state = m_sink->finalize(*m_reply.get()); + if (m_state != State::Succeeded) { qDebug() << "Download failed to finalize:" << m_url.toString(); m_sink->abort(); m_reply.reset(); @@ -250,10 +251,10 @@ void Download::downloadFinished() void Download::downloadReadyRead() { - if (m_status == Job_InProgress) { + if (m_state == State::Running) { auto data = m_reply->readAll(); - m_status = m_sink->write(data); - if (m_status == Job_Failed) { + m_state = m_sink->write(data); + if (m_state == State::Failed) { qCritical() << "Failed to process response chunk for " << m_target_path; } // qDebug() << "Download" << m_url.toString() << "gained" << data.size() << "bytes"; @@ -269,12 +270,7 @@ bool Net::Download::abort() if (m_reply) { m_reply->abort(); } else { - m_status = Job_Aborted; + m_state = State::AbortedByUser; } return true; } - -bool Net::Download::canAbort() -{ - return true; -} diff --git a/launcher/net/Download.h b/launcher/net/Download.h index 0f9bfe7f7..231ad6a73 100644 --- a/launcher/net/Download.h +++ b/launcher/net/Download.h @@ -27,7 +27,7 @@ class Download : public NetAction { Q_OBJECT -public: /* types */ +public: typedef shared_qobject_ptr Ptr; enum class Option { @@ -36,7 +36,7 @@ class Download : public NetAction }; Q_DECLARE_FLAGS(Options, Option) -protected: /* con/des */ +protected: explicit Download(); public: virtual ~Download(){}; @@ -44,16 +44,16 @@ class Download : public NetAction static Download::Ptr makeByteArray(QUrl url, QByteArray *output, Options options = Option::NoOptions); static Download::Ptr makeFile(QUrl url, QString path, Options options = Option::NoOptions); -public: /* methods */ +public: QString getTargetFilepath() { return m_target_path; } void addValidator(Validator * v); bool abort() override; - bool canAbort() override; + bool canAbort() const override { return true; }; -private: /* methods */ +private: bool handleRedirect(); protected slots: @@ -64,9 +64,9 @@ protected slots: void downloadReadyRead() override; public slots: - void startImpl() override; + void executeTask() override; -private: /* data */ +private: // FIXME: remove this, it has no business being here. QString m_target_path; std::unique_ptr m_sink; diff --git a/launcher/net/FileSink.cpp b/launcher/net/FileSink.cpp index 7e9b8929f..0d8b09bbc 100644 --- a/launcher/net/FileSink.cpp +++ b/launcher/net/FileSink.cpp @@ -1,25 +1,15 @@ #include "FileSink.h" + #include -#include + #include "FileSystem.h" namespace Net { -FileSink::FileSink(QString filename) - :m_filename(filename) -{ - // nil -} - -FileSink::~FileSink() -{ - // nil -} - -JobStatus FileSink::init(QNetworkRequest& request) +Task::State FileSink::init(QNetworkRequest& request) { auto result = initCache(request); - if(result != Job_InProgress) + if(result != Task::State::Running) { return result; } @@ -27,27 +17,27 @@ JobStatus FileSink::init(QNetworkRequest& request) if (!FS::ensureFilePathExists(m_filename)) { qCritical() << "Could not create folder for " + m_filename; - return Job_Failed; + return Task::State::Failed; } wroteAnyData = false; m_output_file.reset(new QSaveFile(m_filename)); if (!m_output_file->open(QIODevice::WriteOnly)) { qCritical() << "Could not open " + m_filename + " for writing"; - return Job_Failed; + return Task::State::Failed; } if(initAllValidators(request)) - return Job_InProgress; - return Job_Failed; + return Task::State::Running; + return Task::State::Failed; } -JobStatus FileSink::initCache(QNetworkRequest &) +Task::State FileSink::initCache(QNetworkRequest &) { - return Job_InProgress; + return Task::State::Running; } -JobStatus FileSink::write(QByteArray& data) +Task::State FileSink::write(QByteArray& data) { if (!writeAllValidators(data) || m_output_file->write(data) != data.size()) { @@ -55,20 +45,20 @@ JobStatus FileSink::write(QByteArray& data) m_output_file->cancelWriting(); m_output_file.reset(); wroteAnyData = false; - return Job_Failed; + return Task::State::Failed; } wroteAnyData = true; - return Job_InProgress; + return Task::State::Running; } -JobStatus FileSink::abort() +Task::State FileSink::abort() { m_output_file->cancelWriting(); failAllValidators(); - return Job_Failed; + return Task::State::Failed; } -JobStatus FileSink::finalize(QNetworkReply& reply) +Task::State FileSink::finalize(QNetworkReply& reply) { bool gotFile = false; QVariant statusCodeV = reply.attribute(QNetworkRequest::HttpStatusCodeAttribute); @@ -86,13 +76,13 @@ JobStatus FileSink::finalize(QNetworkReply& reply) // ask validators for data consistency // we only do this for actual downloads, not 'your data is still the same' cache hits if(!finalizeAllValidators(reply)) - return Job_Failed; + return Task::State::Failed; // nothing went wrong... if (!m_output_file->commit()) { qCritical() << "Failed to commit changes to " << m_filename; m_output_file->cancelWriting(); - return Job_Failed; + return Task::State::Failed; } } // then get rid of the save file @@ -101,9 +91,9 @@ JobStatus FileSink::finalize(QNetworkReply& reply) return finalizeCache(reply); } -JobStatus FileSink::finalizeCache(QNetworkReply &) +Task::State FileSink::finalizeCache(QNetworkReply &) { - return Job_Finished; + return Task::State::Succeeded; } bool FileSink::hasLocalData() diff --git a/launcher/net/FileSink.h b/launcher/net/FileSink.h index 875fe5110..9d77b3d0f 100644 --- a/launcher/net/FileSink.h +++ b/launcher/net/FileSink.h @@ -1,28 +1,30 @@ #pragma once -#include "Sink.h" + #include +#include "Sink.h" + namespace Net { -class FileSink : public Sink -{ -public: /* con/des */ - FileSink(QString filename); - virtual ~FileSink(); +class FileSink : public Sink { + public: + FileSink(QString filename) : m_filename(filename){}; + virtual ~FileSink() = default; + + public: + auto init(QNetworkRequest& request) -> Task::State override; + auto write(QByteArray& data) -> Task::State override; + auto abort() -> Task::State override; + auto finalize(QNetworkReply& reply) -> Task::State override; -public: /* methods */ - JobStatus init(QNetworkRequest & request) override; - JobStatus write(QByteArray & data) override; - JobStatus abort() override; - JobStatus finalize(QNetworkReply & reply) override; - bool hasLocalData() override; + auto hasLocalData() -> bool override; -protected: /* methods */ - virtual JobStatus initCache(QNetworkRequest &); - virtual JobStatus finalizeCache(QNetworkReply &reply); + protected: + virtual auto initCache(QNetworkRequest&) -> Task::State; + virtual auto finalizeCache(QNetworkReply& reply) -> Task::State; -protected: /* data */ + protected: QString m_filename; bool wroteAnyData = false; std::unique_ptr m_output_file; }; -} +} // namespace Net diff --git a/launcher/net/MetaCacheSink.cpp b/launcher/net/MetaCacheSink.cpp index 5cdf04606..34ba9f566 100644 --- a/launcher/net/MetaCacheSink.cpp +++ b/launcher/net/MetaCacheSink.cpp @@ -12,17 +12,13 @@ MetaCacheSink::MetaCacheSink(MetaEntryPtr entry, ChecksumValidator * md5sum) addValidator(md5sum); } -MetaCacheSink::~MetaCacheSink() -{ - // nil -} - -JobStatus MetaCacheSink::initCache(QNetworkRequest& request) +Task::State MetaCacheSink::initCache(QNetworkRequest& request) { if (!m_entry->isStale()) { - return Job_Finished; + return Task::State::Succeeded; } + // check if file exists, if it does, use its information for the request QFile current(m_filename); if(current.exists() && current.size() != 0) @@ -36,25 +32,31 @@ JobStatus MetaCacheSink::initCache(QNetworkRequest& request) request.setRawHeader(QString("If-None-Match").toLatin1(), m_entry->getETag().toLatin1()); } } - return Job_InProgress; + + return Task::State::Running; } -JobStatus MetaCacheSink::finalizeCache(QNetworkReply & reply) +Task::State MetaCacheSink::finalizeCache(QNetworkReply & reply) { QFileInfo output_file_info(m_filename); + if(wroteAnyData) { m_entry->setMD5Sum(m_md5Node->hash().toHex().constData()); } + m_entry->setETag(reply.rawHeader("ETag").constData()); + if (reply.hasRawHeader("Last-Modified")) { m_entry->setRemoteChangedTimestamp(reply.rawHeader("Last-Modified").constData()); } + m_entry->setLocalChangedTimestamp(output_file_info.lastModified().toUTC().toMSecsSinceEpoch()); m_entry->setStale(false); APPLICATION->metacache()->updateEntry(m_entry); - return Job_Finished; + + return Task::State::Succeeded; } bool MetaCacheSink::hasLocalData() diff --git a/launcher/net/MetaCacheSink.h b/launcher/net/MetaCacheSink.h index edcf7ad17..431e10a87 100644 --- a/launcher/net/MetaCacheSink.h +++ b/launcher/net/MetaCacheSink.h @@ -1,22 +1,23 @@ #pragma once -#include "FileSink.h" + #include "ChecksumValidator.h" +#include "FileSink.h" #include "net/HttpMetaCache.h" namespace Net { -class MetaCacheSink : public FileSink -{ -public: /* con/des */ - MetaCacheSink(MetaEntryPtr entry, ChecksumValidator * md5sum); - virtual ~MetaCacheSink(); - bool hasLocalData() override; +class MetaCacheSink : public FileSink { + public: + MetaCacheSink(MetaEntryPtr entry, ChecksumValidator* md5sum); + virtual ~MetaCacheSink() = default; + + auto hasLocalData() -> bool override; -protected: /* methods */ - JobStatus initCache(QNetworkRequest & request) override; - JobStatus finalizeCache(QNetworkReply & reply) override; + protected: + auto initCache(QNetworkRequest& request) -> Task::State override; + auto finalizeCache(QNetworkReply& reply) -> Task::State override; -private: /* data */ + private: MetaEntryPtr m_entry; - ChecksumValidator * m_md5Node; + ChecksumValidator* m_md5Node; }; -} +} // namespace Net diff --git a/launcher/net/NetAction.h b/launcher/net/NetAction.h index efb20953f..e15716f65 100644 --- a/launcher/net/NetAction.h +++ b/launcher/net/NetAction.h @@ -1,108 +1,81 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #pragma once -#include -#include -#include #include -#include +#include -enum JobStatus -{ - Job_NotStarted, - Job_InProgress, - Job_Finished, - Job_Failed, - Job_Aborted, - /* - * FIXME: @NUKE this confuses the task failing with us having a fallback in the form of local data. Clear up the confusion. - * Same could be true for aborted task - the presence of pre-existing result is a separate concern - */ - Job_Failed_Proceed -}; +#include "QObjectPtr.h" +#include "tasks/Task.h" -class NetAction : public QObject -{ +class NetAction : public Task { Q_OBJECT -protected: - explicit NetAction() : QObject(nullptr) {}; + protected: + explicit NetAction() : Task(nullptr) {}; -public: + public: using Ptr = shared_qobject_ptr; - virtual ~NetAction() {}; - - bool isRunning() const - { - return m_status == Job_InProgress; - } - bool isFinished() const - { - return m_status >= Job_Finished; - } - bool wasSuccessful() const - { - return m_status == Job_Finished || m_status == Job_Failed_Proceed; - } + virtual ~NetAction() = default; - qint64 totalProgress() const - { - return m_total_progress; - } - qint64 currentProgress() const - { - return m_progress; - } - virtual bool abort() - { - return false; - } - virtual bool canAbort() - { - return false; - } - QUrl url() - { - return m_url; - } + QUrl url() { return m_url; } -signals: + signals: void started(int index); void netActionProgress(int index, qint64 current, qint64 total); void succeeded(int index); void failed(int index); void aborted(int index); -protected slots: + protected slots: virtual void downloadProgress(qint64 bytesReceived, qint64 bytesTotal) = 0; virtual void downloadError(QNetworkReply::NetworkError error) = 0; virtual void downloadFinished() = 0; virtual void downloadReadyRead() = 0; -public slots: - void start(shared_qobject_ptr network) { + public slots: + void startAction(shared_qobject_ptr network) + { m_network = network; - startImpl(); + executeTask(); } -protected: - virtual void startImpl() = 0; + protected: + void executeTask() override {}; -public: + public: shared_qobject_ptr m_network; /// index within the parent job, FIXME: nuke @@ -113,10 +86,4 @@ public slots: /// source URL QUrl m_url; - - qint64 m_progress = 0; - qint64 m_total_progress = 1; - -protected: - JobStatus m_status = Job_NotStarted; }; diff --git a/launcher/net/NetJob.cpp b/launcher/net/NetJob.cpp index 9bad89edd..d08d6c4d3 100644 --- a/launcher/net/NetJob.cpp +++ b/launcher/net/NetJob.cpp @@ -1,79 +1,170 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #include "NetJob.h" #include "Download.h" -#include +auto NetJob::addNetAction(NetAction::Ptr action) -> bool +{ + action->m_index_within_job = m_downloads.size(); + m_downloads.append(action); + part_info pi; + m_parts_progress.append(pi); + + partProgress(m_parts_progress.count() - 1, action->getProgress(), action->getTotalProgress()); + + if (action->isRunning()) { + connect(action.get(), &NetAction::succeeded, this, &NetJob::partSucceeded); + connect(action.get(), &NetAction::failed, this, &NetJob::partFailed); + connect(action.get(), &NetAction::netActionProgress, this, &NetJob::partProgress); + } else { + m_todo.append(m_parts_progress.size() - 1); + } + + return true; +} + +auto NetJob::canAbort() const -> bool +{ + bool canFullyAbort = true; + + // can abort the downloads on the queue? + for (auto index : m_todo) { + auto part = m_downloads[index]; + canFullyAbort &= part->canAbort(); + } + // can abort the active downloads? + for (auto index : m_doing) { + auto part = m_downloads[index]; + canFullyAbort &= part->canAbort(); + } + + return canFullyAbort; +} + +void NetJob::executeTask() +{ + // hack that delays early failures so they can be caught easier + QMetaObject::invokeMethod(this, "startMoreParts", Qt::QueuedConnection); +} + +auto NetJob::getFailedFiles() -> QStringList +{ + QStringList failed; + for (auto index : m_failed) { + failed.push_back(m_downloads[index]->url().toString()); + } + failed.sort(); + return failed; +} + +auto NetJob::abort() -> bool +{ + bool fullyAborted = true; + + // fail all downloads on the queue + m_failed.unite(m_todo.toSet()); + m_todo.clear(); + + // abort active downloads + auto toKill = m_doing.toList(); + for (auto index : toKill) { + auto part = m_downloads[index]; + fullyAborted &= part->abort(); + } + + return fullyAborted; +} void NetJob::partSucceeded(int index) { // do progress. all slots are 1 in size at least - auto &slot = parts_progress[index]; + auto& slot = m_parts_progress[index]; partProgress(index, slot.total_progress, slot.total_progress); m_doing.remove(index); m_done.insert(index); - downloads[index].get()->disconnect(this); + m_downloads[index].get()->disconnect(this); + startMoreParts(); } void NetJob::partFailed(int index) { m_doing.remove(index); - auto &slot = parts_progress[index]; - if (slot.failures == 3) - { + + auto& slot = m_parts_progress[index]; + // Can try 3 times before failing by definitive + if (slot.failures == 3) { m_failed.insert(index); - } - else - { + } else { slot.failures++; m_todo.enqueue(index); } - downloads[index].get()->disconnect(this); + + m_downloads[index].get()->disconnect(this); + startMoreParts(); } void NetJob::partAborted(int index) { m_aborted = true; + m_doing.remove(index); m_failed.insert(index); - downloads[index].get()->disconnect(this); + m_downloads[index].get()->disconnect(this); + startMoreParts(); } void NetJob::partProgress(int index, qint64 bytesReceived, qint64 bytesTotal) { - auto &slot = parts_progress[index]; + auto& slot = m_parts_progress[index]; slot.current_progress = bytesReceived; slot.total_progress = bytesTotal; int done = m_done.size(); int doing = m_doing.size(); - int all = parts_progress.size(); + int all = m_parts_progress.size(); qint64 bytesAll = 0; qint64 bytesTotalAll = 0; - for(auto & partIdx: m_doing) - { - auto part = parts_progress[partIdx]; + for (auto& partIdx : m_doing) { + auto part = m_parts_progress[partIdx]; // do not count parts with unknown/nonsensical total size - if(part.total_progress <= 0) - { + if (part.total_progress <= 0) { continue; } bytesAll += part.current_progress; @@ -85,134 +176,53 @@ void NetJob::partProgress(int index, qint64 bytesReceived, qint64 bytesTotal) auto current_total = all * 1000; // HACK: make sure it never jumps backwards. // FAIL: This breaks if the size is not known (or is it something else?) and jumps to 1000, so if it is 1000 reset it to inprogress - if(m_current_progress == 1000) { + if (m_current_progress == 1000) { m_current_progress = inprogress; } - if(m_current_progress > current) - { + if (m_current_progress > current) { current = m_current_progress; } m_current_progress = current; setProgress(current, current_total); } -void NetJob::executeTask() -{ - // hack that delays early failures so they can be caught easier - QMetaObject::invokeMethod(this, "startMoreParts", Qt::QueuedConnection); -} - void NetJob::startMoreParts() { - if(!isRunning()) - { - // this actually makes sense. You can put running downloads into a NetJob and then not start it until much later. + if (!isRunning()) { + // this actually makes sense. You can put running m_downloads into a NetJob and then not start it until much later. return; } + // OK. We are actively processing tasks, proceed. // Check for final conditions if there's nothing in the queue. - if(!m_todo.size()) - { - if(!m_doing.size()) - { - if(!m_failed.size()) - { + if (!m_todo.size()) { + if (!m_doing.size()) { + if (!m_failed.size()) { emitSucceeded(); - } - else if(m_aborted) - { + } else if (m_aborted) { emitAborted(); - } - else - { + } else { emitFailed(tr("Job '%1' failed to process:\n%2").arg(objectName()).arg(getFailedFiles().join("\n"))); } } return; } - // There's work to do, try to start more parts. - while (m_doing.size() < 6) - { - if(!m_todo.size()) + + // There's work to do, try to start more parts, to a maximum of 6 concurrent ones. + while (m_doing.size() < 6) { + if (m_todo.size() == 0) return; int doThis = m_todo.dequeue(); m_doing.insert(doThis); - auto part = downloads[doThis]; - // connect signals :D - connect(part.get(), SIGNAL(succeeded(int)), SLOT(partSucceeded(int))); - connect(part.get(), SIGNAL(failed(int)), SLOT(partFailed(int))); - connect(part.get(), SIGNAL(aborted(int)), SLOT(partAborted(int))); - connect(part.get(), SIGNAL(netActionProgress(int, qint64, qint64)), - SLOT(partProgress(int, qint64, qint64))); - part->start(m_network); - } -} - - -QStringList NetJob::getFailedFiles() -{ - QStringList failed; - for (auto index: m_failed) - { - failed.push_back(downloads[index]->url().toString()); - } - failed.sort(); - return failed; -} -bool NetJob::canAbort() const -{ - bool canFullyAbort = true; - // can abort the waiting? - for(auto index: m_todo) - { - auto part = downloads[index]; - canFullyAbort &= part->canAbort(); - } - // can abort the active? - for(auto index: m_doing) - { - auto part = downloads[index]; - canFullyAbort &= part->canAbort(); - } - return canFullyAbort; -} + auto part = m_downloads[doThis]; -bool NetJob::abort() -{ - bool fullyAborted = true; - // fail all waiting - m_failed.unite(m_todo.toSet()); - m_todo.clear(); - // abort active - auto toKill = m_doing.toList(); - for(auto index: toKill) - { - auto part = downloads[index]; - fullyAborted &= part->abort(); - } - return fullyAborted; -} + // connect signals :D + connect(part.get(), &NetAction::succeeded, this, &NetJob::partSucceeded); + connect(part.get(), &NetAction::failed, this, &NetJob::partFailed); + connect(part.get(), &NetAction::aborted, this, &NetJob::partAborted); + connect(part.get(), &NetAction::netActionProgress, this, &NetJob::partProgress); -bool NetJob::addNetAction(NetAction::Ptr action) -{ - action->m_index_within_job = downloads.size(); - downloads.append(action); - part_info pi; - parts_progress.append(pi); - partProgress(parts_progress.count() - 1, action->currentProgress(), action->totalProgress()); - - if(action->isRunning()) - { - connect(action.get(), SIGNAL(succeeded(int)), SLOT(partSucceeded(int))); - connect(action.get(), SIGNAL(failed(int)), SLOT(partFailed(int))); - connect(action.get(), SIGNAL(netActionProgress(int, qint64, qint64)), SLOT(partProgress(int, qint64, qint64))); + part->startAction(m_network); } - else - { - m_todo.append(parts_progress.size() - 1); - } - return true; } - -NetJob::~NetJob() = default; diff --git a/launcher/net/NetJob.h b/launcher/net/NetJob.h index fdea710fc..c397e2a1f 100644 --- a/launcher/net/NetJob.h +++ b/launcher/net/NetJob.h @@ -1,88 +1,97 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #pragma once + #include + +#include #include "NetAction.h" -#include "Download.h" -#include "HttpMetaCache.h" #include "tasks/Task.h" -#include "QObjectPtr.h" -class NetJob; +// Those are included so that they are also included by anyone using NetJob +#include "net/Download.h" +#include "net/HttpMetaCache.h" -class NetJob : public Task -{ +class NetJob : public Task { Q_OBJECT -public: + + public: using Ptr = shared_qobject_ptr; explicit NetJob(QString job_name, shared_qobject_ptr network) : Task(), m_network(network) { setObjectName(job_name); } - virtual ~NetJob(); + virtual ~NetJob() = default; - bool addNetAction(NetAction::Ptr action); + void executeTask() override; - NetAction::Ptr operator[](int index) - { - return downloads[index]; - } - const NetAction::Ptr at(const int index) - { - return downloads.at(index); - } - NetAction::Ptr first() - { - if (downloads.size()) - return downloads[0]; - return NetAction::Ptr(); - } - int size() const - { - return downloads.size(); - } - QStringList getFailedFiles(); + auto canAbort() const -> bool override; - bool canAbort() const override; + auto addNetAction(NetAction::Ptr action) -> bool; -private slots: - void startMoreParts(); + auto operator[](int index) -> NetAction::Ptr { return m_downloads[index]; } + auto at(int index) -> const NetAction::Ptr { return m_downloads.at(index); } + auto size() const -> int { return m_downloads.size(); } + auto first() -> NetAction::Ptr { return m_downloads.size() != 0 ? m_downloads[0] : NetAction::Ptr{}; } -public slots: - virtual void executeTask() override; - virtual bool abort() override; + auto getFailedFiles() -> QStringList; + + public slots: + // Qt can't handle auto at the start for some reason? + bool abort() override; + + private slots: + void startMoreParts(); -private slots: void partProgress(int index, qint64 bytesReceived, qint64 bytesTotal); void partSucceeded(int index); void partFailed(int index); void partAborted(int index); -private: + private: shared_qobject_ptr m_network; - struct part_info - { + struct part_info { qint64 current_progress = 0; qint64 total_progress = 1; int failures = 0; }; - QList downloads; - QList parts_progress; + + QList m_downloads; + QList m_parts_progress; QQueue m_todo; QSet m_doing; QSet m_done; diff --git a/launcher/net/Sink.h b/launcher/net/Sink.h index d367fb15c..3b2a7f8dd 100644 --- a/launcher/net/Sink.h +++ b/launcher/net/Sink.h @@ -5,33 +5,30 @@ #include "Validator.h" namespace Net { -class Sink -{ -public: /* con/des */ - Sink() {}; - virtual ~Sink() {}; +class Sink { + public: + Sink() = default; + virtual ~Sink(){}; -public: /* methods */ - virtual JobStatus init(QNetworkRequest & request) = 0; - virtual JobStatus write(QByteArray & data) = 0; - virtual JobStatus abort() = 0; - virtual JobStatus finalize(QNetworkReply & reply) = 0; + public: + virtual Task::State init(QNetworkRequest& request) = 0; + virtual Task::State write(QByteArray& data) = 0; + virtual Task::State abort() = 0; + virtual Task::State finalize(QNetworkReply& reply) = 0; virtual bool hasLocalData() = 0; - void addValidator(Validator * validator) + void addValidator(Validator* validator) { - if(validator) - { + if (validator) { validators.push_back(std::shared_ptr(validator)); } } -protected: /* methods */ - bool finalizeAllValidators(QNetworkReply & reply) + protected: /* methods */ + bool finalizeAllValidators(QNetworkReply& reply) { - for(auto & validator: validators) - { - if(!validator->validate(reply)) + for (auto& validator : validators) { + if (!validator->validate(reply)) return false; } return true; @@ -39,32 +36,29 @@ class Sink bool failAllValidators() { bool success = true; - for(auto & validator: validators) - { + for (auto& validator : validators) { success &= validator->abort(); } return success; } - bool initAllValidators(QNetworkRequest & request) + bool initAllValidators(QNetworkRequest& request) { - for(auto & validator: validators) - { - if(!validator->init(request)) + for (auto& validator : validators) { + if (!validator->init(request)) return false; } return true; } - bool writeAllValidators(QByteArray & data) + bool writeAllValidators(QByteArray& data) { - for(auto & validator: validators) - { - if(!validator->write(data)) + for (auto& validator : validators) { + if (!validator->write(data)) return false; } return true; } -protected: /* data */ + protected: /* data */ std::vector> validators; }; -} +} // namespace Net diff --git a/launcher/screenshots/ImgurAlbumCreation.cpp b/launcher/screenshots/ImgurAlbumCreation.cpp index d5de302a0..81fac929d 100644 --- a/launcher/screenshots/ImgurAlbumCreation.cpp +++ b/launcher/screenshots/ImgurAlbumCreation.cpp @@ -13,12 +13,12 @@ ImgurAlbumCreation::ImgurAlbumCreation(QList screenshots) : NetAction(), m_screenshots(screenshots) { m_url = BuildConfig.IMGUR_BASE_URL + "album.json"; - m_status = Job_NotStarted; + m_state = State::Inactive; } -void ImgurAlbumCreation::startImpl() +void ImgurAlbumCreation::executeTask() { - m_status = Job_InProgress; + m_state = State::Running; QNetworkRequest request(m_url); request.setHeader(QNetworkRequest::UserAgentHeader, BuildConfig.USER_AGENT_UNCACHED); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); @@ -43,11 +43,11 @@ void ImgurAlbumCreation::startImpl() void ImgurAlbumCreation::downloadError(QNetworkReply::NetworkError error) { qDebug() << m_reply->errorString(); - m_status = Job_Failed; + m_state = State::Failed; } void ImgurAlbumCreation::downloadFinished() { - if (m_status != Job_Failed) + if (m_state != State::Failed) { QByteArray data = m_reply->readAll(); m_reply.reset(); @@ -68,7 +68,7 @@ void ImgurAlbumCreation::downloadFinished() } m_deleteHash = object.value("data").toObject().value("deletehash").toString(); m_id = object.value("data").toObject().value("id").toString(); - m_status = Job_Finished; + m_state = State::Succeeded; emit succeeded(m_index_within_job); return; } @@ -82,7 +82,6 @@ void ImgurAlbumCreation::downloadFinished() } void ImgurAlbumCreation::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) { - m_total_progress = bytesTotal; - m_progress = bytesReceived; + setProgress(bytesReceived, bytesTotal); emit netActionProgress(m_index_within_job, bytesReceived, bytesTotal); } diff --git a/launcher/screenshots/ImgurAlbumCreation.h b/launcher/screenshots/ImgurAlbumCreation.h index cb048a233..4cb0ed5dd 100644 --- a/launcher/screenshots/ImgurAlbumCreation.h +++ b/launcher/screenshots/ImgurAlbumCreation.h @@ -24,16 +24,14 @@ class ImgurAlbumCreation : public NetAction protected slots: - virtual void downloadProgress(qint64 bytesReceived, qint64 bytesTotal); - virtual void downloadError(QNetworkReply::NetworkError error); - virtual void downloadFinished(); - virtual void downloadReadyRead() - { - } + void downloadProgress(qint64 bytesReceived, qint64 bytesTotal) override; + void downloadError(QNetworkReply::NetworkError error) override; + void downloadFinished() override; + void downloadReadyRead() override {} public slots: - virtual void startImpl(); + void executeTask() override; private: QList m_screenshots; diff --git a/launcher/screenshots/ImgurUpload.cpp b/launcher/screenshots/ImgurUpload.cpp index 76a84947b..0f0fd79c1 100644 --- a/launcher/screenshots/ImgurUpload.cpp +++ b/launcher/screenshots/ImgurUpload.cpp @@ -13,13 +13,13 @@ ImgurUpload::ImgurUpload(ScreenShot::Ptr shot) : NetAction(), m_shot(shot) { m_url = BuildConfig.IMGUR_BASE_URL + "upload.json"; - m_status = Job_NotStarted; + m_state = State::Inactive; } -void ImgurUpload::startImpl() +void ImgurUpload::executeTask() { finished = false; - m_status = Job_InProgress; + m_state = Task::State::Running; QNetworkRequest request(m_url); request.setHeader(QNetworkRequest::UserAgentHeader, BuildConfig.USER_AGENT_UNCACHED); request.setRawHeader("Authorization", QString("Client-ID %1").arg(BuildConfig.IMGUR_CLIENT_ID).toStdString().c_str()); @@ -63,7 +63,7 @@ void ImgurUpload::downloadError(QNetworkReply::NetworkError error) qCritical() << "Double finished ImgurUpload!"; return; } - m_status = Job_Failed; + m_state = Task::State::Failed; finished = true; m_reply.reset(); emit failed(m_index_within_job); @@ -99,14 +99,13 @@ void ImgurUpload::downloadFinished() m_shot->m_imgurId = object.value("data").toObject().value("id").toString(); m_shot->m_url = object.value("data").toObject().value("link").toString(); m_shot->m_imgurDeleteHash = object.value("data").toObject().value("deletehash").toString(); - m_status = Job_Finished; + m_state = Task::State::Succeeded; finished = true; emit succeeded(m_index_within_job); return; } void ImgurUpload::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) { - m_total_progress = bytesTotal; - m_progress = bytesReceived; + setProgress(bytesReceived, bytesTotal); emit netActionProgress(m_index_within_job, bytesReceived, bytesTotal); } diff --git a/launcher/screenshots/ImgurUpload.h b/launcher/screenshots/ImgurUpload.h index cf54f58dc..a10405510 100644 --- a/launcher/screenshots/ImgurUpload.h +++ b/launcher/screenshots/ImgurUpload.h @@ -21,7 +21,7 @@ protected public slots: - void startImpl() override; + void executeTask() override; private: ScreenShot::Ptr m_shot; diff --git a/launcher/tasks/Task.h b/launcher/tasks/Task.h index 344a024ee..618551601 100644 --- a/launcher/tasks/Task.h +++ b/launcher/tasks/Task.h @@ -52,6 +52,8 @@ class Task : public QObject { virtual bool canAbort() const { return false; } + auto getState() const -> State { return m_state; } + QString getStatus() { return m_status; } virtual auto getStepStatus() const -> QString { return m_status; } @@ -90,7 +92,7 @@ class Task : public QObject { void setStatus(const QString& status); void setProgress(qint64 current, qint64 total); - private: + protected: State m_state = State::Inactive; QStringList m_Warnings; QString m_failReason = ""; diff --git a/launcher/translations/TranslationsModel.cpp b/launcher/translations/TranslationsModel.cpp index 250854d3b..fbd170607 100644 --- a/launcher/translations/TranslationsModel.cpp +++ b/launcher/translations/TranslationsModel.cpp @@ -667,7 +667,7 @@ void TranslationsModel::downloadTranslation(QString key) auto dl = Net::Download::makeCached(QUrl(BuildConfig.TRANSLATIONS_BASE_URL + lang->file_name), entry); auto rawHash = QByteArray::fromHex(lang->file_sha1.toLatin1()); dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, rawHash)); - dl->m_total_progress = lang->file_size; + dl->setProgress(dl->getProgress(), lang->file_size); d->m_dl_job = new NetJob("Translation for " + key, APPLICATION->network()); d->m_dl_job->addNetAction(dl); From efa3fbff39bf0dabebdf1c6330090ee320895a4d Mon Sep 17 00:00:00 2001 From: flow Date: Tue, 26 Apr 2022 21:25:42 -0300 Subject: [PATCH 058/157] refactor: remove some superfluous signals Since now we're inheriting from Task, some signals can be reused. --- launcher/net/Download.cpp | 21 ++++++++++----------- launcher/net/NetAction.h | 10 ++-------- launcher/net/NetJob.cpp | 14 +++++++------- launcher/screenshots/ImgurAlbumCreation.cpp | 10 +++++----- launcher/screenshots/ImgurUpload.cpp | 12 ++++++------ launcher/tasks/Task.cpp | 3 +-- launcher/tasks/Task.h | 3 ++- 7 files changed, 33 insertions(+), 40 deletions(-) diff --git a/launcher/net/Download.cpp b/launcher/net/Download.cpp index 5b5a04db3..5e5d64fac 100644 --- a/launcher/net/Download.cpp +++ b/launcher/net/Download.cpp @@ -72,7 +72,7 @@ void Download::executeTask() { if (getState() == Task::State::AbortedByUser) { qWarning() << "Attempt to start an aborted Download:" << m_url.toString(); - emit aborted(m_index_within_job); + emitAborted(); return; } @@ -80,7 +80,7 @@ void Download::executeTask() m_state = m_sink->init(request); switch (m_state) { case State::Succeeded: - emit succeeded(m_index_within_job); + emit succeeded(); qDebug() << "Download cache hit " << m_url.toString(); return; case State::Running: @@ -88,7 +88,7 @@ void Download::executeTask() break; case State::Inactive: case State::Failed: - emit failed(m_index_within_job); + emitFailed(); return; case State::AbortedByUser: return; @@ -102,8 +102,8 @@ void Download::executeTask() QNetworkReply* rep = m_network->get(request); m_reply.reset(rep); - connect(rep, SIGNAL(downloadProgress(qint64, qint64)), SLOT(downloadProgress(qint64, qint64))); - connect(rep, SIGNAL(finished()), SLOT(downloadFinished())); + connect(rep, &QNetworkReply::downloadProgress, this, &Download::downloadProgress); + connect(rep, &QNetworkReply::finished, this, &Download::downloadFinished); connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), SLOT(downloadError(QNetworkReply::NetworkError))); connect(rep, &QNetworkReply::sslErrors, this, &Download::sslErrors); connect(rep, &QNetworkReply::readyRead, this, &Download::downloadReadyRead); @@ -112,7 +112,6 @@ void Download::executeTask() void Download::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) { setProgress(bytesReceived, bytesTotal); - emit netActionProgress(m_index_within_job, bytesReceived, bytesTotal); } void Download::downloadError(QNetworkReply::NetworkError error) @@ -212,19 +211,19 @@ void Download::downloadFinished() qDebug() << "Download failed but we are allowed to proceed:" << m_url.toString(); m_sink->abort(); m_reply.reset(); - emit succeeded(m_index_within_job); + emit succeeded(); return; } else if (m_state == State::Failed) { qDebug() << "Download failed in previous step:" << m_url.toString(); m_sink->abort(); m_reply.reset(); - emit failed(m_index_within_job); + emitFailed(); return; } else if (m_state == State::AbortedByUser) { qDebug() << "Download aborted in previous step:" << m_url.toString(); m_sink->abort(); m_reply.reset(); - emit aborted(m_index_within_job); + emitAborted(); return; } @@ -241,12 +240,12 @@ void Download::downloadFinished() qDebug() << "Download failed to finalize:" << m_url.toString(); m_sink->abort(); m_reply.reset(); - emit failed(m_index_within_job); + emitFailed(); return; } m_reply.reset(); qDebug() << "Download succeeded:" << m_url.toString(); - emit succeeded(m_index_within_job); + emit succeeded(); } void Download::downloadReadyRead() diff --git a/launcher/net/NetAction.h b/launcher/net/NetAction.h index e15716f65..86a37ee6d 100644 --- a/launcher/net/NetAction.h +++ b/launcher/net/NetAction.h @@ -43,7 +43,7 @@ class NetAction : public Task { Q_OBJECT protected: - explicit NetAction() : Task(nullptr) {}; + explicit NetAction() : Task() {}; public: using Ptr = shared_qobject_ptr; @@ -51,13 +51,7 @@ class NetAction : public Task { virtual ~NetAction() = default; QUrl url() { return m_url; } - - signals: - void started(int index); - void netActionProgress(int index, qint64 current, qint64 total); - void succeeded(int index); - void failed(int index); - void aborted(int index); + auto index() -> int { return m_index_within_job; } protected slots: virtual void downloadProgress(qint64 bytesReceived, qint64 bytesTotal) = 0; diff --git a/launcher/net/NetJob.cpp b/launcher/net/NetJob.cpp index d08d6c4d3..a9f89da4c 100644 --- a/launcher/net/NetJob.cpp +++ b/launcher/net/NetJob.cpp @@ -45,9 +45,9 @@ auto NetJob::addNetAction(NetAction::Ptr action) -> bool partProgress(m_parts_progress.count() - 1, action->getProgress(), action->getTotalProgress()); if (action->isRunning()) { - connect(action.get(), &NetAction::succeeded, this, &NetJob::partSucceeded); - connect(action.get(), &NetAction::failed, this, &NetJob::partFailed); - connect(action.get(), &NetAction::netActionProgress, this, &NetJob::partProgress); + connect(action.get(), &NetAction::succeeded, [this, action]{ partSucceeded(action->index()); }); + connect(action.get(), &NetAction::failed, [this, action](QString){ partFailed(action->index()); }); + connect(action.get(), &NetAction::progress, [this, action](qint64 done, qint64 total) { partProgress(action->index(), done, total); }); } else { m_todo.append(m_parts_progress.size() - 1); } @@ -218,10 +218,10 @@ void NetJob::startMoreParts() auto part = m_downloads[doThis]; // connect signals :D - connect(part.get(), &NetAction::succeeded, this, &NetJob::partSucceeded); - connect(part.get(), &NetAction::failed, this, &NetJob::partFailed); - connect(part.get(), &NetAction::aborted, this, &NetJob::partAborted); - connect(part.get(), &NetAction::netActionProgress, this, &NetJob::partProgress); + connect(part.get(), &NetAction::succeeded, this, [this, part]{ partSucceeded(part->index()); }); + connect(part.get(), &NetAction::failed, this, [this, part](QString){ partFailed(part->index()); }); + connect(part.get(), &NetAction::aborted, this, [this, part]{ partAborted(part->index()); }); + connect(part.get(), &NetAction::progress, this, [this, part](qint64 done, qint64 total) { partProgress(part->index(), done, total); }); part->startAction(m_network); } diff --git a/launcher/screenshots/ImgurAlbumCreation.cpp b/launcher/screenshots/ImgurAlbumCreation.cpp index 81fac929d..f94527c8b 100644 --- a/launcher/screenshots/ImgurAlbumCreation.cpp +++ b/launcher/screenshots/ImgurAlbumCreation.cpp @@ -56,32 +56,32 @@ void ImgurAlbumCreation::downloadFinished() if (jsonError.error != QJsonParseError::NoError) { qDebug() << jsonError.errorString(); - emit failed(m_index_within_job); + emitFailed(); return; } auto object = doc.object(); if (!object.value("success").toBool()) { qDebug() << doc.toJson(); - emit failed(m_index_within_job); + emitFailed(); return; } m_deleteHash = object.value("data").toObject().value("deletehash").toString(); m_id = object.value("data").toObject().value("id").toString(); m_state = State::Succeeded; - emit succeeded(m_index_within_job); + emit succeeded(); return; } else { qDebug() << m_reply->readAll(); m_reply.reset(); - emit failed(m_index_within_job); + emitFailed(); return; } } void ImgurAlbumCreation::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) { setProgress(bytesReceived, bytesTotal); - emit netActionProgress(m_index_within_job, bytesReceived, bytesTotal); + emit progress(bytesReceived, bytesTotal); } diff --git a/launcher/screenshots/ImgurUpload.cpp b/launcher/screenshots/ImgurUpload.cpp index 0f0fd79c1..05314de75 100644 --- a/launcher/screenshots/ImgurUpload.cpp +++ b/launcher/screenshots/ImgurUpload.cpp @@ -28,7 +28,7 @@ void ImgurUpload::executeTask() QFile f(m_shot->m_file.absoluteFilePath()); if (!f.open(QFile::ReadOnly)) { - emit failed(m_index_within_job); + emitFailed(); return; } @@ -66,7 +66,7 @@ void ImgurUpload::downloadError(QNetworkReply::NetworkError error) m_state = Task::State::Failed; finished = true; m_reply.reset(); - emit failed(m_index_within_job); + emitFailed(); } void ImgurUpload::downloadFinished() { @@ -84,7 +84,7 @@ void ImgurUpload::downloadFinished() qDebug() << "imgur server did not reply with JSON" << jsonError.errorString(); finished = true; m_reply.reset(); - emit failed(m_index_within_job); + emitFailed(); return; } auto object = doc.object(); @@ -93,7 +93,7 @@ void ImgurUpload::downloadFinished() qDebug() << "Screenshot upload not successful:" << doc.toJson(); finished = true; m_reply.reset(); - emit failed(m_index_within_job); + emitFailed(); return; } m_shot->m_imgurId = object.value("data").toObject().value("id").toString(); @@ -101,11 +101,11 @@ void ImgurUpload::downloadFinished() m_shot->m_imgurDeleteHash = object.value("data").toObject().value("deletehash").toString(); m_state = Task::State::Succeeded; finished = true; - emit succeeded(m_index_within_job); + emit succeeded(); return; } void ImgurUpload::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) { setProgress(bytesReceived, bytesTotal); - emit netActionProgress(m_index_within_job, bytesReceived, bytesTotal); + emit progress(bytesReceived, bytesTotal); } diff --git a/launcher/tasks/Task.cpp b/launcher/tasks/Task.cpp index 57307b431..68e0e8a7d 100644 --- a/launcher/tasks/Task.cpp +++ b/launcher/tasks/Task.cpp @@ -99,8 +99,7 @@ void Task::emitAborted() m_state = State::AbortedByUser; m_failReason = "Aborted."; qDebug() << "Task" << describe() << "aborted."; - emit failed(m_failReason); - emit finished(); + emit aborted(); } void Task::emitSucceeded() diff --git a/launcher/tasks/Task.h b/launcher/tasks/Task.h index 618551601..e09c57aec 100644 --- a/launcher/tasks/Task.h +++ b/launcher/tasks/Task.h @@ -73,6 +73,7 @@ class Task : public QObject { virtual void progress(qint64 current, qint64 total); void finished(); void succeeded(); + void aborted(); void failed(QString reason); void status(QString status); @@ -86,7 +87,7 @@ class Task : public QObject { protected slots: virtual void emitSucceeded(); virtual void emitAborted(); - virtual void emitFailed(QString reason); + virtual void emitFailed(QString reason = ""); public slots: void setStatus(const QString& status); From 040ee919e5ea71364daa08c30e09c843976f5734 Mon Sep 17 00:00:00 2001 From: flow Date: Wed, 27 Apr 2022 18:36:11 -0300 Subject: [PATCH 059/157] refactor: more net cleanup This runs clang-tidy on some other files in launcher/net/. This also makes use of some JSON wrappers in HttpMetaCache, instead of using the Qt stuff directly. Lastly, this removes useless null checks (crashes don't occur because of this, but because of concurrent usage / free of the QByteArray pointer), and fix a fixme in Download.h --- launcher/net/ByteArraySink.h | 11 +- launcher/net/ChecksumValidator.h | 48 ++++---- launcher/net/Download.cpp | 24 ++-- launcher/net/Download.h | 59 +++++----- launcher/net/FileSink.cpp | 47 ++++---- launcher/net/HttpMetaCache.cpp | 184 ++++++++++++++----------------- launcher/net/HttpMetaCache.h | 107 ++++++++---------- launcher/net/Mode.h | 9 +- launcher/net/Sink.h | 33 +++--- 9 files changed, 225 insertions(+), 297 deletions(-) diff --git a/launcher/net/ByteArraySink.h b/launcher/net/ByteArraySink.h index 75a66574d..8ae30bb31 100644 --- a/launcher/net/ByteArraySink.h +++ b/launcher/net/ByteArraySink.h @@ -6,6 +6,8 @@ namespace Net { /* * Sink object for downloads that uses an external QByteArray it doesn't own as a target. + * FIXME: It is possible that the QByteArray is freed while we're doing some operation on it, + * causing a segmentation fault. */ class ByteArraySink : public Sink { public: @@ -16,9 +18,6 @@ class ByteArraySink : public Sink { public: auto init(QNetworkRequest& request) -> Task::State override { - if(!m_output) - return Task::State::Failed; - m_output->clear(); if (initAllValidators(request)) return Task::State::Running; @@ -27,9 +26,6 @@ class ByteArraySink : public Sink { auto write(QByteArray& data) -> Task::State override { - if(!m_output) - return Task::State::Failed; - m_output->append(data); if (writeAllValidators(data)) return Task::State::Running; @@ -38,9 +34,6 @@ class ByteArraySink : public Sink { auto abort() -> Task::State override { - if(!m_output) - return Task::State::Failed; - m_output->clear(); failAllValidators(); return Task::State::Failed; diff --git a/launcher/net/ChecksumValidator.h b/launcher/net/ChecksumValidator.h index 0d6b19c21..8a8b10d57 100644 --- a/launcher/net/ChecksumValidator.h +++ b/launcher/net/ChecksumValidator.h @@ -1,55 +1,47 @@ #pragma once #include "Validator.h" + #include -#include #include namespace Net { -class ChecksumValidator: public Validator -{ -public: /* con/des */ +class ChecksumValidator : public Validator { + public: ChecksumValidator(QCryptographicHash::Algorithm algorithm, QByteArray expected = QByteArray()) - :m_checksum(algorithm), m_expected(expected) - { - }; - virtual ~ChecksumValidator() {}; + : m_checksum(algorithm), m_expected(expected){}; + virtual ~ChecksumValidator() = default; -public: /* methods */ - bool init(QNetworkRequest &) override + public: + auto init(QNetworkRequest&) -> bool override { m_checksum.reset(); return true; } - bool write(QByteArray & data) override + + auto write(QByteArray& data) -> bool override { m_checksum.addData(data); return true; } - bool abort() override - { - return true; - } - bool validate(QNetworkReply &) override + + auto abort() -> bool override { return true; } + + auto validate(QNetworkReply&) -> bool override { - if(m_expected.size() && m_expected != hash()) - { + if (m_expected.size() && m_expected != hash()) { qWarning() << "Checksum mismatch, download is bad."; return false; } return true; } - QByteArray hash() - { - return m_checksum.result(); - } - void setExpected(QByteArray expected) - { - m_expected = expected; - } -private: /* data */ + auto hash() -> QByteArray { return m_checksum.result(); } + + void setExpected(QByteArray expected) { m_expected = expected; } + + private: QCryptographicHash m_checksum; QByteArray m_expected; }; -} \ No newline at end of file +} // namespace Net diff --git a/launcher/net/Download.cpp b/launcher/net/Download.cpp index 5e5d64fac..3d6ca3382 100644 --- a/launcher/net/Download.cpp +++ b/launcher/net/Download.cpp @@ -33,30 +33,29 @@ Download::Download() : NetAction() m_state = State::Inactive; } -Download::Ptr Download::makeCached(QUrl url, MetaEntryPtr entry, Options options) +auto Download::makeCached(QUrl url, MetaEntryPtr entry, Options options) -> Download::Ptr { - Download* dl = new Download(); + auto* dl = new Download(); dl->m_url = url; dl->m_options = options; auto md5Node = new ChecksumValidator(QCryptographicHash::Md5); auto cachedNode = new MetaCacheSink(entry, md5Node); dl->m_sink.reset(cachedNode); - dl->m_target_path = entry->getFullPath(); return dl; } -Download::Ptr Download::makeByteArray(QUrl url, QByteArray* output, Options options) +auto Download::makeByteArray(QUrl url, QByteArray* output, Options options) -> Download::Ptr { - Download* dl = new Download(); + auto* dl = new Download(); dl->m_url = url; dl->m_options = options; dl->m_sink.reset(new ByteArraySink(output)); return dl; } -Download::Ptr Download::makeFile(QUrl url, QString path, Options options) +auto Download::makeFile(QUrl url, QString path, Options options) -> Download::Ptr { - Download* dl = new Download(); + auto* dl = new Download(); dl->m_url = url; dl->m_options = options; dl->m_sink.reset(new FileSink(path)); @@ -143,7 +142,7 @@ void Download::sslErrors(const QList& errors) } } -bool Download::handleRedirect() +auto Download::handleRedirect() -> bool { QUrl redirect = m_reply->header(QNetworkRequest::LocationHeader).toUrl(); if (!redirect.isValid()) { @@ -230,7 +229,7 @@ void Download::downloadFinished() // make sure we got all the remaining data, if any auto data = m_reply->readAll(); if (data.size()) { - qDebug() << "Writing extra" << data.size() << "bytes to" << m_target_path; + qDebug() << "Writing extra" << data.size() << "bytes"; m_state = m_sink->write(data); } @@ -243,6 +242,7 @@ void Download::downloadFinished() emitFailed(); return; } + m_reply.reset(); qDebug() << "Download succeeded:" << m_url.toString(); emit succeeded(); @@ -254,17 +254,17 @@ void Download::downloadReadyRead() auto data = m_reply->readAll(); m_state = m_sink->write(data); if (m_state == State::Failed) { - qCritical() << "Failed to process response chunk for " << m_target_path; + qCritical() << "Failed to process response chunk"; } // qDebug() << "Download" << m_url.toString() << "gained" << data.size() << "bytes"; } else { - qCritical() << "Cannot write to " << m_target_path << ", illegal status" << m_status; + qCritical() << "Cannot write download data! illegal status " << m_status; } } } // namespace Net -bool Net::Download::abort() +auto Net::Download::abort() -> bool { if (m_reply) { m_reply->abort(); diff --git a/launcher/net/Download.h b/launcher/net/Download.h index 231ad6a73..9fb671275 100644 --- a/launcher/net/Download.h +++ b/launcher/net/Download.h @@ -15,63 +15,54 @@ #pragma once -#include "NetAction.h" #include "HttpMetaCache.h" -#include "Validator.h" +#include "NetAction.h" #include "Sink.h" +#include "Validator.h" #include "QObjectPtr.h" namespace Net { -class Download : public NetAction -{ +class Download : public NetAction { Q_OBJECT -public: - typedef shared_qobject_ptr Ptr; - enum class Option - { - NoOptions = 0, - AcceptLocalFiles = 1 - }; + public: + using Ptr = shared_qobject_ptr; + enum class Option { NoOptions = 0, AcceptLocalFiles = 1 }; Q_DECLARE_FLAGS(Options, Option) -protected: + protected: explicit Download(); -public: - virtual ~Download(){}; - static Download::Ptr makeCached(QUrl url, MetaEntryPtr entry, Options options = Option::NoOptions); - static Download::Ptr makeByteArray(QUrl url, QByteArray *output, Options options = Option::NoOptions); - static Download::Ptr makeFile(QUrl url, QString path, Options options = Option::NoOptions); -public: - QString getTargetFilepath() - { - return m_target_path; - } - void addValidator(Validator * v); - bool abort() override; - bool canAbort() const override { return true; }; + public: + ~Download() override = default; + + static auto makeCached(QUrl url, MetaEntryPtr entry, Options options = Option::NoOptions) -> Download::Ptr; + static auto makeByteArray(QUrl url, QByteArray* output, Options options = Option::NoOptions) -> Download::Ptr; + static auto makeFile(QUrl url, QString path, Options options = Option::NoOptions) -> Download::Ptr; + + public: + void addValidator(Validator* v); + auto abort() -> bool override; + auto canAbort() const -> bool override { return true; }; -private: - bool handleRedirect(); + private: + auto handleRedirect() -> bool; -protected slots: + protected slots: void downloadProgress(qint64 bytesReceived, qint64 bytesTotal) override; void downloadError(QNetworkReply::NetworkError error) override; - void sslErrors(const QList & errors); + void sslErrors(const QList& errors); void downloadFinished() override; void downloadReadyRead() override; -public slots: + public slots: void executeTask() override; -private: - // FIXME: remove this, it has no business being here. - QString m_target_path; + private: std::unique_ptr m_sink; Options m_options; }; -} +} // namespace Net Q_DECLARE_OPERATORS_FOR_FLAGS(Net::Download::Options) diff --git a/launcher/net/FileSink.cpp b/launcher/net/FileSink.cpp index 0d8b09bbc..d2d2b06fb 100644 --- a/launcher/net/FileSink.cpp +++ b/launcher/net/FileSink.cpp @@ -1,7 +1,5 @@ #include "FileSink.h" -#include - #include "FileSystem.h" namespace Net { @@ -9,44 +7,38 @@ namespace Net { Task::State FileSink::init(QNetworkRequest& request) { auto result = initCache(request); - if(result != Task::State::Running) - { + if (result != Task::State::Running) { return result; } + // create a new save file and open it for writing - if (!FS::ensureFilePathExists(m_filename)) - { + if (!FS::ensureFilePathExists(m_filename)) { qCritical() << "Could not create folder for " + m_filename; return Task::State::Failed; } + wroteAnyData = false; m_output_file.reset(new QSaveFile(m_filename)); - if (!m_output_file->open(QIODevice::WriteOnly)) - { + if (!m_output_file->open(QIODevice::WriteOnly)) { qCritical() << "Could not open " + m_filename + " for writing"; return Task::State::Failed; } - if(initAllValidators(request)) + if (initAllValidators(request)) return Task::State::Running; return Task::State::Failed; } -Task::State FileSink::initCache(QNetworkRequest &) -{ - return Task::State::Running; -} - Task::State FileSink::write(QByteArray& data) { - if (!writeAllValidators(data) || m_output_file->write(data) != data.size()) - { + if (!writeAllValidators(data) || m_output_file->write(data) != data.size()) { qCritical() << "Failed writing into " + m_filename; m_output_file->cancelWriting(); m_output_file.reset(); wroteAnyData = false; return Task::State::Failed; } + wroteAnyData = true; return Task::State::Running; } @@ -64,34 +56,39 @@ Task::State FileSink::finalize(QNetworkReply& reply) QVariant statusCodeV = reply.attribute(QNetworkRequest::HttpStatusCodeAttribute); bool validStatus = false; int statusCode = statusCodeV.toInt(&validStatus); - if(validStatus) - { + if (validStatus) { // this leaves out 304 Not Modified gotFile = statusCode == 200 || statusCode == 203; } + // if we wrote any data to the save file, we try to commit the data to the real file. // if it actually got a proper file, we write it even if it was empty - if (gotFile || wroteAnyData) - { + if (gotFile || wroteAnyData) { // ask validators for data consistency // we only do this for actual downloads, not 'your data is still the same' cache hits - if(!finalizeAllValidators(reply)) + if (!finalizeAllValidators(reply)) return Task::State::Failed; + // nothing went wrong... - if (!m_output_file->commit()) - { + if (!m_output_file->commit()) { qCritical() << "Failed to commit changes to " << m_filename; m_output_file->cancelWriting(); return Task::State::Failed; } } + // then get rid of the save file m_output_file.reset(); return finalizeCache(reply); } -Task::State FileSink::finalizeCache(QNetworkReply &) +Task::State FileSink::initCache(QNetworkRequest&) +{ + return Task::State::Running; +} + +Task::State FileSink::finalizeCache(QNetworkReply&) { return Task::State::Succeeded; } @@ -101,4 +98,4 @@ bool FileSink::hasLocalData() QFileInfo info(m_filename); return info.exists() && info.size() != 0; } -} +} // namespace Net diff --git a/launcher/net/HttpMetaCache.cpp b/launcher/net/HttpMetaCache.cpp index 8734e0bfb..b41a18b14 100644 --- a/launcher/net/HttpMetaCache.cpp +++ b/launcher/net/HttpMetaCache.cpp @@ -15,29 +15,26 @@ #include "HttpMetaCache.h" #include "FileSystem.h" +#include "Json.h" -#include -#include -#include #include +#include +#include +#include #include -#include -#include -#include - -QString MetaEntry::getFullPath() +auto MetaEntry::getFullPath() -> QString { // FIXME: make local? return FS::PathCombine(basePath, relativePath); } -HttpMetaCache::HttpMetaCache(QString path) : QObject() +HttpMetaCache::HttpMetaCache(QString path) : QObject(), m_index_file(path) { - m_index_file = path; saveBatchingTimer.setSingleShot(true); saveBatchingTimer.setTimerType(Qt::VeryCoarseTimer); + connect(&saveBatchingTimer, SIGNAL(timeout()), SLOT(SaveNow())); } @@ -47,45 +44,42 @@ HttpMetaCache::~HttpMetaCache() SaveNow(); } -MetaEntryPtr HttpMetaCache::getEntry(QString base, QString resource_path) +auto HttpMetaCache::getEntry(QString base, QString resource_path) -> MetaEntryPtr { // no base. no base path. can't store - if (!m_entries.contains(base)) - { + if (!m_entries.contains(base)) { // TODO: log problem - return MetaEntryPtr(); + return {}; } - EntryMap &map = m_entries[base]; - if (map.entry_list.contains(resource_path)) - { + + EntryMap& map = m_entries[base]; + if (map.entry_list.contains(resource_path)) { return map.entry_list[resource_path]; } - return MetaEntryPtr(); + + return {}; } -MetaEntryPtr HttpMetaCache::resolveEntry(QString base, QString resource_path, QString expected_etag) +auto HttpMetaCache::resolveEntry(QString base, QString resource_path, QString expected_etag) -> MetaEntryPtr { auto entry = getEntry(base, resource_path); // it's not present? generate a default stale entry - if (!entry) - { + if (!entry) { return staleEntry(base, resource_path); } - auto &selected_base = m_entries[base]; + auto& selected_base = m_entries[base]; QString real_path = FS::PathCombine(selected_base.base_path, resource_path); QFileInfo finfo(real_path); // is the file really there? if not -> stale - if (!finfo.isFile() || !finfo.isReadable()) - { + if (!finfo.isFile() || !finfo.isReadable()) { // if the file doesn't exist, we disown the entry selected_base.entry_list.remove(resource_path); return staleEntry(base, resource_path); } - if (!expected_etag.isEmpty() && expected_etag != entry->etag) - { + if (!expected_etag.isEmpty() && expected_etag != entry->etag) { // if the etag doesn't match expected, we disown the entry selected_base.entry_list.remove(resource_path); return staleEntry(base, resource_path); @@ -93,18 +87,15 @@ MetaEntryPtr HttpMetaCache::resolveEntry(QString base, QString resource_path, QS // if the file changed, check md5sum qint64 file_last_changed = finfo.lastModified().toUTC().toMSecsSinceEpoch(); - if (file_last_changed != entry->local_changed_timestamp) - { + if (file_last_changed != entry->local_changed_timestamp) { QFile input(real_path); input.open(QIODevice::ReadOnly); - QString md5sum = QCryptographicHash::hash(input.readAll(), QCryptographicHash::Md5) - .toHex() - .constData(); - if (entry->md5sum != md5sum) - { + QString md5sum = QCryptographicHash::hash(input.readAll(), QCryptographicHash::Md5).toHex().constData(); + if (entry->md5sum != md5sum) { selected_base.entry_list.remove(resource_path); return staleEntry(base, resource_path); } + // md5sums matched... keep entry and save the new state to file entry->local_changed_timestamp = file_last_changed; SaveEventually(); @@ -115,42 +106,42 @@ MetaEntryPtr HttpMetaCache::resolveEntry(QString base, QString resource_path, QS return entry; } -bool HttpMetaCache::updateEntry(MetaEntryPtr stale_entry) +auto HttpMetaCache::updateEntry(MetaEntryPtr stale_entry) -> bool { - if (!m_entries.contains(stale_entry->baseId)) - { - qCritical() << "Cannot add entry with unknown base: " - << stale_entry->baseId.toLocal8Bit(); + if (!m_entries.contains(stale_entry->baseId)) { + qCritical() << "Cannot add entry with unknown base: " << stale_entry->baseId.toLocal8Bit(); return false; } - if (stale_entry->stale) - { + + if (stale_entry->stale) { qCritical() << "Cannot add stale entry: " << stale_entry->getFullPath().toLocal8Bit(); return false; } + m_entries[stale_entry->baseId].entry_list[stale_entry->relativePath] = stale_entry; SaveEventually(); + return true; } -bool HttpMetaCache::evictEntry(MetaEntryPtr entry) +auto HttpMetaCache::evictEntry(MetaEntryPtr entry) -> bool { - if(entry) - { - entry->stale = true; - SaveEventually(); - return true; - } - return false; + if (!entry) + return false; + + entry->stale = true; + SaveEventually(); + return true; } -MetaEntryPtr HttpMetaCache::staleEntry(QString base, QString resource_path) +auto HttpMetaCache::staleEntry(QString base, QString resource_path) -> MetaEntryPtr { auto foo = new MetaEntry(); foo->baseId = base; foo->basePath = getBasePath(base); foo->relativePath = resource_path; foo->stale = true; + return MetaEntryPtr(foo); } @@ -159,24 +150,25 @@ void HttpMetaCache::addBase(QString base, QString base_root) // TODO: report error if (m_entries.contains(base)) return; + // TODO: check if the base path is valid EntryMap foo; foo.base_path = base_root; m_entries[base] = foo; } -QString HttpMetaCache::getBasePath(QString base) +auto HttpMetaCache::getBasePath(QString base) -> QString { - if (m_entries.contains(base)) - { + if (m_entries.contains(base)) { return m_entries[base].base_path; } - return QString(); + + return {}; } void HttpMetaCache::Load() { - if(m_index_file.isNull()) + if (m_index_file.isNull()) return; QFile index(m_index_file); @@ -184,41 +176,35 @@ void HttpMetaCache::Load() return; QJsonDocument json = QJsonDocument::fromJson(index.readAll()); - if (!json.isObject()) - return; - auto root = json.object(); + + auto root = Json::requireObject(json, "HttpMetaCache root"); + // check file version first - auto version_val = root.value("version"); - if (!version_val.isString()) - return; - if (version_val.toString() != "1") + auto version_val = Json::ensureString(root, "version"); + if (version_val != "1") return; // read the entry array - auto entries_val = root.value("entries"); - if (!entries_val.isArray()) - return; - QJsonArray array = entries_val.toArray(); - for (auto element : array) - { - if (!element.isObject()) - return; - auto element_obj = element.toObject(); - QString base = element_obj.value("base").toString(); + auto array = Json::ensureArray(root, "entries"); + for (auto element : array) { + auto element_obj = Json::ensureObject(element); + auto base = Json::ensureString(element_obj, "base"); if (!m_entries.contains(base)) continue; - auto &entrymap = m_entries[base]; + + auto& entrymap = m_entries[base]; + auto foo = new MetaEntry(); foo->baseId = base; - QString path = foo->relativePath = element_obj.value("path").toString(); - foo->md5sum = element_obj.value("md5sum").toString(); - foo->etag = element_obj.value("etag").toString(); - foo->local_changed_timestamp = element_obj.value("last_changed_timestamp").toDouble(); - foo->remote_changed_timestamp = - element_obj.value("remote_changed_timestamp").toString(); + foo->relativePath = Json::ensureString(element_obj, "path"); + foo->md5sum = Json::ensureString(element_obj, "md5sum"); + foo->etag = Json::ensureString(element_obj, "etag"); + foo->local_changed_timestamp = Json::ensureDouble(element_obj, "last_changed_timestamp"); + foo->remote_changed_timestamp = Json::ensureString(element_obj, "remote_changed_timestamp"); // presumed innocent until closer examination foo->stale = false; - entrymap.entry_list[path] = MetaEntryPtr(foo); + + entrymap.entry_list[foo->relativePath] = MetaEntryPtr(foo); } } @@ -231,42 +217,36 @@ void HttpMetaCache::SaveEventually() void HttpMetaCache::SaveNow() { - if(m_index_file.isNull()) + if (m_index_file.isNull()) return; + QJsonObject toplevel; - toplevel.insert("version", QJsonValue(QString("1"))); + Json::writeString(toplevel, "version", "1"); + QJsonArray entriesArr; - for (auto group : m_entries) - { - for (auto entry : group.entry_list) - { + for (auto group : m_entries) { + for (auto entry : group.entry_list) { // do not save stale entries. they are dead. - if(entry->stale) - { + if (entry->stale) { continue; } + QJsonObject entryObj; - entryObj.insert("base", QJsonValue(entry->baseId)); - entryObj.insert("path", QJsonValue(entry->relativePath)); - entryObj.insert("md5sum", QJsonValue(entry->md5sum)); - entryObj.insert("etag", QJsonValue(entry->etag)); - entryObj.insert("last_changed_timestamp", - QJsonValue(double(entry->local_changed_timestamp))); + Json::writeString(entryObj, "base", entry->baseId); + Json::writeString(entryObj, "path", entry->relativePath); + Json::writeString(entryObj, "md5sum", entry->md5sum); + Json::writeString(entryObj, "etag", entry->etag); + entryObj.insert("last_changed_timestamp", QJsonValue(double(entry->local_changed_timestamp))); if (!entry->remote_changed_timestamp.isEmpty()) - entryObj.insert("remote_changed_timestamp", - QJsonValue(entry->remote_changed_timestamp)); + entryObj.insert("remote_changed_timestamp", QJsonValue(entry->remote_changed_timestamp)); entriesArr.append(entryObj); } } toplevel.insert("entries", entriesArr); - QJsonDocument doc(toplevel); - try - { - FS::write(m_index_file, doc.toJson()); - } - catch (const Exception &e) - { + try { + Json::write(toplevel, m_index_file); + } catch (const Exception& e) { qWarning() << e.what(); } } diff --git a/launcher/net/HttpMetaCache.h b/launcher/net/HttpMetaCache.h index 1c10e8c79..d8d1608e7 100644 --- a/launcher/net/HttpMetaCache.h +++ b/launcher/net/HttpMetaCache.h @@ -14,109 +14,88 @@ */ #pragma once -#include -#include #include +#include +#include #include class HttpMetaCache; -class MetaEntry -{ -friend class HttpMetaCache; -protected: - MetaEntry() {} -public: - bool isStale() - { - return stale; - } - void setStale(bool stale) - { - this->stale = stale; - } - QString getFullPath(); - QString getRemoteChangedTimestamp() - { - return remote_changed_timestamp; - } - void setRemoteChangedTimestamp(QString remote_changed_timestamp) - { - this->remote_changed_timestamp = remote_changed_timestamp; - } - void setLocalChangedTimestamp(qint64 timestamp) - { - local_changed_timestamp = timestamp; - } - QString getETag() - { - return etag; - } - void setETag(QString etag) - { - this->etag = etag; - } - QString getMD5Sum() - { - return md5sum; - } - void setMD5Sum(QString md5sum) - { - this->md5sum = md5sum; - } -protected: +class MetaEntry { + friend class HttpMetaCache; + + protected: + MetaEntry() = default; + + public: + auto isStale() -> bool { return stale; } + void setStale(bool stale) { this->stale = stale; } + + auto getFullPath() -> QString; + + auto getRemoteChangedTimestamp() -> QString { return remote_changed_timestamp; } + void setRemoteChangedTimestamp(QString remote_changed_timestamp) { this->remote_changed_timestamp = remote_changed_timestamp; } + void setLocalChangedTimestamp(qint64 timestamp) { local_changed_timestamp = timestamp; } + + auto getETag() -> QString { return etag; } + void setETag(QString etag) { this->etag = etag; } + + auto getMD5Sum() -> QString { return md5sum; } + void setMD5Sum(QString md5sum) { this->md5sum = md5sum; } + + protected: QString baseId; QString basePath; QString relativePath; QString md5sum; QString etag; qint64 local_changed_timestamp = 0; - QString remote_changed_timestamp; // QString for now, RFC 2822 encoded time + QString remote_changed_timestamp; // QString for now, RFC 2822 encoded time bool stale = true; }; -typedef std::shared_ptr MetaEntryPtr; +using MetaEntryPtr = std::shared_ptr; -class HttpMetaCache : public QObject -{ +class HttpMetaCache : public QObject { Q_OBJECT -public: + public: // supply path to the cache index file HttpMetaCache(QString path = QString()); - ~HttpMetaCache(); + ~HttpMetaCache() override; // get the entry solely from the cache // you probably don't want this, unless you have some specific caching needs. - MetaEntryPtr getEntry(QString base, QString resource_path); + auto getEntry(QString base, QString resource_path) -> MetaEntryPtr; // get the entry from cache and verify that it isn't stale (within reason) - MetaEntryPtr resolveEntry(QString base, QString resource_path, - QString expected_etag = QString()); + auto resolveEntry(QString base, QString resource_path, QString expected_etag = QString()) -> MetaEntryPtr; // add a previously resolved stale entry - bool updateEntry(MetaEntryPtr stale_entry); + auto updateEntry(MetaEntryPtr stale_entry) -> bool; // evict selected entry from cache - bool evictEntry(MetaEntryPtr entry); + auto evictEntry(MetaEntryPtr entry) -> bool; void addBase(QString base, QString base_root); // (re)start a timer that calls SaveNow later. void SaveEventually(); void Load(); - QString getBasePath(QString base); -public -slots: + + auto getBasePath(QString base) -> QString; + + public slots: void SaveNow(); -private: + private: // create a new stale entry, given the parameters - MetaEntryPtr staleEntry(QString base, QString resource_path); - struct EntryMap - { + auto staleEntry(QString base, QString resource_path) -> MetaEntryPtr; + + struct EntryMap { QString base_path; QMap entry_list; }; + QMap m_entries; QString m_index_file; QTimer saveBatchingTimer; diff --git a/launcher/net/Mode.h b/launcher/net/Mode.h index 9a95f5ad4..3d75981fb 100644 --- a/launcher/net/Mode.h +++ b/launcher/net/Mode.h @@ -1,10 +1,5 @@ #pragma once -namespace Net -{ -enum class Mode -{ - Offline, - Online -}; +namespace Net { +enum class Mode { Offline, Online }; } diff --git a/launcher/net/Sink.h b/launcher/net/Sink.h index 3b2a7f8dd..c88002203 100644 --- a/launcher/net/Sink.h +++ b/launcher/net/Sink.h @@ -8,14 +8,15 @@ namespace Net { class Sink { public: Sink() = default; - virtual ~Sink(){}; + virtual ~Sink() = default; public: - virtual Task::State init(QNetworkRequest& request) = 0; - virtual Task::State write(QByteArray& data) = 0; - virtual Task::State abort() = 0; - virtual Task::State finalize(QNetworkReply& reply) = 0; - virtual bool hasLocalData() = 0; + virtual auto init(QNetworkRequest& request) -> Task::State = 0; + virtual auto write(QByteArray& data) -> Task::State = 0; + virtual auto abort() -> Task::State = 0; + virtual auto finalize(QNetworkReply& reply) -> Task::State = 0; + + virtual auto hasLocalData() -> bool = 0; void addValidator(Validator* validator) { @@ -24,7 +25,15 @@ class Sink { } } - protected: /* methods */ + protected: + bool initAllValidators(QNetworkRequest& request) + { + for (auto& validator : validators) { + if (!validator->init(request)) + return false; + } + return true; + } bool finalizeAllValidators(QNetworkReply& reply) { for (auto& validator : validators) { @@ -41,14 +50,6 @@ class Sink { } return success; } - bool initAllValidators(QNetworkRequest& request) - { - for (auto& validator : validators) { - if (!validator->init(request)) - return false; - } - return true; - } bool writeAllValidators(QByteArray& data) { for (auto& validator : validators) { @@ -58,7 +59,7 @@ class Sink { return true; } - protected: /* data */ + protected: std::vector> validators; }; } // namespace Net From 57d65177c8ebb5463c88dd8e26f1e0a33f648bed Mon Sep 17 00:00:00 2001 From: flow Date: Sun, 1 May 2022 11:05:31 -0300 Subject: [PATCH 060/157] fix: abort and fail logic in tasks Also sets up correctly the status connections --- launcher/net/Download.cpp | 9 ++++++--- launcher/net/NetJob.cpp | 3 +++ launcher/tasks/Task.cpp | 1 + launcher/tasks/Task.h | 2 +- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/launcher/net/Download.cpp b/launcher/net/Download.cpp index 3d6ca3382..9c01fa8dd 100644 --- a/launcher/net/Download.cpp +++ b/launcher/net/Download.cpp @@ -69,6 +69,8 @@ void Download::addValidator(Validator* v) void Download::executeTask() { + setStatus(tr("Downloading %1").arg(m_url.toString())); + if (getState() == Task::State::AbortedByUser) { qWarning() << "Attempt to start an aborted Download:" << m_url.toString(); emitAborted(); @@ -90,6 +92,7 @@ void Download::executeTask() emitFailed(); return; case State::AbortedByUser: + emitAborted(); return; } @@ -216,13 +219,13 @@ void Download::downloadFinished() qDebug() << "Download failed in previous step:" << m_url.toString(); m_sink->abort(); m_reply.reset(); - emitFailed(); + emit failed(""); return; } else if (m_state == State::AbortedByUser) { qDebug() << "Download aborted in previous step:" << m_url.toString(); m_sink->abort(); m_reply.reset(); - emitAborted(); + emit aborted(); return; } @@ -239,7 +242,7 @@ void Download::downloadFinished() qDebug() << "Download failed to finalize:" << m_url.toString(); m_sink->abort(); m_reply.reset(); - emitFailed(); + emit failed(""); return; } diff --git a/launcher/net/NetJob.cpp b/launcher/net/NetJob.cpp index a9f89da4c..906a735f6 100644 --- a/launcher/net/NetJob.cpp +++ b/launcher/net/NetJob.cpp @@ -47,7 +47,9 @@ auto NetJob::addNetAction(NetAction::Ptr action) -> bool if (action->isRunning()) { connect(action.get(), &NetAction::succeeded, [this, action]{ partSucceeded(action->index()); }); connect(action.get(), &NetAction::failed, [this, action](QString){ partFailed(action->index()); }); + connect(action.get(), &NetAction::aborted, [this, action](){ partAborted(action->index()); }); connect(action.get(), &NetAction::progress, [this, action](qint64 done, qint64 total) { partProgress(action->index(), done, total); }); + connect(action.get(), &NetAction::status, this, &NetJob::status); } else { m_todo.append(m_parts_progress.size() - 1); } @@ -222,6 +224,7 @@ void NetJob::startMoreParts() connect(part.get(), &NetAction::failed, this, [this, part](QString){ partFailed(part->index()); }); connect(part.get(), &NetAction::aborted, this, [this, part]{ partAborted(part->index()); }); connect(part.get(), &NetAction::progress, this, [this, part](qint64 done, qint64 total) { partProgress(part->index(), done, total); }); + connect(part.get(), &NetAction::status, this, &NetJob::status); part->startAction(m_network); } diff --git a/launcher/tasks/Task.cpp b/launcher/tasks/Task.cpp index 68e0e8a7d..d2d62c9eb 100644 --- a/launcher/tasks/Task.cpp +++ b/launcher/tasks/Task.cpp @@ -100,6 +100,7 @@ void Task::emitAborted() m_failReason = "Aborted."; qDebug() << "Task" << describe() << "aborted."; emit aborted(); + emit finished(); } void Task::emitSucceeded() diff --git a/launcher/tasks/Task.h b/launcher/tasks/Task.h index e09c57aec..0ca37e021 100644 --- a/launcher/tasks/Task.h +++ b/launcher/tasks/Task.h @@ -79,7 +79,7 @@ class Task : public QObject { public slots: virtual void start(); - virtual bool abort() { return false; }; + virtual bool abort() { if(canAbort()) emitAborted(); return canAbort(); }; protected: virtual void executeTask() = 0; From 0bce08d30f2bbdeca19c375840880f69ffeac81b Mon Sep 17 00:00:00 2001 From: flow Date: Mon, 2 May 2022 12:56:24 -0300 Subject: [PATCH 061/157] chore: add polymc license headers to launcher/net files --- launcher/net/ByteArraySink.h | 35 ++++++++++++++++++++++++++ launcher/net/ChecksumValidator.h | 35 ++++++++++++++++++++++++++ launcher/net/Download.cpp | 41 ++++++++++++++++++++++-------- launcher/net/Download.h | 40 +++++++++++++++++++++-------- launcher/net/FileSink.cpp | 35 ++++++++++++++++++++++++++ launcher/net/FileSink.h | 35 ++++++++++++++++++++++++++ launcher/net/HttpMetaCache.cpp | 40 +++++++++++++++++++++-------- launcher/net/HttpMetaCache.h | 43 ++++++++++++++++++++++++-------- launcher/net/MetaCacheSink.cpp | 35 ++++++++++++++++++++++++++ launcher/net/MetaCacheSink.h | 35 ++++++++++++++++++++++++++ launcher/net/NetAction.h | 1 + launcher/net/NetJob.cpp | 1 + launcher/net/NetJob.h | 1 + launcher/net/PasteUpload.cpp | 35 ++++++++++++++++++++++++++ launcher/net/PasteUpload.h | 36 ++++++++++++++++++++++++++ launcher/net/Sink.h | 35 ++++++++++++++++++++++++++ launcher/net/Validator.h | 35 ++++++++++++++++++++++++++ 17 files changed, 476 insertions(+), 42 deletions(-) diff --git a/launcher/net/ByteArraySink.h b/launcher/net/ByteArraySink.h index 8ae30bb31..501318a11 100644 --- a/launcher/net/ByteArraySink.h +++ b/launcher/net/ByteArraySink.h @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #pragma once #include "Sink.h" diff --git a/launcher/net/ChecksumValidator.h b/launcher/net/ChecksumValidator.h index 8a8b10d57..a2ca2c7a4 100644 --- a/launcher/net/ChecksumValidator.h +++ b/launcher/net/ChecksumValidator.h @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #pragma once #include "Validator.h" diff --git a/launcher/net/Download.cpp b/launcher/net/Download.cpp index 9c01fa8dd..97033de1a 100644 --- a/launcher/net/Download.cpp +++ b/launcher/net/Download.cpp @@ -1,22 +1,41 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #include "Download.h" #include -#include #include #include "ByteArraySink.h" diff --git a/launcher/net/Download.h b/launcher/net/Download.h index 9fb671275..209329445 100644 --- a/launcher/net/Download.h +++ b/launcher/net/Download.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #pragma once diff --git a/launcher/net/FileSink.cpp b/launcher/net/FileSink.cpp index d2d2b06fb..ba0caf6c0 100644 --- a/launcher/net/FileSink.cpp +++ b/launcher/net/FileSink.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "FileSink.h" #include "FileSystem.h" diff --git a/launcher/net/FileSink.h b/launcher/net/FileSink.h index 9d77b3d0f..dffbdca67 100644 --- a/launcher/net/FileSink.h +++ b/launcher/net/FileSink.h @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #pragma once #include diff --git a/launcher/net/HttpMetaCache.cpp b/launcher/net/HttpMetaCache.cpp index b41a18b14..4d86c0b84 100644 --- a/launcher/net/HttpMetaCache.cpp +++ b/launcher/net/HttpMetaCache.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #include "HttpMetaCache.h" diff --git a/launcher/net/HttpMetaCache.h b/launcher/net/HttpMetaCache.h index d8d1608e7..e944b3d5d 100644 --- a/launcher/net/HttpMetaCache.h +++ b/launcher/net/HttpMetaCache.h @@ -1,22 +1,43 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #pragma once -#include + #include #include +#include #include class HttpMetaCache; diff --git a/launcher/net/MetaCacheSink.cpp b/launcher/net/MetaCacheSink.cpp index 34ba9f566..f86dd8704 100644 --- a/launcher/net/MetaCacheSink.cpp +++ b/launcher/net/MetaCacheSink.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "MetaCacheSink.h" #include #include diff --git a/launcher/net/MetaCacheSink.h b/launcher/net/MetaCacheSink.h index 431e10a87..c9f7edfe7 100644 --- a/launcher/net/MetaCacheSink.h +++ b/launcher/net/MetaCacheSink.h @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #pragma once #include "ChecksumValidator.h" diff --git a/launcher/net/NetAction.h b/launcher/net/NetAction.h index 86a37ee6d..729d41329 100644 --- a/launcher/net/NetAction.h +++ b/launcher/net/NetAction.h @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/launcher/net/NetJob.cpp b/launcher/net/NetJob.cpp index 906a735f6..df899178f 100644 --- a/launcher/net/NetJob.cpp +++ b/launcher/net/NetJob.cpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/launcher/net/NetJob.h b/launcher/net/NetJob.h index c397e2a1f..63c1cf517 100644 --- a/launcher/net/NetJob.h +++ b/launcher/net/NetJob.h @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/launcher/net/PasteUpload.cpp b/launcher/net/PasteUpload.cpp index 52b82a0e1..e88c89877 100644 --- a/launcher/net/PasteUpload.cpp +++ b/launcher/net/PasteUpload.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "PasteUpload.h" #include "BuildConfig.h" #include "Application.h" diff --git a/launcher/net/PasteUpload.h b/launcher/net/PasteUpload.h index 62b2dc361..53979352c 100644 --- a/launcher/net/PasteUpload.h +++ b/launcher/net/PasteUpload.h @@ -1,4 +1,40 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #pragma once + #include "tasks/Task.h" #include #include diff --git a/launcher/net/Sink.h b/launcher/net/Sink.h index c88002203..3870f29bc 100644 --- a/launcher/net/Sink.h +++ b/launcher/net/Sink.h @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #pragma once #include "net/NetAction.h" diff --git a/launcher/net/Validator.h b/launcher/net/Validator.h index 59b72a0b0..e1d71d1ce 100644 --- a/launcher/net/Validator.h +++ b/launcher/net/Validator.h @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #pragma once #include "net/NetAction.h" From dd2b324d8f7081f52decd90210ce11ef37625315 Mon Sep 17 00:00:00 2001 From: flow Date: Mon, 2 May 2022 14:33:21 -0300 Subject: [PATCH 062/157] chore: add license header to remaining files Also remove some unused imports --- launcher/InstanceImportTask.cpp | 40 ++++++++++++++----- launcher/minecraft/AssetsUtils.cpp | 40 ++++++++++++++----- launcher/screenshots/ImgurAlbumCreation.cpp | 35 ++++++++++++++++ launcher/screenshots/ImgurAlbumCreation.h | 37 ++++++++++++++++- launcher/screenshots/ImgurUpload.cpp | 35 ++++++++++++++++ launcher/screenshots/ImgurUpload.h | 37 ++++++++++++++++- launcher/tasks/Task.cpp | 40 ++++++++++++++----- launcher/tasks/Task.h | 44 ++++++++++++++------- launcher/translations/TranslationsModel.cpp | 35 ++++++++++++++++ 9 files changed, 297 insertions(+), 46 deletions(-) diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index fc3432c19..ca7e05903 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #include "InstanceImportTask.h" diff --git a/launcher/minecraft/AssetsUtils.cpp b/launcher/minecraft/AssetsUtils.cpp index 281f730f5..15062c2b4 100644 --- a/launcher/minecraft/AssetsUtils.cpp +++ b/launcher/minecraft/AssetsUtils.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #include diff --git a/launcher/screenshots/ImgurAlbumCreation.cpp b/launcher/screenshots/ImgurAlbumCreation.cpp index f94527c8b..7afdc5ccf 100644 --- a/launcher/screenshots/ImgurAlbumCreation.cpp +++ b/launcher/screenshots/ImgurAlbumCreation.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "ImgurAlbumCreation.h" #include diff --git a/launcher/screenshots/ImgurAlbumCreation.h b/launcher/screenshots/ImgurAlbumCreation.h index 4cb0ed5dd..0228b6e4a 100644 --- a/launcher/screenshots/ImgurAlbumCreation.h +++ b/launcher/screenshots/ImgurAlbumCreation.h @@ -1,7 +1,42 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #pragma once + #include "net/NetAction.h" #include "Screenshot.h" -#include "QObjectPtr.h" typedef shared_qobject_ptr ImgurAlbumCreationPtr; class ImgurAlbumCreation : public NetAction diff --git a/launcher/screenshots/ImgurUpload.cpp b/launcher/screenshots/ImgurUpload.cpp index 05314de75..fbcfb95f5 100644 --- a/launcher/screenshots/ImgurUpload.cpp +++ b/launcher/screenshots/ImgurUpload.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "ImgurUpload.h" #include "BuildConfig.h" diff --git a/launcher/screenshots/ImgurUpload.h b/launcher/screenshots/ImgurUpload.h index a10405510..404dc8765 100644 --- a/launcher/screenshots/ImgurUpload.h +++ b/launcher/screenshots/ImgurUpload.h @@ -1,5 +1,40 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #pragma once -#include "QObjectPtr.h" + #include "net/NetAction.h" #include "Screenshot.h" diff --git a/launcher/tasks/Task.cpp b/launcher/tasks/Task.cpp index d2d62c9eb..bb71b98c8 100644 --- a/launcher/tasks/Task.cpp +++ b/launcher/tasks/Task.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #include "Task.h" diff --git a/launcher/tasks/Task.h b/launcher/tasks/Task.h index 0ca37e021..f0e6e4023 100644 --- a/launcher/tasks/Task.h +++ b/launcher/tasks/Task.h @@ -1,24 +1,40 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #pragma once -#include -#include -#include - #include "QObjectPtr.h" class Task : public QObject { diff --git a/launcher/translations/TranslationsModel.cpp b/launcher/translations/TranslationsModel.cpp index fbd170607..53722d690 100644 --- a/launcher/translations/TranslationsModel.cpp +++ b/launcher/translations/TranslationsModel.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "TranslationsModel.h" #include From 067484a6a8647e6012f3fdad61653716cfb44470 Mon Sep 17 00:00:00 2001 From: icelimetea Date: Fri, 13 May 2022 16:59:00 +0100 Subject: [PATCH 063/157] Fix formatting --- libraries/launcher/org/multimc/EntryPoint.java | 4 +++- libraries/launcher/org/multimc/applet/LegacyFrame.java | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/libraries/launcher/org/multimc/EntryPoint.java b/libraries/launcher/org/multimc/EntryPoint.java index 416f21890..0244a04d8 100644 --- a/libraries/launcher/org/multimc/EntryPoint.java +++ b/libraries/launcher/org/multimc/EntryPoint.java @@ -1,4 +1,4 @@ -package org.multimc;/* +/* * Copyright 2012-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,6 +14,8 @@ * limitations under the License. */ +package org.multimc; + import org.multimc.exception.ParseException; import org.multimc.utils.Parameters; diff --git a/libraries/launcher/org/multimc/applet/LegacyFrame.java b/libraries/launcher/org/multimc/applet/LegacyFrame.java index f82cb6057..caec079c3 100644 --- a/libraries/launcher/org/multimc/applet/LegacyFrame.java +++ b/libraries/launcher/org/multimc/applet/LegacyFrame.java @@ -1,4 +1,4 @@ -package org.multimc.applet;/* +/* * Copyright 2012-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,6 +14,8 @@ * limitations under the License. */ +package org.multimc.applet; + import net.minecraft.Launcher; import javax.imageio.ImageIO; From c054d0f329a9d1d3ae76a605d82f0ad8e0ebdc99 Mon Sep 17 00:00:00 2001 From: icelimetea Date: Fri, 13 May 2022 17:21:35 +0100 Subject: [PATCH 064/157] Add the license header to LauncherFactory --- .../launcher/org/multimc/LauncherFactory.java | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/libraries/launcher/org/multimc/LauncherFactory.java b/libraries/launcher/org/multimc/LauncherFactory.java index 17e0d9058..007ce7e83 100644 --- a/libraries/launcher/org/multimc/LauncherFactory.java +++ b/libraries/launcher/org/multimc/LauncherFactory.java @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 icelimetea, + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.multimc; import org.multimc.impl.OneSixLauncher; From c3336251e0789fae6da5935c0e2b7f38eab08763 Mon Sep 17 00:00:00 2001 From: icelimetea Date: Fri, 13 May 2022 18:10:11 +0100 Subject: [PATCH 065/157] Add the license header to EntryPoint --- .../launcher/org/multimc/EntryPoint.java | 39 ++++++++++++++----- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/libraries/launcher/org/multimc/EntryPoint.java b/libraries/launcher/org/multimc/EntryPoint.java index 0244a04d8..ba5b0926f 100644 --- a/libraries/launcher/org/multimc/EntryPoint.java +++ b/libraries/launcher/org/multimc/EntryPoint.java @@ -1,17 +1,36 @@ +// SPDX-License-Identifier: GPL-3.0-only /* - * Copyright 2012-2021 MultiMC Contributors + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 icelimetea, * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ package org.multimc; From 84b962f256a492ae9a82846be40b726c8bd90e9c Mon Sep 17 00:00:00 2001 From: flow Date: Fri, 13 May 2022 17:21:35 -0300 Subject: [PATCH 066/157] fix: Handle icons with a dot in their names E.g. some FTB modpacks. Also fixes an issue with the name viewing on the Icon Chooser dialog when the name was too big. --- launcher/icons/IconList.cpp | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/launcher/icons/IconList.cpp b/launcher/icons/IconList.cpp index 584edd69e..c269d10a2 100644 --- a/launcher/icons/IconList.cpp +++ b/launcher/icons/IconList.cpp @@ -36,7 +36,7 @@ IconList::IconList(const QStringList &builtinPaths, QString path, QObject *paren auto file_info_list = instance_icons.entryInfoList(QDir::Files, QDir::Name); for (auto file_info : file_info_list) { - builtinNames.insert(file_info.baseName()); + builtinNames.insert(file_info.completeBaseName()); } } for(auto & builtinName : builtinNames) @@ -51,6 +51,9 @@ IconList::IconList(const QStringList &builtinPaths, QString path, QObject *paren connect(m_watcher.get(), SIGNAL(fileChanged(QString)), SLOT(fileChanged(QString))); directoryChanged(path); + + // Forces the UI to update, so that lengthy icon names are shown properly from the start + emit iconUpdated({}); } void IconList::directoryChanged(const QString &path) @@ -94,7 +97,13 @@ void IconList::directoryChanged(const QString &path) { qDebug() << "Removing " << remove; QFileInfo rmfile(remove); - QString key = rmfile.baseName(); + QString key = rmfile.completeBaseName(); + + QString suffix = rmfile.suffix(); + // The icon doesnt have a suffix, but it can have other .s in the name, so we account for those as well + if (suffix != "jpeg" && suffix != "png" && suffix != "jpg" && suffix != "ico" && suffix != "svg" && suffix != "gif") + key = rmfile.fileName(); + int idx = getIconIndex(key); if (idx == -1) continue; @@ -117,8 +126,15 @@ void IconList::directoryChanged(const QString &path) for (auto add : to_add) { qDebug() << "Adding " << add; + QFileInfo addfile(add); - QString key = addfile.baseName(); + QString key = addfile.completeBaseName(); + + QString suffix = addfile.suffix(); + // The icon doesnt have a suffix, but it can have other .s in the name, so we account for those as well + if (suffix != "jpeg" && suffix != "png" && suffix != "jpg" && suffix != "ico" && suffix != "svg" && suffix != "gif") + key = addfile.fileName(); + if (addIcon(key, QString(), addfile.filePath(), IconType::FileBased)) { m_watcher->addPath(add); @@ -133,7 +149,7 @@ void IconList::fileChanged(const QString &path) QFileInfo checkfile(path); if (!checkfile.exists()) return; - QString key = checkfile.baseName(); + QString key = checkfile.completeBaseName(); int idx = getIconIndex(key); if (idx == -1) return; From fac0b027b31ba2ac29730f6091b3d19ba78b40d2 Mon Sep 17 00:00:00 2001 From: icelimetea Date: Sat, 14 May 2022 16:46:57 +0100 Subject: [PATCH 067/157] Fix the license header --- .../launcher/org/multimc/LauncherFactory.java | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/libraries/launcher/org/multimc/LauncherFactory.java b/libraries/launcher/org/multimc/LauncherFactory.java index 007ce7e83..1b30a4151 100644 --- a/libraries/launcher/org/multimc/LauncherFactory.java +++ b/libraries/launcher/org/multimc/LauncherFactory.java @@ -14,23 +14,6 @@ * * You should have received a copy of the GNU General Public License * along with this program. If not, see . - * - * This file incorporates work covered by the following copyright and - * permission notice: - * - * Copyright 2013-2021 MultiMC Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. */ package org.multimc; From 3f259eb97a207c6d4d0ae3ad481541eda96df798 Mon Sep 17 00:00:00 2001 From: icelimetea Date: Sat, 14 May 2022 16:48:14 +0100 Subject: [PATCH 068/157] Refactor script parsing --- .../launcher/org/multimc/EntryPoint.java | 43 ++++++------------- .../launcher/org/multimc/LauncherFactory.java | 4 +- 2 files changed, 16 insertions(+), 31 deletions(-) diff --git a/libraries/launcher/org/multimc/EntryPoint.java b/libraries/launcher/org/multimc/EntryPoint.java index ba5b0926f..c0500bbec 100644 --- a/libraries/launcher/org/multimc/EntryPoint.java +++ b/libraries/launcher/org/multimc/EntryPoint.java @@ -51,8 +51,6 @@ public final class EntryPoint { private final Parameters params = new Parameters(); - private String launcherType; - public static void main(String[] args) { EntryPoint listener = new EntryPoint(); @@ -80,15 +78,6 @@ private Action parseLine(String inData) throws ParseException { return Action.Abort; } - case "launcher": { - if (tokens.length != 2) - throw new ParseException("Expected 2 tokens, got " + tokens.length); - - launcherType = tokens[1]; - - return Action.Proceed; - } - default: { if (tokens.length != 2) throw new ParseException("Error while parsing:" + inData); @@ -129,30 +118,24 @@ public int listen() { return 1; } - if (launcherType != null) { - try { - Launcher launcher = - LauncherFactory - .getInstance() - .createLauncher(launcherType, params); + try { + Launcher launcher = + LauncherFactory + .getInstance() + .createLauncher(params); - launcher.launch(); + launcher.launch(); - return 0; - } catch (IllegalArgumentException e) { - LOGGER.log(Level.SEVERE, "Wrong argument.", e); + return 0; + } catch (IllegalArgumentException e) { + LOGGER.log(Level.SEVERE, "Wrong argument.", e); - return 1; - } catch (Exception e) { - LOGGER.log(Level.SEVERE, "Exception caught from launcher.", e); + return 1; + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Exception caught from launcher.", e); - return 1; - } + return 1; } - - LOGGER.log(Level.SEVERE, "No valid launcher implementation specified."); - - return 1; } private enum Action { diff --git a/libraries/launcher/org/multimc/LauncherFactory.java b/libraries/launcher/org/multimc/LauncherFactory.java index 1b30a4151..a2af8581a 100644 --- a/libraries/launcher/org/multimc/LauncherFactory.java +++ b/libraries/launcher/org/multimc/LauncherFactory.java @@ -39,7 +39,9 @@ public Launcher provide(Parameters parameters) { }); } - public Launcher createLauncher(String name, Parameters parameters) { + public Launcher createLauncher(Parameters parameters) { + String name = parameters.first("launcher"); + LauncherProvider launcherProvider = launcherRegistry.get(name); if (launcherProvider == null) From c6b3eccbdf2785b59ab33ed99fabf6f3f5d81d2a Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sat, 14 May 2022 19:46:52 +0200 Subject: [PATCH 069/157] refactor: rename Modrinth classes to ModrinthMod --- launcher/CMakeLists.txt | 8 ++++---- launcher/ui/dialogs/ModDownloadDialog.cpp | 4 ++-- launcher/ui/dialogs/ModDownloadDialog.h | 4 ++-- .../{ModrinthModel.cpp => ModrinthModModel.cpp} | 2 +- .../{ModrinthModel.h => ModrinthModModel.h} | 4 ++-- .../{ModrinthPage.cpp => ModrinthModPage.cpp} | 16 ++++++++-------- .../{ModrinthPage.h => ModrinthModPage.h} | 6 +++--- 7 files changed, 22 insertions(+), 22 deletions(-) rename launcher/ui/pages/modplatform/modrinth/{ModrinthModel.cpp => ModrinthModModel.cpp} (97%) rename launcher/ui/pages/modplatform/modrinth/{ModrinthModel.h => ModrinthModModel.h} (85%) rename launcher/ui/pages/modplatform/modrinth/{ModrinthPage.cpp => ModrinthModPage.cpp} (85%) rename launcher/ui/pages/modplatform/modrinth/{ModrinthPage.h => ModrinthModPage.h} (93%) diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index b79f03c86..16ec4c047 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -782,10 +782,10 @@ SET(LAUNCHER_SOURCES ui/pages/modplatform/ImportPage.cpp ui/pages/modplatform/ImportPage.h - ui/pages/modplatform/modrinth/ModrinthModel.cpp - ui/pages/modplatform/modrinth/ModrinthModel.h - ui/pages/modplatform/modrinth/ModrinthPage.cpp - ui/pages/modplatform/modrinth/ModrinthPage.h + ui/pages/modplatform/modrinth/ModrinthModModel.cpp + ui/pages/modplatform/modrinth/ModrinthModModel.h + ui/pages/modplatform/modrinth/ModrinthModPage.cpp + ui/pages/modplatform/modrinth/ModrinthModPage.h # GUI - dialogs ui/dialogs/AboutDialog.cpp diff --git a/launcher/ui/dialogs/ModDownloadDialog.cpp b/launcher/ui/dialogs/ModDownloadDialog.cpp index d02ea4769..305e85c06 100644 --- a/launcher/ui/dialogs/ModDownloadDialog.cpp +++ b/launcher/ui/dialogs/ModDownloadDialog.cpp @@ -13,7 +13,7 @@ #include #include "ui/widgets/PageContainer.h" -#include "ui/pages/modplatform/modrinth/ModrinthPage.h" +#include "ui/pages/modplatform/modrinth/ModrinthModPage.h" #include "ModDownloadTask.h" @@ -98,7 +98,7 @@ void ModDownloadDialog::accept() QList ModDownloadDialog::getPages() { - modrinthPage = new ModrinthPage(this, m_instance); + modrinthPage = new ModrinthModPage(this, m_instance); flameModPage = new FlameModPage(this, m_instance); return { diff --git a/launcher/ui/dialogs/ModDownloadDialog.h b/launcher/ui/dialogs/ModDownloadDialog.h index 309d89d06..782dc3619 100644 --- a/launcher/ui/dialogs/ModDownloadDialog.h +++ b/launcher/ui/dialogs/ModDownloadDialog.h @@ -16,7 +16,7 @@ class ModDownloadDialog; class PageContainer; class QDialogButtonBox; -class ModrinthPage; +class ModrinthModPage; class ModDownloadDialog : public QDialog, public BasePageProvider { @@ -50,7 +50,7 @@ public slots: QVBoxLayout *m_verticalLayout = nullptr; - ModrinthPage *modrinthPage = nullptr; + ModrinthModPage *modrinthPage = nullptr; FlameModPage *flameModPage = nullptr; QHash modTask; BaseInstance *m_instance; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.cpp similarity index 97% rename from launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp rename to launcher/ui/pages/modplatform/modrinth/ModrinthModModel.cpp index b788860a3..1d9f4d60b 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.cpp @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -#include "ModrinthModel.h" +#include "ModrinthModModel.h" #include "modplatform/modrinth/ModrinthPackIndex.h" diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.h similarity index 85% rename from launcher/ui/pages/modplatform/modrinth/ModrinthModel.h rename to launcher/ui/pages/modplatform/modrinth/ModrinthModModel.h index 45a6090a7..63c23bbeb 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.h @@ -1,6 +1,6 @@ #pragma once -#include "ModrinthPage.h" +#include "ModrinthModPage.h" namespace Modrinth { @@ -8,7 +8,7 @@ class ListModel : public ModPlatform::ListModel { Q_OBJECT public: - ListModel(ModrinthPage* parent) : ModPlatform::ListModel(parent){}; + ListModel(ModrinthModPage* parent) : ModPlatform::ListModel(parent){}; ~ListModel() override = default; private: diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.cpp similarity index 85% rename from launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp rename to launcher/ui/pages/modplatform/modrinth/ModrinthModPage.cpp index 98bde0ae3..d3a1f8594 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.cpp @@ -33,14 +33,14 @@ * limitations under the License. */ -#include "ModrinthPage.h" +#include "ModrinthModPage.h" #include "modplatform/modrinth/ModrinthAPI.h" #include "ui_ModPage.h" -#include "ModrinthModel.h" +#include "ModrinthModModel.h" #include "ui/dialogs/ModDownloadDialog.h" -ModrinthPage::ModrinthPage(ModDownloadDialog* dialog, BaseInstance* instance) +ModrinthModPage::ModrinthModPage(ModDownloadDialog* dialog, BaseInstance* instance) : ModPage(dialog, instance, new ModrinthAPI()) { listModel = new Modrinth::ListModel(this); @@ -56,12 +56,12 @@ ModrinthPage::ModrinthPage(ModDownloadDialog* dialog, BaseInstance* instance) // sometimes Qt just ignores virtual slots and doesn't work as intended it seems, // so it's best not to connect them in the parent's constructor... connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); - connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthPage::onSelectionChanged); - connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &ModrinthPage::onVersionSelectionChanged); - connect(ui->modSelectionButton, &QPushButton::clicked, this, &ModrinthPage::onModSelected); + connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthModPage::onSelectionChanged); + connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &ModrinthModPage::onVersionSelectionChanged); + connect(ui->modSelectionButton, &QPushButton::clicked, this, &ModrinthModPage::onModSelected); } -auto ModrinthPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderType loader) const -> bool +auto ModrinthModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderType loader) const -> bool { auto loaderStrings = ModrinthAPI::getModLoaderStrings(loader); @@ -79,4 +79,4 @@ auto ModrinthPage::validateVersion(ModPlatform::IndexedVersion& ver, QString min // I don't know why, but doing this on the parent class makes it so that // other mod providers start loading before being selected, at least with // my Qt, so we need to implement this in every derived class... -auto ModrinthPage::shouldDisplay() const -> bool { return true; } +auto ModrinthModPage::shouldDisplay() const -> bool { return true; } diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.h similarity index 93% rename from launcher/ui/pages/modplatform/modrinth/ModrinthPage.h rename to launcher/ui/pages/modplatform/modrinth/ModrinthModPage.h index e3a0e1f00..b1e72bfea 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.h @@ -40,12 +40,12 @@ #include "modplatform/modrinth/ModrinthAPI.h" -class ModrinthPage : public ModPage { +class ModrinthModPage : public ModPage { Q_OBJECT public: - explicit ModrinthPage(ModDownloadDialog* dialog, BaseInstance* instance); - ~ModrinthPage() override = default; + explicit ModrinthModPage(ModDownloadDialog* dialog, BaseInstance* instance); + ~ModrinthModPage() override = default; inline auto displayName() const -> QString override { return "Modrinth"; } inline auto icon() const -> QIcon override { return APPLICATION->getThemedIcon("modrinth"); } From db038463581400005f045a277a249ab07175ab2b Mon Sep 17 00:00:00 2001 From: kb1000 Date: Mon, 31 Jan 2022 15:25:36 +0100 Subject: [PATCH 070/157] Add support for importing Modrinth packs from files --- launcher/CMakeLists.txt | 10 ++ launcher/InstanceImportTask.cpp | 170 +++++++++++++++++- launcher/InstanceImportTask.h | 6 +- .../modrinth/ModrinthPackManifest.cpp | 16 ++ .../modrinth/ModrinthPackManifest.h | 32 ++++ launcher/resources/multimc/multimc.qrc | 3 + .../resources/multimc/scalable/modrinth.svg | 4 + launcher/ui/dialogs/NewInstanceDialog.cpp | 2 + launcher/ui/pages/modplatform/ImportPage.cpp | 4 +- .../modplatform/modrinth/ModrinthPage.cpp | 55 ++++++ .../pages/modplatform/modrinth/ModrinthPage.h | 62 +++++++ .../modplatform/modrinth/ModrinthPage.ui | 94 ++++++++++ 12 files changed, 452 insertions(+), 6 deletions(-) create mode 100644 launcher/modplatform/modrinth/ModrinthPackManifest.cpp create mode 100644 launcher/modplatform/modrinth/ModrinthPackManifest.h create mode 100644 launcher/resources/multimc/scalable/modrinth.svg create mode 100644 launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp create mode 100644 launcher/ui/pages/modplatform/modrinth/ModrinthPage.h create mode 100644 launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 16ec4c047..cbe135e2b 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -563,6 +563,11 @@ set(ATLAUNCHER_SOURCES modplatform/atlauncher/ATLShareCode.h ) +set(MODRINTH_SOURCES + modplatform/modrinth/ModrinthPackManifest.cpp + modplatform/modrinth/ModrinthPackManifest.h +) + add_unit_test(Index SOURCES meta/Index_test.cpp LIBS Launcher_logic @@ -596,6 +601,7 @@ set(LOGIC_SOURCES ${MODPACKSCH_SOURCES} ${TECHNIC_SOURCES} ${ATLAUNCHER_SOURCES} + ${MODRINTH_SOURCES} ) SET(LAUNCHER_SOURCES @@ -774,6 +780,9 @@ SET(LAUNCHER_SOURCES ui/pages/modplatform/flame/FlameModPage.cpp ui/pages/modplatform/flame/FlameModPage.h + ui/pages/modplatform/modrinth/ModrinthPage.cpp + ui/pages/modplatform/modrinth/ModrinthPage.h + ui/pages/modplatform/technic/TechnicModel.cpp ui/pages/modplatform/technic/TechnicModel.h ui/pages/modplatform/technic/TechnicPage.cpp @@ -908,6 +917,7 @@ qt5_wrap_ui(LAUNCHER_UI ui/pages/modplatform/legacy_ftb/Page.ui ui/pages/modplatform/ImportPage.ui ui/pages/modplatform/ftb/FtbPage.ui + ui/pages/modplatform/modrinth/ModrinthPage.ui ui/pages/modplatform/technic/TechnicPage.ui ui/widgets/InstanceCardWidget.ui ui/widgets/CustomCommands.ui diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index 1a13c9973..517155811 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -30,10 +30,15 @@ #include "modplatform/flame/PackManifest.h" #include "Json.h" #include +#include "modplatform/modrinth/ModrinthPackManifest.h" #include "modplatform/technic/TechnicPackProcessor.h" #include "icons/IconList.h" #include "Application.h" +#include "net/ChecksumValidator.h" + +#include +#include InstanceImportTask::InstanceImportTask(const QUrl sourceUrl) { @@ -109,6 +114,7 @@ void InstanceImportTask::processZipPack() QString mmcFound = MMCZip::findFolderOfFileInZip(m_packZip.get(), "instance.cfg"); bool technicFound = QuaZipDir(m_packZip.get()).exists("/bin/modpack.jar") || QuaZipDir(m_packZip.get()).exists("/bin/version.json"); QString flameFound = MMCZip::findFolderOfFileInZip(m_packZip.get(), "manifest.json"); + QString modrinthFound = MMCZip::findFolderOfFileInZip(m_packZip.get(), "modrinth.index.json"); QString root; if(!mmcFound.isNull()) { @@ -132,6 +138,13 @@ void InstanceImportTask::processZipPack() root = flameFound; m_modpackType = ModpackType::Flame; } + else if(!modrinthFound.isNull()) + { + // process as Modrinth pack + qDebug() << "Modrinth:" << modrinthFound; + root = modrinthFound; + m_modpackType = ModpackType::Modrinth; + } if(m_modpackType == ModpackType::Unknown) { emitFailed(tr("Archive does not contain a recognized modpack type.")); @@ -188,15 +201,18 @@ void InstanceImportTask::extractFinished() switch(m_modpackType) { - case ModpackType::Flame: - processFlame(); - return; case ModpackType::MultiMC: processMultiMC(); return; case ModpackType::Technic: processTechnic(); return; + case ModpackType::Flame: + processFlame(); + return; + case ModpackType::Modrinth: + processModrinth(); + return; case ModpackType::Unknown: emitFailed(tr("Archive does not contain a recognized modpack type.")); return; @@ -461,3 +477,151 @@ void InstanceImportTask::processMultiMC() } emitSucceeded(); } + +void InstanceImportTask::processModrinth() { + std::vector files; + QString minecraftVersion, fabricVersion, forgeVersion; + try + { + QString indexPath = FS::PathCombine(m_stagingPath, "modrinth.index.json"); + auto doc = Json::requireDocument(indexPath); + auto obj = Json::requireObject(doc, "modrinth.index.json"); + int formatVersion = Json::requireInteger(obj, "formatVersion", "modrinth.index.json"); + if (formatVersion == 1) + { + auto game = Json::requireString(obj, "game", "modrinth.index.json"); + if (game != "minecraft") + { + throw JSONValidationError("Unknown game: " + game); + } + + auto jsonFiles = Json::requireIsArrayOf(obj, "files", "modrinth.index.json"); + std::transform(jsonFiles.begin(), jsonFiles.end(), std::back_inserter(files), [](const QJsonObject& obj) + { + Modrinth::File file; + file.path = Json::requireString(obj, "path"); + QString supported = Json::ensureString(Json::ensureObject(obj, "env")); + QJsonObject hashes = Json::requireObject(obj, "hashes"); + QString hash; + QCryptographicHash::Algorithm hashAlgorithm; + hash = Json::ensureString(hashes, "sha256"); + hashAlgorithm = QCryptographicHash::Sha256; + if (hash.isEmpty()) + { + hash = Json::ensureString(hashes, "sha512"); + hashAlgorithm = QCryptographicHash::Sha512; + if (hash.isEmpty()) + { + hash = Json::ensureString(hashes, "sha1"); + hashAlgorithm = QCryptographicHash::Sha1; + if (hash.isEmpty()) + { + throw JSONValidationError("No hash found for: " + file.path); + } + } + } + file.hash = QByteArray::fromHex(hash.toLatin1()); + file.hashAlgorithm = hashAlgorithm; + // Do not use requireUrl, which uses StrictMode, instead use QUrl's default TolerantMode (as Modrinth seems to incorrectly handle spaces) + file.download = Json::requireString(Json::ensureArray(obj, "downloads").first(), "Download URL for " + file.path); + if (!file.download.isValid()) + { + throw JSONValidationError("Download URL for " + file.path + " is not a correctly formatted URL"); + } + return file; + }); + + auto dependencies = Json::requireObject(obj, "dependencies", "modrinth.index.json"); + for (auto it = dependencies.begin(), end = dependencies.end(); it != end; ++it) + { + QString name = it.key(); + if (name == "minecraft") + { + if (!minecraftVersion.isEmpty()) + throw JSONValidationError("Duplicate Minecraft version"); + minecraftVersion = Json::requireString(*it, "Minecraft version"); + } + else if (name == "fabric-loader") + { + if (!fabricVersion.isEmpty()) + throw JSONValidationError("Duplicate Fabric Loader version"); + fabricVersion = Json::requireString(*it, "Fabric Loader version"); + } + else if (name == "forge") + { + if (!forgeVersion.isEmpty()) + throw JSONValidationError("Duplicate Forge version"); + forgeVersion = Json::requireString(*it, "Forge version"); + } + else + { + throw JSONValidationError("Unknown dependency type: " + name); + } + } + } + else + { + throw JSONValidationError(QStringLiteral("Unknown format version: %s").arg(formatVersion)); + } + QFile::remove(indexPath); + } + catch (const JSONValidationError &e) + { + emitFailed(tr("Could not understand pack index:\n") + e.cause()); + return; + } + QString overridePath = FS::PathCombine(m_stagingPath, "overrides"); + if (QFile::exists(overridePath)) { + QString mcPath = FS::PathCombine(m_stagingPath, ".minecraft"); + if (!QFile::rename(overridePath, mcPath)) { + emitFailed(tr("Could not rename the overrides folder:\n") + "overrides"); + return; + } + } + + QString configPath = FS::PathCombine(m_stagingPath, "instance.cfg"); + auto instanceSettings = std::make_shared(configPath); + instanceSettings->registerSetting("InstanceType", "Legacy"); + instanceSettings->set("InstanceType", "OneSix"); + MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath); + auto components = instance.getPackProfile(); + components->buildingFromScratch(); + components->setComponentVersion("net.minecraft", minecraftVersion, true); + if (!fabricVersion.isEmpty()) + components->setComponentVersion("net.fabricmc.fabric-loader", fabricVersion, true); + if (!forgeVersion.isEmpty()) + components->setComponentVersion("net.minecraftforge", forgeVersion, true); + if (m_instIcon != "default") + { + instance.setIconKey(m_instIcon); + } + instance.setName(m_instName); + instance.saveNow(); + + m_filesNetJob = new NetJob(tr("Mod download"), APPLICATION->network()); + for (auto &file : files) + { + auto path = FS::PathCombine(m_stagingPath, ".minecraft", file.path); + qDebug() << "Will download" << file.download << "to" << path; + auto dl = Net::Download::makeFile(file.download, path); + dl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash)); + m_filesNetJob->addNetAction(dl); + } + connect(m_filesNetJob.get(), &NetJob::succeeded, this, [&]() + { + m_filesNetJob.reset(); + emitSucceeded(); + } + ); + connect(m_filesNetJob.get(), &NetJob::failed, [&](const QString &reason) + { + m_filesNetJob.reset(); + emitFailed(reason); + }); + connect(m_filesNetJob.get(), &NetJob::progress, [&](qint64 current, qint64 total) + { + setProgress(current, total); + }); + setStatus(tr("Downloading mods...")); + m_filesNetJob->start(); +} diff --git a/launcher/InstanceImportTask.h b/launcher/InstanceImportTask.h index 365c3dc47..317562d91 100644 --- a/launcher/InstanceImportTask.h +++ b/launcher/InstanceImportTask.h @@ -47,8 +47,9 @@ class InstanceImportTask : public InstanceTask private: void processZipPack(); void processMultiMC(); - void processFlame(); void processTechnic(); + void processFlame(); + void processModrinth(); private slots: void downloadSucceeded(); @@ -69,7 +70,8 @@ private slots: enum class ModpackType{ Unknown, MultiMC, + Technic, Flame, - Technic + Modrinth, } m_modpackType = ModpackType::Unknown; }; diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp new file mode 100644 index 000000000..2100aaf91 --- /dev/null +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp @@ -0,0 +1,16 @@ +/* Copyright 2022 kb1000 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ModrinthPackManifest.h" diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.h b/launcher/modplatform/modrinth/ModrinthPackManifest.h new file mode 100644 index 000000000..9742aeb21 --- /dev/null +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.h @@ -0,0 +1,32 @@ +/* Copyright 2022 kb1000 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include + +namespace Modrinth { +struct File +{ + QString path; + QCryptographicHash::Algorithm hashAlgorithm; + QByteArray hash; + // TODO: should this support multiple download URLs, like the JSON does? + QUrl download; +}; +} diff --git a/launcher/resources/multimc/multimc.qrc b/launcher/resources/multimc/multimc.qrc index 0fe673ff5..1671093d7 100644 --- a/launcher/resources/multimc/multimc.qrc +++ b/launcher/resources/multimc/multimc.qrc @@ -20,6 +20,9 @@ scalable/atlauncher.svg scalable/atlauncher-placeholder.png + + scalable/modrinth.svg + scalable/proxy.svg diff --git a/launcher/resources/multimc/scalable/modrinth.svg b/launcher/resources/multimc/scalable/modrinth.svg new file mode 100644 index 000000000..32715f5ce --- /dev/null +++ b/launcher/resources/multimc/scalable/modrinth.svg @@ -0,0 +1,4 @@ + + + + diff --git a/launcher/ui/dialogs/NewInstanceDialog.cpp b/launcher/ui/dialogs/NewInstanceDialog.cpp index b402839cf..05ea091de 100644 --- a/launcher/ui/dialogs/NewInstanceDialog.cpp +++ b/launcher/ui/dialogs/NewInstanceDialog.cpp @@ -39,6 +39,7 @@ #include "ui/pages/modplatform/legacy_ftb/Page.h" #include "ui/pages/modplatform/flame/FlamePage.h" #include "ui/pages/modplatform/ImportPage.h" +#include "ui/pages/modplatform/modrinth/ModrinthPage.h" #include "ui/pages/modplatform/technic/TechnicPage.h" @@ -134,6 +135,7 @@ QList NewInstanceDialog::getPages() flamePage, new FtbPage(this), new LegacyFTB::Page(this), + new ModrinthPage(this), technicPage }; } diff --git a/launcher/ui/pages/modplatform/ImportPage.cpp b/launcher/ui/pages/modplatform/ImportPage.cpp index 1b53dd402..8ae38f8dd 100644 --- a/launcher/ui/pages/modplatform/ImportPage.cpp +++ b/launcher/ui/pages/modplatform/ImportPage.cpp @@ -109,7 +109,8 @@ void ImportPage::updateState() { // FIXME: actually do some validation of what's inside here... this is fake AF QFileInfo fi(input); - if(fi.exists() && fi.suffix() == "zip") + // mrpack is a modrinth pack + if(fi.exists() && (fi.suffix() == "zip" || fi.suffix() == "mrpack")) { QFileInfo fi(url.fileName()); dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url)); @@ -143,6 +144,7 @@ void ImportPage::setUrl(const QString& url) void ImportPage::on_modpackBtn_clicked() { + // TODO: Add .mrpack filter auto filter = QMimeDatabase().mimeTypeForName("application/zip").filterString(); const QUrl url = QFileDialog::getOpenFileUrl(this, tr("Choose modpack"), modpackUrl(), filter); if (url.isValid()) diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp new file mode 100644 index 000000000..93b1ca027 --- /dev/null +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -0,0 +1,55 @@ +/* + * Copyright 2013-2021 MultiMC Contributors + * Copyright 2021-2022 kb1000 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ModrinthPage.h" + +#include "ui_ModrinthPage.h" + +#include + +ModrinthPage::ModrinthPage(NewInstanceDialog *dialog, QWidget *parent) : QWidget(parent), ui(new Ui::ModrinthPage), dialog(dialog) +{ + ui->setupUi(this); +} + +ModrinthPage::~ModrinthPage() +{ + delete ui; +} + +void ModrinthPage::openedImpl() +{ + BasePage::openedImpl(); + triggerSearch(); +} + +bool ModrinthPage::eventFilter(QObject *watched, QEvent *event) +{ + if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) { + auto *keyEvent = reinterpret_cast(event); + if (keyEvent->key() == Qt::Key_Return) { + this->triggerSearch(); + keyEvent->accept(); + return true; + } + } + return QObject::eventFilter(watched, event); +} + +void ModrinthPage::triggerSearch() { + +} diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h new file mode 100644 index 000000000..6c75b60dd --- /dev/null +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h @@ -0,0 +1,62 @@ +/* + * Copyright 2013-2021 MultiMC Contributors + * Copyright 2021-2022 kb1000 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "Application.h" +#include "ui/dialogs/NewInstanceDialog.h" +#include "ui/pages/BasePage.h" + +#include + +namespace Ui +{ + class ModrinthPage; +} + +class ModrinthPage : public QWidget, public BasePage +{ + Q_OBJECT + +public: + explicit ModrinthPage(NewInstanceDialog *dialog, QWidget *parent = nullptr); + ~ModrinthPage() override; + + QString displayName() const override + { + return tr("Modrinth"); + } + QIcon icon() const override + { + return APPLICATION->getThemedIcon("modrinth"); + } + QString id() const override + { + return "modrinth"; + } + + void openedImpl() override; + + bool eventFilter(QObject *watched, QEvent *event) override; + +private slots: + void triggerSearch(); + +private: + Ui::ModrinthPage *ui; + NewInstanceDialog *dialog; +}; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui new file mode 100644 index 000000000..7ef099d34 --- /dev/null +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui @@ -0,0 +1,94 @@ + + + ModrinthPage + + + + 0 + 0 + 837 + 685 + + + + + + + + + Search and filter ... + + + + + + + Search + + + + + + + + + + + Qt::ScrollBarAlwaysOff + + + true + + + + 48 + 48 + + + + + + + + true + + + true + + + + + + + + + + + + + + Version selected: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + + searchEdit + searchButton + packView + packDescription + sortByBox + versionSelectionBox + + + + From 31988f0529f6c316d6a9ba3e66cf981a807ed710 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sat, 14 May 2022 19:56:38 +0200 Subject: [PATCH 071/157] fix: adapt upstream Modrinth code to our codebase --- launcher/CMakeLists.txt | 8 +-- launcher/InstanceImportTask.cpp | 2 - .../multimc/128x128/instances/modrinth.png | Bin 10575 -> 0 bytes .../multimc/32x32/instances/modrinth.png | Bin 1913 -> 0 bytes launcher/resources/multimc/multimc.qrc | 3 -- launcher/ui/pages/modplatform/ImportPage.cpp | 2 +- .../modplatform/modrinth/ModrinthPage.cpp | 45 ++++++++++++----- .../pages/modplatform/modrinth/ModrinthPage.h | 46 +++++++++++++----- 8 files changed, 72 insertions(+), 34 deletions(-) delete mode 100644 launcher/resources/multimc/128x128/instances/modrinth.png delete mode 100644 launcher/resources/multimc/32x32/instances/modrinth.png diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index cbe135e2b..7984d3c98 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -532,6 +532,8 @@ set(FLAME_SOURCES set(MODRINTH_SOURCES modplatform/modrinth/ModrinthPackIndex.cpp modplatform/modrinth/ModrinthPackIndex.h + modplatform/modrinth/ModrinthPackManifest.cpp + modplatform/modrinth/ModrinthPackManifest.h ) set(MODPACKSCH_SOURCES @@ -563,11 +565,6 @@ set(ATLAUNCHER_SOURCES modplatform/atlauncher/ATLShareCode.h ) -set(MODRINTH_SOURCES - modplatform/modrinth/ModrinthPackManifest.cpp - modplatform/modrinth/ModrinthPackManifest.h -) - add_unit_test(Index SOURCES meta/Index_test.cpp LIBS Launcher_logic @@ -601,7 +598,6 @@ set(LOGIC_SOURCES ${MODPACKSCH_SOURCES} ${TECHNIC_SOURCES} ${ATLAUNCHER_SOURCES} - ${MODRINTH_SOURCES} ) SET(LAUNCHER_SOURCES diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index 517155811..ec0f58e08 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -581,8 +581,6 @@ void InstanceImportTask::processModrinth() { QString configPath = FS::PathCombine(m_stagingPath, "instance.cfg"); auto instanceSettings = std::make_shared(configPath); - instanceSettings->registerSetting("InstanceType", "Legacy"); - instanceSettings->set("InstanceType", "OneSix"); MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath); auto components = instance.getPackProfile(); components->buildingFromScratch(); diff --git a/launcher/resources/multimc/128x128/instances/modrinth.png b/launcher/resources/multimc/128x128/instances/modrinth.png deleted file mode 100644 index 740bc8f02469f108db79d92d05484aff17bf8801..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10575 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 diff --git a/launcher/resources/multimc/32x32/instances/modrinth.png b/launcher/resources/multimc/32x32/instances/modrinth.png deleted file mode 100644 index 025ed06534234458f6de2456868d0a66b4324f3b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1913 zcmV-<2Zs2GP)3&289m><@6E99O-OKpvFD)x&4M7Lr726LBnr*iX251# zY=ak;1huGYRobdkkfK&qNs2(JcO2sdCV_^irMOE#t=fh*Qq(0R_ShaEPN9Gj2W$^B z@7>cMGh@bV32pwh_t(7p&Uepw-}l`+=K}xhLv92d+zYKg4BTbevcLedXgCD~m6EfB zIhIVQqjTQt{A)N2^N9*5teQCmezRUHStCkrzaG{Y#z@wnKk-7#`V;T}w*sW7F@FP; z-XemDnMso8VDNV+ISo+J$gQIIX%QKhg*i*tY;G>?s`-xu1b5CE;~SIQtu!7_FW7Gk zYLPHa$?;upMwY`Hh7Zs2>1hM^DZegLA_XGi13=>;k~lX|64~_b;Kvpa+&O2Ae_ZmA z2o{2*F?iV|+^1X1drl7nhM^D!0Wf=df!6xPhRS!rwm02;Jq z$P4T8?pK!edWM{aJv4aEb4^*Kvik%8P)r^GD3J#(ZU1>??aT!L&>M?>l?Hzh5m8z_ z8?4QlbWH(btEWX|JjoADTs8v0it4HbTm#y8&{xJxU}r$MxJb=PkJprAf~p=GOIipW@!?YqCy z`49kv8l8Pg%ag`1=5%-FXAY|&0V`?(pYe@Uew z7ffzjS@&v}j0*APET3rrY~`;74_jA1O5cFxLRt;~;>RUK^r+A_~e!b>cmcNzeI zcq=7cof?WE4z?6WpS^^KH8m%1@qpMz53G6TJPh7~256+exwzvg04!>9iU8yq!zou} zAQ*(6R{=X1gvK>SWOeimDX*oHf`!5Qz^4G9us*OjC(r4%M_9ieYkRA%6%VF+PA>J{ zkO1GZ!1h_6cP{uIarULsc-RHdn+}*=P@jKWsL^>@gbx9Lh)nj|(jIDbnzbzlM9EF5 z?Tf?dm1n1LJc&z3^(_FPM9yS*he`mTGu0@Z4?-Xyj2wYN0E%lYrJfOyQ9!EeA-G9I z?h%c90Dz=xl3dkV5&g%KFf1i_A~laAsS@RmATUAzh~VujG5{BU0szr+Yhmq-J5xla zEB&=oL}V1nJ~zp4SY9$0l$X-P2^#*w^RTLAb@UYgm^R%hgOb|-z)QH1R7PX*(Szgi z=$gmff#jhK?X?b~CJIMsg)adjX(Afed#a_hdmjJU)cQ(|84w8N8FZPVfZ;l!qcej`D9wqU< zh>TaZs`JO4&Y~A*ZVNT!mlxDK_W=M-1tw{Q9T{?ykT15C#$pd|5BM!xS1TnsqEKiv zKFkvg6VO`TbK1E0UMe@Dgz#5azL9KQ)BSr~^cENouK|E4nIQ^aQ?{Vy6J$;q3wcr14Uvv=jq?@vyhKxMRh2dtj)Z z*_I4|A=?viwKh`zPR}r4IGw@Enya8TFxU308VKeAfH6FrI6JuPYQFGBOliW9ZRR2x zyz}wbFJCNJl{a2%eZS>HB?QYvM3W>8U+Rkwlw8dZUROZoeP~x;y=b-dGFv_fqQiLn zmxzccOoJdGB6b$$obk-&%PrWA3IG5+x#Nz>BgVKJKJLXt$QttrT}+0J;9h8TA#k@P zvVaPAWUgk~QfKIS$6RBM&N|uiw;Q>d{eS!$l(>|MSu=Nq00000NkvXXu0mjfgB*o# diff --git a/launcher/resources/multimc/multimc.qrc b/launcher/resources/multimc/multimc.qrc index 1671093d7..86ebf753c 100644 --- a/launcher/resources/multimc/multimc.qrc +++ b/launcher/resources/multimc/multimc.qrc @@ -275,9 +275,6 @@ 32x32/instances/flame.png 128x128/instances/flame.png - 32x32/instances/modrinth.png - 128x128/instances/modrinth.png - 32x32/instances/gear.png 128x128/instances/gear.png diff --git a/launcher/ui/pages/modplatform/ImportPage.cpp b/launcher/ui/pages/modplatform/ImportPage.cpp index 8ae38f8dd..3b65de9d4 100644 --- a/launcher/ui/pages/modplatform/ImportPage.cpp +++ b/launcher/ui/pages/modplatform/ImportPage.cpp @@ -144,8 +144,8 @@ void ImportPage::setUrl(const QString& url) void ImportPage::on_modpackBtn_clicked() { - // TODO: Add .mrpack filter auto filter = QMimeDatabase().mimeTypeForName("application/zip").filterString(); + filter += ";;" + tr("Modrinth pack (*.mrpack)"); const QUrl url = QFileDialog::getOpenFileUrl(this, tr("Choose modpack"), modpackUrl(), filter); if (url.isValid()) { diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index 93b1ca027..0d65ef166 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -1,18 +1,36 @@ +// SPDX-License-Identifier: GPL-3.0-only /* - * Copyright 2013-2021 MultiMC Contributors - * Copyright 2021-2022 kb1000 + * PolyMC - Minecraft Launcher * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * Copyright 2021-2022 kb1000 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #include "ModrinthPage.h" @@ -31,6 +49,11 @@ ModrinthPage::~ModrinthPage() delete ui; } +void ModrinthPage::retranslate() +{ + ui->retranslateUi(this); +} + void ModrinthPage::openedImpl() { BasePage::openedImpl(); diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h index 6c75b60dd..562049b48 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h @@ -1,18 +1,36 @@ +// SPDX-License-Identifier: GPL-3.0-only /* - * Copyright 2013-2021 MultiMC Contributors - * Copyright 2021-2022 kb1000 + * PolyMC - Minecraft Launcher * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * Copyright 2021-2022 kb1000 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #pragma once @@ -49,6 +67,12 @@ class ModrinthPage : public QWidget, public BasePage return "modrinth"; } + virtual QString helpPage() const override + { + return "Modrinth-platform"; + } + void retranslate() override; + void openedImpl() override; bool eventFilter(QObject *watched, QEvent *event) override; From 4fda35b466e4e3f242955cf8cb692a10e8820f0b Mon Sep 17 00:00:00 2001 From: flow Date: Sat, 14 May 2022 20:17:05 -0300 Subject: [PATCH 072/157] feat: add modrinth pack downloading Things that don't work / work poorly (there's more for sure but those are the evident ones): - Icons are broken in the import dialog - No way to search for private packs - Icons are not downloaded when downloading a mod - No support for multiple download URLs - Probably a lot more... --- launcher/CMakeLists.txt | 2 + .../modrinth/ModrinthPackManifest.cpp | 84 ++++++ .../modrinth/ModrinthPackManifest.h | 50 ++++ .../modplatform/modrinth/ModrinthModel.cpp | 274 ++++++++++++++++++ .../modplatform/modrinth/ModrinthModel.h | 81 ++++++ .../modplatform/modrinth/ModrinthPage.cpp | 207 ++++++++++++- .../pages/modplatform/modrinth/ModrinthPage.h | 67 +++-- 7 files changed, 729 insertions(+), 36 deletions(-) create mode 100644 launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp create mode 100644 launcher/ui/pages/modplatform/modrinth/ModrinthModel.h diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 7984d3c98..8e75be204 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -778,6 +778,8 @@ SET(LAUNCHER_SOURCES ui/pages/modplatform/modrinth/ModrinthPage.cpp ui/pages/modplatform/modrinth/ModrinthPage.h + ui/pages/modplatform/modrinth/ModrinthModel.cpp + ui/pages/modplatform/modrinth/ModrinthModel.h ui/pages/modplatform/technic/TechnicModel.cpp ui/pages/modplatform/technic/TechnicModel.h diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp index 2100aaf91..4dcd2fd49 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp @@ -14,3 +14,87 @@ */ #include "ModrinthPackManifest.h" +#include "Json.h" + +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" + +namespace Modrinth { + +void loadIndexedPack(Modpack& pack, QJsonObject& obj) +{ + pack.id = Json::ensureString(obj, "project_id"); + + pack.name = Json::ensureString(obj, "title"); + pack.description = Json::ensureString(obj, "description"); + pack.authors << Json::ensureString(obj, "author"); + pack.iconName = QString("modrinth_%1").arg(Json::ensureString(obj, "slug")); + pack.iconUrl = Json::ensureString(obj, "icon_url"); +} + +void loadIndexedInfo(Modpack& pack, QJsonObject& obj) +{ + pack.extra.body = Json::ensureString(obj, "body"); + pack.extra.sourceUrl = Json::ensureString(obj, "source_url"); + pack.extra.wikiUrl = Json::ensureString(obj, "wiki_url"); + + pack.extraInfoLoaded = true; +} + +void loadIndexedVersions(Modpack& pack, QJsonDocument& doc) +{ + QVector unsortedVersions; + + auto arr = Json::requireArray(doc); + + for (auto versionIter : arr) { + auto obj = Json::requireObject(versionIter); + auto file = loadIndexedVersion(obj); + + if(!file.id.isEmpty()) // Heuristic to check if the returned value is valid + unsortedVersions.append(file); + } + auto orderSortPredicate = [](const ModpackVersion& a, const ModpackVersion& b) -> bool { + // dates are in RFC 3339 format + return a.date > b.date; + }; + + std::sort(unsortedVersions.begin(), unsortedVersions.end(), orderSortPredicate); + + pack.versions.swap(unsortedVersions); + + pack.versionsLoaded = true; +} + +auto loadIndexedVersion(QJsonObject &obj) -> ModpackVersion +{ + ModpackVersion file; + + file.name = Json::requireString(obj, "name"); + file.version = Json::requireString(obj, "version_number"); + + file.id = Json::requireString(obj, "id"); + file.project_id = Json::requireString(obj, "project_id"); + + file.date = Json::requireString(obj, "date_published"); + + auto files = Json::requireArray(obj, "files"); + + for (auto file_iter : files) { + File indexed_file; + auto parent = Json::requireObject(file_iter); + if (!Json::ensureBoolean(parent, "primary", false)) { + continue; + } + + file.download_url = Json::requireString(parent, "url"); + break; + } + + if(file.download_url.isEmpty()) + return {}; + + return file; +} + +} // namespace Modrinth diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.h b/launcher/modplatform/modrinth/ModrinthPackManifest.h index 9742aeb21..7dab893ca 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.h +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.h @@ -15,18 +15,68 @@ #pragma once +#include + #include #include #include #include +class MinecraftInstance; + namespace Modrinth { + struct File { QString path; + QCryptographicHash::Algorithm hashAlgorithm; QByteArray hash; // TODO: should this support multiple download URLs, like the JSON does? QUrl download; }; + +struct ModpackExtra { + QString body; + + QString sourceUrl; + QString wikiUrl; +}; + +struct ModpackVersion { + QString name; + QString version; + + QString id; + QString project_id; + + QString date; + + QString download_url; +}; + +struct Modpack { + QString id; + + QString name; + QString description; + QStringList authors; + QString iconName; + QUrl iconUrl; + + bool versionsLoaded = false; + bool extraInfoLoaded = false; + + ModpackExtra extra; + QVector versions; +}; + +void loadIndexedPack(Modpack&, QJsonObject&); +void loadIndexedInfo(Modpack&, QJsonObject&); +void loadIndexedVersions(Modpack&, QJsonDocument&); +auto loadIndexedVersion(QJsonObject&) -> ModpackVersion; + } + +Q_DECLARE_METATYPE(Modrinth::Modpack); +Q_DECLARE_METATYPE(Modrinth::ModpackVersion); diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp new file mode 100644 index 000000000..2890e27d5 --- /dev/null +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -0,0 +1,274 @@ +#include "ModrinthModel.h" + +#include "BuildConfig.h" +#include "Json.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" +#include "ui/dialogs/ModDownloadDialog.h" + +#include + +namespace Modrinth { + +ModpackListModel::ModpackListModel(ModrinthPage* parent) : QAbstractListModel(parent), m_parent(parent) {} + +auto ModpackListModel::debugName() const -> QString +{ + return m_parent->debugName(); +} + +/******** Make data requests ********/ + +void ModpackListModel::fetchMore(const QModelIndex& parent) +{ + if (parent.isValid()) + return; + if (nextSearchOffset == 0) { + qWarning() << "fetchMore with 0 offset is wrong..."; + return; + } + performPaginatedSearch(); +} + +auto ModpackListModel::data(const QModelIndex& index, int role) const -> QVariant +{ + int pos = index.row(); + if (pos >= modpacks.size() || pos < 0 || !index.isValid()) { + return QString("INVALID INDEX %1").arg(pos); + } + + Modrinth::Modpack pack = modpacks.at(pos); + if (role == Qt::DisplayRole) { + return pack.name; + } else if (role == Qt::ToolTipRole) { + if (pack.description.length() > 100) { + // some magic to prevent to long tooltips and replace html linebreaks + QString edit = pack.description.left(97); + edit = edit.left(edit.lastIndexOf("
")).left(edit.lastIndexOf(" ")).append("..."); + return edit; + } + return pack.description; + } else if (role == Qt::DecorationRole) { + // FIXME: help the icons dont have the same size ;-; + if (m_logoMap.contains(pack.iconName)) { + return (m_logoMap.value(pack.iconName)); + } + QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); + ((ModpackListModel*)this)->requestLogo(pack.iconName, pack.iconUrl.toString()); + return icon; + } else if (role == Qt::UserRole) { + QVariant v; + v.setValue(pack); + return v; + } + + return {}; +} + +/* +void ModpackListModel::requestModVersions(ModPlatform::IndexedPack const& current) +{ + auto profile = (dynamic_cast((dynamic_cast(parent()))->m_instance))->getPackProfile(); + + m_parent->apiProvider()->getVersions(this, { current.addonId.toString(), getMineVersions(), profile->getModLoader() }); +}*/ + +void ModpackListModel::performPaginatedSearch() +{ + // TODO: Move to standalone API + NetJob* netJob = new NetJob("Modrinth::SearchModpack", APPLICATION->network()); + auto searchAllUrl = QString( + "https://staging-api.modrinth.com/v2/search?" + "query=%1&" + "facets=[[\"project_type:modpack\"]]") + .arg(currentSearchTerm); + + netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchAllUrl), &m_all_response)); + + QObject::connect(netJob, &NetJob::succeeded, this, [this] { + QJsonParseError parse_error_all {}; + + QJsonDocument doc_all = QJsonDocument::fromJson(m_all_response, &parse_error_all); + if (parse_error_all.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from " << debugName() << " at " << parse_error_all.offset + << " reason: " << parse_error_all.errorString(); + qWarning() << m_all_response; + return; + } + + searchRequestFinished(doc_all); + }); + QObject::connect(netJob, &NetJob::failed, this, &ModpackListModel::searchRequestFailed); + + jobPtr = netJob; + jobPtr->start(); +} + +void ModpackListModel::refresh() +{ + if (jobPtr) { + jobPtr->abort(); + searchState = ResetRequested; + return; + } else { + beginResetModel(); + modpacks.clear(); + endResetModel(); + searchState = None; + } + nextSearchOffset = 0; + performPaginatedSearch(); +} + +void ModpackListModel::searchWithTerm(const QString& term, const int sort) +{ + if (currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort) { + return; + } + + currentSearchTerm = term; + currentSort = sort; + + refresh(); +} + +void ModpackListModel::getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback) +{ + if (m_logoMap.contains(logo)) { + callback(APPLICATION->metacache() + ->resolveEntry(m_parent->metaEntryBase(), QString("logos/%1").arg(logo.section(".", 0, 0))) + ->getFullPath()); + } else { + requestLogo(logo, logoUrl); + } +} + +void ModpackListModel::requestLogo(QString logo, QString url) +{ + if (m_loadingLogos.contains(logo) || m_failedLogos.contains(logo)) { + return; + } + + MetaEntryPtr entry = + APPLICATION->metacache()->resolveEntry(m_parent->metaEntryBase(), QString("logos/%1").arg(logo.section(".", 0, 0))); + auto job = new NetJob(QString("%1 Icon Download %2").arg(m_parent->debugName()).arg(logo), APPLICATION->network()); + job->addNetAction(Net::Download::makeCached(QUrl(url), entry)); + + auto fullPath = entry->getFullPath(); + QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] { + job->deleteLater(); + emit logoLoaded(logo, QIcon(fullPath)); + if (waitingCallbacks.contains(logo)) { + waitingCallbacks.value(logo)(fullPath); + } + }); + + QObject::connect(job, &NetJob::failed, this, [this, logo, job] { + job->deleteLater(); + emit logoFailed(logo); + }); + + job->start(); + m_loadingLogos.append(logo); +} + +/******** Request callbacks ********/ + +void ModpackListModel::logoLoaded(QString logo, QIcon out) +{ + m_loadingLogos.removeAll(logo); + m_logoMap.insert(logo, out); + for (int i = 0; i < modpacks.size(); i++) { + if (modpacks[i].iconName == logo) { + emit dataChanged(createIndex(i, 0), createIndex(i, 0), { Qt::DecorationRole }); + } + } +} + +void ModpackListModel::logoFailed(QString logo) +{ + m_failedLogos.append(logo); + m_loadingLogos.removeAll(logo); +} + +void ModpackListModel::searchRequestFinished(QJsonDocument& doc_all) +{ + jobPtr.reset(); + + QList newList; + + auto packs_all = doc_all.object().value("hits").toArray(); + for (auto packRaw : packs_all) { + auto packObj = packRaw.toObject(); + + Modrinth::Modpack pack; + try { + Modrinth::loadIndexedPack(pack, packObj); + newList.append(pack); + } catch (const JSONValidationError& e) { + qWarning() << "Error while loading mod from " << m_parent->debugName() << ": " << e.cause(); + continue; + } + } + + if (packs_all.size() < 25) { + searchState = Finished; + } else { + nextSearchOffset += 25; + searchState = CanPossiblyFetchMore; + } + + beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1); + modpacks.append(newList); + endInsertRows(); +} + +void ModpackListModel::searchRequestFailed(QString reason) +{ + if (!jobPtr->first()->m_reply) { + // Network error + QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load mods.")); + } else if (jobPtr->first()->m_reply && jobPtr->first()->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 409) { + // 409 Gone, notify user to update + QMessageBox::critical(nullptr, tr("Error"), + //: %1 refers to the launcher itself + QString("%1 %2") + .arg(m_parent->displayName()) + .arg(tr("API version too old!\nPlease update %1!").arg(BuildConfig.LAUNCHER_NAME))); + } + jobPtr.reset(); + + if (searchState == ResetRequested) { + beginResetModel(); + modpacks.clear(); + endResetModel(); + + nextSearchOffset = 0; + performPaginatedSearch(); + } else { + searchState = Finished; + } +} + +void ModpackListModel::versionRequestSucceeded(QJsonDocument doc, QString id) +{ + auto& current = m_parent->getCurrent(); + if (id != current.id) { + return; + } + + auto arr = doc.isObject() ? Json::ensureArray(doc.object(), "data") : doc.array(); + + try { + // loadIndexedPackVersions(current, arr); + } catch (const JSONValidationError& e) { + qDebug() << doc; + qWarning() << "Error while reading " << debugName() << " mod version: " << e.cause(); + } + + // m_parent->updateModVersions(); +} + +} // namespace Modrinth + +/******** Helpers ********/ diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h new file mode 100644 index 000000000..1fdbe278b --- /dev/null +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h @@ -0,0 +1,81 @@ +#pragma once + +#include + +#include "modplatform/modrinth/ModrinthPackManifest.h" +#include "ui/pages/modplatform/modrinth/ModrinthPage.h" + +class ModPage; +class Version; + +namespace Modrinth { + +using LogoMap = QMap; +using LogoCallback = std::function; + +class ModpackListModel : public QAbstractListModel { + Q_OBJECT + + public: + ModpackListModel(ModrinthPage* parent); + ~ModpackListModel() override = default; + + inline auto rowCount(const QModelIndex& parent) const -> int override { return modpacks.size(); }; + inline auto columnCount(const QModelIndex& parent) const -> int override { return 1; }; + inline auto flags(const QModelIndex& index) const -> Qt::ItemFlags override { return QAbstractListModel::flags(index); }; + + auto debugName() const -> QString; + + /* Retrieve information from the model at a given index with the given role */ + auto data(const QModelIndex& index, int role) const -> QVariant override; + + inline void setActiveJob(NetJob::Ptr ptr) { jobPtr = ptr; } + + /* Ask the API for more information */ + void fetchMore(const QModelIndex& parent) override; + void refresh(); + void searchWithTerm(const QString& term, const int sort); + + void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback); + + inline auto canFetchMore(const QModelIndex& parent) const -> bool override { return searchState == CanPossiblyFetchMore; }; + + public slots: + void searchRequestFinished(QJsonDocument& doc_all); + void searchRequestFailed(QString reason); + + void versionRequestSucceeded(QJsonDocument doc, QString addonId); + + protected slots: + + void logoFailed(QString logo); + void logoLoaded(QString logo, QIcon out); + + void performPaginatedSearch(); + + protected: + void requestLogo(QString file, QString url); + + inline auto getMineVersions() const -> std::list; + + protected: + ModrinthPage* m_parent; + + QList modpacks; + + LogoMap m_logoMap; + QMap waitingCallbacks; + QStringList m_failedLogos; + QStringList m_loadingLogos; + + QString currentSearchTerm; + int currentSort = 0; + int nextSearchOffset = 0; + enum SearchState { None, CanPossiblyFetchMore, ResetRequested, Finished } searchState = None; + + NetJob::Ptr jobPtr; + + QByteArray m_all_response; + QByteArray m_specific_response; +}; +} // namespace ModPlatform diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index 0d65ef166..688053166 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -34,14 +34,41 @@ */ #include "ModrinthPage.h" - #include "ui_ModrinthPage.h" +#include "ModrinthModel.h" + +#include "InstanceImportTask.h" +#include "Json.h" + +#include + +#include #include +#include -ModrinthPage::ModrinthPage(NewInstanceDialog *dialog, QWidget *parent) : QWidget(parent), ui(new Ui::ModrinthPage), dialog(dialog) +ModrinthPage::ModrinthPage(NewInstanceDialog* dialog, QWidget* parent) : QWidget(parent), ui(new Ui::ModrinthPage), dialog(dialog) { ui->setupUi(this); + + connect(ui->searchButton, &QPushButton::clicked, this, &ModrinthPage::triggerSearch); + ui->searchEdit->installEventFilter(this); + m_model = new Modrinth::ModpackListModel(this); + ui->packView->setModel(m_model); + + ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); + + ui->sortByBox->addItem(tr("Sort by Featured")); + ui->sortByBox->addItem(tr("Sort by Popularity")); + ui->sortByBox->addItem(tr("Sort by Last Updated")); + ui->sortByBox->addItem(tr("Sort by Name")); + ui->sortByBox->addItem(tr("Sort by Author")); + ui->sortByBox->addItem(tr("Sort by Total Downloads")); + + connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); + connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthPage::onSelectionChanged); + connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &ModrinthPage::onVersionSelectionChanged); } ModrinthPage::~ModrinthPage() @@ -60,10 +87,10 @@ void ModrinthPage::openedImpl() triggerSearch(); } -bool ModrinthPage::eventFilter(QObject *watched, QEvent *event) +bool ModrinthPage::eventFilter(QObject* watched, QEvent* event) { if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) { - auto *keyEvent = reinterpret_cast(event); + auto* keyEvent = reinterpret_cast(event); if (keyEvent->key() == Qt::Key_Return) { this->triggerSearch(); keyEvent->accept(); @@ -73,6 +100,176 @@ bool ModrinthPage::eventFilter(QObject *watched, QEvent *event) return QObject::eventFilter(watched, event); } -void ModrinthPage::triggerSearch() { +void ModrinthPage::onSelectionChanged(QModelIndex first, QModelIndex second) +{ + ui->versionSelectionBox->clear(); + + if (!first.isValid()) { + if (isOpened) { + dialog->setSuggestedPack(); + } + return; + } + + current = m_model->data(first, Qt::UserRole).value(); + auto name = current.name; + + if (!current.extraInfoLoaded) { + qDebug() << "Loading modrinth modpack information"; + + auto netJob = new NetJob(QString("Modrinth::PackInformation(%1)").arg(current.name), APPLICATION->network()); + auto response = new QByteArray(); + + QString id = current.id; + netJob->addNetAction(Net::Download::makeByteArray(QString("https://staging-api.modrinth.com/v2/project/%1").arg(id), response)); + + QObject::connect(netJob, &NetJob::succeeded, this, [this, response, id] { + if (id != current.id) { + return; // wrong request? + } + + QJsonParseError parse_error; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from Modrinth at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + return; + } + + auto obj = Json::requireObject(doc); + + try { + Modrinth::loadIndexedInfo(current, obj); + } catch (const JSONValidationError& e) { + qDebug() << *response; + qWarning() << "Error while reading modrinth modpack version: " << e.cause(); + } + + updateUI(); + suggestCurrent(); + }); + QObject::connect(netJob, &NetJob::finished, this, [response, netJob] { + netJob->deleteLater(); + delete response; + }); + netJob->start(); + } else + updateUI(); + + if (!current.versionsLoaded) { + qDebug() << "Loading modrinth modpack versions"; + + auto netJob = new NetJob(QString("Modrinth::PackVersions(%1)").arg(current.name), APPLICATION->network()); + auto response = new QByteArray(); + + QString id = current.id; + + netJob->addNetAction( + Net::Download::makeByteArray(QString("https://staging-api.modrinth.com/v2/project/%1/version").arg(id), response)); + + QObject::connect(netJob, &NetJob::succeeded, this, [this, response, id] { + if (id != current.id) { + return; // wrong request? + } + + QJsonParseError parse_error; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from Modrinth at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + return; + } + + try { + Modrinth::loadIndexedVersions(current, doc); + } catch (const JSONValidationError& e) { + qDebug() << *response; + qWarning() << "Error while reading modrinth modpack version: " << e.cause(); + } + + for (auto version : current.versions) { + ui->versionSelectionBox->addItem(version.version, QVariant(version.id)); + } + + updateVersionsUI(); + suggestCurrent(); + }); + QObject::connect(netJob, &NetJob::finished, this, [response, netJob] { + netJob->deleteLater(); + delete response; + }); + netJob->start(); + + } else { + for (auto version : current.versions) { + ui->versionSelectionBox->addItem(QString("%1 - %2").arg(version.name, version.version), QVariant(version.id)); + } + + suggestCurrent(); + } +} + +void ModrinthPage::updateUI() +{ + QString text = ""; + + if (current.extra.sourceUrl.isEmpty()) + text = current.name; + else + text = "
" + current.name + ""; + + if (!current.authors.empty()) { + // TODO: Implement multiple authors with links + text += "
" + tr(" by ") + current.authors.at(0); + } + + text += "
"; + + HoeDown h; + text += h.process(current.extra.body.toUtf8()); + + ui->packDescription->setHtml(text + current.description); +} + +void ModrinthPage::updateVersionsUI() +{ + // idk +} + +void ModrinthPage::suggestCurrent() +{ + if (!isOpened) { + return; + } + + if (selectedVersion.isEmpty()) { + dialog->setSuggestedPack(); + return; + } + + for (auto& ver : current.versions) { + if (ver.id == selectedVersion) { + dialog->setSuggestedPack(current.name, new InstanceImportTask(ver.download_url)); + + break; + } + } +} + +void ModrinthPage::triggerSearch() +{ + m_model->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex()); +} + +void ModrinthPage::onVersionSelectionChanged(QString data) +{ + if (data.isNull() || data.isEmpty()) { + selectedVersion = ""; + return; + } + selectedVersion = ui->versionSelectionBox->currentData().toString(); + suggestCurrent(); } diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h index 562049b48..f72a5071f 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h @@ -39,48 +39,53 @@ #include "ui/dialogs/NewInstanceDialog.h" #include "ui/pages/BasePage.h" +#include "modplatform/modrinth/ModrinthPackManifest.h" + #include -namespace Ui -{ - class ModrinthPage; +namespace Ui { +class ModrinthPage; +} + +namespace Modrinth { +class ModpackListModel; } -class ModrinthPage : public QWidget, public BasePage -{ +class ModrinthPage : public QWidget, public BasePage { Q_OBJECT -public: - explicit ModrinthPage(NewInstanceDialog *dialog, QWidget *parent = nullptr); + public: + explicit ModrinthPage(NewInstanceDialog* dialog, QWidget* parent = nullptr); ~ModrinthPage() override; - QString displayName() const override - { - return tr("Modrinth"); - } - QIcon icon() const override - { - return APPLICATION->getThemedIcon("modrinth"); - } - QString id() const override - { - return "modrinth"; - } - - virtual QString helpPage() const override - { - return "Modrinth-platform"; - } - void retranslate() override; + QString displayName() const override { return tr("Modrinth"); } + QIcon icon() const override { return APPLICATION->getThemedIcon("modrinth"); } + QString id() const override { return "modrinth"; } + QString helpPage() const override { return "Modrinth-platform"; } - void openedImpl() override; + inline auto debugName() const -> QString { return "Modrinth"; } + inline auto metaEntryBase() const -> QString { return "ModrinthModpacks"; }; - bool eventFilter(QObject *watched, QEvent *event) override; + auto getCurrent() -> Modrinth::Modpack& { return current; } + void suggestCurrent(); -private slots: + void updateUI(); + void updateVersionsUI(); + + void retranslate() override; + void openedImpl() override; + bool eventFilter(QObject* watched, QEvent* event) override; + + private slots: + void onSelectionChanged(QModelIndex first, QModelIndex second); + void onVersionSelectionChanged(QString data); void triggerSearch(); -private: - Ui::ModrinthPage *ui; - NewInstanceDialog *dialog; + private: + Ui::ModrinthPage* ui; + NewInstanceDialog* dialog; + Modrinth::ModpackListModel* m_model; + + Modrinth::Modpack current; + QString selectedVersion; }; From 9dd70ca9ae6fdab913a77467e803bf90ddd949ed Mon Sep 17 00:00:00 2001 From: flow Date: Sat, 14 May 2022 20:26:20 -0300 Subject: [PATCH 073/157] fix: download icon as well when importing modrinth modpacks --- launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp | 3 +++ launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui | 3 +++ 2 files changed, 6 insertions(+) diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index 688053166..b21fdf4a5 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -253,6 +253,9 @@ void ModrinthPage::suggestCurrent() for (auto& ver : current.versions) { if (ver.id == selectedVersion) { dialog->setSuggestedPack(current.name, new InstanceImportTask(ver.download_url)); + auto iconName = current.iconName; + m_model->getLogo(iconName, current.iconUrl.toString(), + [this, iconName](QString logo) { dialog->setSuggestedIconFromFile(logo, iconName); }); break; } diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui index 7ef099d34..8de53a693 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui @@ -45,6 +45,9 @@ 48 + + true + From 5ea8cec16f6dfbaeaca56ccf7f9151039a1dd145 Mon Sep 17 00:00:00 2001 From: flow Date: Sat, 14 May 2022 21:29:48 -0300 Subject: [PATCH 074/157] fix: make all modrinth modpacks have the same icon size --- .../modplatform/modrinth/ModrinthModel.cpp | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index 2890e27d5..121f5d4e4 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -49,9 +49,10 @@ auto ModpackListModel::data(const QModelIndex& index, int role) const -> QVarian } return pack.description; } else if (role == Qt::DecorationRole) { - // FIXME: help the icons dont have the same size ;-; if (m_logoMap.contains(pack.iconName)) { - return (m_logoMap.value(pack.iconName)); + return (m_logoMap.value(pack.iconName) + .pixmap(48, 48) + .scaled(48, 48, Qt::IgnoreAspectRatio, Qt::TransformationMode::SmoothTransformation)); } QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); ((ModpackListModel*)this)->requestLogo(pack.iconName, pack.iconUrl.toString()); @@ -65,14 +66,6 @@ auto ModpackListModel::data(const QModelIndex& index, int role) const -> QVarian return {}; } -/* -void ModpackListModel::requestModVersions(ModPlatform::IndexedPack const& current) -{ - auto profile = (dynamic_cast((dynamic_cast(parent()))->m_instance))->getPackProfile(); - - m_parent->apiProvider()->getVersions(this, { current.addonId.toString(), getMineVersions(), profile->getModLoader() }); -}*/ - void ModpackListModel::performPaginatedSearch() { // TODO: Move to standalone API @@ -86,7 +79,7 @@ void ModpackListModel::performPaginatedSearch() netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchAllUrl), &m_all_response)); QObject::connect(netJob, &NetJob::succeeded, this, [this] { - QJsonParseError parse_error_all {}; + QJsonParseError parse_error_all{}; QJsonDocument doc_all = QJsonDocument::fromJson(m_all_response, &parse_error_all); if (parse_error_all.error != QJsonParseError::NoError) { @@ -210,7 +203,7 @@ void ModpackListModel::searchRequestFinished(QJsonDocument& doc_all) continue; } } - + if (packs_all.size() < 25) { searchState = Finished; } else { From 9899a0e098e5cfb76a754fa9da2f73be46cc880a Mon Sep 17 00:00:00 2001 From: flow Date: Sat, 14 May 2022 21:47:35 -0300 Subject: [PATCH 075/157] fix: Have the URL be the project URL itself (I think, doesn't seem to work for the waffle though, probably because of the staging API :/) --- launcher/modplatform/modrinth/ModrinthPackManifest.cpp | 1 + launcher/modplatform/modrinth/ModrinthPackManifest.h | 1 + launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp index 4dcd2fd49..4b8a9a9b5 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp @@ -35,6 +35,7 @@ void loadIndexedPack(Modpack& pack, QJsonObject& obj) void loadIndexedInfo(Modpack& pack, QJsonObject& obj) { pack.extra.body = Json::ensureString(obj, "body"); + pack.extra.projectUrl = QString("https://modrinth.com/modpack/%1").arg(Json::ensureString(obj, "slug")); pack.extra.sourceUrl = Json::ensureString(obj, "source_url"); pack.extra.wikiUrl = Json::ensureString(obj, "wiki_url"); diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.h b/launcher/modplatform/modrinth/ModrinthPackManifest.h index 7dab893ca..aaaacf2cd 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.h +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.h @@ -39,6 +39,7 @@ struct File struct ModpackExtra { QString body; + QString projectUrl; QString sourceUrl; QString wikiUrl; }; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index b21fdf4a5..cf519b8c7 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -219,7 +219,7 @@ void ModrinthPage::updateUI() if (current.extra.sourceUrl.isEmpty()) text = current.name; else - text = "" + current.name + ""; + text = "" + current.name + ""; if (!current.authors.empty()) { // TODO: Implement multiple authors with links From 365cc198ba1e4e8129c95291e60e2c3c7ffbbf7a Mon Sep 17 00:00:00 2001 From: flow Date: Sat, 14 May 2022 21:50:54 -0300 Subject: [PATCH 076/157] refactor: some random improvements --- launcher/InstanceImportTask.cpp | 8 ++++---- launcher/ui/pages/modplatform/ImportPage.cpp | 5 ++++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index ec0f58e08..29e3a26cd 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -504,16 +504,16 @@ void InstanceImportTask::processModrinth() { QJsonObject hashes = Json::requireObject(obj, "hashes"); QString hash; QCryptographicHash::Algorithm hashAlgorithm; - hash = Json::ensureString(hashes, "sha256"); - hashAlgorithm = QCryptographicHash::Sha256; + hash = Json::ensureString(hashes, "sha1"); + hashAlgorithm = QCryptographicHash::Sha1; if (hash.isEmpty()) { hash = Json::ensureString(hashes, "sha512"); hashAlgorithm = QCryptographicHash::Sha512; if (hash.isEmpty()) { - hash = Json::ensureString(hashes, "sha1"); - hashAlgorithm = QCryptographicHash::Sha1; + hash = Json::ensureString(hashes, "sha256"); + hashAlgorithm = QCryptographicHash::Sha256; if (hash.isEmpty()) { throw JSONValidationError("No hash found for: " + file.path); diff --git a/launcher/ui/pages/modplatform/ImportPage.cpp b/launcher/ui/pages/modplatform/ImportPage.cpp index 3b65de9d4..c86d02cae 100644 --- a/launcher/ui/pages/modplatform/ImportPage.cpp +++ b/launcher/ui/pages/modplatform/ImportPage.cpp @@ -110,7 +110,10 @@ void ImportPage::updateState() // FIXME: actually do some validation of what's inside here... this is fake AF QFileInfo fi(input); // mrpack is a modrinth pack - if(fi.exists() && (fi.suffix() == "zip" || fi.suffix() == "mrpack")) + + // Allow non-latin people to use ZIP files! + auto zip = QMimeDatabase().mimeTypeForUrl(url).suffixes().contains("zip"); + if(fi.exists() && (zip || fi.suffix() == "mrpack")) { QFileInfo fi(url.fileName()); dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url)); From 49de5d9b07c8e05681ef9d485ccfd3d8e4bca784 Mon Sep 17 00:00:00 2001 From: flow Date: Sat, 14 May 2022 22:04:40 -0300 Subject: [PATCH 077/157] change: list what file types can be entered in the importer --- launcher/ui/pages/modplatform/ImportPage.ui | 78 +++++++++++++++++---- 1 file changed, 66 insertions(+), 12 deletions(-) diff --git a/launcher/ui/pages/modplatform/ImportPage.ui b/launcher/ui/pages/modplatform/ImportPage.ui index eb63cbe90..77bc5da5b 100644 --- a/launcher/ui/pages/modplatform/ImportPage.ui +++ b/launcher/ui/pages/modplatform/ImportPage.ui @@ -11,28 +11,75 @@ - - - - Browse - - - - + http:// - - + + - Local file or link to a direct download: + Browse - + + + + + + The following file types are implemented (both for local files and URLs): + + + Qt::AlignCenter + + + + + + + - Curseforge modpacks (ZIP) + + + Qt::AlignCenter + + + + + + + - Modrinth modpacks (ZIP and mrpack) + + + Qt::AlignCenter + + + + + + + - PolyMC / MultiMC exported instances (ZIP) + + + Qt::AlignCenter + + + + + + + - Technic modpacks (ZIP) + + + Qt::AlignCenter + + + + + + Qt::Vertical @@ -45,6 +92,13 @@ + + + + Local file or link to a direct download: + + + From 4745ed28186f46de60de155826c8f2bfb54f45cb Mon Sep 17 00:00:00 2001 From: flow Date: Sat, 14 May 2022 22:12:51 -0300 Subject: [PATCH 078/157] fix: choose valid download url even if it's not the primary one It seems to be possible to have modpack versions that have to primary file. In those cases, we pick a valid one "at random". --- .../modplatform/modrinth/ModrinthPackManifest.cpp | 14 +++++++++++--- .../modplatform/modrinth/ModrinthPackManifest.h | 4 ++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp index 4b8a9a9b5..88ca808af 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp @@ -81,15 +81,23 @@ auto loadIndexedVersion(QJsonObject &obj) -> ModpackVersion auto files = Json::requireArray(obj, "files"); + qWarning() << files; + for (auto file_iter : files) { File indexed_file; auto parent = Json::requireObject(file_iter); - if (!Json::ensureBoolean(parent, "primary", false)) { - continue; + auto is_primary = Json::ensureBoolean(parent, "primary", false); + if (!is_primary) { + auto filename = Json::ensureString(parent, "filename"); + // Checking suffix here is fine because it's the response from Modrinth, + // so one would assume it will always be in English. + if(!filename.endsWith("mrpack") && !filename.endsWith("zip")) + continue; } file.download_url = Json::requireString(parent, "url"); - break; + if(is_primary) + break; } if(file.download_url.isEmpty()) diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.h b/launcher/modplatform/modrinth/ModrinthPackManifest.h index aaaacf2cd..585f692aa 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.h +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.h @@ -79,5 +79,5 @@ auto loadIndexedVersion(QJsonObject&) -> ModpackVersion; } -Q_DECLARE_METATYPE(Modrinth::Modpack); -Q_DECLARE_METATYPE(Modrinth::ModpackVersion); +Q_DECLARE_METATYPE(Modrinth::Modpack) +Q_DECLARE_METATYPE(Modrinth::ModpackVersion) From 9731e06728ab1bdf11f6891b563d9f7123c1a0d8 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 15 May 2022 11:49:27 +0200 Subject: [PATCH 079/157] fix: fix build on Qt 5.12 --- launcher/modplatform/modrinth/ModrinthPackManifest.h | 1 + 1 file changed, 1 insertion(+) diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.h b/launcher/modplatform/modrinth/ModrinthPackManifest.h index 585f692aa..33c3fc5ea 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.h +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.h @@ -21,6 +21,7 @@ #include #include #include +#include class MinecraftInstance; From a43f882d482061b86a339c1338e26246f6fc5f70 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 15 May 2022 12:06:01 +0200 Subject: [PATCH 080/157] feat: add support for Quilt Loader in Modrinth packs --- launcher/InstanceImportTask.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index 29e3a26cd..293105380 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -480,7 +480,7 @@ void InstanceImportTask::processMultiMC() void InstanceImportTask::processModrinth() { std::vector files; - QString minecraftVersion, fabricVersion, forgeVersion; + QString minecraftVersion, fabricVersion, quiltVersion, forgeVersion; try { QString indexPath = FS::PathCombine(m_stagingPath, "modrinth.index.json"); @@ -547,6 +547,12 @@ void InstanceImportTask::processModrinth() { throw JSONValidationError("Duplicate Fabric Loader version"); fabricVersion = Json::requireString(*it, "Fabric Loader version"); } + else if (name == "quilt-loader") + { + if (!quiltVersion.isEmpty()) + throw JSONValidationError("Duplicate Quilt Loader version"); + quiltVersion = Json::requireString(*it, "Quilt Loader version"); + } else if (name == "forge") { if (!forgeVersion.isEmpty()) @@ -587,6 +593,8 @@ void InstanceImportTask::processModrinth() { components->setComponentVersion("net.minecraft", minecraftVersion, true); if (!fabricVersion.isEmpty()) components->setComponentVersion("net.fabricmc.fabric-loader", fabricVersion, true); + if (!quiltVersion.isEmpty()) + components->setComponentVersion("org.quiltmc.quilt-loader", quiltVersion, true); if (!forgeVersion.isEmpty()) components->setComponentVersion("net.minecraftforge", forgeVersion, true); if (m_instIcon != "default") From 4a0e4fdb85ae6782406919c4b4df9554a81356aa Mon Sep 17 00:00:00 2001 From: flow Date: Sun, 15 May 2022 07:12:31 -0300 Subject: [PATCH 081/157] fix: add author page url --- launcher/modplatform/modrinth/ModrinthPackManifest.cpp | 7 ++++++- launcher/modplatform/modrinth/ModrinthPackManifest.h | 2 +- launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp | 8 +++----- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp index 88ca808af..f690984b8 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp @@ -16,9 +16,13 @@ #include "ModrinthPackManifest.h" #include "Json.h" +#include "modplatform/modrinth/ModrinthAPI.h" + #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" +static ModrinthAPI api; + namespace Modrinth { void loadIndexedPack(Modpack& pack, QJsonObject& obj) @@ -27,7 +31,8 @@ void loadIndexedPack(Modpack& pack, QJsonObject& obj) pack.name = Json::ensureString(obj, "title"); pack.description = Json::ensureString(obj, "description"); - pack.authors << Json::ensureString(obj, "author"); + auto temp_author_name = Json::ensureString(obj, "author"); + pack.author = std::make_tuple(temp_author_name, api.getAuthorURL(temp_author_name)); pack.iconName = QString("modrinth_%1").arg(Json::ensureString(obj, "slug")); pack.iconUrl = Json::ensureString(obj, "icon_url"); } diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.h b/launcher/modplatform/modrinth/ModrinthPackManifest.h index 33c3fc5ea..47817bad1 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.h +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.h @@ -62,7 +62,7 @@ struct Modpack { QString name; QString description; - QStringList authors; + std::tuple author; QString iconName; QUrl iconUrl; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index cf519b8c7..acfd14b5e 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -221,10 +221,8 @@ void ModrinthPage::updateUI() else text = "" + current.name + ""; - if (!current.authors.empty()) { - // TODO: Implement multiple authors with links - text += "
" + tr(" by ") + current.authors.at(0); - } + // TODO: Implement multiple authors with links + text += "
" + tr(" by ") + QString("%2").arg(std::get<1>(current.author).toString(), std::get<0>(current.author)); text += "
"; @@ -255,7 +253,7 @@ void ModrinthPage::suggestCurrent() dialog->setSuggestedPack(current.name, new InstanceImportTask(ver.download_url)); auto iconName = current.iconName; m_model->getLogo(iconName, current.iconUrl.toString(), - [this, iconName](QString logo) { dialog->setSuggestedIconFromFile(logo, iconName); }); + [this, iconName](QString logo) { dialog->setSuggestedIconFromFile(logo, iconName); }); break; } From 4bb429a0fbe698d0f4dbdbf02719e76730b5b6bd Mon Sep 17 00:00:00 2001 From: flow Date: Sun, 15 May 2022 07:43:02 -0300 Subject: [PATCH 082/157] change: use build variables for the modrinth API URLs Make it more consistent with the others --- buildconfig/BuildConfig.h | 3 +++ launcher/modplatform/modrinth/ModrinthAPI.h | 22 ++++++++++--------- .../modplatform/modrinth/ModrinthModel.cpp | 6 ++--- .../modplatform/modrinth/ModrinthPage.cpp | 5 +++-- 4 files changed, 21 insertions(+), 15 deletions(-) diff --git a/buildconfig/BuildConfig.h b/buildconfig/BuildConfig.h index a920a3d41..8594e46dc 100644 --- a/buildconfig/BuildConfig.h +++ b/buildconfig/BuildConfig.h @@ -151,6 +151,9 @@ class Config { */ QString TECHNIC_API_BUILD = "multimc"; + QString MODRINTH_STAGING_URL = "https://staging-api.modrinth.com/v2"; + QString MODRINTH_PROD_URL = "https://api.modrinth.com/v2"; + /** * \brief Converts the Version to a string. * \return The version number in string format (major.minor.revision.build). diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index 86852c946..874383757 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -1,5 +1,6 @@ #pragma once +#include "BuildConfig.h" #include "modplatform/ModAPI.h" #include "modplatform/helpers/NetworkModAPI.h" @@ -47,13 +48,13 @@ class ModrinthAPI : public NetworkModAPI { return ""; } - return QString( - "https://api.modrinth.com/v2/search?" - "offset=%1&" - "limit=25&" - "query=%2&" - "index=%3&" - "facets=[[%4],%5[\"project_type:mod\"]]") + return QString(BuildConfig.MODRINTH_PROD_URL + + "/search?" + "offset=%1&" + "limit=25&" + "query=%2&" + "index=%3&" + "facets=[[%4],%5[\"project_type:mod\"]]") .arg(args.offset) .arg(args.search) .arg(args.sorting) @@ -63,9 +64,10 @@ class ModrinthAPI : public NetworkModAPI { inline auto getVersionsURL(VersionSearchArgs& args) const -> QString override { - return QString("https://api.modrinth.com/v2/project/%1/version?" - "game_versions=[%2]" - "loaders=[\"%3\"]") + return QString(BuildConfig.MODRINTH_PROD_URL + + "/project/%1/version?" + "game_versions=[%2]" + "loaders=[\"%3\"]") .arg(args.addonId) .arg(getGameVersionsString(args.mcVersions)) .arg(getModLoaderStrings(args.loader).join("\",\"")); diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index 121f5d4e4..1d1b4c8e7 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -71,10 +71,10 @@ void ModpackListModel::performPaginatedSearch() // TODO: Move to standalone API NetJob* netJob = new NetJob("Modrinth::SearchModpack", APPLICATION->network()); auto searchAllUrl = QString( - "https://staging-api.modrinth.com/v2/search?" - "query=%1&" + "%1/search?" + "query=%2&" "facets=[[\"project_type:modpack\"]]") - .arg(currentSearchTerm); + .arg(BuildConfig.MODRINTH_STAGING_URL, currentSearchTerm); netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchAllUrl), &m_all_response)); diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index acfd14b5e..5dc66e56a 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -38,6 +38,7 @@ #include "ModrinthModel.h" +#include "BuildConfig.h" #include "InstanceImportTask.h" #include "Json.h" @@ -122,7 +123,7 @@ void ModrinthPage::onSelectionChanged(QModelIndex first, QModelIndex second) QString id = current.id; - netJob->addNetAction(Net::Download::makeByteArray(QString("https://staging-api.modrinth.com/v2/project/%1").arg(id), response)); + netJob->addNetAction(Net::Download::makeByteArray(QString("%1/project/%2").arg(BuildConfig.MODRINTH_STAGING_URL, id), response)); QObject::connect(netJob, &NetJob::succeeded, this, [this, response, id] { if (id != current.id) { @@ -167,7 +168,7 @@ void ModrinthPage::onSelectionChanged(QModelIndex first, QModelIndex second) QString id = current.id; netJob->addNetAction( - Net::Download::makeByteArray(QString("https://staging-api.modrinth.com/v2/project/%1/version").arg(id), response)); + Net::Download::makeByteArray(QString("%1/project/%2/version").arg(BuildConfig.MODRINTH_STAGING_URL, id), response)); QObject::connect(netJob, &NetJob::succeeded, this, [this, response, id] { if (id != current.id) { From 3abf466632588f9285579a9822b5da2c9fea7bec Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 15 May 2022 13:20:05 +0200 Subject: [PATCH 083/157] chore: add/update license headers --- launcher/InstanceImportTask.cpp | 40 ++++++++++++++----- launcher/InstanceImportTask.h | 40 ++++++++++++++----- launcher/modplatform/modrinth/ModrinthAPI.h | 17 ++++++++ .../modrinth/ModrinthPackIndex.cpp | 17 ++++++++ .../modplatform/modrinth/ModrinthPackIndex.h | 17 ++++++++ .../modrinth/ModrinthPackManifest.cpp | 40 ++++++++++++++----- .../modrinth/ModrinthPackManifest.h | 40 ++++++++++++++----- launcher/ui/pages/modplatform/ImportPage.cpp | 1 + .../modplatform/modrinth/ModrinthModModel.h | 18 +++++++++ .../modplatform/modrinth/ModrinthModel.cpp | 34 ++++++++++++++++ .../modplatform/modrinth/ModrinthModel.h | 34 ++++++++++++++++ 11 files changed, 258 insertions(+), 40 deletions(-) diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index 293105380..8a0432c9f 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #include "InstanceImportTask.h" diff --git a/launcher/InstanceImportTask.h b/launcher/InstanceImportTask.h index 317562d91..0dc6ba88b 100644 --- a/launcher/InstanceImportTask.h +++ b/launcher/InstanceImportTask.h @@ -1,16 +1,36 @@ -/* Copyright 2013-2021 MultiMC Contributors +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #pragma once diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index 874383757..09eefcd1a 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -1,3 +1,20 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + #pragma once #include "BuildConfig.h" diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp index a3c2f166f..f7fa98641 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp @@ -1,3 +1,20 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + #include "ModrinthPackIndex.h" #include "ModrinthAPI.h" diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.h b/launcher/modplatform/modrinth/ModrinthPackIndex.h index fd17847af..7f306f25f 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.h +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.h @@ -1,3 +1,20 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + #pragma once #include "modplatform/ModIndex.h" diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp index f690984b8..f77baa6ae 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp @@ -1,16 +1,36 @@ -/* Copyright 2022 kb1000 +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * Copyright 2022 kb1000 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #include "ModrinthPackManifest.h" diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.h b/launcher/modplatform/modrinth/ModrinthPackManifest.h index 47817bad1..d350477b5 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.h +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.h @@ -1,16 +1,36 @@ -/* Copyright 2022 kb1000 +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * Copyright 2022 kb1000 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #pragma once diff --git a/launcher/ui/pages/modplatform/ImportPage.cpp b/launcher/ui/pages/modplatform/ImportPage.cpp index c86d02cae..c7bc13d88 100644 --- a/launcher/ui/pages/modplatform/ImportPage.cpp +++ b/launcher/ui/pages/modplatform/ImportPage.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield + * Copyright (c) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.h index 63c23bbeb..ae7b0bddc 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModModel.h @@ -1,3 +1,21 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + #pragma once #include "ModrinthModPage.h" diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index 1d1b4c8e7..b0dfb1b75 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -1,3 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "ModrinthModel.h" #include "BuildConfig.h" diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h index 1fdbe278b..6ec3bb976 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h @@ -1,3 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #pragma once #include From 5f2398fe59b0053d94c0600f54bddc642751bf74 Mon Sep 17 00:00:00 2001 From: flow Date: Sun, 15 May 2022 08:25:58 -0300 Subject: [PATCH 084/157] chore: license headers 2 --- launcher/InstanceImportTask.cpp | 1 + launcher/modplatform/modrinth/ModrinthAPI.h | 1 + launcher/modplatform/modrinth/ModrinthPackManifest.cpp | 1 + launcher/modplatform/modrinth/ModrinthPackManifest.h | 1 + launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp | 1 + launcher/ui/pages/modplatform/modrinth/ModrinthModel.h | 1 + launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp | 1 + launcher/ui/pages/modplatform/modrinth/ModrinthPage.h | 1 + 8 files changed, 8 insertions(+) diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index 8a0432c9f..26d46be03 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (c) 2022 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index 09eefcd1a..6d642b5e8 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp index f77baa6ae..facf5ddbd 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.h b/launcher/modplatform/modrinth/ModrinthPackManifest.h index d350477b5..55ad40d94 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.h +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.h @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index b0dfb1b75..50974e13f 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h index 6ec3bb976..e61eae7cf 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index 5dc66e56a..f69983ee5 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h index f72a5071f..9aa702f92 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * PolyMC - Minecraft Launcher + * Copyright (c) 2022 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by From 682a7fb6bad2c2d07ae5ddf67c139ac3f15672bb Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 15 May 2022 13:36:55 +0200 Subject: [PATCH 085/157] feat: add version of Modrinth modpack to instance name --- launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index f69983ee5..fd9adc247 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -252,7 +252,7 @@ void ModrinthPage::suggestCurrent() for (auto& ver : current.versions) { if (ver.id == selectedVersion) { - dialog->setSuggestedPack(current.name, new InstanceImportTask(ver.download_url)); + dialog->setSuggestedPack(current.name + " " + ver.version, new InstanceImportTask(ver.download_url)); auto iconName = current.iconName; m_model->getLogo(iconName, current.iconUrl.toString(), [this, iconName](QString logo) { dialog->setSuggestedIconFromFile(logo, iconName); }); From 93e0041d0e6c3d7859f7d8b058a0fd014329bec6 Mon Sep 17 00:00:00 2001 From: flow Date: Sun, 15 May 2022 11:09:45 -0300 Subject: [PATCH 086/157] change: use modrinth icon as default on modrinth packs --- launcher/InstanceImportTask.cpp | 4 ++++ launcher/resources/multimc/multimc.qrc | 2 +- .../resources/multimc/scalable/{ => instances}/modrinth.svg | 0 3 files changed, 5 insertions(+), 1 deletion(-) rename launcher/resources/multimc/scalable/{ => instances}/modrinth.svg (100%) diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index 26d46be03..f02aed910 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -622,6 +622,10 @@ void InstanceImportTask::processModrinth() { { instance.setIconKey(m_instIcon); } + else + { + instance.setIconKey("modrinth"); + } instance.setName(m_instName); instance.saveNow(); diff --git a/launcher/resources/multimc/multimc.qrc b/launcher/resources/multimc/multimc.qrc index 86ebf753c..e22fe7eef 100644 --- a/launcher/resources/multimc/multimc.qrc +++ b/launcher/resources/multimc/multimc.qrc @@ -21,7 +21,7 @@ scalable/atlauncher-placeholder.png - scalable/modrinth.svg + scalable/instances/modrinth.svg scalable/proxy.svg diff --git a/launcher/resources/multimc/scalable/modrinth.svg b/launcher/resources/multimc/scalable/instances/modrinth.svg similarity index 100% rename from launcher/resources/multimc/scalable/modrinth.svg rename to launcher/resources/multimc/scalable/instances/modrinth.svg From 4adc61bda91bb01e603fb975b05651df7decaf52 Mon Sep 17 00:00:00 2001 From: flow Date: Sun, 15 May 2022 11:26:15 -0300 Subject: [PATCH 087/157] change: update modrinth icon Updates to the version at https://github.com/modrinth/docs/blob/master/static/img/logo.svg --- launcher/resources/multimc/scalable/instances/modrinth.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/launcher/resources/multimc/scalable/instances/modrinth.svg b/launcher/resources/multimc/scalable/instances/modrinth.svg index 32715f5ce..a40f0e72b 100644 --- a/launcher/resources/multimc/scalable/instances/modrinth.svg +++ b/launcher/resources/multimc/scalable/instances/modrinth.svg @@ -1,4 +1,4 @@ - - + + From 78cf0c73c89f0d1207bb079bf4670cc032607c4d Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 15 May 2022 20:38:27 +0200 Subject: [PATCH 088/157] fix: always show project url, if available --- launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index fd9adc247..a2e18d19a 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -218,7 +218,7 @@ void ModrinthPage::updateUI() { QString text = ""; - if (current.extra.sourceUrl.isEmpty()) + if (current.extra.projectUrl.isEmpty()) text = current.name; else text = "" + current.name + ""; From 7194bb1b8114a2ec96d3cb30a4fe3338f3962d4c Mon Sep 17 00:00:00 2001 From: flow Date: Sun, 15 May 2022 15:58:23 -0300 Subject: [PATCH 089/157] fix: validate whitelisted download urls --- launcher/InstanceImportTask.cpp | 2 +- .../modrinth/ModrinthPackManifest.cpp | 25 +++++++++++++++++-- .../modrinth/ModrinthPackManifest.h | 2 ++ 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index f02aed910..3ca82923e 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -545,7 +545,7 @@ void InstanceImportTask::processModrinth() { file.hashAlgorithm = hashAlgorithm; // Do not use requireUrl, which uses StrictMode, instead use QUrl's default TolerantMode (as Modrinth seems to incorrectly handle spaces) file.download = Json::requireString(Json::ensureArray(obj, "downloads").first(), "Download URL for " + file.path); - if (!file.download.isValid()) + if (!file.download.isValid() || !Modrinth::validadeDownloadUrl(file.download)) { throw JSONValidationError("Download URL for " + file.path + " is not a correctly formatted URL"); } diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp index facf5ddbd..947ac1823 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp @@ -93,6 +93,23 @@ void loadIndexedVersions(Modpack& pack, QJsonDocument& doc) pack.versionsLoaded = true; } +auto validadeDownloadUrl(QUrl url) -> bool +{ + auto domain = url.host(); + if(domain == "cdn.modrinth.com") + return true; + if(domain == "edge.forgecdn.net") + return true; + if(domain == "media.forgecdn.net") + return true; + if(domain == "github.com") + return true; + if(domain == "raw.githubusercontent.com") + return true; + + return false; +} + auto loadIndexedVersion(QJsonObject &obj) -> ModpackVersion { ModpackVersion file; @@ -107,7 +124,6 @@ auto loadIndexedVersion(QJsonObject &obj) -> ModpackVersion auto files = Json::requireArray(obj, "files"); - qWarning() << files; for (auto file_iter : files) { File indexed_file; @@ -121,7 +137,12 @@ auto loadIndexedVersion(QJsonObject &obj) -> ModpackVersion continue; } - file.download_url = Json::requireString(parent, "url"); + auto url = Json::requireString(parent, "url"); + + if(!validadeDownloadUrl(url)) + continue; + + file.download_url = url; if(is_primary) break; } diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.h b/launcher/modplatform/modrinth/ModrinthPackManifest.h index 55ad40d94..4db4a75d8 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.h +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.h @@ -99,6 +99,8 @@ void loadIndexedInfo(Modpack&, QJsonObject&); void loadIndexedVersions(Modpack&, QJsonDocument&); auto loadIndexedVersion(QJsonObject&) -> ModpackVersion; +auto validadeDownloadUrl(QUrl) -> bool; + } Q_DECLARE_METATYPE(Modrinth::Modpack) From 80908efdcb1f8d4deb35c0df65651cc96fae71ac Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Sun, 15 May 2022 16:33:52 -0400 Subject: [PATCH 090/157] Fix indentation of macOS resources --- cmake/MacOSXBundleInfo.plist.in | 8 ++++---- program_info/App.entitlements | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/cmake/MacOSXBundleInfo.plist.in b/cmake/MacOSXBundleInfo.plist.in index 0e3a43c67..9e663d312 100644 --- a/cmake/MacOSXBundleInfo.plist.in +++ b/cmake/MacOSXBundleInfo.plist.in @@ -2,10 +2,10 @@ - NSCameraUsageDescription - A Minecraft mod wants to access your camera. - NSMicrophoneUsageDescription - A Minecraft mod wants to access your microphone. + NSCameraUsageDescription + A Minecraft mod wants to access your camera. + NSMicrophoneUsageDescription + A Minecraft mod wants to access your microphone. NSPrincipalClass NSApplication NSHighResolutionCapable diff --git a/program_info/App.entitlements b/program_info/App.entitlements index 1850b9900..032308a18 100644 --- a/program_info/App.entitlements +++ b/program_info/App.entitlements @@ -2,11 +2,11 @@ - com.apple.security.cs.disable-library-validation - - com.apple.security.device.audio-input - - com.apple.security.device.camera - + com.apple.security.cs.disable-library-validation + + com.apple.security.device.audio-input + + com.apple.security.device.camera + From 7f305aad1b80e28d15dda71ba62bec2e8eb9dac3 Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Sun, 15 May 2022 16:34:53 -0400 Subject: [PATCH 091/157] Add Allow DYLD Environment Variables Entitlement to macOS build This allows the Steam overlay to be injected into Minecraft. --- program_info/App.entitlements | 2 ++ 1 file changed, 2 insertions(+) diff --git a/program_info/App.entitlements b/program_info/App.entitlements index 032308a18..b46e8ff2a 100644 --- a/program_info/App.entitlements +++ b/program_info/App.entitlements @@ -4,6 +4,8 @@ com.apple.security.cs.disable-library-validation + com.apple.security.cs.allow-dyld-environment-variables + com.apple.security.device.audio-input com.apple.security.device.camera From a110d445ac48a2493bafef8c4ce59a234d0e648e Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 15 May 2022 23:00:09 +0200 Subject: [PATCH 092/157] feat: support quilt.mod.json metadata --- launcher/minecraft/mod/LocalModParseTask.cpp | 53 +++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/launcher/minecraft/mod/LocalModParseTask.cpp b/launcher/minecraft/mod/LocalModParseTask.cpp index f01da8aee..699cf7eeb 100644 --- a/launcher/minecraft/mod/LocalModParseTask.cpp +++ b/launcher/minecraft/mod/LocalModParseTask.cpp @@ -8,6 +8,7 @@ #include #include +#include "Json.h" #include "settings/INIFile.h" #include "FileSystem.h" @@ -262,6 +263,43 @@ std::shared_ptr ReadFabricModInfo(QByteArray contents) return details; } +// https://github.com/QuiltMC/rfcs/blob/master/specification/0002-quilt.mod.json.md#the-schema_version-field +std::shared_ptr ReadQuiltModInfo(QByteArray contents) +{ + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); + auto object = Json::requireObject(jsonDoc, "quilt.mod.json"); + auto schemaVersion = Json::ensureInteger(object.value("schema_version"), 0, "Quilt schema_version"); + + std::shared_ptr details = std::make_shared(); + + if (schemaVersion == 1) + { + auto modInfo = Json::requireObject(object.value("quilt_loader"), "Quilt mod info"); + + details->mod_id = Json::requireString(modInfo.value("id"), "Mod ID"); + details->version = Json::requireString(modInfo.value("version"), "Mod version"); + + auto modMetadata = Json::ensureObject(modInfo.value("metadata")); + + details->name = Json::ensureString(modMetadata.value("name"), details->mod_id); + details->description = Json::ensureString(modMetadata.value("description")); + + auto modContributors = Json::ensureObject(modMetadata.value("contributors")); + + // We don't really care about the role of a contributor here + details->authors += modContributors.keys(); + + auto modContact = Json::ensureObject(modMetadata.value("contact")); + + if (modContact.contains("homepage")) + { + details->homeurl = Json::requireString(modContact.value("homepage")); + } + } + return details; +} + std::shared_ptr ReadForgeInfo(QByteArray contents) { std::shared_ptr details = std::make_shared(); @@ -391,7 +429,7 @@ void LocalModParseTask::processAsZip() zip.close(); return; } - else if (zip.setCurrentFile("fabric.mod.json")) // TODO: Support quilt.mod.json + else if (zip.setCurrentFile("fabric.mod.json")) { if (!file.open(QIODevice::ReadOnly)) { @@ -404,6 +442,19 @@ void LocalModParseTask::processAsZip() zip.close(); return; } + else if (zip.setCurrentFile("quilt.mod.json")) + { + if (!file.open(QIODevice::ReadOnly)) + { + zip.close(); + return; + } + + m_result->details = ReadQuiltModInfo(file.readAll()); + file.close(); + zip.close(); + return; + } else if (zip.setCurrentFile("forgeversion.properties")) { if (!file.open(QIODevice::ReadOnly)) From 66ce5a4a2d38803bf667d7326f2ffb1bc06bff99 Mon Sep 17 00:00:00 2001 From: flow Date: Sun, 15 May 2022 20:45:27 -0300 Subject: [PATCH 093/157] fix: pack sorting and other search parameters --- .../modplatform/modrinth/ModrinthModel.cpp | 24 ++++++++++++++----- .../modplatform/modrinth/ModrinthModel.h | 2 +- .../modplatform/modrinth/ModrinthPage.cpp | 9 ++++--- 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index 50974e13f..6786b0dad 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -105,11 +105,16 @@ void ModpackListModel::performPaginatedSearch() { // TODO: Move to standalone API NetJob* netJob = new NetJob("Modrinth::SearchModpack", APPLICATION->network()); - auto searchAllUrl = QString( - "%1/search?" + auto searchAllUrl = QString(BuildConfig.MODRINTH_STAGING_URL + + "/search?" + "offset=%1&" + "limit=20&" "query=%2&" + "index=%3&" "facets=[[\"project_type:modpack\"]]") - .arg(BuildConfig.MODRINTH_STAGING_URL, currentSearchTerm); + .arg(nextSearchOffset) + .arg(currentSearchTerm) + .arg(currentSort); netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchAllUrl), &m_all_response)); @@ -148,14 +153,21 @@ void ModpackListModel::refresh() performPaginatedSearch(); } +static std::array sorts {"relevance", "downloads", "follows", "newest", "updated"}; + void ModpackListModel::searchWithTerm(const QString& term, const int sort) { - if (currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort) { + if(sort > 5 || sort < 0) + return; + + auto sort_str = sorts.at(sort); + + if (currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort_str) { return; } currentSearchTerm = term; - currentSort = sort; + currentSort = sort_str; refresh(); } @@ -255,7 +267,7 @@ void ModpackListModel::searchRequestFailed(QString reason) { if (!jobPtr->first()->m_reply) { // Network error - QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load mods.")); + QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load modpacks.")); } else if (jobPtr->first()->m_reply && jobPtr->first()->m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 409) { // 409 Gone, notify user to update QMessageBox::critical(nullptr, tr("Error"), diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h index e61eae7cf..bffea54da 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h @@ -104,7 +104,7 @@ class ModpackListModel : public QAbstractListModel { QStringList m_loadingLogos; QString currentSearchTerm; - int currentSort = 0; + QString currentSort; int nextSearchOffset = 0; enum SearchState { None, CanPossiblyFetchMore, ResetRequested, Finished } searchState = None; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index a2e18d19a..ceddcfb51 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -61,12 +61,11 @@ ModrinthPage::ModrinthPage(NewInstanceDialog* dialog, QWidget* parent) : QWidget ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); - ui->sortByBox->addItem(tr("Sort by Featured")); - ui->sortByBox->addItem(tr("Sort by Popularity")); - ui->sortByBox->addItem(tr("Sort by Last Updated")); - ui->sortByBox->addItem(tr("Sort by Name")); - ui->sortByBox->addItem(tr("Sort by Author")); + ui->sortByBox->addItem(tr("Sort by Relevance")); ui->sortByBox->addItem(tr("Sort by Total Downloads")); + ui->sortByBox->addItem(tr("Sort by Follows")); + ui->sortByBox->addItem(tr("Sort by Newest")); + ui->sortByBox->addItem(tr("Sort by Last Updated")); connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthPage::onSelectionChanged); From ec3c882a44624f18b088322b28efe7153e7db083 Mon Sep 17 00:00:00 2001 From: flow Date: Sun, 15 May 2022 20:52:57 -0300 Subject: [PATCH 094/157] change: add alpha note to modrinth page --- .../ui/pages/modplatform/modrinth/ModrinthPage.ui | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui index 8de53a693..90e8dba3c 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui @@ -11,6 +11,21 @@ + + + + + true + + + + Note: Modrinth modpacks is still in alpha phase. Some things may be rough on the edges, or not working at all! Use it with caution. + + + Qt::AlignCenter + + + From e7bb3b277647a21b85cb01ee90bf640e22d01552 Mon Sep 17 00:00:00 2001 From: flow Date: Sun, 15 May 2022 20:59:07 -0300 Subject: [PATCH 095/157] fix: macos compilation i forgor macos is cringe with static arrays :skull: edit: WHY DONT MAC LET ME USE STD::ARRAY ;----; --- .../modplatform/modrinth/ModrinthModel.cpp | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index 6786b0dad..2504b294b 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -153,14 +153,31 @@ void ModpackListModel::refresh() performPaginatedSearch(); } -static std::array sorts {"relevance", "downloads", "follows", "newest", "updated"}; +static auto sortFromIndex(int index) -> QString +{ + switch(index){ + default: + case 1: + return "relevance"; + case 2: + return "downloads"; + case 3: + return "follows"; + case 4: + return "newest"; + case 5: + return "updated"; + } + + return {}; +} void ModpackListModel::searchWithTerm(const QString& term, const int sort) { if(sort > 5 || sort < 0) return; - auto sort_str = sorts.at(sort); + auto sort_str = sortFromIndex(sort); if (currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort_str) { return; From e92b7bd25e9eccf293ff97652364def63a674df2 Mon Sep 17 00:00:00 2001 From: flow Date: Sun, 15 May 2022 21:50:42 -0300 Subject: [PATCH 096/157] change: switch to modrinth production servers --- launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp | 2 +- launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index 2504b294b..0cf53659b 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -105,7 +105,7 @@ void ModpackListModel::performPaginatedSearch() { // TODO: Move to standalone API NetJob* netJob = new NetJob("Modrinth::SearchModpack", APPLICATION->network()); - auto searchAllUrl = QString(BuildConfig.MODRINTH_STAGING_URL + + auto searchAllUrl = QString(BuildConfig.MODRINTH_PROD_URL + "/search?" "offset=%1&" "limit=20&" diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index ceddcfb51..fadad9df7 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -123,7 +123,7 @@ void ModrinthPage::onSelectionChanged(QModelIndex first, QModelIndex second) QString id = current.id; - netJob->addNetAction(Net::Download::makeByteArray(QString("%1/project/%2").arg(BuildConfig.MODRINTH_STAGING_URL, id), response)); + netJob->addNetAction(Net::Download::makeByteArray(QString("%1/project/%2").arg(BuildConfig.MODRINTH_PROD_URL, id), response)); QObject::connect(netJob, &NetJob::succeeded, this, [this, response, id] { if (id != current.id) { @@ -168,7 +168,7 @@ void ModrinthPage::onSelectionChanged(QModelIndex first, QModelIndex second) QString id = current.id; netJob->addNetAction( - Net::Download::makeByteArray(QString("%1/project/%2/version").arg(BuildConfig.MODRINTH_STAGING_URL, id), response)); + Net::Download::makeByteArray(QString("%1/project/%2/version").arg(BuildConfig.MODRINTH_PROD_URL, id), response)); QObject::connect(netJob, &NetJob::succeeded, this, [this, response, id] { if (id != current.id) { From 62e099ace5db8e04bba684e6c2517291dcce3578 Mon Sep 17 00:00:00 2001 From: flow Date: Sun, 15 May 2022 22:16:52 -0300 Subject: [PATCH 097/157] feat: better handling of optional mods This disables the optional mods by default and tell the user about it. Pretty hackish, but a better solution would involve the modrinth metadata to have the mod names... Also sorry for the diffs, my clangd went rogue x.x --- launcher/InstanceImportTask.cpp | 153 +++++++++--------- launcher/InstanceImportTask.h | 5 +- .../modplatform/modrinth/ModrinthPage.cpp | 2 +- 3 files changed, 80 insertions(+), 80 deletions(-) diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index 3ca82923e..64f2dd021 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -35,35 +35,38 @@ */ #include "InstanceImportTask.h" +#include +#include "Application.h" #include "BaseInstance.h" #include "FileSystem.h" -#include "Application.h" #include "MMCZip.h" #include "NullInstance.h" -#include "settings/INISettingsObject.h" #include "icons/IconUtils.h" -#include +#include "settings/INISettingsObject.h" // FIXME: this does not belong here, it's Minecraft/Flame specific +#include +#include "Json.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" #include "modplatform/flame/FileResolvingTask.h" #include "modplatform/flame/PackManifest.h" -#include "Json.h" -#include #include "modplatform/modrinth/ModrinthPackManifest.h" #include "modplatform/technic/TechnicPackProcessor.h" -#include "icons/IconList.h" #include "Application.h" +#include "icons/IconList.h" #include "net/ChecksumValidator.h" +#include "ui/dialogs/CustomMessageBox.h" + #include #include -InstanceImportTask::InstanceImportTask(const QUrl sourceUrl) +InstanceImportTask::InstanceImportTask(const QUrl sourceUrl, QWidget* parent) { m_sourceUrl = sourceUrl; + m_parent = parent; } bool InstanceImportTask::abort() @@ -476,124 +479,118 @@ void InstanceImportTask::processMultiMC() instance.setName(m_instName); // if the icon was specified by user, use that. otherwise pull icon from the pack - if (m_instIcon != "default") - { + if (m_instIcon != "default") { instance.setIconKey(m_instIcon); - } - else - { + } else { m_instIcon = instance.iconKey(); auto importIconPath = IconUtils::findBestIconIn(instance.instanceRoot(), m_instIcon); - if (!importIconPath.isNull() && QFile::exists(importIconPath)) - { + if (!importIconPath.isNull() && QFile::exists(importIconPath)) { // import icon auto iconList = APPLICATION->icons(); - if (iconList->iconFileExists(m_instIcon)) - { + if (iconList->iconFileExists(m_instIcon)) { iconList->deleteIcon(m_instIcon); } - iconList->installIcons({importIconPath}); + iconList->installIcons({ importIconPath }); } } emitSucceeded(); } -void InstanceImportTask::processModrinth() { +void InstanceImportTask::processModrinth() +{ std::vector files; QString minecraftVersion, fabricVersion, quiltVersion, forgeVersion; - try - { + try { QString indexPath = FS::PathCombine(m_stagingPath, "modrinth.index.json"); auto doc = Json::requireDocument(indexPath); auto obj = Json::requireObject(doc, "modrinth.index.json"); int formatVersion = Json::requireInteger(obj, "formatVersion", "modrinth.index.json"); - if (formatVersion == 1) - { + if (formatVersion == 1) { auto game = Json::requireString(obj, "game", "modrinth.index.json"); - if (game != "minecraft") - { + if (game != "minecraft") { throw JSONValidationError("Unknown game: " + game); } auto jsonFiles = Json::requireIsArrayOf(obj, "files", "modrinth.index.json"); - std::transform(jsonFiles.begin(), jsonFiles.end(), std::back_inserter(files), [](const QJsonObject& obj) - { - Modrinth::File file; - file.path = Json::requireString(obj, "path"); - QString supported = Json::ensureString(Json::ensureObject(obj, "env")); - QJsonObject hashes = Json::requireObject(obj, "hashes"); - QString hash; - QCryptographicHash::Algorithm hashAlgorithm; - hash = Json::ensureString(hashes, "sha1"); - hashAlgorithm = QCryptographicHash::Sha1; - if (hash.isEmpty()) - { - hash = Json::ensureString(hashes, "sha512"); - hashAlgorithm = QCryptographicHash::Sha512; - if (hash.isEmpty()) - { - hash = Json::ensureString(hashes, "sha256"); - hashAlgorithm = QCryptographicHash::Sha256; - if (hash.isEmpty()) - { - throw JSONValidationError("No hash found for: " + file.path); - } - } + bool had_optional = false; + for (auto& obj : jsonFiles) { + Modrinth::File file; + file.path = Json::requireString(obj, "path"); + + auto env = Json::ensureObject(obj, "env"); + QString support = Json::ensureString(env, "client", "unsupported"); + if (support == "unsupported") { + continue; + } else if (support == "optional") { + // TODO: Make a review dialog for choosing which ones the user wants! + if (!had_optional) { + had_optional = true; + auto info = CustomMessageBox::selectable( + m_parent, tr("Optional mod detected!"), + tr("One or more mods from this modpack are optional. They will be downloaded, but disabled by default!"), QMessageBox::Information); + info->exec(); } - file.hash = QByteArray::fromHex(hash.toLatin1()); - file.hashAlgorithm = hashAlgorithm; - // Do not use requireUrl, which uses StrictMode, instead use QUrl's default TolerantMode (as Modrinth seems to incorrectly handle spaces) - file.download = Json::requireString(Json::ensureArray(obj, "downloads").first(), "Download URL for " + file.path); - if (!file.download.isValid() || !Modrinth::validadeDownloadUrl(file.download)) - { - throw JSONValidationError("Download URL for " + file.path + " is not a correctly formatted URL"); + + if (file.path.endsWith(".jar")) + file.path += ".disabled"; + } + + QJsonObject hashes = Json::requireObject(obj, "hashes"); + QString hash; + QCryptographicHash::Algorithm hashAlgorithm; + hash = Json::ensureString(hashes, "sha1"); + hashAlgorithm = QCryptographicHash::Sha1; + if (hash.isEmpty()) { + hash = Json::ensureString(hashes, "sha512"); + hashAlgorithm = QCryptographicHash::Sha512; + if (hash.isEmpty()) { + hash = Json::ensureString(hashes, "sha256"); + hashAlgorithm = QCryptographicHash::Sha256; + if (hash.isEmpty()) { + throw JSONValidationError("No hash found for: " + file.path); + } } - return file; - }); + } + file.hash = QByteArray::fromHex(hash.toLatin1()); + file.hashAlgorithm = hashAlgorithm; + // Do not use requireUrl, which uses StrictMode, instead use QUrl's default TolerantMode (as Modrinth seems to incorrectly + // handle spaces) + file.download = Json::requireString(Json::ensureArray(obj, "downloads").first(), "Download URL for " + file.path); + if (!file.download.isValid() || !Modrinth::validadeDownloadUrl(file.download)) { + throw JSONValidationError("Download URL for " + file.path + " is not a correctly formatted URL"); + } + files.push_back(file); + } auto dependencies = Json::requireObject(obj, "dependencies", "modrinth.index.json"); - for (auto it = dependencies.begin(), end = dependencies.end(); it != end; ++it) - { + for (auto it = dependencies.begin(), end = dependencies.end(); it != end; ++it) { QString name = it.key(); - if (name == "minecraft") - { + if (name == "minecraft") { if (!minecraftVersion.isEmpty()) throw JSONValidationError("Duplicate Minecraft version"); minecraftVersion = Json::requireString(*it, "Minecraft version"); - } - else if (name == "fabric-loader") - { + } else if (name == "fabric-loader") { if (!fabricVersion.isEmpty()) throw JSONValidationError("Duplicate Fabric Loader version"); fabricVersion = Json::requireString(*it, "Fabric Loader version"); - } - else if (name == "quilt-loader") - { + } else if (name == "quilt-loader") { if (!quiltVersion.isEmpty()) throw JSONValidationError("Duplicate Quilt Loader version"); quiltVersion = Json::requireString(*it, "Quilt Loader version"); - } - else if (name == "forge") - { + } else if (name == "forge") { if (!forgeVersion.isEmpty()) throw JSONValidationError("Duplicate Forge version"); forgeVersion = Json::requireString(*it, "Forge version"); - } - else - { + } else { throw JSONValidationError("Unknown dependency type: " + name); } } - } - else - { + } else { throw JSONValidationError(QStringLiteral("Unknown format version: %s").arg(formatVersion)); } QFile::remove(indexPath); - } - catch (const JSONValidationError &e) - { + } catch (const JSONValidationError& e) { emitFailed(tr("Could not understand pack index:\n") + e.cause()); return; } diff --git a/launcher/InstanceImportTask.h b/launcher/InstanceImportTask.h index 0dc6ba88b..5e4d32351 100644 --- a/launcher/InstanceImportTask.h +++ b/launcher/InstanceImportTask.h @@ -55,7 +55,7 @@ class InstanceImportTask : public InstanceTask { Q_OBJECT public: - explicit InstanceImportTask(const QUrl sourceUrl); + explicit InstanceImportTask(const QUrl sourceUrl, QWidget* parent = nullptr); bool canAbort() const override { return true; } bool abort() override; @@ -94,4 +94,7 @@ private slots: Flame, Modrinth, } m_modpackType = ModpackType::Unknown; + + //FIXME: nuke + QWidget* m_parent; }; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index fadad9df7..f24d36518 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -251,7 +251,7 @@ void ModrinthPage::suggestCurrent() for (auto& ver : current.versions) { if (ver.id == selectedVersion) { - dialog->setSuggestedPack(current.name + " " + ver.version, new InstanceImportTask(ver.download_url)); + dialog->setSuggestedPack(current.name + " " + ver.version, new InstanceImportTask(ver.download_url, this)); auto iconName = current.iconName; m_model->getLogo(iconName, current.iconUrl.toString(), [this, iconName](QString logo) { dialog->setSuggestedIconFromFile(logo, iconName); }); From 82760f4b916ef122eabb644e8679f9ae76587e44 Mon Sep 17 00:00:00 2001 From: flow Date: Mon, 16 May 2022 10:50:46 -0300 Subject: [PATCH 098/157] fix: import modrinth packs with weird overrides structure Probably because of Packwiz limitations, or an space optimizer that did this :) --- launcher/MMCZip.cpp | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/launcher/MMCZip.cpp b/launcher/MMCZip.cpp index b92f17817..8591fcc06 100644 --- a/launcher/MMCZip.cpp +++ b/launcher/MMCZip.cpp @@ -297,20 +297,40 @@ nonstd::optional MMCZip::extractSubDir(QuaZip *zip, const QString & { continue; } + name.remove(0, subdir.size()); - QString absFilePath = directory.absoluteFilePath(name); + auto original_name = name; + + // Fix weird "folders with a single file get squashed" thing + QString path; + if(name.contains('/') && !name.endsWith('/')){ + path = name.section('/', 0, -2) + "/"; + FS::ensureFolderPathExists(path); + + name = name.split('/').last(); + } + + QString absFilePath; if(name.isEmpty()) { - absFilePath += "/"; + absFilePath = directory.absoluteFilePath(name) + "/"; } + else + { + absFilePath = directory.absoluteFilePath(path + name); + } + if (!JlCompress::extractFile(zip, "", absFilePath)) { - qWarning() << "Failed to extract file" << name << "to" << absFilePath; + qWarning() << "Failed to extract file" << original_name << "to" << absFilePath; JlCompress::removeFile(extracted); return nonstd::nullopt; } + extracted.append(absFilePath); - qDebug() << "Extracted file" << name; + QFile::setPermissions(absFilePath, QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser); + + qDebug() << "Extracted file" << name << "to" << absFilePath; } while (zip->goToNextFile()); return extracted; } From a6d2c5e18131ab155ed482aeab548dabc2741d62 Mon Sep 17 00:00:00 2001 From: flow Date: Mon, 16 May 2022 12:59:32 -0300 Subject: [PATCH 099/157] fix: better hack for icons that cant be natively scaled to 48x48 --- launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index 0cf53659b..bb54bc20f 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -85,9 +85,10 @@ auto ModpackListModel::data(const QModelIndex& index, int role) const -> QVarian return pack.description; } else if (role == Qt::DecorationRole) { if (m_logoMap.contains(pack.iconName)) { - return (m_logoMap.value(pack.iconName) - .pixmap(48, 48) - .scaled(48, 48, Qt::IgnoreAspectRatio, Qt::TransformationMode::SmoothTransformation)); + auto icon = m_logoMap.value(pack.iconName); + auto icon_scaled = QIcon(icon.pixmap(48, 48).scaledToWidth(48)); + + return icon_scaled; } QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); ((ModpackListModel*)this)->requestLogo(pack.iconName, pack.iconUrl.toString()); From cd9e0e0cc0228ffa24466814a649abef43045745 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Mon, 16 May 2022 20:17:19 +0200 Subject: [PATCH 100/157] fix: use own metacache base for modrinth icons --- launcher/Application.cpp | 1 + launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 11109857f..afb33a502 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -819,6 +819,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) m_metacache->addBase("ModpacksCHPacks", QDir("cache/ModpacksCHPacks").absolutePath()); m_metacache->addBase("TechnicPacks", QDir("cache/TechnicPacks").absolutePath()); m_metacache->addBase("FlamePacks", QDir("cache/FlamePacks").absolutePath()); + m_metacache->addBase("ModrinthPacks", QDir("cache/ModrinthPacks").absolutePath()); m_metacache->addBase("root", QDir::currentPath()); m_metacache->addBase("translations", QDir("translations").absolutePath()); m_metacache->addBase("icons", QDir("cache/icons").absolutePath()); diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index bb54bc20f..bc1046ad5 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -208,7 +208,7 @@ void ModpackListModel::requestLogo(QString logo, QString url) } MetaEntryPtr entry = - APPLICATION->metacache()->resolveEntry(m_parent->metaEntryBase(), QString("logos/%1").arg(logo.section(".", 0, 0))); + APPLICATION->metacache()->resolveEntry("ModrinthPacks", QString("logos/%1").arg(logo.section(".", 0, 0))); auto job = new NetJob(QString("%1 Icon Download %2").arg(m_parent->debugName()).arg(logo), APPLICATION->network()); job->addNetAction(Net::Download::makeCached(QUrl(url), entry)); From 2b52cf01f5999db4a8b1ea009cb5d24dd4eb4e1c Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Sat, 14 May 2022 19:51:23 -0400 Subject: [PATCH 101/157] Build Windows installer --- .github/workflows/build.yml | 17 +++- program_info/win_install.nsi | 169 +++++++++++++++++++++++++++++++++++ 2 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 program_info/win_install.nsi diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0590b3480..7ab30d456 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -63,6 +63,7 @@ jobs: ninja:p qt5:p ccache:p + nsis:p - name: Setup ccache if: runner.os != 'Windows' && inputs.build_type == 'Debug' @@ -100,7 +101,7 @@ jobs: run: | brew update brew install qt@5 ninja - + - name: Update Qt (AppImage) if: runner.os == 'Linux' && matrix.appimage == true run: | @@ -190,6 +191,13 @@ jobs: cp -r ${{ env.INSTALL_DIR }} ${{ env.INSTALL_PORTABLE_DIR }} # cmake install on Windows is slow, let's just copy instead cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable + - name: Package (Windows, installer) + if: runner.os == 'Windows' + shell: msys2 {0} + run: | + cd ${{ env.INSTALL_PORTABLE_DIR }} + makensis -NOCD "-DVERSION=${{ env.VERSION }}" "-DMUI_ICON=${{ github.workspace }}/program_info/polymc.ico" "-XOutFile ${{ github.workspace }}/PolyMC-Setup.exe" "${{ github.workspace }}/program_info/win_install.nsi" + - name: Package (Linux) if: runner.os == 'Linux' && matrix.appimage != true run: | @@ -257,6 +265,13 @@ jobs: name: PolyMC-${{ matrix.name }}-Portable-${{ env.VERSION }}-${{ inputs.build_type }} path: ${{ env.INSTALL_PORTABLE_DIR }}/** + - name: Upload installer (Windows) + if: runner.os == 'Windows' + uses: actions/upload-artifact@v3 + with: + name: PolyMC-${{ matrix.name }}-${{ env.VERSION }}-${{ inputs.build_type }}-Setup + path: PolyMC-Setup.exe + - name: Upload binary tarball (Linux) if: runner.os == 'Linux' && matrix.appimage != true uses: actions/upload-artifact@v3 diff --git a/program_info/win_install.nsi b/program_info/win_install.nsi new file mode 100644 index 000000000..2b0d97603 --- /dev/null +++ b/program_info/win_install.nsi @@ -0,0 +1,169 @@ +!define MULTIUSER_EXECUTIONLEVEL Highest +!define MULTIUSER_MUI +!define MULTIUSER_INSTALLMODE_COMMANDLINE + +!define MULTIUSER_INSTALLMODE_INSTDIR PolyMC +!define MULTIUSER_INSTALLMODE_INSTDIR_REGISTRY_KEY Software\PolyMC +!define MULTIUSER_INSTALLMODE_INSTDIR_REGISTRY_VALUENAME InstallDir + +!include "FileFunc.nsh" +!include "MUI2.nsh" +!include "MultiUser.nsh" + +Name "PolyMC" +RequestExecutionLevel highest + +;-------------------------------- + +; Pages + +!insertmacro MUI_PAGE_WELCOME +!insertmacro MULTIUSER_PAGE_INSTALLMODE +!define MUI_COMPONENTSPAGE_NODESC +!insertmacro MUI_PAGE_COMPONENTS +!insertmacro MUI_PAGE_DIRECTORY +!insertmacro MUI_PAGE_INSTFILES +!define MUI_FINISHPAGE_RUN "$InstDir\polymc.exe" +!insertmacro MUI_PAGE_FINISH + +!insertmacro MUI_UNPAGE_CONFIRM +!insertmacro MUI_UNPAGE_INSTFILES + +!insertmacro MUI_LANGUAGE "English" + +;-------------------------------- + +; The stuff to install +Section "PolyMC" + + SectionIn RO + + nsExec::Exec /TIMEOUT=2000 'TaskKill /IM polymc.exe /F' + + SetOutPath $INSTDIR + + File "polymc.exe" + File "qt.conf" + File *.dll + File /r "iconengines" + File /r "imageformats" + File /r "jars" + File /r "platforms" + File /r "styles" + + ; Write the installation path into the registry + WriteRegStr SHCTX SOFTWARE\PolyMC "InstallDir" "$INSTDIR" + + ; Write the uninstall keys for Windows + !define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\PolyMC" + WriteRegStr SHCTX "${UNINST_KEY}" "DisplayName" "PolyMC" + WriteRegStr SHCTX "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\polymc.exe" + WriteRegStr SHCTX "${UNINST_KEY}" "UninstallString" '"$INSTDIR\uninstall.exe" /$MultiUser.InstallMode' + WriteRegStr SHCTX "${UNINST_KEY}" "QuietUninstallString" '"$INSTDIR\uninstall.exe" /$MultiUser.InstallMode /S' + WriteRegStr SHCTX "${UNINST_KEY}" "InstallLocation" "$INSTDIR" + WriteRegStr SHCTX "${UNINST_KEY}" "Publisher" "PolyMC Contributors" + WriteRegStr SHCTX "${UNINST_KEY}" "ProductVersion" "${VERSION}" + ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2 + IntFmt $0 "0x%08X" $0 + WriteRegDWORD SHCTX "${UNINST_KEY}" "EstimatedSize" "$0" + WriteRegDWORD SHCTX "${UNINST_KEY}" "NoModify" 1 + WriteRegDWORD SHCTX "${UNINST_KEY}" "NoRepair" 1 + WriteUninstaller "$INSTDIR\uninstall.exe" + +SectionEnd + +Section "Start Menu Shortcuts" + + CreateShortcut "$SMPROGRAMS\PolyMC.lnk" "$INSTDIR\polymc.exe" "" "$INSTDIR\polymc.exe" 0 + +SectionEnd + +Section /o "Portable" + + SetOutPath $INSTDIR + File "portable.txt" + +SectionEnd + +;-------------------------------- + +; Uninstaller + +Section "Uninstall" + + nsExec::Exec /TIMEOUT=2000 'TaskKill /IM polymc.exe /F' + + DeleteRegKey SHCTX "Software\Microsoft\Windows\CurrentVersion\Uninstall\PolyMC" + DeleteRegKey SHCTX SOFTWARE\PolyMC + + Delete $INSTDIR\polymc.exe + Delete $INSTDIR\uninstall.exe + Delete $INSTDIR\portable.txt + + Delete $INSTDIR\libbrotlicommon.dll + Delete $INSTDIR\libbrotlidec.dll + Delete $INSTDIR\libbz2-1.dll + Delete $INSTDIR\libcrypto-1_1-x64.dll + Delete $INSTDIR\libcrypto-1_1.dll + Delete $INSTDIR\libdouble-conversion.dll + Delete $INSTDIR\libfreetype-6.dll + Delete $INSTDIR\libgcc_s_seh-1.dll + Delete $INSTDIR\libgcc_s_dw2-1.dll + Delete $INSTDIR\libglib-2.0-0.dll + Delete $INSTDIR\libgraphite2.dll + Delete $INSTDIR\libharfbuzz-0.dll + Delete $INSTDIR\libiconv-2.dll + Delete $INSTDIR\libicudt69.dll + Delete $INSTDIR\libicuin69.dll + Delete $INSTDIR\libicuuc69.dll + Delete $INSTDIR\libintl-8.dll + Delete $INSTDIR\libjasper-4.dll + Delete $INSTDIR\libjpeg-8.dll + Delete $INSTDIR\libmd4c.dll + Delete $INSTDIR\libpcre-1.dll + Delete $INSTDIR\libpcre2-16-0.dll + Delete $INSTDIR\libpng16-16.dll + Delete $INSTDIR\libssl-1_1-x64.dll + Delete $INSTDIR\libssl-1_1.dll + Delete $INSTDIR\libssp-0.dll + Delete $INSTDIR\libstdc++-6.dll + Delete $INSTDIR\libwebp-7.dll + Delete $INSTDIR\libwebpdemux-2.dll + Delete $INSTDIR\libwebpmux-3.dll + Delete $INSTDIR\libwinpthread-1.dll + Delete $INSTDIR\libzstd.dll + Delete $INSTDIR\Qt5Core.dll + Delete $INSTDIR\Qt5Gui.dll + Delete $INSTDIR\Qt5Network.dll + Delete $INSTDIR\Qt5Qml.dll + Delete $INSTDIR\Qt5QmlModels.dll + Delete $INSTDIR\Qt5Quick.dll + Delete $INSTDIR\Qt5Svg.dll + Delete $INSTDIR\Qt5WebSockets.dll + Delete $INSTDIR\Qt5Widgets.dll + Delete $INSTDIR\Qt5Xml.dll + Delete $INSTDIR\zlib1.dll + + Delete $INSTDIR\qt.conf + + RMDir /r $INSTDIR\iconengines + RMDir /r $INSTDIR\imageformats + RMDir /r $INSTDIR\jars + RMDir /r $INSTDIR\platforms + RMDir /r $INSTDIR\styles + + Delete "$SMPROGRAMS\PolyMC.lnk" + + RMDir "$INSTDIR" + +SectionEnd + +; Multi-user + +Function .onInit + !insertmacro MULTIUSER_INIT +FunctionEnd + +Function un.onInit + !insertmacro MULTIUSER_UNINIT +FunctionEnd From 2993318d195812f4b03c703aa9e68aeff941aece Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Mon, 16 May 2022 15:29:37 -0400 Subject: [PATCH 102/157] Remove admin requirement (no multi-user install option) --- program_info/win_install.nsi | 54 +++++++++++++----------------------- 1 file changed, 20 insertions(+), 34 deletions(-) diff --git a/program_info/win_install.nsi b/program_info/win_install.nsi index 2b0d97603..18a1b64ec 100644 --- a/program_info/win_install.nsi +++ b/program_info/win_install.nsi @@ -1,24 +1,16 @@ -!define MULTIUSER_EXECUTIONLEVEL Highest -!define MULTIUSER_MUI -!define MULTIUSER_INSTALLMODE_COMMANDLINE - -!define MULTIUSER_INSTALLMODE_INSTDIR PolyMC -!define MULTIUSER_INSTALLMODE_INSTDIR_REGISTRY_KEY Software\PolyMC -!define MULTIUSER_INSTALLMODE_INSTDIR_REGISTRY_VALUENAME InstallDir - !include "FileFunc.nsh" !include "MUI2.nsh" -!include "MultiUser.nsh" Name "PolyMC" -RequestExecutionLevel highest +InstallDir "$LOCALAPPDATA\PolyMC" +InstallDirRegKey HKCU "Software\PolyMC" "InstallDir" +RequestExecutionLevel user ;-------------------------------- ; Pages !insertmacro MUI_PAGE_WELCOME -!insertmacro MULTIUSER_PAGE_INSTALLMODE !define MUI_COMPONENTSPAGE_NODESC !insertmacro MUI_PAGE_COMPONENTS !insertmacro MUI_PAGE_DIRECTORY @@ -29,6 +21,10 @@ RequestExecutionLevel highest !insertmacro MUI_UNPAGE_CONFIRM !insertmacro MUI_UNPAGE_INSTFILES +;-------------------------------- + +; Languages + !insertmacro MUI_LANGUAGE "English" ;-------------------------------- @@ -52,22 +48,22 @@ Section "PolyMC" File /r "styles" ; Write the installation path into the registry - WriteRegStr SHCTX SOFTWARE\PolyMC "InstallDir" "$INSTDIR" + WriteRegStr HKCU Software\PolyMC "InstallDir" "$INSTDIR" ; Write the uninstall keys for Windows !define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\PolyMC" - WriteRegStr SHCTX "${UNINST_KEY}" "DisplayName" "PolyMC" - WriteRegStr SHCTX "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\polymc.exe" - WriteRegStr SHCTX "${UNINST_KEY}" "UninstallString" '"$INSTDIR\uninstall.exe" /$MultiUser.InstallMode' - WriteRegStr SHCTX "${UNINST_KEY}" "QuietUninstallString" '"$INSTDIR\uninstall.exe" /$MultiUser.InstallMode /S' - WriteRegStr SHCTX "${UNINST_KEY}" "InstallLocation" "$INSTDIR" - WriteRegStr SHCTX "${UNINST_KEY}" "Publisher" "PolyMC Contributors" - WriteRegStr SHCTX "${UNINST_KEY}" "ProductVersion" "${VERSION}" + WriteRegStr HKCU "${UNINST_KEY}" "DisplayName" "PolyMC" + WriteRegStr HKCU "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\polymc.exe" + WriteRegStr HKCU "${UNINST_KEY}" "UninstallString" '"$INSTDIR\uninstall.exe"' + WriteRegStr HKCU "${UNINST_KEY}" "QuietUninstallString" '"$INSTDIR\uninstall.exe" /S' + WriteRegStr HKCU "${UNINST_KEY}" "InstallLocation" "$INSTDIR" + WriteRegStr HKCU "${UNINST_KEY}" "Publisher" "PolyMC Contributors" + WriteRegStr HKCU "${UNINST_KEY}" "ProductVersion" "${VERSION}" ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2 IntFmt $0 "0x%08X" $0 - WriteRegDWORD SHCTX "${UNINST_KEY}" "EstimatedSize" "$0" - WriteRegDWORD SHCTX "${UNINST_KEY}" "NoModify" 1 - WriteRegDWORD SHCTX "${UNINST_KEY}" "NoRepair" 1 + WriteRegDWORD HKCU "${UNINST_KEY}" "EstimatedSize" "$0" + WriteRegDWORD HKCU "${UNINST_KEY}" "NoModify" 1 + WriteRegDWORD HKCU "${UNINST_KEY}" "NoRepair" 1 WriteUninstaller "$INSTDIR\uninstall.exe" SectionEnd @@ -93,8 +89,8 @@ Section "Uninstall" nsExec::Exec /TIMEOUT=2000 'TaskKill /IM polymc.exe /F' - DeleteRegKey SHCTX "Software\Microsoft\Windows\CurrentVersion\Uninstall\PolyMC" - DeleteRegKey SHCTX SOFTWARE\PolyMC + DeleteRegKey HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\PolyMC" + DeleteRegKey HKCU SOFTWARE\PolyMC Delete $INSTDIR\polymc.exe Delete $INSTDIR\uninstall.exe @@ -157,13 +153,3 @@ Section "Uninstall" RMDir "$INSTDIR" SectionEnd - -; Multi-user - -Function .onInit - !insertmacro MULTIUSER_INIT -FunctionEnd - -Function un.onInit - !insertmacro MULTIUSER_UNINIT -FunctionEnd From 887246a66b5391b16d5b0d275ba77fe3a8bf540b Mon Sep 17 00:00:00 2001 From: flow Date: Mon, 16 May 2022 17:05:54 -0300 Subject: [PATCH 103/157] fix: typo and useless code --- launcher/InstanceImportTask.cpp | 27 +++++++++---------- .../modrinth/ModrinthPackManifest.cpp | 4 +-- .../modrinth/ModrinthPackManifest.h | 2 +- .../modplatform/modrinth/ModrinthPage.cpp | 6 ----- .../pages/modplatform/modrinth/ModrinthPage.h | 1 - 5 files changed, 15 insertions(+), 25 deletions(-) diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index 64f2dd021..8f68b95f0 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -554,10 +554,10 @@ void InstanceImportTask::processModrinth() } file.hash = QByteArray::fromHex(hash.toLatin1()); file.hashAlgorithm = hashAlgorithm; - // Do not use requireUrl, which uses StrictMode, instead use QUrl's default TolerantMode (as Modrinth seems to incorrectly - // handle spaces) + // Do not use requireUrl, which uses StrictMode, instead use QUrl's default TolerantMode + // (as Modrinth seems to incorrectly handle spaces) file.download = Json::requireString(Json::ensureArray(obj, "downloads").first(), "Download URL for " + file.path); - if (!file.download.isValid() || !Modrinth::validadeDownloadUrl(file.download)) { + if (!file.download.isValid() || !Modrinth::validateDownloadUrl(file.download)) { throw JSONValidationError("Download URL for " + file.path + " is not a correctly formatted URL"); } files.push_back(file); @@ -567,22 +567,18 @@ void InstanceImportTask::processModrinth() for (auto it = dependencies.begin(), end = dependencies.end(); it != end; ++it) { QString name = it.key(); if (name == "minecraft") { - if (!minecraftVersion.isEmpty()) - throw JSONValidationError("Duplicate Minecraft version"); minecraftVersion = Json::requireString(*it, "Minecraft version"); - } else if (name == "fabric-loader") { - if (!fabricVersion.isEmpty()) - throw JSONValidationError("Duplicate Fabric Loader version"); + } + else if (name == "fabric-loader") { fabricVersion = Json::requireString(*it, "Fabric Loader version"); - } else if (name == "quilt-loader") { - if (!quiltVersion.isEmpty()) - throw JSONValidationError("Duplicate Quilt Loader version"); + } + else if (name == "quilt-loader") { quiltVersion = Json::requireString(*it, "Quilt Loader version"); - } else if (name == "forge") { - if (!forgeVersion.isEmpty()) - throw JSONValidationError("Duplicate Forge version"); + } + else if (name == "forge") { forgeVersion = Json::requireString(*it, "Forge version"); - } else { + } + else { throw JSONValidationError("Unknown dependency type: " + name); } } @@ -594,6 +590,7 @@ void InstanceImportTask::processModrinth() emitFailed(tr("Could not understand pack index:\n") + e.cause()); return; } + QString overridePath = FS::PathCombine(m_stagingPath, "overrides"); if (QFile::exists(overridePath)) { QString mcPath = FS::PathCombine(m_stagingPath, ".minecraft"); diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp index 947ac1823..f1ad39cea 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp @@ -93,7 +93,7 @@ void loadIndexedVersions(Modpack& pack, QJsonDocument& doc) pack.versionsLoaded = true; } -auto validadeDownloadUrl(QUrl url) -> bool +auto validateDownloadUrl(QUrl url) -> bool { auto domain = url.host(); if(domain == "cdn.modrinth.com") @@ -139,7 +139,7 @@ auto loadIndexedVersion(QJsonObject &obj) -> ModpackVersion auto url = Json::requireString(parent, "url"); - if(!validadeDownloadUrl(url)) + if(!validateDownloadUrl(url)) continue; file.download_url = url; diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.h b/launcher/modplatform/modrinth/ModrinthPackManifest.h index 4db4a75d8..e5fc9a700 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.h +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.h @@ -99,7 +99,7 @@ void loadIndexedInfo(Modpack&, QJsonObject&); void loadIndexedVersions(Modpack&, QJsonDocument&); auto loadIndexedVersion(QJsonObject&) -> ModpackVersion; -auto validadeDownloadUrl(QUrl) -> bool; +auto validateDownloadUrl(QUrl) -> bool; } diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index f24d36518..9bd24b578 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -195,7 +195,6 @@ void ModrinthPage::onSelectionChanged(QModelIndex first, QModelIndex second) ui->versionSelectionBox->addItem(version.version, QVariant(version.id)); } - updateVersionsUI(); suggestCurrent(); }); QObject::connect(netJob, &NetJob::finished, this, [response, netJob] { @@ -233,11 +232,6 @@ void ModrinthPage::updateUI() ui->packDescription->setHtml(text + current.description); } -void ModrinthPage::updateVersionsUI() -{ - // idk -} - void ModrinthPage::suggestCurrent() { if (!isOpened) { diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h index 9aa702f92..db5e1a3d6 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h @@ -71,7 +71,6 @@ class ModrinthPage : public QWidget, public BasePage { void suggestCurrent(); void updateUI(); - void updateVersionsUI(); void retranslate() override; void openedImpl() override; From 696a711e397440275a55f4dbe02947a78ab0b208 Mon Sep 17 00:00:00 2001 From: flow Date: Mon, 16 May 2022 19:10:31 -0300 Subject: [PATCH 104/157] fix: missed change to metacache entry lookup --- launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index bc1046ad5..701a20324 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -194,7 +194,7 @@ void ModpackListModel::getLogo(const QString& logo, const QString& logoUrl, Logo { if (m_logoMap.contains(logo)) { callback(APPLICATION->metacache() - ->resolveEntry(m_parent->metaEntryBase(), QString("logos/%1").arg(logo.section(".", 0, 0))) + ->resolveEntry("ModrinthPacks", QString("logos/%1").arg(logo.section(".", 0, 0))) ->getFullPath()); } else { requestLogo(logo, logoUrl); From 2e9d7f5c3d3cbc33ad95d830af4fdcab6eab6a06 Mon Sep 17 00:00:00 2001 From: flow Date: Mon, 16 May 2022 19:17:37 -0300 Subject: [PATCH 105/157] fix: mod skipping between pages and remove dead code --- .../modplatform/modrinth/ModrinthModel.cpp | 30 ++++--------------- .../modplatform/modrinth/ModrinthModel.h | 4 +-- 2 files changed, 8 insertions(+), 26 deletions(-) diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index 701a20324..7cacf37ad 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -109,11 +109,12 @@ void ModpackListModel::performPaginatedSearch() auto searchAllUrl = QString(BuildConfig.MODRINTH_PROD_URL + "/search?" "offset=%1&" - "limit=20&" - "query=%2&" - "index=%3&" + "limit=%2&" + "query=%3&" + "index=%4&" "facets=[[\"project_type:modpack\"]]") .arg(nextSearchOffset) + .arg(m_modpacks_per_page) .arg(currentSearchTerm) .arg(currentSort); @@ -269,10 +270,10 @@ void ModpackListModel::searchRequestFinished(QJsonDocument& doc_all) } } - if (packs_all.size() < 25) { + if (packs_all.size() < m_modpacks_per_page) { searchState = Finished; } else { - nextSearchOffset += 25; + nextSearchOffset += m_modpacks_per_page; searchState = CanPossiblyFetchMore; } @@ -308,25 +309,6 @@ void ModpackListModel::searchRequestFailed(QString reason) } } -void ModpackListModel::versionRequestSucceeded(QJsonDocument doc, QString id) -{ - auto& current = m_parent->getCurrent(); - if (id != current.id) { - return; - } - - auto arr = doc.isObject() ? Json::ensureArray(doc.object(), "data") : doc.array(); - - try { - // loadIndexedPackVersions(current, arr); - } catch (const JSONValidationError& e) { - qDebug() << doc; - qWarning() << "Error while reading " << debugName() << " mod version: " << e.cause(); - } - - // m_parent->updateModVersions(); -} - } // namespace Modrinth /******** Helpers ********/ diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h index bffea54da..14aa67473 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h @@ -79,8 +79,6 @@ class ModpackListModel : public QAbstractListModel { void searchRequestFinished(QJsonDocument& doc_all); void searchRequestFailed(QString reason); - void versionRequestSucceeded(QJsonDocument doc, QString addonId); - protected slots: void logoFailed(QString logo); @@ -112,5 +110,7 @@ class ModpackListModel : public QAbstractListModel { QByteArray m_all_response; QByteArray m_specific_response; + + int m_modpacks_per_page = 20; }; } // namespace ModPlatform From 6dfec4db40f09697f34f65419edb7d689e3c5dc7 Mon Sep 17 00:00:00 2001 From: Lenny McLennington Date: Tue, 17 May 2022 00:21:57 +0100 Subject: [PATCH 106/157] Fix toolbar disappearing in a certain circumstance. --- launcher/ui/MainWindow.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index ca345b1f6..3f8545119 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -1865,6 +1865,9 @@ void MainWindow::globalSettingsClosed() updateMainToolBar(); updateToolsMenu(); updateStatusCenter(); + // This needs to be done to prevent UI elements disappearing in the event the config is changed + // but PolyMC exits abnormally, causing the window state to never be saved: + APPLICATION->settings()->set("MainWindowState", saveState().toBase64()); update(); } From 85ec9d95a43cc884224095477e7321b84d2cc99f Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Mon, 16 May 2022 19:28:04 -0400 Subject: [PATCH 107/157] Support installer languages other than English --- program_info/win_install.nsi | 68 ++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/program_info/win_install.nsi b/program_info/win_install.nsi index 18a1b64ec..ce13b8b00 100644 --- a/program_info/win_install.nsi +++ b/program_info/win_install.nsi @@ -1,6 +1,8 @@ !include "FileFunc.nsh" !include "MUI2.nsh" +Unicode true + Name "PolyMC" InstallDir "$LOCALAPPDATA\PolyMC" InstallDirRegKey HKCU "Software\PolyMC" "InstallDir" @@ -26,6 +28,72 @@ RequestExecutionLevel user ; Languages !insertmacro MUI_LANGUAGE "English" +!insertmacro MUI_LANGUAGE "French" +!insertmacro MUI_LANGUAGE "German" +!insertmacro MUI_LANGUAGE "Spanish" +!insertmacro MUI_LANGUAGE "SpanishInternational" +!insertmacro MUI_LANGUAGE "SimpChinese" +!insertmacro MUI_LANGUAGE "TradChinese" +!insertmacro MUI_LANGUAGE "Japanese" +!insertmacro MUI_LANGUAGE "Korean" +!insertmacro MUI_LANGUAGE "Italian" +!insertmacro MUI_LANGUAGE "Dutch" +!insertmacro MUI_LANGUAGE "Danish" +!insertmacro MUI_LANGUAGE "Swedish" +!insertmacro MUI_LANGUAGE "Norwegian" +!insertmacro MUI_LANGUAGE "NorwegianNynorsk" +!insertmacro MUI_LANGUAGE "Finnish" +!insertmacro MUI_LANGUAGE "Greek" +!insertmacro MUI_LANGUAGE "Russian" +!insertmacro MUI_LANGUAGE "Portuguese" +!insertmacro MUI_LANGUAGE "PortugueseBR" +!insertmacro MUI_LANGUAGE "Polish" +!insertmacro MUI_LANGUAGE "Ukrainian" +!insertmacro MUI_LANGUAGE "Czech" +!insertmacro MUI_LANGUAGE "Slovak" +!insertmacro MUI_LANGUAGE "Croatian" +!insertmacro MUI_LANGUAGE "Bulgarian" +!insertmacro MUI_LANGUAGE "Hungarian" +!insertmacro MUI_LANGUAGE "Thai" +!insertmacro MUI_LANGUAGE "Romanian" +!insertmacro MUI_LANGUAGE "Latvian" +!insertmacro MUI_LANGUAGE "Macedonian" +!insertmacro MUI_LANGUAGE "Estonian" +!insertmacro MUI_LANGUAGE "Turkish" +!insertmacro MUI_LANGUAGE "Lithuanian" +!insertmacro MUI_LANGUAGE "Slovenian" +!insertmacro MUI_LANGUAGE "Serbian" +!insertmacro MUI_LANGUAGE "SerbianLatin" +!insertmacro MUI_LANGUAGE "Arabic" +!insertmacro MUI_LANGUAGE "Farsi" +!insertmacro MUI_LANGUAGE "Hebrew" +!insertmacro MUI_LANGUAGE "Indonesian" +!insertmacro MUI_LANGUAGE "Mongolian" +!insertmacro MUI_LANGUAGE "Luxembourgish" +!insertmacro MUI_LANGUAGE "Albanian" +!insertmacro MUI_LANGUAGE "Breton" +!insertmacro MUI_LANGUAGE "Belarusian" +!insertmacro MUI_LANGUAGE "Icelandic" +!insertmacro MUI_LANGUAGE "Malay" +!insertmacro MUI_LANGUAGE "Bosnian" +!insertmacro MUI_LANGUAGE "Kurdish" +!insertmacro MUI_LANGUAGE "Irish" +!insertmacro MUI_LANGUAGE "Uzbek" +!insertmacro MUI_LANGUAGE "Galician" +!insertmacro MUI_LANGUAGE "Afrikaans" +!insertmacro MUI_LANGUAGE "Catalan" +!insertmacro MUI_LANGUAGE "Esperanto" +!insertmacro MUI_LANGUAGE "Asturian" +!insertmacro MUI_LANGUAGE "Basque" +!insertmacro MUI_LANGUAGE "Pashto" +!insertmacro MUI_LANGUAGE "ScotsGaelic" +!insertmacro MUI_LANGUAGE "Georgian" +!insertmacro MUI_LANGUAGE "Vietnamese" +!insertmacro MUI_LANGUAGE "Welsh" +!insertmacro MUI_LANGUAGE "Armenian" +!insertmacro MUI_LANGUAGE "Corsican" +!insertmacro MUI_LANGUAGE "Tatar" +!insertmacro MUI_LANGUAGE "Hindi" ;-------------------------------- From 96deb5b09d6729f4b5f3a5c1880b526ee9882326 Mon Sep 17 00:00:00 2001 From: flow Date: Tue, 17 May 2022 06:36:30 -0300 Subject: [PATCH 108/157] chore: remove copyright from files i didnt mess with This is what happens when you auto-pilot stuff xdd --- launcher/net/PasteUpload.cpp | 1 - launcher/net/PasteUpload.h | 1 - launcher/net/Validator.h | 1 - 3 files changed, 3 deletions(-) diff --git a/launcher/net/PasteUpload.cpp b/launcher/net/PasteUpload.cpp index e88c89877..3d106c927 100644 --- a/launcher/net/PasteUpload.cpp +++ b/launcher/net/PasteUpload.cpp @@ -1,7 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-only /* * PolyMC - Minecraft Launcher - * Copyright (c) 2022 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/launcher/net/PasteUpload.h b/launcher/net/PasteUpload.h index 53979352c..ea3a06d3d 100644 --- a/launcher/net/PasteUpload.h +++ b/launcher/net/PasteUpload.h @@ -1,7 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-only /* * PolyMC - Minecraft Launcher - * Copyright (c) 2022 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by diff --git a/launcher/net/Validator.h b/launcher/net/Validator.h index e1d71d1ce..6b3d46352 100644 --- a/launcher/net/Validator.h +++ b/launcher/net/Validator.h @@ -1,7 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-only /* * PolyMC - Minecraft Launcher - * Copyright (c) 2022 flowln * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by From 17bbfe8d8951ddc7acca0222c6d2e38fb29eef25 Mon Sep 17 00:00:00 2001 From: flow Date: Tue, 17 May 2022 06:47:00 -0300 Subject: [PATCH 109/157] fix: virtual signal in Task.h --- launcher/tasks/Task.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/tasks/Task.h b/launcher/tasks/Task.h index f0e6e4023..f7765c3db 100644 --- a/launcher/tasks/Task.h +++ b/launcher/tasks/Task.h @@ -86,7 +86,7 @@ class Task : public QObject { signals: void started(); - virtual void progress(qint64 current, qint64 total); + void progress(qint64 current, qint64 total); void finished(); void succeeded(); void aborted(); From ddc3b5eb0bbd17edd60d96f5094d65dbdb922765 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Tue, 17 May 2022 15:14:53 +0200 Subject: [PATCH 110/157] Update launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui Co-authored-by: DioEgizio <83089242+DioEgizio@users.noreply.github.com> --- launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui index 90e8dba3c..4fb59cdf0 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui @@ -19,7 +19,7 @@ - Note: Modrinth modpacks is still in alpha phase. Some things may be rough on the edges, or not working at all! Use it with caution. + Note: Modrinth modpacks are still in alpha phase. Some things may be rough on the edges, or not working at all! Use it with caution. Qt::AlignCenter From edbd90a4e671486bdc1ca6f8f051445ae473fdcc Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Tue, 17 May 2022 15:17:20 +0200 Subject: [PATCH 111/157] fix: update links for Quilt metadata format --- launcher/minecraft/mod/LocalModParseTask.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/launcher/minecraft/mod/LocalModParseTask.cpp b/launcher/minecraft/mod/LocalModParseTask.cpp index 699cf7eeb..631c3abbc 100644 --- a/launcher/minecraft/mod/LocalModParseTask.cpp +++ b/launcher/minecraft/mod/LocalModParseTask.cpp @@ -263,7 +263,7 @@ std::shared_ptr ReadFabricModInfo(QByteArray contents) return details; } -// https://github.com/QuiltMC/rfcs/blob/master/specification/0002-quilt.mod.json.md#the-schema_version-field +// https://github.com/QuiltMC/rfcs/blob/master/specification/0002-quilt.mod.json.md std::shared_ptr ReadQuiltModInfo(QByteArray contents) { QJsonParseError jsonError; @@ -273,6 +273,7 @@ std::shared_ptr ReadQuiltModInfo(QByteArray contents) std::shared_ptr details = std::make_shared(); + // https://github.com/QuiltMC/rfcs/blob/be6ba280d785395fefa90a43db48e5bfc1d15eb4/specification/0002-quilt.mod.json.md if (schemaVersion == 1) { auto modInfo = Json::requireObject(object.value("quilt_loader"), "Quilt mod info"); From c1700054f4d3e7a070fd0b9b25f6ccb67b72aa06 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Fri, 4 Feb 2022 15:02:12 +0100 Subject: [PATCH 112/157] fix: replace deprecated stuff as of Qt 5.12 --- launcher/ui/dialogs/ExportInstanceDialog.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/launcher/ui/dialogs/ExportInstanceDialog.cpp b/launcher/ui/dialogs/ExportInstanceDialog.cpp index 5fac1015e..8631edf63 100644 --- a/launcher/ui/dialogs/ExportInstanceDialog.cpp +++ b/launcher/ui/dialogs/ExportInstanceDialog.cpp @@ -117,7 +117,7 @@ class PackIgnoreProxy : public QSortFilterProxyModel flags |= Qt::ItemIsUserCheckable; if (sourceIndex.model()->hasChildren(sourceIndex)) { - flags |= Qt::ItemIsTristate; + flags |= Qt::ItemIsAutoTristate; } } @@ -210,7 +210,7 @@ class PackIgnoreProxy : public QSortFilterProxyModel QStack todo; while (1) { - auto node = doing.child(row, 0); + auto node = fsm->index(row, 0, doing); if (!node.isValid()) { if (!todo.size()) @@ -259,7 +259,7 @@ class PackIgnoreProxy : public QSortFilterProxyModel QStack todo; while (1) { - auto node = doing.child(row, 0); + auto node = this->index(row, 0, doing); if (!node.isValid()) { if (!todo.size()) @@ -460,7 +460,7 @@ void ExportInstanceDialog::rowsInserted(QModelIndex parent, int top, int bottom) //WARNING: possible off-by-one? for(int i = top; i < bottom; i++) { - auto node = parent.child(i, 0); + auto node = proxyModel->index(i, 0, parent); if(proxyModel->shouldExpand(node)) { auto expNode = node.parent(); From cc27bb3231e355b147c529695317cf15e84178e1 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Fri, 4 Feb 2022 15:31:49 +0100 Subject: [PATCH 113/157] fix(updater): remove Windows version check Qt 5.12 doesn't support anything older than Windows 7 anyway, so we can't really check if we are on an older platform. --- launcher/UpdateController.cpp | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/launcher/UpdateController.cpp b/launcher/UpdateController.cpp index c02cd1e7c..646f8e573 100644 --- a/launcher/UpdateController.cpp +++ b/launcher/UpdateController.cpp @@ -138,20 +138,6 @@ void UpdateController::installUpdates() } #endif QFileInfo destination (FS::PathCombine(m_root, op.destination)); -#ifdef Q_OS_WIN32 - if(QSysInfo::windowsVersion() < QSysInfo::WV_VISTA) - { - if(destination.fileName() == windowsExeName) - { - QDir rootDir(m_root); - exeOrigin = rootDir.relativeFilePath(op.source); - exePath = rootDir.relativeFilePath(op.destination); - exeBackup = rootDir.relativeFilePath(FS::PathCombine(backupPath, destination.fileName())); - useXPHack = true; - continue; - } - } -#endif if(destination.exists()) { QString backupName = op.destination; From 4b06fc53235ab7c16ec819dd7e9d1b89aad19db9 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Thu, 3 Feb 2022 23:11:10 +0100 Subject: [PATCH 114/157] chore!: drop support for Qt <5.12 BREAKING CHANGE: If there are references to stuff that's deprecated as of Qt 5.12, the compilation will fail. This means that support for versions below 5.12 is hereby dropped --- CMakeLists.txt | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4d3683d7c..dde3578f3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.9.4) +cmake_minimum_required(VERSION 3.15) # minimum version required by QuaZip if(WIN32) # In Qt 5.1+ we have our own main() function, don't autolink to qtmain on Windows @@ -34,14 +34,12 @@ set(CMAKE_C_STANDARD_REQUIRED true) set(CMAKE_CXX_STANDARD 11) set(CMAKE_C_STANDARD 11) include(GenerateExportHeader) -set(CMAKE_CXX_FLAGS " -Wall -pedantic -Werror -Wno-deprecated-declarations -D_GLIBCXX_USE_CXX11_ABI=0 -fstack-protector-strong --param=ssp-buffer-size=4 -O3 -D_FORTIFY_SOURCE=2 ${CMAKE_CXX_FLAGS}") +set(CMAKE_CXX_FLAGS " -Wall -pedantic -D_GLIBCXX_USE_CXX11_ABI=0 -fstack-protector-strong --param=ssp-buffer-size=4 -O3 -D_FORTIFY_SOURCE=2 ${CMAKE_CXX_FLAGS}") if(UNIX AND APPLE) set(CMAKE_CXX_FLAGS " -stdlib=libc++ ${CMAKE_CXX_FLAGS}") endif() set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -Werror=return-type") - -# Fix build with Qt 5.13 -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_NO_DEPRECATED_WARNINGS=Y") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_DISABLE_DEPRECATED_BEFORE=0x050C00") option(ENABLE_LTO "Enable Link Time Optimization" off) From 8e9f1bcf18f5914ff448c640d492eaf24f436302 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 13 Feb 2022 20:04:49 +0100 Subject: [PATCH 115/157] fix: remove unnecessary Qt version checks --- launcher/Application.cpp | 4 +--- launcher/main.cpp | 2 -- launcher/ui/pages/global/ExternalToolsPage.cpp | 2 -- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index afb33a502..dc8a7b0d3 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -223,9 +223,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) setApplicationName(BuildConfig.LAUNCHER_NAME); setApplicationDisplayName(BuildConfig.LAUNCHER_DISPLAYNAME); setApplicationVersion(BuildConfig.printableVersionString()); - #if (QT_VERSION >= QT_VERSION_CHECK(5,7,0)) - setDesktopFileName(BuildConfig.LAUNCHER_DESKTOPFILENAME); - #endif + setDesktopFileName(BuildConfig.LAUNCHER_DESKTOPFILENAME); startTime = QDateTime::currentDateTime(); // Don't quit on hiding the last window diff --git a/launcher/main.cpp b/launcher/main.cpp index 275fff326..85c5fdeee 100644 --- a/launcher/main.cpp +++ b/launcher/main.cpp @@ -24,10 +24,8 @@ int main(int argc, char *argv[]) return 42; #endif -#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); -#endif #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) QApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); diff --git a/launcher/ui/pages/global/ExternalToolsPage.cpp b/launcher/ui/pages/global/ExternalToolsPage.cpp index 693ca5c15..5ba0ebc28 100644 --- a/launcher/ui/pages/global/ExternalToolsPage.cpp +++ b/launcher/ui/pages/global/ExternalToolsPage.cpp @@ -54,9 +54,7 @@ ExternalToolsPage::ExternalToolsPage(QWidget *parent) : ui->setupUi(this); ui->tabWidget->tabBar()->hide(); - #if QT_VERSION >= QT_VERSION_CHECK(5, 2, 0) ui->jsonEditorTextBox->setClearButtonEnabled(true); - #endif ui->mceditLink->setOpenExternalLinks(true); ui->jvisualvmLink->setOpenExternalLinks(true); From a21bd41580ba8ba4c0052efa8196d617e7211335 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Mon, 16 May 2022 22:37:26 +0200 Subject: [PATCH 116/157] fix: ignore deprecation again --- CMakeLists.txt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index dde3578f3..e07d2aa64 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -34,11 +34,15 @@ set(CMAKE_C_STANDARD_REQUIRED true) set(CMAKE_CXX_STANDARD 11) set(CMAKE_C_STANDARD 11) include(GenerateExportHeader) -set(CMAKE_CXX_FLAGS " -Wall -pedantic -D_GLIBCXX_USE_CXX11_ABI=0 -fstack-protector-strong --param=ssp-buffer-size=4 -O3 -D_FORTIFY_SOURCE=2 ${CMAKE_CXX_FLAGS}") +set(CMAKE_CXX_FLAGS " -Wall -pedantic -Werror -Wno-deprecated-declarations -D_GLIBCXX_USE_CXX11_ABI=0 -fstack-protector-strong --param=ssp-buffer-size=4 -O3 -D_FORTIFY_SOURCE=2 ${CMAKE_CXX_FLAGS}") if(UNIX AND APPLE) set(CMAKE_CXX_FLAGS " -stdlib=libc++ ${CMAKE_CXX_FLAGS}") endif() set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -Werror=return-type") + +# Fix build with Qt 5.13 +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_NO_DEPRECATED_WARNINGS=Y") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_DISABLE_DEPRECATED_BEFORE=0x050C00") option(ENABLE_LTO "Enable Link Time Optimization" off) From 127dfadc6cd2f0c72816af86716e88c3b4af2848 Mon Sep 17 00:00:00 2001 From: DioEgizio <83089242+DioEgizio@users.noreply.github.com> Date: Wed, 18 May 2022 14:33:58 +0200 Subject: [PATCH 117/157] fix(quilt) always prefer qmj over fmj this fixes Quilt-only mods like ok zoomer showing wrong metadata --- launcher/minecraft/mod/LocalModParseTask.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/launcher/minecraft/mod/LocalModParseTask.cpp b/launcher/minecraft/mod/LocalModParseTask.cpp index 631c3abbc..a7bec5ae5 100644 --- a/launcher/minecraft/mod/LocalModParseTask.cpp +++ b/launcher/minecraft/mod/LocalModParseTask.cpp @@ -430,7 +430,7 @@ void LocalModParseTask::processAsZip() zip.close(); return; } - else if (zip.setCurrentFile("fabric.mod.json")) + else if (zip.setCurrentFile("quilt.mod.json")) { if (!file.open(QIODevice::ReadOnly)) { @@ -438,12 +438,12 @@ void LocalModParseTask::processAsZip() return; } - m_result->details = ReadFabricModInfo(file.readAll()); + m_result->details = ReadQuiltModInfo(file.readAll()); file.close(); zip.close(); return; } - else if (zip.setCurrentFile("quilt.mod.json")) + else if (zip.setCurrentFile("fabric.mod.json")) { if (!file.open(QIODevice::ReadOnly)) { @@ -451,7 +451,7 @@ void LocalModParseTask::processAsZip() return; } - m_result->details = ReadQuiltModInfo(file.readAll()); + m_result->details = ReadFabricModInfo(file.readAll()); file.close(); zip.close(); return; From 441075f61051cce8e5d6b0311febdefc087fdbbf Mon Sep 17 00:00:00 2001 From: flow Date: Wed, 18 May 2022 17:17:16 -0300 Subject: [PATCH 118/157] fix: version field in technic pack manifest being null Sometimes, the version field, that is supposed to be a string, was a null instead. Inspecting other entries, seems like the default for not having a version should be "", so I made it like that in case the version was null. I hope this fixes the issue :^) --- launcher/modplatform/technic/SolderPackManifest.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/modplatform/technic/SolderPackManifest.cpp b/launcher/modplatform/technic/SolderPackManifest.cpp index 16fe0b0e6..e52a7ec07 100644 --- a/launcher/modplatform/technic/SolderPackManifest.cpp +++ b/launcher/modplatform/technic/SolderPackManifest.cpp @@ -37,7 +37,7 @@ void loadPack(Pack& v, QJsonObject& obj) static void loadPackBuildMod(PackBuildMod& b, QJsonObject& obj) { b.name = Json::requireString(obj, "name"); - b.version = Json::requireString(obj, "version"); + b.version = Json::ensureString(obj, "version", ""); b.md5 = Json::requireString(obj, "md5"); b.url = Json::requireString(obj, "url"); } From f66e0fa0e8cdd39ca9561f72759377db468f94b7 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Wed, 18 May 2022 22:51:14 +0200 Subject: [PATCH 119/157] fix: support split natives Mojang introduced a new structure for natives, notably for LWJGL. Now instead of using the `natives` structure of the version format, Mojang chose to create a seperate library entry for each platform, which uses the `rules` structure to specify the platform. These new split natives carry the same groupId and artifactId, as the main library, but have an additional classifier, like `natives-linux`. When comparing GradleSpecifiers we don't look at the classifier, so when the launcher sees an artifact called `org.lwjgl:lwjgl:3.3.1` and right after that an artifact called `org.lwjgl:lwjgl:3.3.1:natives-linux`, it will treat it as "already added" and forget it. This change will include the classifier in that comparison. --- launcher/minecraft/GradleSpecifier.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/minecraft/GradleSpecifier.h b/launcher/minecraft/GradleSpecifier.h index 60e0a726d..d9bb02079 100644 --- a/launcher/minecraft/GradleSpecifier.h +++ b/launcher/minecraft/GradleSpecifier.h @@ -124,7 +124,7 @@ struct GradleSpecifier } bool matchName(const GradleSpecifier & other) const { - return other.artifactId() == artifactId() && other.groupId() == groupId(); + return other.artifactId() == artifactId() && other.groupId() == groupId() && other.classifier() == classifier(); } bool operator==(const GradleSpecifier & other) const { From 77caaca50dab7ba8e455d641ac6b448052bc6799 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Thu, 19 May 2022 08:09:18 +0200 Subject: [PATCH 120/157] fix: only consider enabled mod loaders --- launcher/minecraft/PackProfile.cpp | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/launcher/minecraft/PackProfile.cpp b/launcher/minecraft/PackProfile.cpp index d53f41e1b..87d11c4c7 100644 --- a/launcher/minecraft/PackProfile.cpp +++ b/launcher/minecraft/PackProfile.cpp @@ -36,6 +36,13 @@ #include "ComponentUpdateTask.h" #include "Application.h" +#include "modplatform/ModAPI.h" + +static const QMap modloaderMapping{ + {"net.minecraftforge", ModAPI::Forge}, + {"net.fabricmc.fabric-loader", ModAPI::Fabric}, + {"org.quiltmc.quilt-loader", ModAPI::Quilt} +}; PackProfile::PackProfile(MinecraftInstance * instance) : QAbstractListModel() @@ -973,17 +980,15 @@ void PackProfile::disableInteraction(bool disable) ModAPI::ModLoaderType PackProfile::getModLoader() { - if (!getComponentVersion("net.minecraftforge").isEmpty()) - { - return ModAPI::Forge; - } - else if (!getComponentVersion("net.fabricmc.fabric-loader").isEmpty()) - { - return ModAPI::Fabric; - } - else if (!getComponentVersion("org.quiltmc.quilt-loader").isEmpty()) + QMapIterator i(modloaderMapping); + + while (i.hasNext()) { - return ModAPI::Quilt; + i.next(); + Component* c = getComponent(i.key()); + if (c != nullptr && c->isEnabled()) { + return i.value(); + } } return ModAPI::Unspecified; } From 943090db98dbbe969afed8a4fb59f4bbb43449cc Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Thu, 19 May 2022 08:40:28 +0200 Subject: [PATCH 121/157] refactor: allow tracking multiple mod loaders --- launcher/minecraft/PackProfile.cpp | 8 +++-- launcher/minecraft/PackProfile.h | 2 +- launcher/modplatform/ModAPI.h | 15 +++++--- launcher/modplatform/flame/FlameAPI.h | 17 +++++---- launcher/modplatform/modrinth/ModrinthAPI.h | 35 ++++++++----------- launcher/ui/pages/instance/ModFolderPage.cpp | 2 +- launcher/ui/pages/modplatform/ModModel.cpp | 4 +-- launcher/ui/pages/modplatform/ModPage.cpp | 2 +- launcher/ui/pages/modplatform/ModPage.h | 2 +- .../pages/modplatform/flame/FlameModPage.cpp | 4 +-- .../ui/pages/modplatform/flame/FlameModPage.h | 2 +- .../modplatform/modrinth/ModrinthModPage.cpp | 4 +-- .../modplatform/modrinth/ModrinthModPage.h | 2 +- 13 files changed, 54 insertions(+), 45 deletions(-) diff --git a/launcher/minecraft/PackProfile.cpp b/launcher/minecraft/PackProfile.cpp index 87d11c4c7..125048f05 100644 --- a/launcher/minecraft/PackProfile.cpp +++ b/launcher/minecraft/PackProfile.cpp @@ -978,8 +978,10 @@ void PackProfile::disableInteraction(bool disable) } } -ModAPI::ModLoaderType PackProfile::getModLoader() +ModAPI::ModLoaderTypes PackProfile::getModLoaders() { + ModAPI::ModLoaderTypes result = ModAPI::Unspecified; + QMapIterator i(modloaderMapping); while (i.hasNext()) @@ -987,8 +989,8 @@ ModAPI::ModLoaderType PackProfile::getModLoader() i.next(); Component* c = getComponent(i.key()); if (c != nullptr && c->isEnabled()) { - return i.value(); + result |= i.value(); } } - return ModAPI::Unspecified; + return result; } diff --git a/launcher/minecraft/PackProfile.h b/launcher/minecraft/PackProfile.h index ab4cd5c88..918e7f7ad 100644 --- a/launcher/minecraft/PackProfile.h +++ b/launcher/minecraft/PackProfile.h @@ -118,7 +118,7 @@ class PackProfile : public QAbstractListModel // todo(merged): is this the best approach void appendComponent(ComponentPtr component); - ModAPI::ModLoaderType getModLoader(); + ModAPI::ModLoaderTypes getModLoaders(); private: void scheduleSave(); diff --git a/launcher/modplatform/ModAPI.h b/launcher/modplatform/ModAPI.h index 8e6cd45c9..4230df0bc 100644 --- a/launcher/modplatform/ModAPI.h +++ b/launcher/modplatform/ModAPI.h @@ -16,14 +16,21 @@ class ModAPI { public: virtual ~ModAPI() = default; - // https://docs.curseforge.com/?http#tocS_ModLoaderType - enum ModLoaderType { Unspecified = 0, Forge = 1, Cauldron = 2, LiteLoader = 3, Fabric = 4, Quilt = 5 }; + enum ModLoaderType { + Unspecified = 0, + Forge = 1 << 0, + Cauldron = 1 << 1, + LiteLoader = 1 << 2, + Fabric = 1 << 3, + Quilt = 1 << 4 + }; + Q_DECLARE_FLAGS(ModLoaderTypes, ModLoaderType) struct SearchArgs { int offset; QString search; QString sorting; - ModLoaderType mod_loader; + ModLoaderTypes loaders; std::list versions; }; @@ -33,7 +40,7 @@ class ModAPI { struct VersionSearchArgs { QString addonId; std::list mcVersions; - ModLoaderType loader; + ModLoaderTypes loaders; }; virtual void getVersions(CallerType* caller, VersionSearchArgs&& args) const = 0; diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index 61628e603..8bb33d477 100644 --- a/launcher/modplatform/flame/FlameAPI.h +++ b/launcher/modplatform/flame/FlameAPI.h @@ -37,14 +37,14 @@ class FlameAPI : public NetworkModAPI { .arg(args.offset) .arg(args.search) .arg(getSortFieldInt(args.sorting)) - .arg(getMappedModLoader(args.mod_loader)) + .arg(getMappedModLoader(args.loaders)) .arg(gameVersionStr); }; inline auto getVersionsURL(VersionSearchArgs& args) const -> QString override { QString gameVersionQuery = args.mcVersions.size() == 1 ? QString("gameVersion=%1&").arg(args.mcVersions.front().toString()) : ""; - QString modLoaderQuery = QString("modLoaderType=%1&").arg(getMappedModLoader(args.loader)); + QString modLoaderQuery = QString("modLoaderType=%1&").arg(getMappedModLoader(args.loaders)); return QString("https://api.curseforge.com/v1/mods/%1/files?pageSize=10000&%2%3") .arg(args.addonId) @@ -53,11 +53,16 @@ class FlameAPI : public NetworkModAPI { }; public: - static auto getMappedModLoader(const ModLoaderType type) -> const ModLoaderType + static auto getMappedModLoader(const ModLoaderTypes loaders) -> const int { + // https://docs.curseforge.com/?http#tocS_ModLoaderType + if (loaders & Forge) + return 1; + if (loaders & Fabric) + return 4; // TODO: remove this once Quilt drops official Fabric support - if (type == Quilt) // NOTE: Most if not all Fabric mods should work *currently* - return Fabric; - return type; + if (loaders & Quilt) // NOTE: Most if not all Fabric mods should work *currently* + return 4; // Quilt would probably be 5 + return 0; } }; diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index 6d642b5e8..39f6c49a0 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -28,30 +28,25 @@ class ModrinthAPI : public NetworkModAPI { public: inline auto getAuthorURL(const QString& name) const -> QString { return "https://modrinth.com/user/" + name; }; - static auto getModLoaderStrings(ModLoaderType type) -> const QStringList + static auto getModLoaderStrings(const ModLoaderTypes types) -> const QStringList { QStringList l; - switch (type) + for (auto loader : {Forge, Fabric, Quilt}) { - case Unspecified: - for (auto loader : {Forge, Fabric, Quilt}) - { - l << ModAPI::getModLoaderString(loader); - } - break; - - case Quilt: - l << ModAPI::getModLoaderString(Fabric); - default: - l << ModAPI::getModLoaderString(type); + if (types & loader || types == Unspecified) + { + l << ModAPI::getModLoaderString(loader); + } } + if (types & Quilt && ~types & Fabric) // Add Fabric if Quilt is in use, if Fabric isn't already there + l << ModAPI::getModLoaderString(Fabric); return l; } - static auto getModLoaderFilters(ModLoaderType type) -> const QString + static auto getModLoaderFilters(ModLoaderTypes types) -> const QString { QStringList l; - for (auto loader : getModLoaderStrings(type)) + for (auto loader : getModLoaderStrings(types)) { l << QString("\"categories:%1\"").arg(loader); } @@ -61,7 +56,7 @@ class ModrinthAPI : public NetworkModAPI { private: inline auto getModSearchURL(SearchArgs& args) const -> QString override { - if (!validateModLoader(args.mod_loader)) { + if (!validateModLoaders(args.loaders)) { qWarning() << "Modrinth only have Forge and Fabric-compatible mods!"; return ""; } @@ -76,7 +71,7 @@ class ModrinthAPI : public NetworkModAPI { .arg(args.offset) .arg(args.search) .arg(args.sorting) - .arg(getModLoaderFilters(args.mod_loader)) + .arg(getModLoaderFilters(args.loaders)) .arg(getGameVersionsArray(args.versions)); }; @@ -88,7 +83,7 @@ class ModrinthAPI : public NetworkModAPI { "loaders=[\"%3\"]") .arg(args.addonId) .arg(getGameVersionsString(args.mcVersions)) - .arg(getModLoaderStrings(args.loader).join("\",\"")); + .arg(getModLoaderStrings(args.loaders).join("\",\"")); }; auto getGameVersionsArray(std::list mcVersions) const -> QString @@ -101,9 +96,9 @@ class ModrinthAPI : public NetworkModAPI { return s.isEmpty() ? QString() : QString("[%1],").arg(s); } - inline auto validateModLoader(ModLoaderType modLoader) const -> bool + inline auto validateModLoaders(ModLoaderTypes loaders) const -> bool { - return modLoader == Unspecified || modLoader == Forge || modLoader == Fabric || modLoader == Quilt; + return loaders == Unspecified || loaders & (Forge | Fabric | Quilt); } }; diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index 8113fe857..5574f9d2f 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -391,7 +391,7 @@ void ModFolderPage::on_actionInstall_mods_triggered() return; //this is a null instance or a legacy instance } auto profile = ((MinecraftInstance *)m_inst)->getPackProfile(); - if (profile->getModLoader() == ModAPI::Unspecified) { + if (profile->getModLoaders() == ModAPI::Unspecified) { QMessageBox::critical(this,tr("Error"),tr("Please install a mod loader first!")); return; } diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp index 540ee2fdc..9dd8f7379 100644 --- a/launcher/ui/pages/modplatform/ModModel.cpp +++ b/launcher/ui/pages/modplatform/ModModel.cpp @@ -68,7 +68,7 @@ void ListModel::requestModVersions(ModPlatform::IndexedPack const& current) { auto profile = (dynamic_cast((dynamic_cast(parent()))->m_instance))->getPackProfile(); - m_parent->apiProvider()->getVersions(this, { current.addonId.toString(), getMineVersions(), profile->getModLoader() }); + m_parent->apiProvider()->getVersions(this, { current.addonId.toString(), getMineVersions(), profile->getModLoaders() }); } void ListModel::performPaginatedSearch() @@ -76,7 +76,7 @@ void ListModel::performPaginatedSearch() auto profile = (dynamic_cast((dynamic_cast(parent()))->m_instance))->getPackProfile(); m_parent->apiProvider()->searchMods( - this, { nextSearchOffset, currentSearchTerm, getSorts()[currentSort], profile->getModLoader(), getMineVersions() }); + this, { nextSearchOffset, currentSearchTerm, getSorts()[currentSort], profile->getModLoaders(), getMineVersions() }); } void ListModel::refresh() diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index 6dd3a4535..ad36cf2f8 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -175,7 +175,7 @@ void ModPage::updateModVersions(int prev_count) bool valid = false; for(auto& mcVer : m_filter->versions){ //NOTE: Flame doesn't care about loader, so passing it changes nothing. - if (validateVersion(version, mcVer.toString(), packProfile->getModLoader())) { + if (validateVersion(version, mcVer.toString(), packProfile->getModLoaders())) { valid = true; break; } diff --git a/launcher/ui/pages/modplatform/ModPage.h b/launcher/ui/pages/modplatform/ModPage.h index eb89b0e28..0e658a8de 100644 --- a/launcher/ui/pages/modplatform/ModPage.h +++ b/launcher/ui/pages/modplatform/ModPage.h @@ -37,7 +37,7 @@ class ModPage : public QWidget, public BasePage { void retranslate() override; auto shouldDisplay() const -> bool override = 0; - virtual auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderType loader = ModAPI::Unspecified) const -> bool = 0; + virtual auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders = ModAPI::Unspecified) const -> bool = 0; auto apiProvider() const -> const ModAPI* { return api.get(); }; auto getFilter() const -> const std::shared_ptr { return m_filter; } diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp index 70759994c..1c160fd4b 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.cpp @@ -61,9 +61,9 @@ FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance* instance) connect(ui->modSelectionButton, &QPushButton::clicked, this, &FlameModPage::onModSelected); } -auto FlameModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderType loader) const -> bool +auto FlameModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders) const -> bool { - Q_UNUSED(loader); + Q_UNUSED(loaders); return ver.mcVersion.contains(mineVer); } diff --git a/launcher/ui/pages/modplatform/flame/FlameModPage.h b/launcher/ui/pages/modplatform/flame/FlameModPage.h index 27cbdb8cf..86e1a17b3 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModPage.h +++ b/launcher/ui/pages/modplatform/flame/FlameModPage.h @@ -55,7 +55,7 @@ class FlameModPage : public ModPage { inline auto debugName() const -> QString override { return "Flame"; } inline auto metaEntryBase() const -> QString override { return "FlameMods"; }; - auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderType loader = ModAPI::Unspecified) const -> bool override; + auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders = ModAPI::Unspecified) const -> bool override; auto shouldDisplay() const -> bool override; }; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.cpp index d3a1f8594..0b81ea931 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.cpp @@ -61,9 +61,9 @@ ModrinthModPage::ModrinthModPage(ModDownloadDialog* dialog, BaseInstance* instan connect(ui->modSelectionButton, &QPushButton::clicked, this, &ModrinthModPage::onModSelected); } -auto ModrinthModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderType loader) const -> bool +auto ModrinthModPage::validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders) const -> bool { - auto loaderStrings = ModrinthAPI::getModLoaderStrings(loader); + auto loaderStrings = ModrinthAPI::getModLoaderStrings(loaders); auto loaderCompatible = false; for (auto remoteLoader : ver.loaders) diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.h index b1e72bfea..c39acaa0b 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModPage.h @@ -55,7 +55,7 @@ class ModrinthModPage : public ModPage { inline auto debugName() const -> QString override { return "Modrinth"; } inline auto metaEntryBase() const -> QString override { return "ModrinthPacks"; }; - auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderType loader = ModAPI::Unspecified) const -> bool override; + auto validateVersion(ModPlatform::IndexedVersion& ver, QString mineVer, ModAPI::ModLoaderTypes loaders = ModAPI::Unspecified) const -> bool override; auto shouldDisplay() const -> bool override; }; From 36045a8b0aa5c99e8520a39e6cc372ab9b549668 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Thu, 19 May 2022 12:35:44 +0200 Subject: [PATCH 122/157] chore: improve readability Co-authored-by: flow --- launcher/modplatform/modrinth/ModrinthAPI.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index 39f6c49a0..79bc5175a 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -33,12 +33,12 @@ class ModrinthAPI : public NetworkModAPI { QStringList l; for (auto loader : {Forge, Fabric, Quilt}) { - if (types & loader || types == Unspecified) + if ((types & loader) || types == Unspecified) { l << ModAPI::getModLoaderString(loader); } } - if (types & Quilt && ~types & Fabric) // Add Fabric if Quilt is in use, if Fabric isn't already there + if ((types & Quilt) && (~types & Fabric)) // Add Fabric if Quilt is in use, if Fabric isn't already there l << ModAPI::getModLoaderString(Fabric); return l; } @@ -98,7 +98,7 @@ class ModrinthAPI : public NetworkModAPI { inline auto validateModLoaders(ModLoaderTypes loaders) const -> bool { - return loaders == Unspecified || loaders & (Forge | Fabric | Quilt); + return (loaders == Unspecified) || (loaders & (Forge | Fabric | Quilt)); } }; From 97a83c9b7a72d37218acfbf5c325245eab0b5b23 Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Sun, 1 May 2022 18:12:21 +0100 Subject: [PATCH 123/157] ATLauncher: Avoid downloading Forge twice for older packs This resolves a quirk where Forge would still be downloaded for use as a jarmod, even when we detected Forge as a component. --- launcher/modplatform/atlauncher/ATLPackInstallTask.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp index 9dcb35048..991d737c1 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp @@ -574,8 +574,6 @@ void PackInstallTask::downloadMods() jobPtr->addNetAction(dl); auto path = FS::PathCombine(m_stagingPath, "minecraft", relpath, mod.file); - qDebug() << "Will download" << url << "to" << path; - modsToCopy[entry->getFullPath()] = path; if(mod.type == ModType::Forge) { auto vlist = APPLICATION->metadataIndex()->get("net.minecraftforge"); @@ -597,6 +595,10 @@ void PackInstallTask::downloadMods() qDebug() << "Jarmod: " + path; jarmods.push_back(path); } + + // Download after Forge handling, to avoid downloading Forge twice. + qDebug() << "Will download" << url << "to" << path; + modsToCopy[entry->getFullPath()] = path; } } From c329730de848f9ecf864aa4edbbc650faad7f21a Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Sun, 1 May 2022 19:32:34 +0100 Subject: [PATCH 124/157] ATLauncher: Install LiteLoader as a component where possible --- .../atlauncher/ATLPackInstallTask.cpp | 91 ++++++++++++++++--- 1 file changed, 80 insertions(+), 11 deletions(-) diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp index 991d737c1..e9e3b872e 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp @@ -1,18 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0-only /* - * Copyright 2020-2021 Jamie Mansfield - * Copyright 2021 Petr Mrazek + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2020-2021 Jamie Mansfield + * Copyright 2021 Petr Mrazek + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #include "ATLPackInstallTask.h" @@ -305,7 +324,55 @@ bool PackInstallTask::createLibrariesComponent(QString instanceRoot, std::shared auto f = std::make_shared(); f->name = m_pack + " " + m_version_name + " (libraries)"; + const static QMap liteLoaderMap = { + { "61179803bcd5fb7790789b790908663d", "1.12-SNAPSHOT" }, + { "1420785ecbfed5aff4a586c5c9dd97eb", "1.12.2-SNAPSHOT" }, + { "073f68e2fcb518b91fd0d99462441714", "1.6.2_03" }, + { "10a15b52fc59b1bfb9c05b56de1097d6", "1.6.2_02" }, + { "b52f90f08303edd3d4c374e268a5acf1", "1.6.2_04" }, + { "ea747e24e03e24b7cad5bc8a246e0319", "1.6.2_01" }, + { "55785ccc82c07ff0ba038fe24be63ea2", "1.7.10_01" }, + { "63ada46e033d0cb6782bada09ad5ca4e", "1.7.10_04" }, + { "7983e4b28217c9ae8569074388409c86", "1.7.10_03" }, + { "c09882458d74fe0697c7681b8993097e", "1.7.10_02" }, + { "db7235aefd407ac1fde09a7baba50839", "1.7.10_00" }, + { "6e9028816027f53957bd8fcdfabae064", "1.8" }, + { "5e732dc446f9fe2abe5f9decaec40cde", "1.10-SNAPSHOT" }, + { "3a98b5ed95810bf164e71c1a53be568d", "1.11.2-SNAPSHOT" }, + { "ba8e6285966d7d988a96496f48cbddaa", "1.8.9-SNAPSHOT" }, + { "8524af3ac3325a82444cc75ae6e9112f", "1.11-SNAPSHOT" }, + { "53639d52340479ccf206a04f5e16606f", "1.5.2_01" }, + { "1fcdcf66ce0a0806b7ad8686afdce3f7", "1.6.4_00" }, + { "531c116f71ae2b11033f9a11a0f8e668", "1.6.4_01" }, + { "4009eeb99c9068f608d3483a6439af88", "1.7.2_03" }, + { "66f343354b8417abce1a10d557d2c6e9", "1.7.2_04" }, + { "ab554c21f28fbc4ae9b098bcb5f4cceb", "1.7.2_05" }, + { "e1d76a05a3723920e2f80a5e66c45f16", "1.7.2_02" }, + { "00318cb0c787934d523f63cdfe8ddde4", "1.9-SNAPSHOT" }, + { "986fd1ee9525cb0dcab7609401cef754", "1.9.4-SNAPSHOT" }, + { "571ad5e6edd5ff40259570c9be588bb5", "1.9.4" }, + { "1cdd72f7232e45551f16cc8ffd27ccf3", "1.10.2-SNAPSHOT" }, + { "8a7c21f32d77ee08b393dd3921ced8eb", "1.10.2" }, + { "b9bef8abc8dc309069aeba6fbbe58980", "1.12.1-SNAPSHOT" } + }; + for(const auto & lib : m_version.libraries) { + // If the library is LiteLoader, we need to ignore it and handle it separately. + if (liteLoaderMap.contains(lib.md5)) { + auto vlist = APPLICATION->metadataIndex()->get("com.mumfrey.liteloader"); + if (vlist) { + if (!vlist->isLoaded()) + vlist->load(Net::Mode::Online); + + auto ver = vlist->getVersion(liteLoaderMap.value(lib.md5)); + if (ver) { + ver->load(Net::Mode::Online); + componentsToInstall.insert("com.mumfrey.liteloader", ver); + continue; + } + } + } + auto libName = detectLibrary(lib); GradleSpecifier libSpecifier(libName); @@ -579,6 +646,8 @@ void PackInstallTask::downloadMods() auto vlist = APPLICATION->metadataIndex()->get("net.minecraftforge"); if(vlist) { + if (!vlist->isLoaded()) + vlist->load(Net::Mode::Online); auto ver = vlist->getVersion(mod.version); if(ver) { ver->load(Net::Mode::Online); From f5f59203a203318371fbc5257234b8c2c5eeb300 Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Sun, 1 May 2022 22:42:29 +0100 Subject: [PATCH 125/157] ATLauncher: Reduce boilerplate code for fetching versions --- .../atlauncher/ATLPackInstallTask.cpp | 63 +++++++++---------- 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp index e9e3b872e..4b8b8eb01 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp @@ -58,6 +58,8 @@ namespace ATLauncher { +static Meta::VersionPtr getComponentVersion(const QString& uid, const QString& version); + PackInstallTask::PackInstallTask(UserInteractionSupport *support, QString pack, QString version) { m_support = support; @@ -115,19 +117,11 @@ void PackInstallTask::onDownloadSucceeded() } m_version = version; - auto vlist = APPLICATION->metadataIndex()->get("net.minecraft"); - if(!vlist) - { - emitFailed(tr("Failed to get local metadata index for %1").arg("net.minecraft")); - return; - } - - auto ver = vlist->getVersion(m_version.minecraft); + auto ver = getComponentVersion("net.minecraft", m_version.minecraft); if (!ver) { - emitFailed(tr("Failed to get local metadata index for '%1' v%2").arg("net.minecraft").arg(m_version.minecraft)); + emitFailed(tr("Failed to get local metadata index for '%1' v%2").arg("net.minecraft", m_version.minecraft)); return; } - ver->load(Net::Mode::Online); minecraftVersion = ver; if(m_version.noConfigs) { @@ -359,17 +353,10 @@ bool PackInstallTask::createLibrariesComponent(QString instanceRoot, std::shared for(const auto & lib : m_version.libraries) { // If the library is LiteLoader, we need to ignore it and handle it separately. if (liteLoaderMap.contains(lib.md5)) { - auto vlist = APPLICATION->metadataIndex()->get("com.mumfrey.liteloader"); - if (vlist) { - if (!vlist->isLoaded()) - vlist->load(Net::Mode::Online); - - auto ver = vlist->getVersion(liteLoaderMap.value(lib.md5)); - if (ver) { - ver->load(Net::Mode::Online); - componentsToInstall.insert("com.mumfrey.liteloader", ver); - continue; - } + auto ver = getComponentVersion("com.mumfrey.liteloader", liteLoaderMap.value(lib.md5)); + if (ver) { + componentsToInstall.insert("com.mumfrey.liteloader", ver); + continue; } } @@ -643,17 +630,10 @@ void PackInstallTask::downloadMods() auto path = FS::PathCombine(m_stagingPath, "minecraft", relpath, mod.file); if(mod.type == ModType::Forge) { - auto vlist = APPLICATION->metadataIndex()->get("net.minecraftforge"); - if(vlist) - { - if (!vlist->isLoaded()) - vlist->load(Net::Mode::Online); - auto ver = vlist->getVersion(mod.version); - if(ver) { - ver->load(Net::Mode::Online); - componentsToInstall.insert("net.minecraftforge", ver); - continue; - } + auto ver = getComponentVersion("net.minecraftforge", mod.version); + if (ver) { + componentsToInstall.insert("net.minecraftforge", ver); + continue; } qDebug() << "Jarmod: " + path; @@ -850,4 +830,23 @@ void PackInstallTask::install() emitSucceeded(); } +static Meta::VersionPtr getComponentVersion(const QString& uid, const QString& version) +{ + auto vlist = APPLICATION->metadataIndex()->get(uid); + if (!vlist) + return {}; + + if (!vlist->isLoaded()) + vlist->load(Net::Mode::Online); + + auto ver = vlist->getVersion(version); + if (!ver) + return {}; + + if (!ver->isLoaded()) + ver->load(Net::Mode::Online); + + return ver; +} + } From 188c5aaa356323392be1100d74f62d70ab298695 Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Tue, 17 May 2022 18:43:35 +0100 Subject: [PATCH 126/157] Launch: Match Vanilla launcher version string behaviour This removes a means of profiling users. --- launcher/minecraft/MinecraftInstance.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index e20dc24c7..61326fac8 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -487,9 +488,8 @@ QStringList MinecraftInstance::processMinecraftArgs( } } - // blatant self-promotion. - token_mapping["profile_name"] = token_mapping["version_name"] = BuildConfig.LAUNCHER_NAME; - + token_mapping["profile_name"] = name(); + token_mapping["version_name"] = profile->getMinecraftVersion(); token_mapping["version_type"] = profile->getMinecraftVersionType(); QString absRootDir = QDir(gameRoot()).absolutePath(); From 96f16069a93afa320de174e740bc6b915e9a1103 Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Tue, 17 May 2022 21:03:15 +0100 Subject: [PATCH 127/157] Launch: Apply the Minecraft version correctly It was previously using a deprecated field. --- launcher/minecraft/VersionFile.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/launcher/minecraft/VersionFile.cpp b/launcher/minecraft/VersionFile.cpp index 9db30ba2f..f242fbe7b 100644 --- a/launcher/minecraft/VersionFile.cpp +++ b/launcher/minecraft/VersionFile.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2022 Jamie Mansfield * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -55,7 +56,7 @@ void VersionFile::applyTo(LaunchProfile *profile) // Only real Minecraft can set those. Don't let anything override them. if (isMinecraftVersion(uid)) { - profile->applyMinecraftVersion(minecraftVersion); + profile->applyMinecraftVersion(version); profile->applyMinecraftVersionType(type); // HACK: ignore assets from other version files than Minecraft // workaround for stupid assets issue caused by amazon: From 2847cefff701dad137cd04f628c76a9282d04a83 Mon Sep 17 00:00:00 2001 From: dada513 Date: Fri, 20 May 2022 19:56:27 +0200 Subject: [PATCH 128/157] Add cursefrog key override --- launcher/Application.cpp | 11 +++++ launcher/Application.h | 1 + launcher/net/Download.cpp | 3 +- launcher/ui/pages/global/APIPage.cpp | 4 ++ launcher/ui/pages/global/APIPage.ui | 64 ++++++++++++++++++++++------ 5 files changed, 68 insertions(+), 15 deletions(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index dc8a7b0d3..ce62c41af 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -679,6 +679,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) // Custom MSA credentials m_settings->registerSetting("MSAClientIDOverride", ""); + m_settings->registerSetting("CFKeyOverride", ""); // Init page provider { @@ -1508,3 +1509,13 @@ QString Application::getMSAClientID() return BuildConfig.MSA_CLIENT_ID; } + +QString Application::getCurseKey() +{ + QString keyOverride = m_settings->get("CFKeyOverride").toString(); + if (!keyOverride.isEmpty()) { + return keyOverride; + } + + return BuildConfig.CURSEFORGE_API_KEY; +} diff --git a/launcher/Application.h b/launcher/Application.h index 172321c02..3129b4fb8 100644 --- a/launcher/Application.h +++ b/launcher/Application.h @@ -155,6 +155,7 @@ class Application : public QApplication QString getJarsPath(); QString getMSAClientID(); + QString getCurseKey(); /// this is the root of the 'installation'. Used for automatic updates const QString &root() { diff --git a/launcher/net/Download.cpp b/launcher/net/Download.cpp index 65cc8f67a..7a4016094 100644 --- a/launcher/net/Download.cpp +++ b/launcher/net/Download.cpp @@ -25,6 +25,7 @@ #include "MetaCacheSink.h" #include "BuildConfig.h" +#include "Application.h" namespace Net { @@ -96,7 +97,7 @@ void Download::startImpl() request.setHeader(QNetworkRequest::UserAgentHeader, BuildConfig.USER_AGENT); if (request.url().host().contains("api.curseforge.com")) { - request.setRawHeader("x-api-key", BuildConfig.CURSEFORGE_API_KEY.toUtf8()); + request.setRawHeader("x-api-key", APPLICATION->getCurseKey().toUtf8()); }; QNetworkReply* rep = m_network->get(request); diff --git a/launcher/ui/pages/global/APIPage.cpp b/launcher/ui/pages/global/APIPage.cpp index 287eb74f2..8b806bcf1 100644 --- a/launcher/ui/pages/global/APIPage.cpp +++ b/launcher/ui/pages/global/APIPage.cpp @@ -70,6 +70,8 @@ void APIPage::loadSettings() ui->urlChoices->setCurrentText(pastebinURL); QString msaClientID = s->get("MSAClientIDOverride").toString(); ui->msaClientID->setText(msaClientID); + QString curseKey = s->get("CFKeyOverride").toString(); + ui->curseKey->setText(curseKey); } void APIPage::applySettings() @@ -79,6 +81,8 @@ void APIPage::applySettings() s->set("PastebinURL", pastebinURL); QString msaClientID = ui->msaClientID->text(); s->set("MSAClientIDOverride", msaClientID); + QString curseKey = ui->curseKey->text(); + s->set("CFKeyOverride", curseKey); } bool APIPage::apply() diff --git a/launcher/ui/pages/global/APIPage.ui b/launcher/ui/pages/global/APIPage.ui index acde9aef8..eaa44c888 100644 --- a/launcher/ui/pages/global/APIPage.ui +++ b/launcher/ui/pages/global/APIPage.ui @@ -6,8 +6,8 @@ 0 0 - 491 - 474 + 603 + 530 @@ -148,17 +148,56 @@ - - - Qt::Vertical + + + true - - - 20 - 40 - + + &CurseForge Core API - + + + + + Qt::Horizontal + + + + + + + Note: you probably don't need to set this if CurseForge already works. + + + + + + + true + + + (Default) + + + + + + + Enter a custom API Key for CurseForge here. + + + Qt::RichText + + + true + + + true + + + + + @@ -166,9 +205,6 @@ - - tabWidget - From 6afe59e76b6a5d44b8706e8e030ecd0396dc8801 Mon Sep 17 00:00:00 2001 From: timoreo Date: Fri, 20 May 2022 21:19:19 +0200 Subject: [PATCH 129/157] Very Temporary Fix for curseforge --- .../modplatform/flame/FileResolvingTask.cpp | 16 +- launcher/modplatform/flame/PackManifest.cpp | 16 +- .../ui/pages/modplatform/flame/FlamePage.ui | 179 +++++++++--------- 3 files changed, 118 insertions(+), 93 deletions(-) diff --git a/launcher/modplatform/flame/FileResolvingTask.cpp b/launcher/modplatform/flame/FileResolvingTask.cpp index 95924a681..0deb99c4a 100644 --- a/launcher/modplatform/flame/FileResolvingTask.cpp +++ b/launcher/modplatform/flame/FileResolvingTask.cpp @@ -31,7 +31,21 @@ void Flame::FileResolvingTask::netJobFinished() for (auto& bytes : results) { auto& out = m_toProcess.files[index]; try { - failed &= (!out.parseFromBytes(bytes)); + bool fail = (!out.parseFromBytes(bytes)); + if(fail){ + //failed :( probably disabled mod, try to add to the list + auto doc = Json::requireDocument(bytes); + if (!doc.isObject()) { + throw JSONValidationError(QString("data is not an object? that's not supposed to happen")); + } + auto obj = Json::ensureObject(doc.object(), "data"); + //FIXME : HACK, MAY NOT WORK FOR LONG + out.url = QUrl(QString("https://media.forgecdn.net/files/%1/%2/%3") + .arg(QString::number(QString::number(out.fileId).leftRef(4).toInt()) + ,QString::number(QString::number(out.fileId).rightRef(3).toInt()) + ,QUrl::toPercentEncoding(out.fileName)), QUrl::TolerantMode); + } + failed &= fail; } catch (const JSONValidationError& e) { qCritical() << "Resolving of" << out.projectId << out.fileId << "failed because of a parsing error:"; qCritical() << e.cause(); diff --git a/launcher/modplatform/flame/PackManifest.cpp b/launcher/modplatform/flame/PackManifest.cpp index e4f90c1a1..c78783a0a 100644 --- a/launcher/modplatform/flame/PackManifest.cpp +++ b/launcher/modplatform/flame/PackManifest.cpp @@ -71,11 +71,6 @@ bool Flame::File::parseFromBytes(const QByteArray& bytes) fileName = Json::requireString(obj, "fileName"); - QString rawUrl = Json::requireString(obj, "downloadUrl"); - url = QUrl(rawUrl, QUrl::TolerantMode); - if (!url.isValid()) { - throw JSONValidationError(QString("Invalid URL: %1").arg(rawUrl)); - } // This is a piece of a Flame project JSON pulled out into the file metadata (here) for convenience // It is also optional type = File::Type::SingleFile; @@ -87,7 +82,16 @@ bool Flame::File::parseFromBytes(const QByteArray& bytes) // this is probably a mod, dunno what else could modpacks download targetFolder = "mods"; } - + if(!obj.contains("downloadUrl") || obj["downloadUrl"].isNull() || !obj["downloadUrl"].isString() || obj["downloadUrl"].toString().isEmpty()){ + //either there somehow is an emtpy string as a link, or it's null either way it's invalid + //soft failing + return false; + } + QString rawUrl = Json::requireString(obj, "downloadUrl"); + url = QUrl(rawUrl, QUrl::TolerantMode); + if (!url.isValid()) { + throw JSONValidationError(QString("Invalid URL: %1").arg(rawUrl)); + } resolved = true; return true; } diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.ui b/launcher/ui/pages/modplatform/flame/FlamePage.ui index 6d8d8e10d..b337d6720 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.ui +++ b/launcher/ui/pages/modplatform/flame/FlamePage.ui @@ -1,90 +1,97 @@ - FlamePage - - - - 0 - 0 - 837 - 685 - - - - - - - - - - 48 - 48 - - - - Qt::ScrollBarAlwaysOff - - - true - - - - - - - true - - - true - - - - - - - - - - - - - - Version selected: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - - - - - Search - - - - - - - Search and filter... - - - + FlamePage + + + + 0 + 0 + 1989 + 685 + + + + + + + Search + + + + + + + Search and filter... + + + + + + + + + Qt::ScrollBarAlwaysOff + + + true + + + + 48 + 48 + + + + + + + + true + + + true + + + - - - searchEdit - searchButton - packView - packDescription - sortByBox - versionSelectionBox - - - +
+ + + + + + + + + Version selected: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + + WARNING !! Curseforge is very unreliable and low quality. Some mod authors have disabled the ability for third party apps (like polymc) to download the mods, you may need to manually download some mods + + + + + + + searchEdit + searchButton + packView + packDescription + sortByBox + versionSelectionBox + + + From cbc8c1aed63e9cd106b468ae720aa650beec646a Mon Sep 17 00:00:00 2001 From: Kenneth Chew <79120643+kthchew@users.noreply.github.com> Date: Fri, 20 May 2022 15:56:13 -0400 Subject: [PATCH 130/157] Use consistent naming scheme Co-authored-by: Sefa Eyeoglu --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7ab30d456..d12f176c6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -269,7 +269,7 @@ jobs: if: runner.os == 'Windows' uses: actions/upload-artifact@v3 with: - name: PolyMC-${{ matrix.name }}-${{ env.VERSION }}-${{ inputs.build_type }}-Setup + name: PolyMC-${{ matrix.name }}-Setup-${{ env.VERSION }}-${{ inputs.build_type }} path: PolyMC-Setup.exe - name: Upload binary tarball (Linux) From 30b56dbcbd3bb7d61210405a469c7efb28581904 Mon Sep 17 00:00:00 2001 From: timoreo Date: Fri, 20 May 2022 22:00:38 +0200 Subject: [PATCH 131/157] Port temp fix to mods too --- launcher/modplatform/flame/FlameModIndex.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/launcher/modplatform/flame/FlameModIndex.cpp b/launcher/modplatform/flame/FlameModIndex.cpp index ba0824cf5..9846b1562 100644 --- a/launcher/modplatform/flame/FlameModIndex.cpp +++ b/launcher/modplatform/flame/FlameModIndex.cpp @@ -56,8 +56,15 @@ void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, file.fileId = Json::requireInteger(obj, "id"); file.date = Json::requireString(obj, "fileDate"); file.version = Json::requireString(obj, "displayName"); - file.downloadUrl = Json::requireString(obj, "downloadUrl"); file.fileName = Json::requireString(obj, "fileName"); + file.downloadUrl = Json::ensureString(obj, "downloadUrl", ""); + if(file.downloadUrl.isEmpty()){ + //FIXME : HACK, MAY NOT WORK FOR LONG + file.downloadUrl = QString("https://media.forgecdn.net/files/%1/%2/%3") + .arg(QString::number(QString::number(file.fileId.toInt()).leftRef(4).toInt()) + ,QString::number(QString::number(file.fileId.toInt()).rightRef(3).toInt()) + ,QUrl::toPercentEncoding(file.fileName)); + } unsortedVersions.append(file); } From 6542f5f15af31e493c9b46afb3a5b4b330cc9cee Mon Sep 17 00:00:00 2001 From: timoreo Date: Fri, 20 May 2022 22:06:36 +0200 Subject: [PATCH 132/157] Apply suggestions --- launcher/modplatform/flame/PackManifest.cpp | 5 +- .../ui/pages/modplatform/flame/FlamePage.ui | 65 ++++++++++--------- 2 files changed, 38 insertions(+), 32 deletions(-) diff --git a/launcher/modplatform/flame/PackManifest.cpp b/launcher/modplatform/flame/PackManifest.cpp index c78783a0a..3217a7569 100644 --- a/launcher/modplatform/flame/PackManifest.cpp +++ b/launcher/modplatform/flame/PackManifest.cpp @@ -82,12 +82,13 @@ bool Flame::File::parseFromBytes(const QByteArray& bytes) // this is probably a mod, dunno what else could modpacks download targetFolder = "mods"; } - if(!obj.contains("downloadUrl") || obj["downloadUrl"].isNull() || !obj["downloadUrl"].isString() || obj["downloadUrl"].toString().isEmpty()){ + QString rawUrl = Json::ensureString(obj, "downloadUrl"); + + if(rawUrl.isEmpty()){ //either there somehow is an emtpy string as a link, or it's null either way it's invalid //soft failing return false; } - QString rawUrl = Json::requireString(obj, "downloadUrl"); url = QUrl(rawUrl, QUrl::TolerantMode); if (!url.isValid()) { throw JSONValidationError(QString("Invalid URL: %1").arg(rawUrl)); diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.ui b/launcher/ui/pages/modplatform/flame/FlamePage.ui index b337d6720..4c7a6495a 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.ui +++ b/launcher/ui/pages/modplatform/flame/FlamePage.ui @@ -6,26 +6,39 @@ 0 0 - 1989 + 2445 685 - - - - Search - - + + + + + + + + + Version selected: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + - + Search and filter... - + @@ -55,30 +68,22 @@ - - - - - - - - - Version selected: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - + + + + Search + + - + + + + true + + - WARNING !! Curseforge is very unreliable and low quality. Some mod authors have disabled the ability for third party apps (like polymc) to download the mods, you may need to manually download some mods + WARNING: CurseForge's API is very unreliable and low quality. Also, some mod authors have disabled the ability for third party apps (like PolyMC) to download their mods. As such, you may need to manually download some mods to be able to use the modpack. From 3b4b34b3695e655f591347754a724804bea96d71 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Fri, 20 May 2022 22:46:35 +0200 Subject: [PATCH 133/157] fix(ui): make CF and MR modpack dialogs more consistent --- .../ui/pages/modplatform/flame/FlamePage.ui | 100 ++++++++++-------- .../modplatform/modrinth/ModrinthPage.ui | 7 +- 2 files changed, 59 insertions(+), 48 deletions(-) diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.ui b/launcher/ui/pages/modplatform/flame/FlamePage.ui index 4c7a6495a..9fab97737 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.ui +++ b/launcher/ui/pages/modplatform/flame/FlamePage.ui @@ -6,41 +6,50 @@ 0 0 - 2445 - 685 + 800 + 600 - - - - + + + + + true + + + + Note: CurseForge's API is very unreliable. CurseForge and some mod authors have disallowed downloading mods in third-party applications like PolyMC. As such, you may need to manually download some mods to be able to install a modpack. + + + Qt::AlignCenter + + + true + + + + + + + + + Search and filter... + + - - + + - Version selected: - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + Search - - - - - - - Search and filter... - - - - - - + + + Qt::ScrollBarAlwaysOff @@ -56,7 +65,7 @@ - + true @@ -68,30 +77,29 @@ - - - - Search - - - - - - - - true - - - - WARNING: CurseForge's API is very unreliable and low quality. Also, some mod authors have disabled the ability for third party apps (like PolyMC) to download their mods. As such, you may need to manually download some mods to be able to use the modpack. - - + + + + + + + + + Version selected: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + - searchEdit - searchButton packView packDescription sortByBox diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui index 4fb59cdf0..ae9556edf 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui @@ -6,8 +6,8 @@ 0 0 - 837 - 685 + 800 + 600 @@ -24,6 +24,9 @@ Qt::AlignCenter + + true + From 2bc6da038dea701699ba9fc46eb68b3a74d5f488 Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Fri, 20 May 2022 17:09:26 -0400 Subject: [PATCH 134/157] Add installer to release workflow --- .github/workflows/trigger_release.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/trigger_release.yml b/.github/workflows/trigger_release.yml index ff0d2b3fa..91cd04742 100644 --- a/.github/workflows/trigger_release.yml +++ b/.github/workflows/trigger_release.yml @@ -43,10 +43,12 @@ jobs: for d in PolyMC-Windows-*; do cd "${d}" || continue ARCH="$(echo -n ${d} | cut -d '-' -f 3)" + INST="$(echo -n ${d} | grep -o Setup || true)" PORT="$(echo -n ${d} | grep -o Portable || true)" NAME="PolyMC-Windows-${ARCH}" test -z "${PORT}" || NAME="${NAME}-Portable" - zip -r -9 "../${NAME}-${{ env.VERSION }}.zip" * + test -z "${INST}" || mv PolyMC-*.exe ../${NAME}-Setup-${{ env.VERSION }}.exe + test -n "${INST}" || zip -r -9 "../${NAME}-${{ env.VERSION }}.zip" * cd .. done @@ -66,7 +68,9 @@ jobs: PolyMC-Linux-${{ env.VERSION }}-x86_64.AppImage PolyMC-Windows-i686-${{ env.VERSION }}.zip PolyMC-Windows-i686-Portable-${{ env.VERSION }}.zip + PolyMC-Windows-i686-Setup-${{ env.VERSION }}.exe PolyMC-Windows-x86_64-${{ env.VERSION }}.zip PolyMC-Windows-x86_64-Portable-${{ env.VERSION }}.zip + PolyMC-Windows-x86_64-Setup-${{ env.VERSION }}.exe PolyMC-macOS-${{ env.VERSION }}.tar.gz PolyMC-${{ env.VERSION }}.tar.gz From 12cadf3af0a4e3a01330283fae2d6267d3c3f525 Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Fri, 20 May 2022 17:09:42 -0400 Subject: [PATCH 135/157] Add `/NoUninstaller` parameter for Windows installer --- program_info/win_install.nsi | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/program_info/win_install.nsi b/program_info/win_install.nsi index ce13b8b00..2d3f0f576 100644 --- a/program_info/win_install.nsi +++ b/program_info/win_install.nsi @@ -1,4 +1,5 @@ !include "FileFunc.nsh" +!include "LogicLib.nsh" !include "MUI2.nsh" Unicode true @@ -119,20 +120,24 @@ Section "PolyMC" WriteRegStr HKCU Software\PolyMC "InstallDir" "$INSTDIR" ; Write the uninstall keys for Windows - !define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\PolyMC" - WriteRegStr HKCU "${UNINST_KEY}" "DisplayName" "PolyMC" - WriteRegStr HKCU "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\polymc.exe" - WriteRegStr HKCU "${UNINST_KEY}" "UninstallString" '"$INSTDIR\uninstall.exe"' - WriteRegStr HKCU "${UNINST_KEY}" "QuietUninstallString" '"$INSTDIR\uninstall.exe" /S' - WriteRegStr HKCU "${UNINST_KEY}" "InstallLocation" "$INSTDIR" - WriteRegStr HKCU "${UNINST_KEY}" "Publisher" "PolyMC Contributors" - WriteRegStr HKCU "${UNINST_KEY}" "ProductVersion" "${VERSION}" - ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2 - IntFmt $0 "0x%08X" $0 - WriteRegDWORD HKCU "${UNINST_KEY}" "EstimatedSize" "$0" - WriteRegDWORD HKCU "${UNINST_KEY}" "NoModify" 1 - WriteRegDWORD HKCU "${UNINST_KEY}" "NoRepair" 1 - WriteUninstaller "$INSTDIR\uninstall.exe" + ${GetParameters} $R0 + ${GetOptions} $R0 "/NoUninstaller" $R1 + ${If} ${Errors} + !define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\PolyMC" + WriteRegStr HKCU "${UNINST_KEY}" "DisplayName" "PolyMC" + WriteRegStr HKCU "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\polymc.exe" + WriteRegStr HKCU "${UNINST_KEY}" "UninstallString" '"$INSTDIR\uninstall.exe"' + WriteRegStr HKCU "${UNINST_KEY}" "QuietUninstallString" '"$INSTDIR\uninstall.exe" /S' + WriteRegStr HKCU "${UNINST_KEY}" "InstallLocation" "$INSTDIR" + WriteRegStr HKCU "${UNINST_KEY}" "Publisher" "PolyMC Contributors" + WriteRegStr HKCU "${UNINST_KEY}" "ProductVersion" "${VERSION}" + ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2 + IntFmt $0 "0x%08X" $0 + WriteRegDWORD HKCU "${UNINST_KEY}" "EstimatedSize" "$0" + WriteRegDWORD HKCU "${UNINST_KEY}" "NoModify" 1 + WriteRegDWORD HKCU "${UNINST_KEY}" "NoRepair" 1 + WriteUninstaller "$INSTDIR\uninstall.exe" + ${EndIf} SectionEnd From cdd83c279cafdacee6c863d7fb0ae94a6bf34e3e Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Fri, 20 May 2022 17:12:08 -0400 Subject: [PATCH 136/157] Remove portable option in Windows installer --- .github/workflows/build.yml | 2 +- program_info/win_install.nsi | 7 ------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d12f176c6..53db7d761 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -195,7 +195,7 @@ jobs: if: runner.os == 'Windows' shell: msys2 {0} run: | - cd ${{ env.INSTALL_PORTABLE_DIR }} + cd ${{ env.INSTALL_DIR }} makensis -NOCD "-DVERSION=${{ env.VERSION }}" "-DMUI_ICON=${{ github.workspace }}/program_info/polymc.ico" "-XOutFile ${{ github.workspace }}/PolyMC-Setup.exe" "${{ github.workspace }}/program_info/win_install.nsi" - name: Package (Linux) diff --git a/program_info/win_install.nsi b/program_info/win_install.nsi index 2d3f0f576..a47d4ae3f 100644 --- a/program_info/win_install.nsi +++ b/program_info/win_install.nsi @@ -147,13 +147,6 @@ Section "Start Menu Shortcuts" SectionEnd -Section /o "Portable" - - SetOutPath $INSTDIR - File "portable.txt" - -SectionEnd - ;-------------------------------- ; Uninstaller From 1ec7878c07a8ba7d04a9fe860761872547fd5a0d Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Fri, 20 May 2022 17:22:30 -0400 Subject: [PATCH 137/157] Add `/NoShortcuts` parameter for Windows installer --- program_info/win_install.nsi | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/program_info/win_install.nsi b/program_info/win_install.nsi index a47d4ae3f..7d48ccf24 100644 --- a/program_info/win_install.nsi +++ b/program_info/win_install.nsi @@ -141,7 +141,7 @@ Section "PolyMC" SectionEnd -Section "Start Menu Shortcuts" +Section "Start Menu Shortcuts" SHORTCUTS CreateShortcut "$SMPROGRAMS\PolyMC.lnk" "$INSTDIR\polymc.exe" "" "$INSTDIR\polymc.exe" 0 @@ -219,3 +219,15 @@ Section "Uninstall" RMDir "$INSTDIR" SectionEnd + +;-------------------------------- + +; Extra command line parameters + +Function .onInit +${GetParameters} $R0 +${GetOptions} $R0 "/NoShortcuts" $R1 +${IfNot} ${Errors} + !insertmacro UnselectSection ${SHORTCUTS} +${EndIf} +FunctionEnd From 3cab0e69f1c299e385330d97ab5159a0c8c904ec Mon Sep 17 00:00:00 2001 From: Kenneth Chew Date: Fri, 20 May 2022 17:23:11 -0400 Subject: [PATCH 138/157] Fix default install location --- program_info/win_install.nsi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/program_info/win_install.nsi b/program_info/win_install.nsi index 7d48ccf24..4ca4de1ad 100644 --- a/program_info/win_install.nsi +++ b/program_info/win_install.nsi @@ -5,7 +5,7 @@ Unicode true Name "PolyMC" -InstallDir "$LOCALAPPDATA\PolyMC" +InstallDir "$LOCALAPPDATA\Programs\PolyMC" InstallDirRegKey HKCU "Software\PolyMC" "InstallDir" RequestExecutionLevel user From c04adf74521127bc50c67f3e2ddd1edfe2330358 Mon Sep 17 00:00:00 2001 From: timoreo Date: Sat, 21 May 2022 08:31:07 +0200 Subject: [PATCH 139/157] Do the url trick on initial modpack download too --- launcher/modplatform/flame/FlamePackIndex.cpp | 10 +++++++++- launcher/modplatform/flame/FlamePackIndex.h | 1 + 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/launcher/modplatform/flame/FlamePackIndex.cpp b/launcher/modplatform/flame/FlamePackIndex.cpp index ac24c6471..6d48a3bf2 100644 --- a/launcher/modplatform/flame/FlamePackIndex.cpp +++ b/launcher/modplatform/flame/FlamePackIndex.cpp @@ -65,7 +65,15 @@ void Flame::loadIndexedPackVersions(Flame::IndexedPack& pack, QJsonArray& arr) // pick the latest version supported file.mcVersion = versionArray[0].toString(); file.version = Json::requireString(version, "displayName"); - file.downloadUrl = Json::requireString(version, "downloadUrl"); + file.fileName = Json::requireString(version, "fileName"); + file.downloadUrl = Json::ensureString(version, "downloadUrl"); + if(file.downloadUrl.isEmpty()){ + //FIXME : HACK, MAY NOT WORK FOR LONG + file.downloadUrl = QString("https://media.forgecdn.net/files/%1/%2/%3") + .arg(QString::number(QString::number(file.fileId).leftRef(4).toInt()) + ,QString::number(QString::number(file.fileId).rightRef(3).toInt()) + ,QUrl::toPercentEncoding(file.fileName)); + } unsortedVersions.append(file); } diff --git a/launcher/modplatform/flame/FlamePackIndex.h b/launcher/modplatform/flame/FlamePackIndex.h index 7ffa29c3d..a8bb15be4 100644 --- a/launcher/modplatform/flame/FlamePackIndex.h +++ b/launcher/modplatform/flame/FlamePackIndex.h @@ -18,6 +18,7 @@ struct IndexedVersion { QString version; QString mcVersion; QString downloadUrl; + QString fileName; }; struct IndexedPack From 7c251efc473ee90069d1e87a056bde64f1d6fbf7 Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Mon, 2 May 2022 20:27:20 +0100 Subject: [PATCH 140/157] ATLauncher: Display mod colours in optional mod dialog --- .../atlauncher/ATLPackInstallTask.cpp | 2 +- .../atlauncher/ATLPackInstallTask.h | 2 +- .../atlauncher/ATLPackManifest.cpp | 6 ++++++ .../modplatform/atlauncher/ATLPackManifest.h | 6 +++++- .../atlauncher/AtlOptionalModDialog.cpp | 21 +++++++++++++------ .../atlauncher/AtlOptionalModDialog.h | 6 ++++-- .../pages/modplatform/atlauncher/AtlPage.cpp | 5 +++-- .../ui/pages/modplatform/atlauncher/AtlPage.h | 2 +- 8 files changed, 36 insertions(+), 14 deletions(-) diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp index 4b8b8eb01..90dc13654 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp @@ -556,7 +556,7 @@ void PackInstallTask::downloadMods() QVector selectedMods; if (!optionalMods.isEmpty()) { setStatus(tr("Selecting optional mods...")); - selectedMods = m_support->chooseOptionalMods(optionalMods); + selectedMods = m_support->chooseOptionalMods(m_version, optionalMods); } setStatus(tr("Downloading mods...")); diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.h b/launcher/modplatform/atlauncher/ATLPackInstallTask.h index 783ec19b0..6bc30689e 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.h +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.h @@ -37,7 +37,7 @@ class UserInteractionSupport { /** * Requests a user interaction to select which optional mods should be installed. */ - virtual QVector chooseOptionalMods(QVector mods) = 0; + virtual QVector chooseOptionalMods(PackVersion version, QVector mods) = 0; /** * Requests a user interaction to select a component version from a given version list diff --git a/launcher/modplatform/atlauncher/ATLPackManifest.cpp b/launcher/modplatform/atlauncher/ATLPackManifest.cpp index 40be6d537..a8f2711b0 100644 --- a/launcher/modplatform/atlauncher/ATLPackManifest.cpp +++ b/launcher/modplatform/atlauncher/ATLPackManifest.cpp @@ -178,6 +178,7 @@ static void loadVersionMod(ATLauncher::VersionMod & p, QJsonObject & obj) { p.depends.append(Json::requireString(depends)); } } + p.colour = Json::ensureString(obj, QString("colour"), ""); p.client = Json::ensureBoolean(obj, QString("client"), false); @@ -232,4 +233,9 @@ void ATLauncher::loadVersion(PackVersion & v, QJsonObject & obj) auto configsObj = Json::requireObject(obj, "configs"); loadVersionConfigs(v.configs, configsObj); } + + auto colourObj = Json::ensureObject(obj, "colours"); + for (const auto &key : colourObj.keys()) { + v.colours[key] = Json::requireString(colourObj.value(key), "colour"); + } } diff --git a/launcher/modplatform/atlauncher/ATLPackManifest.h b/launcher/modplatform/atlauncher/ATLPackManifest.h index 673f2f8bc..2911107ed 100644 --- a/launcher/modplatform/atlauncher/ATLPackManifest.h +++ b/launcher/modplatform/atlauncher/ATLPackManifest.h @@ -16,9 +16,10 @@ #pragma once +#include +#include #include #include -#include namespace ATLauncher { @@ -109,6 +110,7 @@ struct VersionMod bool library; QString group; QVector depends; + QString colour; bool client; @@ -134,6 +136,8 @@ struct PackVersion QVector libraries; QVector mods; VersionConfigs configs; + + QMap colours; }; void loadVersion(PackVersion & v, QJsonObject & obj); diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp index 26aa60af5..aee5a78e5 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp @@ -43,8 +43,11 @@ #include "modplatform/atlauncher/ATLShareCode.h" #include "Application.h" -AtlOptionalModListModel::AtlOptionalModListModel(QWidget *parent, QVector mods) - : QAbstractListModel(parent), m_mods(mods) { +AtlOptionalModListModel::AtlOptionalModListModel(QWidget* parent, ATLauncher::PackVersion version, QVector mods) + : QAbstractListModel(parent) + , m_version(version) + , m_mods(mods) +{ // fill mod index for (int i = 0; i < m_mods.size(); i++) { auto mod = m_mods.at(i); @@ -97,6 +100,11 @@ QVariant AtlOptionalModListModel::data(const QModelIndex &index, int role) const return mod.description; } } + else if (role == Qt::ForegroundRole) { + if (!mod.colour.isEmpty() && m_version.colours.contains(mod.colour)) { + return QColor(QString("#%1").arg(m_version.colours[mod.colour])); + } + } else if (role == Qt::CheckStateRole) { if (index.column() == EnabledColumn) { return m_selection[mod.name] ? Qt::Checked : Qt::Unchecked; @@ -287,12 +295,13 @@ void AtlOptionalModListModel::setMod(ATLauncher::VersionMod mod, int index, bool } } - -AtlOptionalModDialog::AtlOptionalModDialog(QWidget *parent, QVector mods) - : QDialog(parent), ui(new Ui::AtlOptionalModDialog) { +AtlOptionalModDialog::AtlOptionalModDialog(QWidget* parent, ATLauncher::PackVersion version, QVector mods) + : QDialog(parent) + , ui(new Ui::AtlOptionalModDialog) +{ ui->setupUi(this); - listModel = new AtlOptionalModListModel(this, mods); + listModel = new AtlOptionalModListModel(this, version, mods); ui->treeView->setModel(listModel); ui->treeView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h index 953b288ea..8e02444e4 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h +++ b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h @@ -56,7 +56,7 @@ class AtlOptionalModListModel : public QAbstractListModel { DescriptionColumn, }; - AtlOptionalModListModel(QWidget *parent, QVector mods); + AtlOptionalModListModel(QWidget *parent, ATLauncher::PackVersion version, QVector mods); QVector getResult(); @@ -86,7 +86,9 @@ public slots: NetJob::Ptr m_jobPtr; QByteArray m_response; + ATLauncher::PackVersion m_version; QVector m_mods; + QMap m_selection; QMap m_index; QMap> m_dependants; @@ -96,7 +98,7 @@ class AtlOptionalModDialog : public QDialog { Q_OBJECT public: - AtlOptionalModDialog(QWidget *parent, QVector mods); + AtlOptionalModDialog(QWidget *parent, ATLauncher::PackVersion version, QVector mods); ~AtlOptionalModDialog() override; QVector getResult() { diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp index df9b92070..03923ed9c 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp @@ -169,8 +169,9 @@ void AtlPage::onVersionSelectionChanged(QString data) suggestCurrent(); } -QVector AtlPage::chooseOptionalMods(QVector mods) { - AtlOptionalModDialog optionalModDialog(this, mods); +QVector AtlPage::chooseOptionalMods(ATLauncher::PackVersion version, QVector mods) +{ + AtlOptionalModDialog optionalModDialog(this, version, mods); optionalModDialog.exec(); return optionalModDialog.getResult(); } diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.h b/launcher/ui/pages/modplatform/atlauncher/AtlPage.h index c95b01275..eac86b51b 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlPage.h +++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.h @@ -84,7 +84,7 @@ Q_OBJECT void suggestCurrent(); QString chooseVersion(Meta::VersionListPtr vlist, QString minecraftVersion) override; - QVector chooseOptionalMods(QVector mods) override; + QVector chooseOptionalMods(ATLauncher::PackVersion version, QVector mods) override; private slots: void triggerSearch(); From 305973c0e7c07693a8b08d1908e64fc4986e13e0 Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Thu, 5 May 2022 20:14:19 +0100 Subject: [PATCH 141/157] ATLauncher: Display install messages if applicable --- .../atlauncher/ATLPackInstallTask.cpp | 7 ++- .../atlauncher/ATLPackInstallTask.h | 45 +++++++++++++---- .../atlauncher/ATLPackManifest.cpp | 50 +++++++++++++++---- .../modplatform/atlauncher/ATLPackManifest.h | 46 +++++++++++++---- .../pages/modplatform/atlauncher/AtlPage.cpp | 13 ++++- .../ui/pages/modplatform/atlauncher/AtlPage.h | 1 + 6 files changed, 126 insertions(+), 36 deletions(-) diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp index 90dc13654..9b14f3557 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp @@ -95,14 +95,13 @@ void PackInstallTask::onDownloadSucceeded() qDebug() << "PackInstallTask::onDownloadSucceeded: " << QThread::currentThreadId(); jobPtr.reset(); - QJsonParseError parse_error; + QJsonParseError parse_error {}; QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); if(parse_error.error != QJsonParseError::NoError) { qWarning() << "Error while parsing JSON response from FTB at " << parse_error.offset << " reason: " << parse_error.errorString(); qWarning() << response; return; } - auto obj = doc.object(); ATLauncher::PackVersion version; @@ -117,6 +116,10 @@ void PackInstallTask::onDownloadSucceeded() } m_version = version; + // Display install message if one exists + if (!m_version.messages.install.isEmpty()) + m_support->displayMessage(m_version.messages.install); + auto ver = getComponentVersion("net.minecraft", m_version.minecraft); if (!ver) { emitFailed(tr("Failed to get local metadata index for '%1' v%2").arg("net.minecraft", m_version.minecraft)); diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.h b/launcher/modplatform/atlauncher/ATLPackInstallTask.h index 6bc30689e..f0af4e3a2 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.h +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.h @@ -1,18 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0-only /* - * Copyright 2020-2021 Jamie Mansfield - * Copyright 2021 Petr Mrazek + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2020-2021 Jamie Mansfield + * Copyright 2021 Petr Mrazek + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #pragma once @@ -45,6 +64,10 @@ class UserInteractionSupport { */ virtual QString chooseVersion(Meta::VersionListPtr vlist, QString minecraftVersion) = 0; + /** + * Requests a user interaction to display a message. + */ + virtual void displayMessage(QString message) = 0; }; class PackInstallTask : public InstanceTask diff --git a/launcher/modplatform/atlauncher/ATLPackManifest.cpp b/launcher/modplatform/atlauncher/ATLPackManifest.cpp index a8f2711b0..259c170cc 100644 --- a/launcher/modplatform/atlauncher/ATLPackManifest.cpp +++ b/launcher/modplatform/atlauncher/ATLPackManifest.cpp @@ -1,18 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0-only /* - * Copyright 2020-2021 Jamie Mansfield - * Copyright 2021 Petr Mrazek + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2020-2021 Jamie Mansfield + * Copyright 2021 Petr Mrazek + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #include "ATLPackManifest.h" @@ -186,6 +205,12 @@ static void loadVersionMod(ATLauncher::VersionMod & p, QJsonObject & obj) { p.effectively_hidden = p.hidden || p.library; } +static void loadVersionMessages(ATLauncher::VersionMessages& m, QJsonObject& obj) +{ + m.install = Json::ensureString(obj, "install", ""); + m.update = Json::ensureString(obj, "update", ""); +} + void ATLauncher::loadVersion(PackVersion & v, QJsonObject & obj) { v.version = Json::requireString(obj, "version"); @@ -238,4 +263,7 @@ void ATLauncher::loadVersion(PackVersion & v, QJsonObject & obj) for (const auto &key : colourObj.keys()) { v.colours[key] = Json::requireString(colourObj.value(key), "colour"); } + + auto messages = Json::ensureObject(obj, "messages"); + loadVersionMessages(v.messages, messages); } diff --git a/launcher/modplatform/atlauncher/ATLPackManifest.h b/launcher/modplatform/atlauncher/ATLPackManifest.h index 2911107ed..931a11dc3 100644 --- a/launcher/modplatform/atlauncher/ATLPackManifest.h +++ b/launcher/modplatform/atlauncher/ATLPackManifest.h @@ -1,17 +1,36 @@ +// SPDX-License-Identifier: GPL-3.0-only /* - * Copyright 2020 Jamie Mansfield + * PolyMC - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. * - * http://www.apache.org/licenses/LICENSE-2.0 + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2020 Jamie Mansfield + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ #pragma once @@ -124,6 +143,12 @@ struct VersionConfigs QString sha1; }; +struct VersionMessages +{ + QString install; + QString update; +}; + struct PackVersion { QString version; @@ -138,6 +163,7 @@ struct PackVersion VersionConfigs configs; QMap colours; + VersionMessages messages; }; void loadVersion(PackVersion & v, QJsonObject & obj); diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp index 03923ed9c..7bc6fc6b8 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp @@ -45,8 +45,12 @@ #include -AtlPage::AtlPage(NewInstanceDialog* dialog, QWidget *parent) - : QWidget(parent), ui(new Ui::AtlPage), dialog(dialog) +#include + +AtlPage::AtlPage(NewInstanceDialog* dialog, QWidget* parent) + : QWidget(parent) + , ui(new Ui::AtlPage) + , dialog(dialog) { ui->setupUi(this); @@ -211,3 +215,8 @@ QString AtlPage::chooseVersion(Meta::VersionListPtr vlist, QString minecraftVers vselect.exec(); return vselect.selectedVersion()->descriptor(); } + +void AtlPage::displayMessage(QString message) +{ + QMessageBox::information(this, tr("Installing"), message); +} diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.h b/launcher/ui/pages/modplatform/atlauncher/AtlPage.h index eac86b51b..aa6d5da15 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlPage.h +++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.h @@ -85,6 +85,7 @@ Q_OBJECT QString chooseVersion(Meta::VersionListPtr vlist, QString minecraftVersion) override; QVector chooseOptionalMods(ATLauncher::PackVersion version, QVector mods) override; + void displayMessage(QString message) override; private slots: void triggerSearch(); From b84d52be3d1109efc2c9e35304831314050bd398 Mon Sep 17 00:00:00 2001 From: Jamie Mansfield Date: Thu, 5 May 2022 20:58:12 +0100 Subject: [PATCH 142/157] ATLauncher: Display warnings when selecting optional mods --- .../modplatform/atlauncher/ATLPackManifest.cpp | 6 ++++++ .../modplatform/atlauncher/ATLPackManifest.h | 2 ++ .../atlauncher/AtlOptionalModDialog.cpp | 16 +++++++++++++++- 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/launcher/modplatform/atlauncher/ATLPackManifest.cpp b/launcher/modplatform/atlauncher/ATLPackManifest.cpp index 259c170cc..d01ec32cf 100644 --- a/launcher/modplatform/atlauncher/ATLPackManifest.cpp +++ b/launcher/modplatform/atlauncher/ATLPackManifest.cpp @@ -198,6 +198,7 @@ static void loadVersionMod(ATLauncher::VersionMod & p, QJsonObject & obj) { } } p.colour = Json::ensureString(obj, QString("colour"), ""); + p.warning = Json::ensureString(obj, QString("warning"), ""); p.client = Json::ensureBoolean(obj, QString("client"), false); @@ -264,6 +265,11 @@ void ATLauncher::loadVersion(PackVersion & v, QJsonObject & obj) v.colours[key] = Json::requireString(colourObj.value(key), "colour"); } + auto warningsObj = Json::ensureObject(obj, "warnings"); + for (const auto &key : warningsObj.keys()) { + v.warnings[key] = Json::requireString(warningsObj.value(key), "warning"); + } + auto messages = Json::ensureObject(obj, "messages"); loadVersionMessages(v.messages, messages); } diff --git a/launcher/modplatform/atlauncher/ATLPackManifest.h b/launcher/modplatform/atlauncher/ATLPackManifest.h index 931a11dc3..23e162e30 100644 --- a/launcher/modplatform/atlauncher/ATLPackManifest.h +++ b/launcher/modplatform/atlauncher/ATLPackManifest.h @@ -130,6 +130,7 @@ struct VersionMod QString group; QVector depends; QString colour; + QString warning; bool client; @@ -163,6 +164,7 @@ struct PackVersion VersionConfigs configs; QMap colours; + QMap warnings; VersionMessages messages; }; diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp index aee5a78e5..004fdc57a 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp @@ -231,7 +231,21 @@ void AtlOptionalModListModel::clearAll() { } void AtlOptionalModListModel::toggleMod(ATLauncher::VersionMod mod, int index) { - setMod(mod, index, !m_selection[mod.name]); + auto enable = !m_selection[mod.name]; + + // If there is a warning for the mod, display that first (if we would be enabling the mod) + if (enable && !mod.warning.isEmpty() && m_version.warnings.contains(mod.warning)) { + auto message = QString("%1

%2") + .arg(m_version.warnings[mod.warning], tr("Are you sure that you want to enable this mod?")); + + // fixme: avoid casting here + auto result = QMessageBox::warning((QWidget*) this->parent(), tr("Warning"), message, QMessageBox::Yes | QMessageBox::No); + if (result != QMessageBox::Yes) { + return; + } + } + + setMod(mod, index, enable); } void AtlOptionalModListModel::setMod(ATLauncher::VersionMod mod, int index, bool enable, bool shouldEmit) { From b2a89ee4b99f1d89dddee2918195f73b6b92c9db Mon Sep 17 00:00:00 2001 From: DioEgizio <83089242+DioEgizio@users.noreply.github.com> Date: Sat, 21 May 2022 16:59:01 +0200 Subject: [PATCH 143/157] change cf icon to a more fancy one taken from QuiltMC/art in the emoji folder, so it's licensed under CC0 --- .../multimc/128x128/instances/flame.png | Bin 3375 -> 6226 bytes .../multimc/32x32/instances/flame.png | Bin 849 -> 0 bytes launcher/resources/multimc/multimc.qrc | 4 +--- 3 files changed, 1 insertion(+), 3 deletions(-) delete mode 100644 launcher/resources/multimc/32x32/instances/flame.png diff --git a/launcher/resources/multimc/128x128/instances/flame.png b/launcher/resources/multimc/128x128/instances/flame.png index 8a50a0b418e8dc27ebb91ab3886d8bea40987104..6482975c494e02522f34a10894bb434f1ccc7194 100644 GIT binary patch literal 6226 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 YzkS2@~ literal 3375 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 b(Z!BG!B5Qc$A}(=00000NkvXXu0mjfi>7~8 diff --git a/launcher/resources/multimc/multimc.qrc b/launcher/resources/multimc/multimc.qrc index e22fe7eef..2337acd60 100644 --- a/launcher/resources/multimc/multimc.qrc +++ b/launcher/resources/multimc/multimc.qrc @@ -6,8 +6,7 @@ scalable/reddit-alien.svg - - 32x32/instances/flame.png + 128x128/instances/flame.png @@ -272,7 +271,6 @@ 32x32/instances/ftb_logo.png 128x128/instances/ftb_logo.png - 32x32/instances/flame.png 128x128/instances/flame.png 32x32/instances/gear.png From 35f71f5793ee91a71e00464932ff95eb5e5e4d5e Mon Sep 17 00:00:00 2001 From: Lenny McLennington Date: Wed, 11 May 2022 21:44:06 +0100 Subject: [PATCH 144/157] Support paste.gg, hastebin, and mclo.gs --- launcher/Application.cpp | 33 +++++- launcher/net/PasteUpload.cpp | 165 ++++++++++++++++++++++++--- launcher/net/PasteUpload.h | 28 ++++- launcher/ui/GuiUtil.cpp | 5 +- launcher/ui/pages/global/APIPage.cpp | 51 ++++++++- launcher/ui/pages/global/APIPage.h | 1 + launcher/ui/pages/global/APIPage.ui | 62 +++------- 7 files changed, 271 insertions(+), 74 deletions(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index ce62c41af..b36fd89a3 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -36,6 +36,7 @@ #include "Application.h" #include "BuildConfig.h" +#include "net/PasteUpload.h" #include "ui/MainWindow.h" #include "ui/InstanceWindow.h" @@ -671,8 +672,36 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) m_settings->registerSetting("UpdateDialogGeometry", ""); - // pastebin URL - m_settings->registerSetting("PastebinURL", "https://0x0.st"); + // This code feels so stupid is there a less stupid way of doing this? + { + m_settings->registerSetting("PastebinURL", ""); + QString pastebinURL = m_settings->get("PastebinURL").toString(); + + // If PastebinURL hasn't been set before then use the new default: mclo.gs + if (pastebinURL == "") { + m_settings->registerSetting("PastebinType", PasteUpload::PasteType::Mclogs); + m_settings->registerSetting("PastebinCustomAPIBase", ""); + } + // Otherwise: use 0x0.st + else { + // The default custom endpoint would usually be "" (meaning there is no custom endpoint specified) + // But if the user had customised the paste URL then that should be carried over into the custom endpoint. + QString defaultCustomEndpoint = (pastebinURL == "https://0x0.st") ? "" : pastebinURL; + m_settings->registerSetting("PastebinType", PasteUpload::PasteType::NullPointer); + m_settings->registerSetting("PastebinCustomAPIBase", defaultCustomEndpoint); + + m_settings->reset("PastebinURL"); + } + + bool ok; + unsigned int pasteType = m_settings->get("PastebinType").toUInt(&ok); + // If PastebinType is invalid then reset the related settings. + if (!ok || !(PasteUpload::PasteType::First <= pasteType && pasteType <= PasteUpload::PasteType::Last)) + { + m_settings->reset("PastebinType"); + m_settings->reset("PastebinCustomAPIBase"); + } + } m_settings->registerSetting("CloseAfterLaunch", false); m_settings->registerSetting("QuitAfterGameStop", false); diff --git a/launcher/net/PasteUpload.cpp b/launcher/net/PasteUpload.cpp index 3d106c927..d583216d8 100644 --- a/launcher/net/PasteUpload.cpp +++ b/launcher/net/PasteUpload.cpp @@ -42,8 +42,22 @@ #include #include -PasteUpload::PasteUpload(QWidget *window, QString text, QString url) : m_window(window), m_uploadUrl(url), m_text(text.toUtf8()) +std::array PasteUpload::PasteTypes = { + {{"0x0", "https://0x0.st", ""}, + {"hastebin", "https://hastebin.com", "/documents"}, + {"paste (paste.gg)", "https://paste.gg", "/api/v1/pastes"}, + {"mclogs", "https://api.mclo.gs", "/1/log"}}}; + +PasteUpload::PasteUpload(QWidget *window, QString text, QString baseUrl, PasteType pasteType) : m_window(window), m_baseUrl(baseUrl), m_pasteType(pasteType), m_text(text.toUtf8()) { + if (m_baseUrl == "") + m_baseUrl = PasteTypes.at(pasteType).defaultBase; + + // HACK: Paste's docs say the standard API path is at /api/ but the official instance paste.gg doesn't follow that?? + if (pasteType == PasteGG && m_baseUrl == PasteTypes.at(pasteType).defaultBase) + m_uploadUrl = "https://api.paste.gg/v1/pastes"; + else + m_uploadUrl = m_baseUrl + PasteTypes.at(pasteType).endpointPath; } PasteUpload::~PasteUpload() @@ -53,26 +67,73 @@ PasteUpload::~PasteUpload() void PasteUpload::executeTask() { QNetworkRequest request{QUrl(m_uploadUrl)}; + QNetworkReply *rep{}; + request.setHeader(QNetworkRequest::UserAgentHeader, BuildConfig.USER_AGENT_UNCACHED); - QHttpMultiPart *multiPart = new QHttpMultiPart{QHttpMultiPart::FormDataType}; + switch (m_pasteType) { + case NullPointer: { + QHttpMultiPart *multiPart = + new QHttpMultiPart{QHttpMultiPart::FormDataType}; - QHttpPart filePart; - filePart.setBody(m_text); - filePart.setHeader(QNetworkRequest::ContentTypeHeader, "text/plain"); - filePart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data; name=\"file\"; filename=\"log.txt\""); + QHttpPart filePart; + filePart.setBody(m_text); + filePart.setHeader(QNetworkRequest::ContentTypeHeader, "text/plain"); + filePart.setHeader(QNetworkRequest::ContentDispositionHeader, + "form-data; name=\"file\"; filename=\"log.txt\""); + multiPart->append(filePart); - multiPart->append(filePart); + rep = APPLICATION->network()->post(request, multiPart); + multiPart->setParent(rep); - QNetworkReply *rep = APPLICATION->network()->post(request, multiPart); - multiPart->setParent(rep); + break; + } + case Hastebin: { + request.setHeader(QNetworkRequest::UserAgentHeader, BuildConfig.USER_AGENT_UNCACHED); + rep = APPLICATION->network()->post(request, m_text); + break; + } + case Mclogs: { + QUrlQuery postData; + postData.addQueryItem("content", m_text); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); + rep = APPLICATION->network()->post(request, postData.toString().toUtf8()); + break; + } + case PasteGG: { + QJsonObject obj; + QJsonDocument doc; + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - m_reply = std::shared_ptr(rep); - setStatus(tr("Uploading to %1").arg(m_uploadUrl)); + obj.insert("expires", QDateTime::currentDateTimeUtc().addDays(100).toString(Qt::DateFormat::ISODate)); + + QJsonArray files; + QJsonObject logFileInfo; + QJsonObject logFileContentInfo; + logFileContentInfo.insert("format", "text"); + logFileContentInfo.insert("value", QString::fromUtf8(m_text)); + logFileInfo.insert("name", "log.txt"); + logFileInfo.insert("content", logFileContentInfo); + files.append(logFileInfo); + + obj.insert("files", files); + + doc.setObject(obj); + rep = APPLICATION->network()->post(request, doc.toJson()); + break; + } + } connect(rep, &QNetworkReply::uploadProgress, this, &Task::setProgress); - connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(downloadError(QNetworkReply::NetworkError))); - connect(rep, SIGNAL(finished()), this, SLOT(downloadFinished())); + connect(rep, &QNetworkReply::finished, this, &PasteUpload::downloadFinished); + // This function call would be a lot shorter if we were using the latest Qt + connect(rep, + static_cast(&QNetworkReply::error), + this, &PasteUpload::downloadError); + + m_reply = std::shared_ptr(rep); + + setStatus(tr("Uploading to %1").arg(m_uploadUrl)); } void PasteUpload::downloadError(QNetworkReply::NetworkError error) @@ -102,6 +163,82 @@ void PasteUpload::downloadFinished() return; } - m_pasteLink = QString::fromUtf8(data).trimmed(); + switch (m_pasteType) + { + case NullPointer: + m_pasteLink = QString::fromUtf8(data).trimmed(); + break; + case Hastebin: { + QJsonDocument jsonDoc{QJsonDocument::fromJson(data)}; + QJsonObject jsonObj{jsonDoc.object()}; + if (jsonObj.contains("key") && jsonObj["key"].isString()) + { + QString key = jsonDoc.object()["key"].toString(); + m_pasteLink = m_baseUrl + "/" + key; + } + else + { + emitFailed(tr("Error: %1 returned a malformed response body").arg(m_uploadUrl)); + qCritical() << m_uploadUrl << " returned malformed response body: " << data; + return; + } + break; + } + case Mclogs: { + QJsonDocument jsonDoc{QJsonDocument::fromJson(data)}; + QJsonObject jsonObj{jsonDoc.object()}; + if (jsonObj.contains("success") && jsonObj["success"].isBool()) + { + bool success = jsonObj["success"].toBool(); + if (success) + { + m_pasteLink = jsonObj["url"].toString(); + } + else + { + QString error = jsonObj["error"].toString(); + emitFailed(tr("Error: %1 returned an error: %2").arg(m_uploadUrl, error)); + qCritical() << m_uploadUrl << " returned error: " << error; + qCritical() << "Response body: " << data; + return; + } + } + else + { + emitFailed(tr("Error: %1 returned a malformed response body").arg(m_uploadUrl)); + qCritical() << m_uploadUrl << " returned malformed response body: " << data; + return; + } + break; + } + case PasteGG: + QJsonDocument jsonDoc{QJsonDocument::fromJson(data)}; + QJsonObject jsonObj{jsonDoc.object()}; + if (jsonObj.contains("status") && jsonObj["status"].isString()) + { + QString status = jsonObj["status"].toString(); + if (status == "success") + { + m_pasteLink = m_baseUrl + "/p/anonymous/" + jsonObj["result"].toObject()["id"].toString(); + } + else + { + QString error = jsonObj["error"].toString(); + QString message = (jsonObj.contains("message") && jsonObj["message"].isString()) ? jsonObj["message"].toString() : "none"; + emitFailed(tr("Error: %1 returned an error code: %2\nError message: %3").arg(m_uploadUrl, error, message)); + qCritical() << m_uploadUrl << " returned error: " << error; + qCritical() << "Error message: " << message; + qCritical() << "Response body: " << data; + return; + } + } + else + { + emitFailed(tr("Error: %1 returned a malformed response body").arg(m_uploadUrl)); + qCritical() << m_uploadUrl << " returned malformed response body: " << data; + return; + } + break; + } emitSucceeded(); } diff --git a/launcher/net/PasteUpload.h b/launcher/net/PasteUpload.h index ea3a06d3d..e276234f4 100644 --- a/launcher/net/PasteUpload.h +++ b/launcher/net/PasteUpload.h @@ -36,14 +36,38 @@ #include "tasks/Task.h" #include +#include #include #include +#include class PasteUpload : public Task { Q_OBJECT public: - PasteUpload(QWidget *window, QString text, QString url); + enum PasteType : unsigned int { + // 0x0.st + NullPointer, + // hastebin.com + Hastebin, + // paste.gg + PasteGG, + // mclo.gs + Mclogs, + // Helpful to get the range of valid values on the enum for input sanitisation: + First = NullPointer, + Last = Mclogs + }; + + struct PasteTypeInfo { + const QString name; + const QString defaultBase; + const QString endpointPath; + }; + + static std::array PasteTypes; + + PasteUpload(QWidget *window, QString text, QString url, PasteType pasteType); virtual ~PasteUpload(); QString pasteLink() @@ -56,7 +80,9 @@ class PasteUpload : public Task private: QWidget *m_window; QString m_pasteLink; + QString m_baseUrl; QString m_uploadUrl; + PasteType m_pasteType; QByteArray m_text; std::shared_ptr m_reply; public diff --git a/launcher/ui/GuiUtil.cpp b/launcher/ui/GuiUtil.cpp index 9eb658e23..5e9d1eda9 100644 --- a/launcher/ui/GuiUtil.cpp +++ b/launcher/ui/GuiUtil.cpp @@ -16,8 +16,9 @@ QString GuiUtil::uploadPaste(const QString &text, QWidget *parentWidget) { ProgressDialog dialog(parentWidget); - auto pasteUrlSetting = APPLICATION->settings()->get("PastebinURL").toString(); - std::unique_ptr paste(new PasteUpload(parentWidget, text, pasteUrlSetting)); + auto pasteTypeSetting = static_cast(APPLICATION->settings()->get("PastebinType").toUInt()); + auto pasteCustomAPIBaseSetting = APPLICATION->settings()->get("PastebinCustomAPIBase").toString(); + std::unique_ptr paste(new PasteUpload(parentWidget, text, pasteCustomAPIBaseSetting, pasteTypeSetting)); dialog.execWithTask(paste.get()); if (!paste->wasSuccessful()) diff --git a/launcher/ui/pages/global/APIPage.cpp b/launcher/ui/pages/global/APIPage.cpp index 8b806bcf1..b2827a19f 100644 --- a/launcher/ui/pages/global/APIPage.cpp +++ b/launcher/ui/pages/global/APIPage.cpp @@ -3,6 +3,7 @@ * PolyMC - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (c) 2022 Jamie Mansfield + * Copyright (c) 2022 Lenny McLennington * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -46,15 +47,34 @@ #include "settings/SettingsObject.h" #include "tools/BaseProfiler.h" #include "Application.h" +#include "net/PasteUpload.h" APIPage::APIPage(QWidget *parent) : QWidget(parent), ui(new Ui::APIPage) { + // this is here so you can reorder the entries in the combobox without messing stuff up + unsigned int comboBoxEntries[] = { + PasteUpload::PasteType::Mclogs, + PasteUpload::PasteType::NullPointer, + PasteUpload::PasteType::PasteGG, + PasteUpload::PasteType::Hastebin + }; + static QRegularExpression validUrlRegExp("https?://.+"); + ui->setupUi(this); - ui->urlChoices->setValidator(new QRegularExpressionValidator(validUrlRegExp, ui->urlChoices)); - ui->tabWidget->tabBar()->hide();\ + + for (auto pasteType : comboBoxEntries) { + ui->pasteTypeComboBox->addItem(PasteUpload::PasteTypes.at(pasteType).name, pasteType); + } + + connect(ui->pasteTypeComboBox, static_cast(&QComboBox::currentIndexChanged), this, &APIPage::updateBaseURLPlaceholder); + // This function needs to be called even when the ComboBox's index is still in its default state. + updateBaseURLPlaceholder(ui->pasteTypeComboBox->currentIndex()); + ui->baseURLEntry->setValidator(new QRegularExpressionValidator(validUrlRegExp, ui->baseURLEntry)); + ui->tabWidget->tabBar()->hide(); + loadSettings(); } @@ -63,11 +83,28 @@ APIPage::~APIPage() delete ui; } +void APIPage::updateBaseURLPlaceholder(int index) +{ + ui->baseURLEntry->setPlaceholderText(PasteUpload::PasteTypes.at(ui->pasteTypeComboBox->itemData(index).toUInt()).defaultBase); +} + void APIPage::loadSettings() { auto s = APPLICATION->settings(); - QString pastebinURL = s->get("PastebinURL").toString(); - ui->urlChoices->setCurrentText(pastebinURL); + + unsigned int pasteType = s->get("PastebinType").toUInt(); + QString pastebinURL = s->get("PastebinCustomAPIBase").toString(); + + ui->baseURLEntry->setText(pastebinURL); + int pasteTypeIndex = ui->pasteTypeComboBox->findData(pasteType); + if (pasteTypeIndex == -1) + { + pasteTypeIndex = ui->pasteTypeComboBox->findData(PasteUpload::PasteType::Mclogs); + ui->baseURLEntry->clear(); + } + + ui->pasteTypeComboBox->setCurrentIndex(pasteTypeIndex); + QString msaClientID = s->get("MSAClientIDOverride").toString(); ui->msaClientID->setText(msaClientID); QString curseKey = s->get("CFKeyOverride").toString(); @@ -77,8 +114,10 @@ void APIPage::loadSettings() void APIPage::applySettings() { auto s = APPLICATION->settings(); - QString pastebinURL = ui->urlChoices->currentText(); - s->set("PastebinURL", pastebinURL); + + s->set("PastebinType", ui->pasteTypeComboBox->currentData().toUInt()); + s->set("PastebinCustomAPIBase", ui->baseURLEntry->text()); + QString msaClientID = ui->msaClientID->text(); s->set("MSAClientIDOverride", msaClientID); QString curseKey = ui->curseKey->text(); diff --git a/launcher/ui/pages/global/APIPage.h b/launcher/ui/pages/global/APIPage.h index 203560097..0bb84c895 100644 --- a/launcher/ui/pages/global/APIPage.h +++ b/launcher/ui/pages/global/APIPage.h @@ -73,6 +73,7 @@ class APIPage : public QWidget, public BasePage void retranslate() override; private: + void updateBaseURLPlaceholder(int index); void loadSettings(); void applySettings(); diff --git a/launcher/ui/pages/global/APIPage.ui b/launcher/ui/pages/global/APIPage.ui index eaa44c888..d986c2e22 100644 --- a/launcher/ui/pages/global/APIPage.ui +++ b/launcher/ui/pages/global/APIPage.ui @@ -6,8 +6,8 @@ 0 0 - 603 - 530 + 512 + 538 @@ -36,59 +36,30 @@ - &Pastebin URL + Pastebin Service - - - Qt::Horizontal + + + Paste Service Type - - - - 10 - - - - <html><head/><body><p>Note: only input that starts with <span style=" font-weight:600;">http://</span> or <span style=" font-weight:600;">https://</span> will be accepted.</p></body></html> - - - false - - + - - - true - - - QComboBox::NoInsert + + + Base URL - - - https://0x0.st - - - - - <html><head/><body><p>Here you can choose from a predefined list of paste services, or input the URL of a different paste service of your choice, provided it supports the same protocol as 0x0.st, that is POST a file parameter to the URL and return a link in the response body.</p></body></html> - - - Qt::RichText - - - true - - - true + + + @@ -101,13 +72,6 @@ &Microsoft Authentication - - - - Qt::Horizontal - - - From caf6d027282392a58b935185d787c4c22a861409 Mon Sep 17 00:00:00 2001 From: Lenny McLennington Date: Fri, 13 May 2022 17:48:19 +0100 Subject: [PATCH 145/157] Change paste settings and add copyright headers - There's now a notice reminding people to change the base URL if they had a custom base URL and change the paste type (that was something I personally had problems with when I was testing, so a reminder was helpful for me). - Broke down some of the long lines on APIPage.cpp to be more readable. - Added copyright headers where they were missing. - Changed the paste service display names to the names they are more commonly known by. - Changed the default hastebin base URL to https://hst.sh due to the acquisition of https://hastebin.com by Toptal. --- launcher/Application.cpp | 5 ++-- launcher/net/PasteUpload.cpp | 10 +++++--- launcher/net/PasteUpload.h | 3 ++- launcher/ui/GuiUtil.cpp | 37 +++++++++++++++++++++++++++- launcher/ui/pages/global/APIPage.cpp | 37 +++++++++++++++++++++++----- launcher/ui/pages/global/APIPage.h | 4 +++ launcher/ui/pages/global/APIPage.ui | 13 ++++++++++ 7 files changed, 95 insertions(+), 14 deletions(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index b36fd89a3..40c6e7609 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2022 Lenny McLennington * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -672,7 +673,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) m_settings->registerSetting("UpdateDialogGeometry", ""); - // This code feels so stupid is there a less stupid way of doing this? + // HACK: This code feels so stupid is there a less stupid way of doing this? { m_settings->registerSetting("PastebinURL", ""); QString pastebinURL = m_settings->get("PastebinURL").toString(); @@ -694,7 +695,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) } bool ok; - unsigned int pasteType = m_settings->get("PastebinType").toUInt(&ok); + int pasteType = m_settings->get("PastebinType").toInt(&ok); // If PastebinType is invalid then reset the related settings. if (!ok || !(PasteUpload::PasteType::First <= pasteType && pasteType <= PasteUpload::PasteType::Last)) { diff --git a/launcher/net/PasteUpload.cpp b/launcher/net/PasteUpload.cpp index d583216d8..3855190ab 100644 --- a/launcher/net/PasteUpload.cpp +++ b/launcher/net/PasteUpload.cpp @@ -1,6 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-only /* * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Lenny McLennington + * Copyright (C) 2022 Swirl * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -43,10 +45,10 @@ #include std::array PasteUpload::PasteTypes = { - {{"0x0", "https://0x0.st", ""}, - {"hastebin", "https://hastebin.com", "/documents"}, - {"paste (paste.gg)", "https://paste.gg", "/api/v1/pastes"}, - {"mclogs", "https://api.mclo.gs", "/1/log"}}}; + {{"0x0.st", "https://0x0.st", ""}, + {"hastebin", "https://hst.sh", "/documents"}, + {"paste.gg", "https://paste.gg", "/api/v1/pastes"}, + {"mclo.gs", "https://api.mclo.gs", "/1/log"}}}; PasteUpload::PasteUpload(QWidget *window, QString text, QString baseUrl, PasteType pasteType) : m_window(window), m_baseUrl(baseUrl), m_pasteType(pasteType), m_text(text.toUtf8()) { diff --git a/launcher/net/PasteUpload.h b/launcher/net/PasteUpload.h index e276234f4..eb315c2b8 100644 --- a/launcher/net/PasteUpload.h +++ b/launcher/net/PasteUpload.h @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Lenny McLennington * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -45,7 +46,7 @@ class PasteUpload : public Task { Q_OBJECT public: - enum PasteType : unsigned int { + enum PasteType : int { // 0x0.st NullPointer, // hastebin.com diff --git a/launcher/ui/GuiUtil.cpp b/launcher/ui/GuiUtil.cpp index 5e9d1eda9..320f1502a 100644 --- a/launcher/ui/GuiUtil.cpp +++ b/launcher/ui/GuiUtil.cpp @@ -1,3 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Lenny McLennington + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + #include "GuiUtil.h" #include @@ -16,7 +51,7 @@ QString GuiUtil::uploadPaste(const QString &text, QWidget *parentWidget) { ProgressDialog dialog(parentWidget); - auto pasteTypeSetting = static_cast(APPLICATION->settings()->get("PastebinType").toUInt()); + auto pasteTypeSetting = static_cast(APPLICATION->settings()->get("PastebinType").toInt()); auto pasteCustomAPIBaseSetting = APPLICATION->settings()->get("PastebinCustomAPIBase").toString(); std::unique_ptr paste(new PasteUpload(parentWidget, text, pasteCustomAPIBaseSetting, pasteTypeSetting)); diff --git a/launcher/ui/pages/global/APIPage.cpp b/launcher/ui/pages/global/APIPage.cpp index b2827a19f..2841544fc 100644 --- a/launcher/ui/pages/global/APIPage.cpp +++ b/launcher/ui/pages/global/APIPage.cpp @@ -53,8 +53,8 @@ APIPage::APIPage(QWidget *parent) : QWidget(parent), ui(new Ui::APIPage) { - // this is here so you can reorder the entries in the combobox without messing stuff up - unsigned int comboBoxEntries[] = { + // This is here so you can reorder the entries in the combobox without messing stuff up + int comboBoxEntries[] = { PasteUpload::PasteType::Mclogs, PasteUpload::PasteType::NullPointer, PasteUpload::PasteType::PasteGG, @@ -69,13 +69,18 @@ APIPage::APIPage(QWidget *parent) : ui->pasteTypeComboBox->addItem(PasteUpload::PasteTypes.at(pasteType).name, pasteType); } - connect(ui->pasteTypeComboBox, static_cast(&QComboBox::currentIndexChanged), this, &APIPage::updateBaseURLPlaceholder); + void (QComboBox::*currentIndexChangedSignal)(int) (&QComboBox::currentIndexChanged); + connect(ui->pasteTypeComboBox, currentIndexChangedSignal, this, &APIPage::updateBaseURLPlaceholder); // This function needs to be called even when the ComboBox's index is still in its default state. updateBaseURLPlaceholder(ui->pasteTypeComboBox->currentIndex()); ui->baseURLEntry->setValidator(new QRegularExpressionValidator(validUrlRegExp, ui->baseURLEntry)); ui->tabWidget->tabBar()->hide(); loadSettings(); + + resetBaseURLNote(); + connect(ui->pasteTypeComboBox, currentIndexChangedSignal, this, &APIPage::updateBaseURLNote); + connect(ui->baseURLEntry, &QLineEdit::textEdited, this, &APIPage::resetBaseURLNote); } APIPage::~APIPage() @@ -83,16 +88,36 @@ APIPage::~APIPage() delete ui; } +void APIPage::resetBaseURLNote() +{ + ui->baseURLNote->hide(); + baseURLPasteType = ui->pasteTypeComboBox->currentIndex(); +} + +void APIPage::updateBaseURLNote(int index) +{ + if (baseURLPasteType == index) + { + ui->baseURLNote->hide(); + } + else if (!ui->baseURLEntry->text().isEmpty()) + { + ui->baseURLNote->show(); + } +} + void APIPage::updateBaseURLPlaceholder(int index) { - ui->baseURLEntry->setPlaceholderText(PasteUpload::PasteTypes.at(ui->pasteTypeComboBox->itemData(index).toUInt()).defaultBase); + int pasteType = ui->pasteTypeComboBox->itemData(index).toInt(); + QString pasteDefaultURL = PasteUpload::PasteTypes.at(pasteType).defaultBase; + ui->baseURLEntry->setPlaceholderText(pasteDefaultURL); } void APIPage::loadSettings() { auto s = APPLICATION->settings(); - unsigned int pasteType = s->get("PastebinType").toUInt(); + int pasteType = s->get("PastebinType").toInt(); QString pastebinURL = s->get("PastebinCustomAPIBase").toString(); ui->baseURLEntry->setText(pastebinURL); @@ -115,7 +140,7 @@ void APIPage::applySettings() { auto s = APPLICATION->settings(); - s->set("PastebinType", ui->pasteTypeComboBox->currentData().toUInt()); + s->set("PastebinType", ui->pasteTypeComboBox->currentData().toInt()); s->set("PastebinCustomAPIBase", ui->baseURLEntry->text()); QString msaClientID = ui->msaClientID->text(); diff --git a/launcher/ui/pages/global/APIPage.h b/launcher/ui/pages/global/APIPage.h index 0bb84c895..17e62ae7f 100644 --- a/launcher/ui/pages/global/APIPage.h +++ b/launcher/ui/pages/global/APIPage.h @@ -3,6 +3,7 @@ * PolyMC - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * Copyright (c) 2022 Jamie Mansfield + * Copyright (c) 2022 Lenny McLennington * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -73,6 +74,9 @@ class APIPage : public QWidget, public BasePage void retranslate() override; private: + int baseURLPasteType; + void resetBaseURLNote(); + void updateBaseURLNote(int index); void updateBaseURLPlaceholder(int index); void loadSettings(); void applySettings(); diff --git a/launcher/ui/pages/global/APIPage.ui b/launcher/ui/pages/global/APIPage.ui index d986c2e22..b6af19588 100644 --- a/launcher/ui/pages/global/APIPage.ui +++ b/launcher/ui/pages/global/APIPage.ui @@ -61,6 +61,19 @@ + + true + + + + + + + Note: you probably want to change or clear the Base URL after changing the paste service type. + + + true + From e2ad3b01837e52a55e859412474978fa8a1e9625 Mon Sep 17 00:00:00 2001 From: Lenny McLennington Date: Tue, 17 May 2022 05:00:06 +0100 Subject: [PATCH 146/157] Add migration wizard, fix migration from custom paste instance - Very basic wizard just to allow the user to choose whether to keep their old paste settings or use the new default settings. - People who used custom 0x0 instances would just be kept on those settings and won't see the wizard. --- launcher/Application.cpp | 31 ++++---- launcher/CMakeLists.txt | 3 + launcher/ui/setupwizard/PasteWizardPage.cpp | 42 +++++++++++ launcher/ui/setupwizard/PasteWizardPage.h | 27 +++++++ launcher/ui/setupwizard/PasteWizardPage.ui | 80 +++++++++++++++++++++ 5 files changed, 169 insertions(+), 14 deletions(-) create mode 100644 launcher/ui/setupwizard/PasteWizardPage.cpp create mode 100644 launcher/ui/setupwizard/PasteWizardPage.h create mode 100644 launcher/ui/setupwizard/PasteWizardPage.ui diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 40c6e7609..438c7d613 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -63,6 +63,7 @@ #include "ui/setupwizard/SetupWizard.h" #include "ui/setupwizard/LanguageWizardPage.h" #include "ui/setupwizard/JavaWizardPage.h" +#include "ui/setupwizard/PasteWizardPage.h" #include "ui/dialogs/CustomMessageBox.h" @@ -676,21 +677,17 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) // HACK: This code feels so stupid is there a less stupid way of doing this? { m_settings->registerSetting("PastebinURL", ""); - QString pastebinURL = m_settings->get("PastebinURL").toString(); + m_settings->registerSetting("PastebinType", PasteUpload::PasteType::Mclogs); + m_settings->registerSetting("PastebinCustomAPIBase", ""); - // If PastebinURL hasn't been set before then use the new default: mclo.gs - if (pastebinURL == "") { - m_settings->registerSetting("PastebinType", PasteUpload::PasteType::Mclogs); - m_settings->registerSetting("PastebinCustomAPIBase", ""); - } - // Otherwise: use 0x0.st - else { - // The default custom endpoint would usually be "" (meaning there is no custom endpoint specified) - // But if the user had customised the paste URL then that should be carried over into the custom endpoint. - QString defaultCustomEndpoint = (pastebinURL == "https://0x0.st") ? "" : pastebinURL; - m_settings->registerSetting("PastebinType", PasteUpload::PasteType::NullPointer); - m_settings->registerSetting("PastebinCustomAPIBase", defaultCustomEndpoint); + QString pastebinURL = m_settings->get("PastebinURL").toString(); + bool userHadNoPastebin = pastebinURL == ""; + bool userHadDefaultPastebin = pastebinURL == "https://0x0.st"; + if (!(userHadNoPastebin || userHadDefaultPastebin)) + { + m_settings->set("PastebinType", PasteUpload::PasteType::NullPointer); + m_settings->set("PastebinCustomAPIBase", pastebinURL); m_settings->reset("PastebinURL"); } @@ -929,7 +926,8 @@ bool Application::createSetupWizard() return true; return false; }(); - bool wizardRequired = javaRequired || languageRequired; + bool pasteInterventionRequired = settings()->get("PastebinURL") != ""; + bool wizardRequired = javaRequired || languageRequired || pasteInterventionRequired; if(wizardRequired) { @@ -943,6 +941,11 @@ bool Application::createSetupWizard() { m_setupWizard->addPage(new JavaWizardPage(m_setupWizard)); } + + if (pasteInterventionRequired) + { + m_setupWizard->addPage(new PasteWizardPage(m_setupWizard)); + } connect(m_setupWizard, &QDialog::finished, this, &Application::setupWizardFinished); m_setupWizard->show(); return true; diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 8e75be204..15534c71e 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -661,6 +661,8 @@ SET(LAUNCHER_SOURCES ui/setupwizard/JavaWizardPage.h ui/setupwizard/LanguageWizardPage.cpp ui/setupwizard/LanguageWizardPage.h + ui/setupwizard/PasteWizardPage.cpp + ui/setupwizard/PasteWizardPage.h # GUI - themes ui/themes/FusionTheme.cpp @@ -890,6 +892,7 @@ SET(LAUNCHER_SOURCES ) qt5_wrap_ui(LAUNCHER_UI + ui/setupwizard/PasteWizardPage.ui ui/pages/global/AccountListPage.ui ui/pages/global/JavaPage.ui ui/pages/global/LauncherPage.ui diff --git a/launcher/ui/setupwizard/PasteWizardPage.cpp b/launcher/ui/setupwizard/PasteWizardPage.cpp new file mode 100644 index 000000000..0f47da4b1 --- /dev/null +++ b/launcher/ui/setupwizard/PasteWizardPage.cpp @@ -0,0 +1,42 @@ +#include "PasteWizardPage.h" +#include "ui_PasteWizardPage.h" + +#include "Application.h" +#include "net/PasteUpload.h" + +PasteWizardPage::PasteWizardPage(QWidget *parent) : + BaseWizardPage(parent), + ui(new Ui::PasteWizardPage) +{ + ui->setupUi(this); +} + +PasteWizardPage::~PasteWizardPage() +{ + delete ui; +} + +void PasteWizardPage::initializePage() +{ +} + +bool PasteWizardPage::validatePage() +{ + auto s = APPLICATION->settings(); + QString prevPasteURL = s->get("PastebinURL").toString(); + s->reset("PastebinURL"); + if (ui->previousSettingsRadioButton->isChecked()) + { + bool usingDefaultBase = prevPasteURL == PasteUpload::PasteTypes.at(PasteUpload::PasteType::NullPointer).defaultBase; + s->set("PastebinType", PasteUpload::PasteType::NullPointer); + if (!usingDefaultBase) + s->set("PastebinCustomAPIBase", prevPasteURL); + } + + return true; +} + +void PasteWizardPage::retranslate() +{ + ui->retranslateUi(this); +} diff --git a/launcher/ui/setupwizard/PasteWizardPage.h b/launcher/ui/setupwizard/PasteWizardPage.h new file mode 100644 index 000000000..513a14cb5 --- /dev/null +++ b/launcher/ui/setupwizard/PasteWizardPage.h @@ -0,0 +1,27 @@ +#ifndef PASTEDEFAULTSCONFIRMATIONWIZARD_H +#define PASTEDEFAULTSCONFIRMATIONWIZARD_H + +#include +#include "BaseWizardPage.h" + +namespace Ui { +class PasteWizardPage; +} + +class PasteWizardPage : public BaseWizardPage +{ + Q_OBJECT + +public: + explicit PasteWizardPage(QWidget *parent = nullptr); + ~PasteWizardPage(); + + void initializePage() override; + bool validatePage() override; + void retranslate() override; + +private: + Ui::PasteWizardPage *ui; +}; + +#endif // PASTEDEFAULTSCONFIRMATIONWIZARD_H diff --git a/launcher/ui/setupwizard/PasteWizardPage.ui b/launcher/ui/setupwizard/PasteWizardPage.ui new file mode 100644 index 000000000..247d3a757 --- /dev/null +++ b/launcher/ui/setupwizard/PasteWizardPage.ui @@ -0,0 +1,80 @@ + + + PasteWizardPage + + + + 0 + 0 + 400 + 300 + + + + Form + + + + + + The default paste service has changed to mclo.gs, please choose what you want to do with your settings. + + + true + + + + + + + Qt::Horizontal + + + + + + + Use new default service + + + true + + + buttonGroup + + + + + + + Keep previous settings + + + false + + + buttonGroup + + + + + + + Qt::Vertical + + + + 20 + 156 + + + + + + + + + + + + From de02deac989cc5efc135dc3c817fe72cc2499eca Mon Sep 17 00:00:00 2001 From: LennyMcLennington Date: Fri, 20 May 2022 22:30:00 +0100 Subject: [PATCH 147/157] Make if statement condition more readable Co-authored-by: Sefa Eyeoglu --- launcher/Application.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 438c7d613..91f5ef9df 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -682,9 +682,8 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) QString pastebinURL = m_settings->get("PastebinURL").toString(); - bool userHadNoPastebin = pastebinURL == ""; bool userHadDefaultPastebin = pastebinURL == "https://0x0.st"; - if (!(userHadNoPastebin || userHadDefaultPastebin)) + if (!pastebinURL.isEmpty() && !userHadDefaultPastebin) { m_settings->set("PastebinType", PasteUpload::PasteType::NullPointer); m_settings->set("PastebinCustomAPIBase", pastebinURL); From bfffcb3910b7f8429da16ae503fc79722d32ded6 Mon Sep 17 00:00:00 2001 From: txtsd Date: Sun, 22 May 2022 13:42:33 +0530 Subject: [PATCH 148/157] fix(workflow): Avoid invoking ccache on Release builds --- .github/workflows/build.yml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0590b3480..38868b39f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -39,6 +39,7 @@ jobs: INSTALL_PORTABLE_DIR: "install-portable" INSTALL_APPIMAGE_DIR: "install-appdir" BUILD_DIR: "build" + CCACHE_VAR: "" steps: ## @@ -80,6 +81,12 @@ jobs: ccache -p # Show config ccache -z # Zero stats + - name: Use ccache on Debug builds only + if: inputs.build_type == 'Debug' + shell: bash + run: | + echo "CCACHE_VAR=ccache" >> $GITHUB_ENV + - name: Retrieve ccache cache (Windows) if: runner.os == 'Windows' && inputs.build_type == 'Debug' uses: actions/cache@v3.0.2 @@ -128,18 +135,18 @@ jobs: - name: Configure CMake (macOS) if: runner.os == 'macOS' run: | - cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DQt5_DIR=/usr/local/opt/qt@5 -DCMAKE_PREFIX_PATH=/usr/local/opt/qt@5 -DLauncher_BUILD_PLATFORM=macOS -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -G Ninja + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DQt5_DIR=/usr/local/opt/qt@5 -DCMAKE_PREFIX_PATH=/usr/local/opt/qt@5 -DLauncher_BUILD_PLATFORM=macOS -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -G Ninja - name: Configure CMake (Windows) if: runner.os == 'Windows' shell: msys2 {0} run: | - cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=${{ matrix.name }} -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -G Ninja + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=${{ matrix.name }} -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -G Ninja - name: Configure CMake (Linux) if: runner.os == 'Linux' run: | - cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=Linux -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -G Ninja + cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=Linux -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -G Ninja ## # BUILD From 90007e2d9d4f63cfc9dc73888af34a17657b5102 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 22 May 2022 16:03:21 +0200 Subject: [PATCH 149/157] fix: temporarily ignore stringop-overflow warning --- CMakeLists.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index e07d2aa64..e6d66b8d1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,6 +38,10 @@ set(CMAKE_CXX_FLAGS " -Wall -pedantic -Werror -Wno-deprecated-declarations -D_GL if(UNIX AND APPLE) set(CMAKE_CXX_FLAGS " -stdlib=libc++ ${CMAKE_CXX_FLAGS}") endif() +# FIXME: GCC 12 complains about some random stuff in QuaZip. Need to fix this later +if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + set(CMAKE_CXX_FLAGS "-Wno-error=stringop-overflow ${CMAKE_CXX_FLAGS}") +endif() set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -Werror=return-type") # Fix build with Qt 5.13 From 0922a7f410d8675778bcf4720438efaa128b662b Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 22 May 2022 20:50:37 +0200 Subject: [PATCH 150/157] refactor: use -O2 for release and -O1 for debug builds --- CMakeLists.txt | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e6d66b8d1..f54dd7baf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -34,21 +34,24 @@ set(CMAKE_C_STANDARD_REQUIRED true) set(CMAKE_CXX_STANDARD 11) set(CMAKE_C_STANDARD 11) include(GenerateExportHeader) -set(CMAKE_CXX_FLAGS " -Wall -pedantic -Werror -Wno-deprecated-declarations -D_GLIBCXX_USE_CXX11_ABI=0 -fstack-protector-strong --param=ssp-buffer-size=4 -O3 -D_FORTIFY_SOURCE=2 ${CMAKE_CXX_FLAGS}") +set(CMAKE_CXX_FLAGS "-Wall -pedantic -Werror -Wno-deprecated-declarations -D_GLIBCXX_USE_CXX11_ABI=0 -fstack-protector-strong --param=ssp-buffer-size=4 ${CMAKE_CXX_FLAGS}") if(UNIX AND APPLE) - set(CMAKE_CXX_FLAGS " -stdlib=libc++ ${CMAKE_CXX_FLAGS}") + set(CMAKE_CXX_FLAGS "-stdlib=libc++ ${CMAKE_CXX_FLAGS}") endif() -# FIXME: GCC 12 complains about some random stuff in QuaZip. Need to fix this later +# FIXME: GCC 12 complains about some random stuff in bundled QuaZip. Need to fix this later if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") set(CMAKE_CXX_FLAGS "-Wno-error=stringop-overflow ${CMAKE_CXX_FLAGS}") endif() -set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -Werror=return-type") # Fix build with Qt 5.13 set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_NO_DEPRECATED_WARNINGS=Y") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_DISABLE_DEPRECATED_BEFORE=0x050C00") +# set CXXFLAGS for build targets +set(CMAKE_CXX_FLAGS_DEBUG "-O1 ${CMAKE_CXX_FLAGS}") +set(CMAKE_CXX_FLAGS_RELEASE "-O2 -D_FORTIFY_SOURCE=2 ${CMAKE_CXX_FLAGS}") + option(ENABLE_LTO "Enable Link Time Optimization" off) if(ENABLE_LTO) From 309dcc82cade6aee1af04534c8e307b56fcac848 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 22 May 2022 20:57:52 +0200 Subject: [PATCH 151/157] Revert "fix: temporarily ignore stringop-overflow warning" This reverts commit 90007e2d9d4f63cfc9dc73888af34a17657b5102. --- CMakeLists.txt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index f54dd7baf..ef4adf903 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,10 +38,6 @@ set(CMAKE_CXX_FLAGS "-Wall -pedantic -Werror -Wno-deprecated-declarations -D_GLI if(UNIX AND APPLE) set(CMAKE_CXX_FLAGS "-stdlib=libc++ ${CMAKE_CXX_FLAGS}") endif() -# FIXME: GCC 12 complains about some random stuff in bundled QuaZip. Need to fix this later -if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") - set(CMAKE_CXX_FLAGS "-Wno-error=stringop-overflow ${CMAKE_CXX_FLAGS}") -endif() # Fix build with Qt 5.13 set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_NO_DEPRECATED_WARNINGS=Y") From f00dbdc215c2de3b6906d8182388c27bbc657e24 Mon Sep 17 00:00:00 2001 From: dada513 Date: Wed, 13 Apr 2022 23:00:32 +0200 Subject: [PATCH 152/157] Make Metaserver changable in settings Co-authored-by: Sefa Eyeoglu Co-authored-by: flow --- launcher/Application.cpp | 2 + launcher/meta/BaseEntity.cpp | 11 ++++- launcher/ui/pages/global/APIPage.cpp | 10 ++++ launcher/ui/pages/global/APIPage.ui | 71 +++++++++++++++++++++++++--- 4 files changed, 87 insertions(+), 7 deletions(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 91f5ef9df..ba4096b64 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -699,6 +699,8 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) m_settings->reset("PastebinCustomAPIBase"); } } + // meta URL + m_settings->registerSetting("MetaURLOverride", ""); m_settings->registerSetting("CloseAfterLaunch", false); m_settings->registerSetting("QuitAfterGameStop", false); diff --git a/launcher/meta/BaseEntity.cpp b/launcher/meta/BaseEntity.cpp index 841559221..de4e1012d 100644 --- a/launcher/meta/BaseEntity.cpp +++ b/launcher/meta/BaseEntity.cpp @@ -75,7 +75,16 @@ Meta::BaseEntity::~BaseEntity() QUrl Meta::BaseEntity::url() const { - return QUrl(BuildConfig.META_URL).resolved(localFilename()); + auto s = APPLICATION->settings(); + QString metaOverride = s->get("MetaURLOverride").toString(); + if(metaOverride.isEmpty()) + { + return QUrl(BuildConfig.META_URL).resolved(localFilename()); + } + else + { + return QUrl(metaOverride).resolved(localFilename()); + } } bool Meta::BaseEntity::loadLocalFile() diff --git a/launcher/ui/pages/global/APIPage.cpp b/launcher/ui/pages/global/APIPage.cpp index 2841544fc..af58b8cdf 100644 --- a/launcher/ui/pages/global/APIPage.cpp +++ b/launcher/ui/pages/global/APIPage.cpp @@ -132,6 +132,8 @@ void APIPage::loadSettings() QString msaClientID = s->get("MSAClientIDOverride").toString(); ui->msaClientID->setText(msaClientID); + QString metaURL = s->get("MetaURLOverride").toString(); + ui->metaURL->setText(metaURL); QString curseKey = s->get("CFKeyOverride").toString(); ui->curseKey->setText(curseKey); } @@ -145,6 +147,14 @@ void APIPage::applySettings() QString msaClientID = ui->msaClientID->text(); s->set("MSAClientIDOverride", msaClientID); + QUrl metaURL = ui->metaURL->text(); + // Don't allow HTTP, since meta is basically RCE with all the jar files. + if(!metaURL.isEmpty() && metaURL.scheme() == "http") + { + metaURL.setScheme("https"); + } + + s->set("MetaURLOverride", metaURL); QString curseKey = ui->curseKey->text(); s->set("CFKeyOverride", curseKey); } diff --git a/launcher/ui/pages/global/APIPage.ui b/launcher/ui/pages/global/APIPage.ui index b6af19588..8d80df657 100644 --- a/launcher/ui/pages/global/APIPage.ui +++ b/launcher/ui/pages/global/APIPage.ui @@ -6,8 +6,8 @@ 0 0 - 512 - 538 + 800 + 600 @@ -85,6 +85,13 @@ &Microsoft Authentication + + + + Qt::Horizontal + + + @@ -124,6 +131,58 @@ + + + + Meta&data Server + + + + + + Qt::Horizontal + + + + + + + You can set this to a third-party metadata server to use patched libraries or other hacks. + + + Qt::RichText + + + true + + + + + + + (Default) + + + + + + + Enter a custom URL for meta here. + + + Qt::RichText + + + true + + + true + + + + + + @@ -132,16 +191,16 @@ &CurseForge Core API - + - + Qt::Horizontal - + Note: you probably don't need to set this if CurseForge already works. @@ -158,7 +217,7 @@ - + Enter a custom API Key for CurseForge here. From b181f4bc30f36778f9680eb54e6f3514739161e8 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 22 May 2022 13:41:44 +0200 Subject: [PATCH 153/157] fix: improve spacing in APIPage --- launcher/ui/pages/global/APIPage.ui | 34 +++++++++++------------------ 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/launcher/ui/pages/global/APIPage.ui b/launcher/ui/pages/global/APIPage.ui index 8d80df657..24189c5c5 100644 --- a/launcher/ui/pages/global/APIPage.ui +++ b/launcher/ui/pages/global/APIPage.ui @@ -85,13 +85,6 @@ &Microsoft Authentication - - - - Qt::Horizontal - - - @@ -137,13 +130,6 @@ Meta&data Server - - - - Qt::Horizontal - - - @@ -192,13 +178,6 @@ &CurseForge Core API - - - - Qt::Horizontal - - - @@ -235,6 +214,19 @@ + + + + Qt::Vertical + + + + 20 + 40 + + + + From f2e205313485e458e2f5186f743d527d28609c5e Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 22 May 2022 13:55:19 +0200 Subject: [PATCH 154/157] feat: add trailing slash to meta URL if it is missing --- launcher/ui/pages/global/APIPage.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/launcher/ui/pages/global/APIPage.cpp b/launcher/ui/pages/global/APIPage.cpp index af58b8cdf..6ad243ddc 100644 --- a/launcher/ui/pages/global/APIPage.cpp +++ b/launcher/ui/pages/global/APIPage.cpp @@ -148,6 +148,13 @@ void APIPage::applySettings() QString msaClientID = ui->msaClientID->text(); s->set("MSAClientIDOverride", msaClientID); QUrl metaURL = ui->metaURL->text(); + // Add required trailing slash + if (!metaURL.isEmpty() && !metaURL.path().endsWith('/')) + { + QString path = metaURL.path(); + path.append('/'); + metaURL.setPath(path); + } // Don't allow HTTP, since meta is basically RCE with all the jar files. if(!metaURL.isEmpty() && metaURL.scheme() == "http") { From 0b85051a2363f4fad29477e3a0ccd3fda18fee01 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 22 May 2022 21:41:41 +0200 Subject: [PATCH 155/157] fix: more generous optimizations for debug builds --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ef4adf903..a8c28e990 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -45,7 +45,7 @@ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_NO_DEPRECATED_WARNINGS=Y") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_DISABLE_DEPRECATED_BEFORE=0x050C00") # set CXXFLAGS for build targets -set(CMAKE_CXX_FLAGS_DEBUG "-O1 ${CMAKE_CXX_FLAGS}") +set(CMAKE_CXX_FLAGS_DEBUG "-O2 -D_FORTIFY_SOURCE=2 ${CMAKE_CXX_FLAGS}") set(CMAKE_CXX_FLAGS_RELEASE "-O2 -D_FORTIFY_SOURCE=2 ${CMAKE_CXX_FLAGS}") option(ENABLE_LTO "Enable Link Time Optimization" off) From cb69869836d2b4ed4b50a43694e95c4a801332f7 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 22 May 2022 22:04:24 +0200 Subject: [PATCH 156/157] revert: remove CurseForge workaround We have been asked by CurseForge to remove this workaround as it violates their terms of service. This is just a partial revert, as the UI changes were otherwise unrelated. This reverts commit 92e8aaf36f72b7527322add169b253d0698939d0, reversing changes made to 88a93945d4c9a11bf53016133335d359b819585e. --- launcher/modplatform/flame/FileResolvingTask.cpp | 16 +--------------- launcher/modplatform/flame/FlameModIndex.cpp | 9 +-------- launcher/modplatform/flame/PackManifest.cpp | 15 +++++---------- 3 files changed, 7 insertions(+), 33 deletions(-) diff --git a/launcher/modplatform/flame/FileResolvingTask.cpp b/launcher/modplatform/flame/FileResolvingTask.cpp index 0deb99c4a..95924a681 100644 --- a/launcher/modplatform/flame/FileResolvingTask.cpp +++ b/launcher/modplatform/flame/FileResolvingTask.cpp @@ -31,21 +31,7 @@ void Flame::FileResolvingTask::netJobFinished() for (auto& bytes : results) { auto& out = m_toProcess.files[index]; try { - bool fail = (!out.parseFromBytes(bytes)); - if(fail){ - //failed :( probably disabled mod, try to add to the list - auto doc = Json::requireDocument(bytes); - if (!doc.isObject()) { - throw JSONValidationError(QString("data is not an object? that's not supposed to happen")); - } - auto obj = Json::ensureObject(doc.object(), "data"); - //FIXME : HACK, MAY NOT WORK FOR LONG - out.url = QUrl(QString("https://media.forgecdn.net/files/%1/%2/%3") - .arg(QString::number(QString::number(out.fileId).leftRef(4).toInt()) - ,QString::number(QString::number(out.fileId).rightRef(3).toInt()) - ,QUrl::toPercentEncoding(out.fileName)), QUrl::TolerantMode); - } - failed &= fail; + failed &= (!out.parseFromBytes(bytes)); } catch (const JSONValidationError& e) { qCritical() << "Resolving of" << out.projectId << out.fileId << "failed because of a parsing error:"; qCritical() << e.cause(); diff --git a/launcher/modplatform/flame/FlameModIndex.cpp b/launcher/modplatform/flame/FlameModIndex.cpp index 9846b1562..ba0824cf5 100644 --- a/launcher/modplatform/flame/FlameModIndex.cpp +++ b/launcher/modplatform/flame/FlameModIndex.cpp @@ -56,15 +56,8 @@ void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, file.fileId = Json::requireInteger(obj, "id"); file.date = Json::requireString(obj, "fileDate"); file.version = Json::requireString(obj, "displayName"); + file.downloadUrl = Json::requireString(obj, "downloadUrl"); file.fileName = Json::requireString(obj, "fileName"); - file.downloadUrl = Json::ensureString(obj, "downloadUrl", ""); - if(file.downloadUrl.isEmpty()){ - //FIXME : HACK, MAY NOT WORK FOR LONG - file.downloadUrl = QString("https://media.forgecdn.net/files/%1/%2/%3") - .arg(QString::number(QString::number(file.fileId.toInt()).leftRef(4).toInt()) - ,QString::number(QString::number(file.fileId.toInt()).rightRef(3).toInt()) - ,QUrl::toPercentEncoding(file.fileName)); - } unsortedVersions.append(file); } diff --git a/launcher/modplatform/flame/PackManifest.cpp b/launcher/modplatform/flame/PackManifest.cpp index 3217a7569..e4f90c1a1 100644 --- a/launcher/modplatform/flame/PackManifest.cpp +++ b/launcher/modplatform/flame/PackManifest.cpp @@ -71,6 +71,11 @@ bool Flame::File::parseFromBytes(const QByteArray& bytes) fileName = Json::requireString(obj, "fileName"); + QString rawUrl = Json::requireString(obj, "downloadUrl"); + url = QUrl(rawUrl, QUrl::TolerantMode); + if (!url.isValid()) { + throw JSONValidationError(QString("Invalid URL: %1").arg(rawUrl)); + } // This is a piece of a Flame project JSON pulled out into the file metadata (here) for convenience // It is also optional type = File::Type::SingleFile; @@ -82,17 +87,7 @@ bool Flame::File::parseFromBytes(const QByteArray& bytes) // this is probably a mod, dunno what else could modpacks download targetFolder = "mods"; } - QString rawUrl = Json::ensureString(obj, "downloadUrl"); - if(rawUrl.isEmpty()){ - //either there somehow is an emtpy string as a link, or it's null either way it's invalid - //soft failing - return false; - } - url = QUrl(rawUrl, QUrl::TolerantMode); - if (!url.isValid()) { - throw JSONValidationError(QString("Invalid URL: %1").arg(rawUrl)); - } resolved = true; return true; } From d72c75db239dbff7e41c0d4a20df5337b9685a16 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Sun, 22 May 2022 22:56:52 +0200 Subject: [PATCH 157/157] chore: bump version --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a8c28e990..e2635c3fc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -73,8 +73,8 @@ set(Launcher_HELP_URL "https://polymc.org/wiki/help-pages/%1" CACHE STRING "URL ######## Set version numbers ######## set(Launcher_VERSION_MAJOR 1) -set(Launcher_VERSION_MINOR 2) -set(Launcher_VERSION_HOTFIX 2) +set(Launcher_VERSION_MINOR 3) +set(Launcher_VERSION_HOTFIX 0) # Build number set(Launcher_VERSION_BUILD -1 CACHE STRING "Build number. -1 for no build number.")