From 284e536e8144b3fc7ff4842c39e0d7231f8e4b93 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Tue, 28 Nov 2023 12:30:13 +0000 Subject: [PATCH 001/695] Data pack management Signed-off-by: TheKodeToad --- launcher/Application.cpp | 5 + launcher/CMakeLists.txt | 7 + launcher/minecraft/mod/DataPack.cpp | 47 +++++ launcher/minecraft/mod/DataPack.h | 17 ++ .../minecraft/mod/DataPackFolderModel.cpp | 191 ++++++++++++++++++ launcher/minecraft/mod/DataPackFolderModel.h | 62 ++++++ .../mod/tasks/LocalDataPackParseTask.cpp | 121 +++++++++++ .../mod/tasks/LocalDataPackParseTask.h | 4 + launcher/modplatform/ModIndex.h | 2 +- launcher/modplatform/flame/FlameAPI.h | 1 + launcher/modplatform/modrinth/ModrinthAPI.h | 3 + .../ui/dialogs/ResourceDownloadDialog.cpp | 23 ++- launcher/ui/dialogs/ResourceDownloadDialog.h | 18 ++ launcher/ui/pages/instance/DataPackPage.cpp | 82 ++++++++ launcher/ui/pages/instance/DataPackPage.h | 39 ++++ .../ui/pages/instance/ExternalResourcesPage.h | 1 + launcher/ui/pages/instance/WorldListPage.cpp | 34 +++- launcher/ui/pages/instance/WorldListPage.h | 6 +- launcher/ui/pages/instance/WorldListPage.ui | 8 +- .../ui/pages/modplatform/DataPackModel.cpp | 48 +++++ launcher/ui/pages/modplatform/DataPackModel.h | 44 ++++ .../ui/pages/modplatform/DataPackPage.cpp | 48 +++++ launcher/ui/pages/modplatform/DataPackPage.h | 51 +++++ .../modrinth/ModrinthResourceModels.cpp | 23 +++ .../modrinth/ModrinthResourceModels.h | 19 ++ .../modrinth/ModrinthResourcePages.cpp | 22 ++ .../modrinth/ModrinthResourcePages.h | 24 +++ launcher/ui/widgets/InfoFrame.cpp | 6 + launcher/ui/widgets/InfoFrame.h | 2 + 29 files changed, 943 insertions(+), 15 deletions(-) create mode 100644 launcher/minecraft/mod/DataPackFolderModel.cpp create mode 100644 launcher/minecraft/mod/DataPackFolderModel.h create mode 100644 launcher/ui/pages/instance/DataPackPage.cpp create mode 100644 launcher/ui/pages/instance/DataPackPage.h create mode 100644 launcher/ui/pages/modplatform/DataPackModel.cpp create mode 100644 launcher/ui/pages/modplatform/DataPackModel.h create mode 100644 launcher/ui/pages/modplatform/DataPackPage.cpp create mode 100644 launcher/ui/pages/modplatform/DataPackPage.h diff --git a/launcher/Application.cpp b/launcher/Application.cpp index be252f1c5..2c3189a90 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -683,6 +683,11 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_settings->registerSetting("RPDownloadGeometry", ""); m_settings->registerSetting("TPDownloadGeometry", ""); m_settings->registerSetting("ShaderDownloadGeometry", ""); + m_settings->registerSetting("DataPackDownloadGeometry", ""); + + // data pack window + // in future, more pages may be added - so this name is chosen to avoid needing migration + m_settings->registerSetting("WorldManagementGeometry", ""); // HACK: This code feels so stupid is there a less stupid way of doing this? { diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 99acf8fc5..36451ab8a 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -333,6 +333,8 @@ set(MINECRAFT_SOURCES minecraft/mod/ResourceFolderModel.cpp minecraft/mod/DataPack.h minecraft/mod/DataPack.cpp + minecraft/mod/DataPackFolderModel.h + minecraft/mod/DataPackFolderModel.cpp minecraft/mod/ResourcePack.h minecraft/mod/ResourcePack.cpp minecraft/mod/ResourcePackFolderModel.h @@ -861,6 +863,8 @@ SET(LAUNCHER_SOURCES ui/pages/instance/VersionPage.h ui/pages/instance/ManagedPackPage.cpp ui/pages/instance/ManagedPackPage.h + ui/pages/instance/DataPackPage.h + ui/pages/instance/DataPackPage.cpp ui/pages/instance/TexturePackPage.h ui/pages/instance/TexturePackPage.cpp ui/pages/instance/ResourcePackPage.h @@ -930,6 +934,9 @@ SET(LAUNCHER_SOURCES ui/pages/modplatform/ShaderPackPage.cpp ui/pages/modplatform/ShaderPackModel.cpp + ui/pages/modplatform/DataPackPage.cpp + ui/pages/modplatform/DataPackModel.cpp + ui/pages/modplatform/atlauncher/AtlFilterModel.cpp ui/pages/modplatform/atlauncher/AtlFilterModel.h ui/pages/modplatform/atlauncher/AtlListModel.cpp diff --git a/launcher/minecraft/mod/DataPack.cpp b/launcher/minecraft/mod/DataPack.cpp index fc2d3f68b..8c7d1b086 100644 --- a/launcher/minecraft/mod/DataPack.cpp +++ b/launcher/minecraft/mod/DataPack.cpp @@ -25,7 +25,9 @@ #include #include +#include "MTPixmapCache.h" #include "Version.h" +#include "minecraft/mod/tasks/LocalDataPackParseTask.h" // Values taken from: // https://minecraft.wiki/w/Tutorials/Creating_a_data_pack#%22pack_format%22 @@ -56,6 +58,51 @@ void DataPack::setDescription(QString new_description) m_description = new_description; } +void DataPack::setImage(QImage new_image) const +{ + QMutexLocker locker(&m_data_lock); + + Q_ASSERT(!new_image.isNull()); + + if (m_pack_image_cache_key.key.isValid()) + PixmapCache::instance().remove(m_pack_image_cache_key.key); + + // scale the image to avoid flooding the pixmapcache + auto pixmap = + QPixmap::fromImage(new_image.scaled({ 64, 64 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding, Qt::SmoothTransformation)); + + m_pack_image_cache_key.key = PixmapCache::instance().insert(pixmap); + m_pack_image_cache_key.was_ever_used = true; + + // This can happen if the pixmap is too big to fit in the cache :c + if (!m_pack_image_cache_key.key.isValid()) { + qWarning() << "Could not insert a image cache entry! Ignoring it."; + m_pack_image_cache_key.was_ever_used = false; + } +} + +QPixmap DataPack::image(QSize size, Qt::AspectRatioMode mode) const +{ + QPixmap cached_image; + if (PixmapCache::instance().find(m_pack_image_cache_key.key, &cached_image)) { + if (size.isNull()) + return cached_image; + return cached_image.scaled(size, mode, Qt::SmoothTransformation); + } + + // No valid image we can get + if (!m_pack_image_cache_key.was_ever_used) { + return {}; + } else { + qDebug() << "Resource Pack" << name() << "Had it's image evicted from the cache. reloading..."; + PixmapCache::markCacheMissByEviciton(); + } + + // Imaged got evicted from the cache. Re-process it and retry. + DataPackUtils::processPackPNG(*this); + return image(size); +} + std::pair DataPack::compatibleVersions() const { if (!s_pack_format_versions.contains(m_pack_format)) { diff --git a/launcher/minecraft/mod/DataPack.h b/launcher/minecraft/mod/DataPack.h index b3787b238..3eec4e0e0 100644 --- a/launcher/minecraft/mod/DataPack.h +++ b/launcher/minecraft/mod/DataPack.h @@ -24,6 +24,7 @@ #include "Resource.h" #include +#include class Version; @@ -48,12 +49,18 @@ class DataPack : public Resource { /** Gets the description of the data pack. */ [[nodiscard]] QString description() const { return m_description; } + /** Gets the image of the resource pack, converted to a QPixmap for drawing, and scaled to size. */ + [[nodiscard]] QPixmap image(QSize size, Qt::AspectRatioMode mode = Qt::AspectRatioMode::IgnoreAspectRatio) const; + /** Thread-safe. */ void setPackFormat(int new_format_id); /** Thread-safe. */ void setDescription(QString new_description); + /** Thread-safe. */ + void setImage(QImage new_image) const; + bool valid() const override; [[nodiscard]] auto compare(Resource const& other, SortType type) const -> std::pair override; @@ -70,4 +77,14 @@ class DataPack : public Resource { /** The data pack's description, as defined in the pack.mcmeta file. */ QString m_description; + + /** The data pack's image file cache key, for access in the QPixmapCache global instance. + * + * The 'was_ever_used' state simply identifies whether the key was never inserted on the cache (true), + * so as to tell whether a cache entry is inexistent or if it was just evicted from the cache. + */ + struct { + QPixmapCache::Key key; + bool was_ever_used = false; + } mutable m_pack_image_cache_key; }; diff --git a/launcher/minecraft/mod/DataPackFolderModel.cpp b/launcher/minecraft/mod/DataPackFolderModel.cpp new file mode 100644 index 000000000..9efb37294 --- /dev/null +++ b/launcher/minecraft/mod/DataPackFolderModel.cpp @@ -0,0 +1,191 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 flowln + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2023 TheKodeToad + * + * 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 "DataPackFolderModel.h" +#include +#include + +#include +#include + +#include "Application.h" +#include "Version.h" + +#include "minecraft/mod/tasks/BasicFolderLoadTask.h" +#include "minecraft/mod/tasks/LocalDataPackParseTask.h" +#include "minecraft/mod/tasks/LocalResourcePackParseTask.h" + +DataPackFolderModel::DataPackFolderModel(const QString& dir, BaseInstance* instance) : ResourceFolderModel(QDir(dir), instance) +{ + m_column_names = QStringList({ "Enable", "Image", "Name", "Pack Format", "Last Modified" }); + m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Pack Format"), tr("Last Modified") }); + m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::PACK_FORMAT, SortType::DATE }; + m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive, + QHeaderView::Interactive }; + m_columnsHideable = { false, true, false, true, true }; +} + +QVariant DataPackFolderModel::data(const QModelIndex& index, int role) const +{ + if (!validateIndex(index)) + return {}; + + int row = index.row(); + int column = index.column(); + + switch (role) { + case Qt::DisplayRole: + switch (column) { + case NameColumn: + return m_resources[row]->name(); + case PackFormatColumn: { + auto resource = at(row); + auto pack_format = resource->packFormat(); + if (pack_format == 0) + return tr("Unrecognized"); + + auto version_bounds = resource->compatibleVersions(); + if (version_bounds.first.toString().isEmpty()) + return QString::number(pack_format); + + return QString("%1 (%2 - %3)") + .arg(QString::number(pack_format), version_bounds.first.toString(), version_bounds.second.toString()); + } + case DateColumn: + return m_resources[row]->dateTimeChanged(); + + default: + return {}; + } + case Qt::DecorationRole: { + if (column == NameColumn && (at(row)->isSymLinkUnder(instDirPath()) || at(row)->isMoreThanOneHardLink())) + return APPLICATION->getThemedIcon("status-yellow"); + if (column == ImageColumn) { + return at(row)->image({ 32, 32 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding); + } + return {}; + } + case Qt::ToolTipRole: { + if (column == PackFormatColumn) { + //: The string being explained by this is in the format: ID (Lower version - Upper version) + return tr("The data pack format ID, as well as the Minecraft versions it was designed for."); + } + if (column == NameColumn) { + if (at(row)->isSymLinkUnder(instDirPath())) { + return m_resources[row]->internal_id() + + tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original." + "\nCanonical Path: %1") + .arg(at(row)->fileinfo().canonicalFilePath()); + ; + } + if (at(row)->isMoreThanOneHardLink()) { + return m_resources[row]->internal_id() + + tr("\nWarning: This resource is hard linked elsewhere. Editing it will also change the original."); + } + } + return m_resources[row]->internal_id(); + } + case Qt::SizeHintRole: + if (column == ImageColumn) { + return QSize(32, 32); + } + return {}; + case Qt::CheckStateRole: + switch (column) { + case ActiveColumn: + return at(row)->enabled() ? Qt::Checked : Qt::Unchecked; + default: + return {}; + } + default: + return {}; + } +} + +QVariant DataPackFolderModel::headerData(int section, [[maybe_unused]] Qt::Orientation orientation, int role) const +{ + switch (role) { + case Qt::DisplayRole: + switch (section) { + case ActiveColumn: + case NameColumn: + case PackFormatColumn: + case DateColumn: + case ImageColumn: + return columnNames().at(section); + default: + return {}; + } + + case Qt::ToolTipRole: + switch (section) { + case ActiveColumn: + return tr("Is the data pack enabled? (Only valid for ZIPs)"); + case NameColumn: + return tr("The name of the data pack."); + case PackFormatColumn: + //: The string being explained by this is in the format: ID (Lower version - Upper version) + return tr("The data pack format ID, as well as the Minecraft versions it was designed for."); + case DateColumn: + return tr("The date and time this data pack was last changed (or added)."); + default: + return {}; + } + case Qt::SizeHintRole: + if (section == ImageColumn) { + return QSize(64, 0); + } + return {}; + default: + return {}; + } +} + +int DataPackFolderModel::columnCount(const QModelIndex& parent) const +{ + return parent.isValid() ? 0 : NUM_COLUMNS; +} + +Task* DataPackFolderModel::createUpdateTask() +{ + return new BasicFolderLoadTask(m_dir, [](QFileInfo const& entry) { return makeShared(entry); }); +} + +Task* DataPackFolderModel::createParseTask(Resource& resource) +{ + return new LocalDataPackParseTask(m_next_resolution_ticket, static_cast(resource)); +} diff --git a/launcher/minecraft/mod/DataPackFolderModel.h b/launcher/minecraft/mod/DataPackFolderModel.h new file mode 100644 index 000000000..1c2204af9 --- /dev/null +++ b/launcher/minecraft/mod/DataPackFolderModel.h @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 flowln + * Copyright (C) 2023 TheKodeToad + * + * 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 "ResourceFolderModel.h" + +#include "DataPack.h" +#include "ResourcePack.h" + +class DataPackFolderModel : public ResourceFolderModel { + Q_OBJECT + public: + enum Columns { ActiveColumn = 0, ImageColumn, NameColumn, PackFormatColumn, DateColumn, NUM_COLUMNS }; + + explicit DataPackFolderModel(const QString& dir, BaseInstance* instance); + + virtual QString id() const override { return "datapacks"; } + + [[nodiscard]] QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + + [[nodiscard]] QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + [[nodiscard]] int columnCount(const QModelIndex& parent) const override; + + [[nodiscard]] Task* createUpdateTask() override; + [[nodiscard]] Task* createParseTask(Resource&) override; + + RESOURCE_HELPERS(DataPack) +}; diff --git a/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp index 82f6b9df9..e5148e5be 100644 --- a/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp @@ -81,6 +81,29 @@ bool processFolder(DataPack& pack, ProcessingLevel level) return true; // only need basic info already checked } + auto png_invalid = [&pack]() { + qWarning() << "Data pack at" << pack.fileinfo().filePath() << "does not have a valid pack.png"; + return true; // the png is optional + }; + + QFileInfo image_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.png")); + if (image_file_info.exists() && image_file_info.isFile()) { + QFile pack_png_file(image_file_info.filePath()); + if (!pack_png_file.open(QIODevice::ReadOnly)) + return png_invalid(); // can't open pack.png file + + auto data = pack_png_file.readAll(); + + bool pack_png_result = DataPackUtils::processPackPNG(pack, std::move(data)); + + pack_png_file.close(); + if (!pack_png_result) { + return png_invalid(); // pack.png invalid + } + } else { + return png_invalid(); // pack.png does not exists or is not a valid file. + } + return true; // all tests passed } @@ -128,6 +151,32 @@ bool processZIP(DataPack& pack, ProcessingLevel level) return true; // only need basic info already checked } + auto png_invalid = [&pack]() { + qWarning() << "Data pack at" << pack.fileinfo().filePath() << "does not have a valid pack.png"; + return true; // the png is optional + }; + + if (zip.setCurrentFile("pack.png")) { + if (!file.open(QIODevice::ReadOnly)) { + qCritical() << "Failed to open file in zip."; + zip.close(); + return png_invalid(); + } + + auto data = file.readAll(); + + bool pack_png_result = DataPackUtils::processPackPNG(pack, std::move(data)); + + file.close(); + zip.close(); + if (!pack_png_result) { + return png_invalid(); // pack.png invalid + } + } else { + zip.close(); + return png_invalid(); // could not set pack.mcmeta as current file. + } + zip.close(); return true; @@ -149,6 +198,78 @@ bool processMCMeta(DataPack& pack, QByteArray&& raw_data) return true; } +bool processPackPNG(const DataPack& pack, QByteArray&& raw_data) +{ + auto img = QImage::fromData(raw_data); + if (!img.isNull()) { + pack.setImage(img); + } else { + qWarning() << "Failed to parse pack.png."; + return false; + } + return true; +} + +bool processPackPNG(const DataPack& pack) +{ + auto png_invalid = [&pack]() { + qWarning() << "Data pack at" << pack.fileinfo().filePath() << "does not have a valid pack.png"; + return false; + }; + + switch (pack.type()) { + case ResourceType::FOLDER: { + QFileInfo image_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.png")); + if (image_file_info.exists() && image_file_info.isFile()) { + QFile pack_png_file(image_file_info.filePath()); + if (!pack_png_file.open(QIODevice::ReadOnly)) + return png_invalid(); // can't open pack.png file + + auto data = pack_png_file.readAll(); + + bool pack_png_result = DataPackUtils::processPackPNG(pack, std::move(data)); + + pack_png_file.close(); + if (!pack_png_result) { + return png_invalid(); // pack.png invalid + } + } else { + return png_invalid(); // pack.png does not exists or is not a valid file. + } + return false; // not processed correctly; https://github.com/PrismLauncher/PrismLauncher/issues/1740 + } + case ResourceType::ZIPFILE: { + QuaZip zip(pack.fileinfo().filePath()); + if (!zip.open(QuaZip::mdUnzip)) + return false; // can't open zip file + + QuaZipFile file(&zip); + if (zip.setCurrentFile("pack.png")) { + if (!file.open(QIODevice::ReadOnly)) { + qCritical() << "Failed to open file in zip."; + zip.close(); + return png_invalid(); + } + + auto data = file.readAll(); + + bool pack_png_result = DataPackUtils::processPackPNG(pack, std::move(data)); + + file.close(); + if (!pack_png_result) { + return png_invalid(); // pack.png invalid + } + } else { + return png_invalid(); // could not set pack.mcmeta as current file. + } + return false; // not processed correctly; https://github.com/PrismLauncher/PrismLauncher/issues/1740 + } + default: + qWarning() << "Invalid type for data pack parse task!"; + return false; + } +} + bool validate(QFileInfo file) { DataPack dp{ file }; diff --git a/launcher/minecraft/mod/tasks/LocalDataPackParseTask.h b/launcher/minecraft/mod/tasks/LocalDataPackParseTask.h index 12fd8c82c..4a83437ca 100644 --- a/launcher/minecraft/mod/tasks/LocalDataPackParseTask.h +++ b/launcher/minecraft/mod/tasks/LocalDataPackParseTask.h @@ -38,6 +38,10 @@ bool processZIP(DataPack& pack, ProcessingLevel level = ProcessingLevel::Full); bool processFolder(DataPack& pack, ProcessingLevel level = ProcessingLevel::Full); bool processMCMeta(DataPack& pack, QByteArray&& raw_data); +bool processPackPNG(const DataPack& pack, QByteArray&& raw_data); + +/// processes ONLY the pack.png (rest of the pack may be invalid) +bool processPackPNG(const DataPack& pack); /** Checks whether a file is valid as a data pack or not. */ bool validate(QFileInfo file); diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h index 72294c399..8b3fd15f8 100644 --- a/launcher/modplatform/ModIndex.h +++ b/launcher/modplatform/ModIndex.h @@ -36,7 +36,7 @@ Q_DECLARE_FLAGS(ModLoaderTypes, ModLoaderType) enum class ResourceProvider { MODRINTH, FLAME }; -enum class ResourceType { MOD, RESOURCE_PACK, SHADER_PACK }; +enum class ResourceType { MOD, RESOURCE_PACK, SHADER_PACK, DATA_PACK }; enum class DependencyType { REQUIRED, OPTIONAL, INCOMPATIBLE, EMBEDDED, TOOL, INCLUDE, UNKNOWN }; diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index e22d8f0d8..449a1625a 100644 --- a/launcher/modplatform/flame/FlameAPI.h +++ b/launcher/modplatform/flame/FlameAPI.h @@ -37,6 +37,7 @@ class FlameAPI : public NetworkResourceAPI { case ModPlatform::ResourceType::MOD: return 6; case ModPlatform::ResourceType::RESOURCE_PACK: + case ModPlatform::ResourceType::DATA_PACK: return 12; case ModPlatform::ResourceType::SHADER_PACK: return 6552; diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index d0f0811b2..d21d37d7e 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -61,6 +61,7 @@ class ModrinthAPI : public NetworkResourceAPI { { switch (type) { case ModPlatform::ResourceType::MOD: + case ModPlatform::ResourceType::DATA_PACK: return "mod"; case ModPlatform::ResourceType::RESOURCE_PACK: return "resourcepack"; @@ -81,6 +82,8 @@ class ModrinthAPI : public NetworkResourceAPI { facets_list.append(QString("[%1]").arg(getModLoaderFilters(args.loaders.value()))); if (args.versions.has_value()) facets_list.append(QString("[%1]").arg(getGameVersionsArray(args.versions.value()))); + if (args.type == ModPlatform::ResourceType::DATA_PACK) + facets_list.append("[\"categories:datapack\"]"); facets_list.append(QString("[\"project_type:%1\"]").arg(resourceTypeParameter(args.type))); return QString("[%1]").arg(facets_list.join(',')); diff --git a/launcher/ui/dialogs/ResourceDownloadDialog.cpp b/launcher/ui/dialogs/ResourceDownloadDialog.cpp index 1431ea92c..53c740d94 100644 --- a/launcher/ui/dialogs/ResourceDownloadDialog.cpp +++ b/launcher/ui/dialogs/ResourceDownloadDialog.cpp @@ -57,7 +57,7 @@ ResourceDownloadDialog::ResourceDownloadDialog(QWidget* parent, const std::share { setObjectName(QStringLiteral("ResourceDownloadDialog")); - resize(std::max(0.5 * parent->width(), 400.0), std::max(0.75 * parent->height(), 400.0)); + resize(static_cast(std::max(0.5 * parent->width(), 400.0)), static_cast(std::max(0.75 * parent->height(), 400.0))); setWindowIcon(APPLICATION->getThemedIcon("new")); @@ -356,4 +356,25 @@ QList ShaderPackDownloadDialog::getPages() return pages; } +DataPackDownloadDialog::DataPackDownloadDialog(QWidget* parent, + const std::shared_ptr& data_packs, + BaseInstance* instance) + : ResourceDownloadDialog(parent, data_packs), m_instance(instance) +{ + setWindowTitle(dialogTitle()); + + initializeContainer(); + connectButtons(); + + if (!geometrySaveKey().isEmpty()) + restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get(geometrySaveKey()).toByteArray())); +} + +QList DataPackDownloadDialog::getPages() +{ + QList pages; + pages.append(ModrinthDataPackPage::create(this, *m_instance)); + return pages; +} + } // namespace ResourceDownload diff --git a/launcher/ui/dialogs/ResourceDownloadDialog.h b/launcher/ui/dialogs/ResourceDownloadDialog.h index e9d2cfbe6..f5599041d 100644 --- a/launcher/ui/dialogs/ResourceDownloadDialog.h +++ b/launcher/ui/dialogs/ResourceDownloadDialog.h @@ -25,6 +25,7 @@ #include #include "QObjectPtr.h" +#include "minecraft/mod/DataPackFolderModel.h" #include "minecraft/mod/tasks/GetModDependenciesTask.h" #include "modplatform/ModIndex.h" #include "ui/pages/BasePageProvider.h" @@ -166,4 +167,21 @@ class ShaderPackDownloadDialog final : public ResourceDownloadDialog { BaseInstance* m_instance; }; +class DataPackDownloadDialog final : public ResourceDownloadDialog { + Q_OBJECT + + public: + explicit DataPackDownloadDialog(QWidget* parent, const std::shared_ptr& data_packs, BaseInstance* instance); + ~DataPackDownloadDialog() override = default; + + //: String that gets appended to the data pack download dialog title ("Download " + resourcesString()) + [[nodiscard]] QString resourcesString() const override { return tr("data packs"); } + [[nodiscard]] QString geometrySaveKey() const override { return "DataPackDownloadGeometry"; } + + QList getPages() override; + + private: + BaseInstance* m_instance; +}; + } // namespace ResourceDownload diff --git a/launcher/ui/pages/instance/DataPackPage.cpp b/launcher/ui/pages/instance/DataPackPage.cpp new file mode 100644 index 000000000..f46e7528f --- /dev/null +++ b/launcher/ui/pages/instance/DataPackPage.cpp @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 TheKodeToad + * + * 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 "DataPackPage.h" + +#include "ui/dialogs/CustomMessageBox.h" +#include "ui/dialogs/ProgressDialog.h" +#include "ui/dialogs/ResourceDownloadDialog.h" + +DataPackPage::DataPackPage(MinecraftInstance* instance, std::shared_ptr model, QWidget* parent) + : ExternalResourcesPage(instance, model, parent) +{ + ui->actionDownloadItem->setText(tr("Download packs")); + ui->actionDownloadItem->setToolTip(tr("Download data packs from online platforms")); + ui->actionDownloadItem->setEnabled(true); + connect(ui->actionDownloadItem, &QAction::triggered, this, &DataPackPage::downloadDataPacks); + ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionDownloadItem); + + ui->actionViewConfigs->setVisible(false); +} + +bool DataPackPage::onSelectionChanged(const QModelIndex& current, [[maybe_unused]] const QModelIndex& previous) +{ + auto sourceCurrent = m_filterModel->mapToSource(current); + int row = sourceCurrent.row(); + auto& dp = static_cast(m_model->at(row)); + ui->frame->updateWithDataPack(dp); + + return true; +} + +void DataPackPage::downloadDataPacks() +{ + if (m_instance->typeName() != "Minecraft") + return; // this is a null instance or a legacy instance + + ResourceDownload::DataPackDownloadDialog mdownload(this, std::static_pointer_cast(m_model), m_instance); + if (mdownload.exec()) { + auto tasks = + new ConcurrentTask(this, "Download Data Pack", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); + connect(tasks, &Task::failed, [this, tasks](QString reason) { + CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); + tasks->deleteLater(); + }); + connect(tasks, &Task::aborted, [this, tasks]() { + CustomMessageBox::selectable(this, tr("Aborted"), tr("Download stopped by user."), QMessageBox::Information)->show(); + tasks->deleteLater(); + }); + connect(tasks, &Task::succeeded, [this, tasks]() { + QStringList warnings = tasks->warnings(); + if (warnings.count()) + CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); + + tasks->deleteLater(); + }); + + for (auto& task : mdownload.getTasks()) { + tasks->addTask(task); + } + + ProgressDialog loadDialog(this); + loadDialog.setSkipButton(true, tr("Abort")); + loadDialog.execWithTask(tasks); + + m_model->update(); + } +} diff --git a/launcher/ui/pages/instance/DataPackPage.h b/launcher/ui/pages/instance/DataPackPage.h new file mode 100644 index 000000000..039a9c40f --- /dev/null +++ b/launcher/ui/pages/instance/DataPackPage.h @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2023 TheKodeToad + * + * 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 "ExternalResourcesPage.h" +#include "minecraft/mod/DataPackFolderModel.h" +#include "ui_ExternalResourcesPage.h" + +class DataPackPage : public ExternalResourcesPage { + Q_OBJECT + public: + explicit DataPackPage(MinecraftInstance* instance, std::shared_ptr model, QWidget* parent = 0); + + QString displayName() const override { return tr("Data packs"); } + QIcon icon() const override { return APPLICATION->getThemedIcon("datapacks"); } + QString id() const override { return "datapacks"; } + QString helpPage() const override { return "Data-packs"; } + bool shouldDisplay() const override { return true; } + + public slots: + bool onSelectionChanged(const QModelIndex& current, const QModelIndex& previous) override; + void downloadDataPacks(); +}; diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.h b/launcher/ui/pages/instance/ExternalResourcesPage.h index d29be0fc3..031935544 100644 --- a/launcher/ui/pages/instance/ExternalResourcesPage.h +++ b/launcher/ui/pages/instance/ExternalResourcesPage.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include diff --git a/launcher/ui/pages/instance/WorldListPage.cpp b/launcher/ui/pages/instance/WorldListPage.cpp index 587bb6ce6..133957328 100644 --- a/launcher/ui/pages/instance/WorldListPage.cpp +++ b/launcher/ui/pages/instance/WorldListPage.cpp @@ -57,6 +57,7 @@ #include "ui/GuiUtil.h" #include "Application.h" +#include "DataPackPage.h" class WorldListProxyModel : public QSortFilterProxyModel { Q_OBJECT @@ -82,7 +83,7 @@ class WorldListProxyModel : public QSortFilterProxyModel { } }; -WorldListPage::WorldListPage(BaseInstance* inst, std::shared_ptr worlds, QWidget* parent) +WorldListPage::WorldListPage(MinecraftInstance* inst, std::shared_ptr worlds, QWidget* parent) : QMainWindow(parent), m_inst(inst), ui(new Ui::WorldListPage), m_worlds(worlds) { ui->setupUi(this); @@ -210,7 +211,7 @@ void WorldListPage::on_actionView_Folder_triggered() DesktopServices::openDirectory(m_worlds->dir().absolutePath(), true); } -void WorldListPage::on_actionDatapacks_triggered() +void WorldListPage::on_actionData_Packs_triggered() { QModelIndex index = getSelectedWorld(); @@ -218,12 +219,33 @@ void WorldListPage::on_actionDatapacks_triggered() return; } - if (!worldSafetyNagQuestion(tr("Open World Datapacks Folder"))) + if (!worldSafetyNagQuestion(tr("Manage Data Packs"))) return; - auto fullPath = m_worlds->data(index, WorldList::FolderRole).toString(); + const QString fullPath = m_worlds->data(index, WorldList::FolderRole).toString(); + const QString folder = FS::PathCombine(fullPath, "datapacks"); + + auto dialog = new QDialog(window()); + dialog->setWindowTitle(tr("Data packs for %1").arg(m_worlds->data(index, WorldList::NameRole).toString())); + dialog->setWindowModality(Qt::WindowModal); + + dialog->resize(static_cast(std::max(0.5 * window()->width(), 400.0)), + static_cast(std::max(0.75 * window()->height(), 400.0))); + dialog->restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("DataPackDownloadGeometry").toByteArray())); + + auto layout = new QHBoxLayout(dialog); + auto page = new DataPackPage(m_inst, std::make_shared(folder, m_inst)); + page->setParent(dialog); // HACK: many pages extend QMainWindow; setting the parent manually prevents them from creating a window. + layout->addWidget(page); + dialog->setLayout(layout); + + connect(dialog, &QDialog::finished, this, [dialog, page] { + APPLICATION->settings()->set("DataPackDownloadGeometry", dialog->saveGeometry().toBase64()); + page->closed(); + }); - DesktopServices::openDirectory(FS::PathCombine(fullPath, "datapacks"), true); + dialog->show(); + page->opened(); } void WorldListPage::on_actionReset_Icon_triggered() @@ -336,7 +358,7 @@ void WorldListPage::worldChanged([[maybe_unused]] const QModelIndex& current, [[ ui->actionRemove->setEnabled(enable); ui->actionCopy->setEnabled(enable); ui->actionRename->setEnabled(enable); - ui->actionDatapacks->setEnabled(enable); + ui->actionData_Packs->setEnabled(enable); bool hasIcon = !index.data(WorldList::IconFileRole).isNull(); ui->actionReset_Icon->setEnabled(enable && hasIcon); } diff --git a/launcher/ui/pages/instance/WorldListPage.h b/launcher/ui/pages/instance/WorldListPage.h index 4f83002f4..50c5d20f6 100644 --- a/launcher/ui/pages/instance/WorldListPage.h +++ b/launcher/ui/pages/instance/WorldListPage.h @@ -53,7 +53,7 @@ class WorldListPage : public QMainWindow, public BasePage { Q_OBJECT public: - explicit WorldListPage(BaseInstance* inst, std::shared_ptr worlds, QWidget* parent = 0); + explicit WorldListPage(MinecraftInstance* inst, std::shared_ptr worlds, QWidget* parent = 0); virtual ~WorldListPage(); virtual QString displayName() const override { return tr("Worlds"); } @@ -72,7 +72,7 @@ class WorldListPage : public QMainWindow, public BasePage { QMenu* createPopupMenu() override; protected: - BaseInstance* m_inst; + MinecraftInstance* m_inst; private: QModelIndex getSelectedWorld(); @@ -97,7 +97,7 @@ class WorldListPage : public QMainWindow, public BasePage { void on_actionRename_triggered(); void on_actionRefresh_triggered(); void on_actionView_Folder_triggered(); - void on_actionDatapacks_triggered(); + void on_actionData_Packs_triggered(); void on_actionReset_Icon_triggered(); void worldChanged(const QModelIndex& current, const QModelIndex& previous); void mceditState(LoggedProcess::State state); diff --git a/launcher/ui/pages/instance/WorldListPage.ui b/launcher/ui/pages/instance/WorldListPage.ui index d74dd0796..b30b691d3 100644 --- a/launcher/ui/pages/instance/WorldListPage.ui +++ b/launcher/ui/pages/instance/WorldListPage.ui @@ -85,7 +85,7 @@ - + @@ -140,12 +140,12 @@ Remove world icon to make the game re-generate it on next load. - + - Datapacks + Data Packs - Manage datapacks inside the world. + Manage data packs inside the world. diff --git a/launcher/ui/pages/modplatform/DataPackModel.cpp b/launcher/ui/pages/modplatform/DataPackModel.cpp new file mode 100644 index 000000000..c17703d3c --- /dev/null +++ b/launcher/ui/pages/modplatform/DataPackModel.cpp @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2023 flowln +// SPDX-FileCopyrightText: 2023 TheKodeToad +// +// SPDX-License-Identifier: GPL-3.0-only + +#include "DataPackModel.h" + +#include + +namespace ResourceDownload { + +DataPackResourceModel::DataPackResourceModel(BaseInstance const& base_inst, ResourceAPI* api) + : ResourceModel(api), m_base_instance(base_inst) +{} + +/******** Make data requests ********/ + +ResourceAPI::SearchArgs DataPackResourceModel::createSearchArguments() +{ + auto sort = getCurrentSortingMethodByIndex(); + return { ModPlatform::ResourceType::DATA_PACK, m_next_search_offset, m_search_term, sort }; +} + +ResourceAPI::VersionSearchArgs DataPackResourceModel::createVersionsArguments(QModelIndex& entry) +{ + auto& pack = m_packs[entry.row()]; + return { *pack }; +} + +ResourceAPI::ProjectInfoArgs DataPackResourceModel::createInfoArguments(QModelIndex& entry) +{ + auto& pack = m_packs[entry.row()]; + return { *pack }; +} + +void DataPackResourceModel::searchWithTerm(const QString& term, unsigned int sort) +{ + if (m_search_term == term && m_search_term.isNull() == term.isNull() && m_current_sort_index == sort) { + return; + } + + setSearchTerm(term); + m_current_sort_index = sort; + + refresh(); +} + +} // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/DataPackModel.h b/launcher/ui/pages/modplatform/DataPackModel.h new file mode 100644 index 000000000..4954b7350 --- /dev/null +++ b/launcher/ui/pages/modplatform/DataPackModel.h @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: 2023 flowln +// SPDX-FileCopyrightText: 2023 TheKodeToad +// +// SPDX-License-Identifier: GPL-3.0-only + +#pragma once + +#include + +#include "BaseInstance.h" + +#include "modplatform/ModIndex.h" + +#include "ui/pages/modplatform/ResourceModel.h" + +class Version; + +namespace ResourceDownload { + +class DataPackResourceModel : public ResourceModel { + Q_OBJECT + + public: + DataPackResourceModel(BaseInstance const&, ResourceAPI*); + + /* Ask the API for more information */ + void searchWithTerm(const QString& term, unsigned int sort); + + void loadIndexedPack(ModPlatform::IndexedPack&, QJsonObject&) override = 0; + void loadExtraPackInfo(ModPlatform::IndexedPack&, QJsonObject&) override = 0; + void loadIndexedPackVersions(ModPlatform::IndexedPack&, QJsonArray&) override = 0; + + public slots: + ResourceAPI::SearchArgs createSearchArguments() override; + ResourceAPI::VersionSearchArgs createVersionsArguments(QModelIndex&) override; + ResourceAPI::ProjectInfoArgs createInfoArguments(QModelIndex&) override; + + protected: + const BaseInstance& m_base_instance; + + auto documentToArray(QJsonDocument& obj) const -> QJsonArray override = 0; +}; + +} // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/DataPackPage.cpp b/launcher/ui/pages/modplatform/DataPackPage.cpp new file mode 100644 index 000000000..84a777f3c --- /dev/null +++ b/launcher/ui/pages/modplatform/DataPackPage.cpp @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2023 flowln +// SPDX-FileCopyrightText: 2023 TheKodeToad +// +// SPDX-License-Identifier: GPL-3.0-only + +#include "DataPackPage.h" +#include "modplatform/ModIndex.h" +#include "ui_ResourcePage.h" + +#include "DataPackModel.h" + +#include "ui/dialogs/ResourceDownloadDialog.h" + +#include + +namespace ResourceDownload { + +DataPackResourcePage::DataPackResourcePage(DataPackDownloadDialog* dialog, BaseInstance& instance) : ResourcePage(dialog, instance) +{ + connect(m_ui->searchButton, &QPushButton::clicked, this, &DataPackResourcePage::triggerSearch); + connect(m_ui->packView, &QListView::doubleClicked, this, &DataPackResourcePage::onResourceSelected); +} + +/******** Callbacks to events in the UI (set up in the derived classes) ********/ + +void DataPackResourcePage::triggerSearch() +{ + m_ui->packView->clearSelection(); + m_ui->packDescription->clear(); + m_ui->versionSelectionBox->clear(); + + updateSelectionButton(); + + static_cast(m_model)->searchWithTerm(getSearchTerm(), m_ui->sortByBox->currentData().toUInt()); + m_fetch_progress.watch(m_model->activeSearchJob().get()); +} + +QMap DataPackResourcePage::urlHandlers() const +{ + QMap map; + map.insert(QRegularExpression::anchoredPattern("(?:www\\.)?modrinth\\.com\\/resourcepack\\/([^\\/]+)\\/?"), "modrinth"); + map.insert(QRegularExpression::anchoredPattern("(?:www\\.)?curseforge\\.com\\/minecraft\\/texture-packs\\/([^\\/]+)\\/?"), + "curseforge"); + map.insert(QRegularExpression::anchoredPattern("minecraft\\.curseforge\\.com\\/projects\\/([^\\/]+)\\/?"), "curseforge"); + return map; +} + +} // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/DataPackPage.h b/launcher/ui/pages/modplatform/DataPackPage.h new file mode 100644 index 000000000..55ed205f8 --- /dev/null +++ b/launcher/ui/pages/modplatform/DataPackPage.h @@ -0,0 +1,51 @@ +// SPDX-FileCopyrightText: 2023 flowln +// SPDX-FileCopyrightText: 2023 TheKodeToad +// +// SPDX-License-Identifier: GPL-3.0-only + +#pragma once + +#include "ui/pages/modplatform/ResourcePage.h" +#include "ui/pages/modplatform/DataPackModel.h" + +namespace Ui { +class ResourcePage; +} + +namespace ResourceDownload { + +class DataPackDownloadDialog; + +class DataPackResourcePage : public ResourcePage { + Q_OBJECT + + public: + template + static T* create(DataPackDownloadDialog* dialog, BaseInstance& instance) + { + auto page = new T(dialog, instance); + auto model = static_cast(page->getModel()); + + connect(model, &ResourceModel::versionListUpdated, page, &ResourcePage::updateVersionList); + connect(model, &ResourceModel::projectInfoUpdated, page, &ResourcePage::updateUi); + + return page; + } + + //: The plural version of 'data pack' + [[nodiscard]] inline QString resourcesString() const override { return tr("data packs"); } + //: The singular version of 'data packs' + [[nodiscard]] inline QString resourceString() const override { return tr("data pack"); } + + [[nodiscard]] bool supportsFiltering() const override { return false; }; + + [[nodiscard]] QMap urlHandlers() const override; + + protected: + DataPackResourcePage(DataPackDownloadDialog* dialog, BaseInstance& instance); + + protected slots: + void triggerSearch() override; +}; + +} // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp index 856018294..a2185233d 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp @@ -118,4 +118,27 @@ auto ModrinthShaderPackModel::documentToArray(QJsonDocument& obj) const -> QJson return obj.object().value("hits").toArray(); } +ModrinthDataPackModel::ModrinthDataPackModel(const BaseInstance& base) : DataPackResourceModel(base, new ModrinthAPI) {} + +void ModrinthDataPackModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) +{ + ::Modrinth::loadIndexedPack(m, obj); +} + +void ModrinthDataPackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) +{ + ::Modrinth::loadExtraPackData(m, obj); +} + +void ModrinthDataPackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) +{ + ::Modrinth::loadIndexedPackVersions(m, arr, &m_base_instance); +} + +auto ModrinthDataPackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray +{ + return obj.object().value("hits").toArray(); +} + + } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h index 15cd58544..6a5ba0382 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h @@ -20,6 +20,7 @@ #pragma once +#include "ui/pages/modplatform/DataPackModel.h" #include "ui/pages/modplatform/ModModel.h" #include "ui/pages/modplatform/ResourcePackModel.h" #include "ui/pages/modplatform/modrinth/ModrinthResourcePages.h" @@ -99,4 +100,22 @@ class ModrinthShaderPackModel : public ShaderPackResourceModel { auto documentToArray(QJsonDocument& obj) const -> QJsonArray override; }; +class ModrinthDataPackModel : public DataPackResourceModel { + Q_OBJECT + + public: + ModrinthDataPackModel(const BaseInstance&); + ~ModrinthDataPackModel() override = default; + + private: + [[nodiscard]] QString debugName() const override { return Modrinth::debugName() + " (Model)"; } + [[nodiscard]] QString metaEntryBase() const override { return Modrinth::metaEntryBase(); } + + void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; + void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override; + void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override; + + auto documentToArray(QJsonDocument& obj) const -> QJsonArray override; +}; + } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp index a4197b225..d4adf07d9 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp @@ -124,6 +124,24 @@ ModrinthShaderPackPage::ModrinthShaderPackPage(ShaderPackDownloadDialog* dialog, m_ui->packDescription->setMetaEntry(metaEntryBase()); } +ModrinthDataPackPage::ModrinthDataPackPage(DataPackDownloadDialog* dialog, BaseInstance& instance) + : DataPackResourcePage(dialog, instance) +{ + m_model = new ModrinthDataPackModel(instance); + m_ui->packView->setModel(m_model); + + addSortings(); + + // 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(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); + connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthDataPackPage::onSelectionChanged); + connect(m_ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &ModrinthDataPackPage::onVersionSelectionChanged); + connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &ModrinthDataPackPage::onResourceSelected); + + m_ui->packDescription->setMetaEntry(metaEntryBase()); +} + // 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... @@ -143,5 +161,9 @@ auto ModrinthShaderPackPage::shouldDisplay() const -> bool { return true; } +auto ModrinthDataPackPage::shouldDisplay() const -> bool +{ + return true; +} } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h index 311bcfe32..e9cb33a60 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h @@ -41,6 +41,7 @@ #include "modplatform/ResourceAPI.h" +#include "ui/pages/modplatform/DataPackPage.h" #include "ui/pages/modplatform/ModPage.h" #include "ui/pages/modplatform/ResourcePackPage.h" #include "ui/pages/modplatform/ShaderPackPage.h" @@ -166,4 +167,27 @@ class ModrinthShaderPackPage : public ShaderPackResourcePage { [[nodiscard]] inline auto helpPage() const -> QString override { return ""; } }; +class ModrinthDataPackPage : public DataPackResourcePage { + Q_OBJECT + + public: + static ModrinthDataPackPage* create(DataPackDownloadDialog* dialog, BaseInstance& instance) + { + return DataPackResourcePage::create(dialog, instance); + } + + ModrinthDataPackPage(DataPackDownloadDialog* dialog, BaseInstance& instance); + ~ModrinthDataPackPage() override = default; + + [[nodiscard]] bool shouldDisplay() const override; + + [[nodiscard]] inline auto displayName() const -> QString override { return Modrinth::displayName(); } + [[nodiscard]] inline auto icon() const -> QIcon override { return Modrinth::icon(); } + [[nodiscard]] inline auto id() const -> QString override { return Modrinth::id(); } + [[nodiscard]] inline auto debugName() const -> QString override { return Modrinth::debugName(); } + [[nodiscard]] inline auto metaEntryBase() const -> QString override { return Modrinth::metaEntryBase(); } + + [[nodiscard]] inline auto helpPage() const -> QString override { return ""; } +}; + } // namespace ResourceDownload diff --git a/launcher/ui/widgets/InfoFrame.cpp b/launcher/ui/widgets/InfoFrame.cpp index 69f72fea2..f44e1e3ff 100644 --- a/launcher/ui/widgets/InfoFrame.cpp +++ b/launcher/ui/widgets/InfoFrame.cpp @@ -212,6 +212,12 @@ void InfoFrame::updateWithResourcePack(ResourcePack& resource_pack) setImage(resource_pack.image({ 64, 64 })); } +void InfoFrame::updateWithDataPack(DataPack& data_pack) { + setName(renderColorCodes(data_pack.name())); + setDescription(renderColorCodes(data_pack.description())); + setImage(data_pack.image({ 64, 64 })); +} + void InfoFrame::updateWithTexturePack(TexturePack& texture_pack) { setName(renderColorCodes(texture_pack.name())); diff --git a/launcher/ui/widgets/InfoFrame.h b/launcher/ui/widgets/InfoFrame.h index d6764baa2..20c54e2e5 100644 --- a/launcher/ui/widgets/InfoFrame.h +++ b/launcher/ui/widgets/InfoFrame.h @@ -37,6 +37,7 @@ #include +#include "minecraft/mod/DataPack.h" #include "minecraft/mod/Mod.h" #include "minecraft/mod/ResourcePack.h" #include "minecraft/mod/TexturePack.h" @@ -63,6 +64,7 @@ class InfoFrame : public QFrame { void updateWithMod(Mod const& m); void updateWithResource(Resource const& resource); void updateWithResourcePack(ResourcePack& rp); + void updateWithDataPack(DataPack& rp); void updateWithTexturePack(TexturePack& tp); static QString renderColorCodes(QString input); From a737d5df42ed0a7149bbced10e8fd38fc2b6fe2f Mon Sep 17 00:00:00 2001 From: sshcrack <34072808+sshcrack@users.noreply.github.com> Date: Tue, 22 Oct 2024 20:53:57 +0200 Subject: [PATCH 002/695] added instance shortcut feature Signed-off-by: sshcrack <34072808+sshcrack@users.noreply.github.com> --- launcher/Application.cpp | 3 + launcher/FileSystem.cpp | 5 + launcher/FileSystem.h | 3 + launcher/ui/MainWindow.cpp | 227 ++++++++++++---------- launcher/ui/pages/global/LauncherPage.cpp | 35 ++++ launcher/ui/pages/global/LauncherPage.ui | 70 ++++--- 6 files changed, 216 insertions(+), 127 deletions(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index ea749ca4c..8714799ff 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -701,6 +701,9 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_settings->registerSetting("InstSortMode", "Name"); m_settings->registerSetting("SelectedInstance", QString()); + // Shortcut creation + m_settings->registerSetting("ShortcutCreationMode", "Desktop"); + // Window state and geometry m_settings->registerSetting("MainWindowState", ""); m_settings->registerSetting("MainWindowGeometry", ""); diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp index 512de28c2..8f683a9fb 100644 --- a/launcher/FileSystem.cpp +++ b/launcher/FileSystem.cpp @@ -915,6 +915,11 @@ QString getDesktopDir() return QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); } +QString getApplicationsDir() +{ + return QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation); +} + // Cross-platform Shortcut creation bool createShortcut(QString destination, QString target, QStringList args, QString name, QString icon) { diff --git a/launcher/FileSystem.h b/launcher/FileSystem.h index c5beef7bd..4aa5596ae 100644 --- a/launcher/FileSystem.h +++ b/launcher/FileSystem.h @@ -353,6 +353,9 @@ bool checkProblemticPathJava(QDir folder); // Get the Directory representing the User's Desktop QString getDesktopDir(); +// Get the Directory representing the User's Applications directory +QString getApplicationsDir(); + // Overrides one folder with the contents of another, preserving items exclusive to the first folder // Equivalent to doing QDir::rename, but allowing for overrides bool overrideFolder(QString overwritten_path, QString override_path); diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 09c47b609..0961a5c4e 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -1495,141 +1495,158 @@ void MainWindow::on_actionCreateInstanceShortcut_triggered() { if (!m_selectedInstance) return; - auto desktopPath = FS::getDesktopDir(); - if (desktopPath.isEmpty()) { - // TODO come up with an alternative solution (open "save file" dialog) - QMessageBox::critical(this, tr("Create instance shortcut"), tr("Couldn't find desktop?!")); - return; + + std::vector paths; + QString mode = APPLICATION->settings()->get("ShortcutCreationMode").toString(); + if (mode == "Applications") { + paths.push_back(FS::getApplicationsDir()); + } else if (mode == "Both") { + paths.push_back(FS::getDesktopDir()); + paths.push_back(FS::getApplicationsDir()); + } else { + // Default to desktop + paths.push_back(FS::getDesktopDir()); } - QString desktopFilePath; - QString appPath = QApplication::applicationFilePath(); - QString iconPath; - QStringList args; + for (const QString& shortcutDirPath : paths) { + if (shortcutDirPath.isEmpty()) { + // TODO come up with an alternative solution (open "save file" dialog) + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Couldn't find desktop?!")); + return; + } + + QString shortcutFilePath; + QString appPath = QApplication::applicationFilePath(); + QString iconPath; + QStringList args; #if defined(Q_OS_MACOS) - appPath = QApplication::applicationFilePath(); - if (appPath.startsWith("/private/var/")) { - QMessageBox::critical(this, tr("Create instance shortcut"), - tr("The launcher is in the folder it was extracted from, therefore it cannot create shortcuts.")); - return; - } + appPath = QApplication::applicationFilePath(); + if (appPath.startsWith("/private/var/")) { + QMessageBox::critical(this, tr("Create instance shortcut"), + tr("The launcher is in the folder it was extracted from, therefore it cannot create shortcuts.")); + return; + } - auto pIcon = APPLICATION->icons()->icon(m_selectedInstance->iconKey()); - if (pIcon == nullptr) { - pIcon = APPLICATION->icons()->icon("grass"); - } + auto pIcon = APPLICATION->icons()->icon(m_selectedInstance->iconKey()); + if (pIcon == nullptr) { + pIcon = APPLICATION->icons()->icon("grass"); + } - iconPath = FS::PathCombine(m_selectedInstance->instanceRoot(), "Icon.icns"); + iconPath = FS::PathCombine(m_selectedInstance->instanceRoot(), "Icon.icns"); - QFile iconFile(iconPath); - if (!iconFile.open(QFile::WriteOnly)) { - QMessageBox::critical(this, tr("Create instance Application"), tr("Failed to create icon for Application.")); - return; - } + QFile iconFile(iconPath); + if (!iconFile.open(QFile::WriteOnly)) { + QMessageBox::critical(this, tr("Create instance Application"), tr("Failed to create icon for Application.")); + return; + } - QIcon icon = pIcon->icon(); + QIcon icon = pIcon->icon(); - bool success = icon.pixmap(1024, 1024).save(iconPath, "ICNS"); - iconFile.close(); + bool success = icon.pixmap(1024, 1024).save(iconPath, "ICNS"); + iconFile.close(); - if (!success) { - iconFile.remove(); - QMessageBox::critical(this, tr("Create instance Application"), tr("Failed to create icon for Application.")); - return; - } + if (!success) { + iconFile.remove(); + QMessageBox::critical(this, tr("Create instance Application"), tr("Failed to create icon for Application.")); + return; + } #elif defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) - if (appPath.startsWith("/tmp/.mount_")) { - // AppImage! - appPath = QProcessEnvironment::systemEnvironment().value(QStringLiteral("APPIMAGE")); - if (appPath.isEmpty()) { - QMessageBox::critical(this, tr("Create instance shortcut"), - tr("Launcher is running as misconfigured AppImage? ($APPIMAGE environment variable is missing)")); - } else if (appPath.endsWith("/")) { - appPath.chop(1); + if (appPath.startsWith("/tmp/.mount_")) { + // AppImage! + appPath = QProcessEnvironment::systemEnvironment().value(QStringLiteral("APPIMAGE")); + if (appPath.isEmpty()) { + QMessageBox::critical(this, tr("Create instance shortcut"), + tr("Launcher is running as misconfigured AppImage? ($APPIMAGE environment variable is missing)")); + } else if (appPath.endsWith("/")) { + appPath.chop(1); + } } - } - auto icon = APPLICATION->icons()->icon(m_selectedInstance->iconKey()); - if (icon == nullptr) { - icon = APPLICATION->icons()->icon("grass"); - } + auto icon = APPLICATION->icons()->icon(m_selectedInstance->iconKey()); + if (icon == nullptr) { + icon = APPLICATION->icons()->icon("grass"); + } - iconPath = FS::PathCombine(m_selectedInstance->instanceRoot(), "icon.png"); + iconPath = FS::PathCombine(m_selectedInstance->instanceRoot(), "icon.png"); - QFile iconFile(iconPath); - if (!iconFile.open(QFile::WriteOnly)) { - QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut.")); - return; - } - bool success = icon->icon().pixmap(64, 64).save(&iconFile, "PNG"); - iconFile.close(); + QFile iconFile(iconPath); + if (!iconFile.open(QFile::WriteOnly)) { + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut.")); + return; + } + bool success = icon->icon().pixmap(64, 64).save(&iconFile, "PNG"); + iconFile.close(); - if (!success) { - iconFile.remove(); - QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut.")); - return; - } + if (!success) { + iconFile.remove(); + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut.")); + return; + } - if (DesktopServices::isFlatpak()) { - desktopFilePath = FS::PathCombine(desktopPath, FS::RemoveInvalidFilenameChars(m_selectedInstance->name()) + ".desktop"); - QFileDialog fileDialog; - // workaround to make sure the portal file dialog opens in the desktop directory - fileDialog.setDirectoryUrl(desktopPath); - desktopFilePath = fileDialog.getSaveFileName(this, tr("Create Shortcut"), desktopFilePath, tr("Desktop Entries") + " (*.desktop)"); - if (desktopFilePath.isEmpty()) - return; // file dialog canceled by user - appPath = "flatpak"; - QString flatpakAppId = BuildConfig.LAUNCHER_DESKTOPFILENAME; - flatpakAppId.remove(".desktop"); - args.append({ "run", flatpakAppId }); - } + if (DesktopServices::isFlatpak()) { + shortcutFilePath = FS::PathCombine(shortcutDirPath, FS::RemoveInvalidFilenameChars(m_selectedInstance->name()) + ".desktop"); + QFileDialog fileDialog; + // workaround to make sure the portal file dialog opens in the desktop directory + fileDialog.setDirectoryUrl(shortcutDirPath); + shortcutFilePath = + fileDialog.getSaveFileName(this, tr("Create Shortcut"), shortcutFilePath, tr("Desktop Entries") + " (*.desktop)"); + if (shortcutFilePath.isEmpty()) + return; // file dialog canceled by user + appPath = "flatpak"; + QString flatpakAppId = BuildConfig.LAUNCHER_DESKTOPFILENAME; + flatpakAppId.remove(".desktop"); + args.append({ "run", flatpakAppId }); + } #elif defined(Q_OS_WIN) - auto icon = APPLICATION->icons()->icon(m_selectedInstance->iconKey()); - if (icon == nullptr) { - icon = APPLICATION->icons()->icon("grass"); - } + auto icon = APPLICATION->icons()->icon(m_selectedInstance->iconKey()); + if (icon == nullptr) { + icon = APPLICATION->icons()->icon("grass"); + } - iconPath = FS::PathCombine(m_selectedInstance->instanceRoot(), "icon.ico"); + iconPath = FS::PathCombine(m_selectedInstance->instanceRoot(), "icon.ico"); - // part of fix for weird bug involving the window icon being replaced - // dunno why it happens, but this 2-line fix seems to be enough, so w/e - auto appIcon = APPLICATION->getThemedIcon("logo"); + // part of fix for weird bug involving the window icon being replaced + // dunno why it happens, but this 2-line fix seems to be enough, so w/e + auto appIcon = APPLICATION->getThemedIcon("logo"); - QFile iconFile(iconPath); - if (!iconFile.open(QFile::WriteOnly)) { - QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut.")); - return; - } - bool success = icon->icon().pixmap(64, 64).save(&iconFile, "ICO"); - iconFile.close(); + QFile iconFile(iconPath); + if (!iconFile.open(QFile::WriteOnly)) { + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut.")); + return; + } + bool success = icon->icon().pixmap(64, 64).save(&iconFile, "ICO"); + iconFile.close(); - // restore original window icon - QGuiApplication::setWindowIcon(appIcon); + // restore original window icon + QGuiApplication::setWindowIcon(appIcon); - if (!success) { - iconFile.remove(); - QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut.")); - return; - } + if (!success) { + iconFile.remove(); + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut.")); + return; + } #else - QMessageBox::critical(this, tr("Create instance shortcut"), tr("Not supported on your platform!")); - return; + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Not supported on your platform!")); + return; #endif - args.append({ "--launch", m_selectedInstance->id() }); - if (FS::createShortcut(desktopFilePath, appPath, args, m_selectedInstance->name(), iconPath)) { + args.append({ "--launch", m_selectedInstance->id() }); + + if (shortcutFilePath.isEmpty()) + shortcutFilePath = FS::PathCombine(shortcutDirPath, FS::RemoveInvalidFilenameChars(m_selectedInstance->name())); + if (!FS::createShortcut(shortcutFilePath, appPath, args, m_selectedInstance->name(), iconPath)) { #if not defined(Q_OS_MACOS) - QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance on your desktop!")); -#else - QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance!")); + iconFile.remove(); #endif - } else { + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create instance shortcut!")); + } + } #if not defined(Q_OS_MACOS) - iconFile.remove(); + QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance!")); +#else + QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance!")); #endif - QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create instance shortcut!")); - } } void MainWindow::taskEnd() diff --git a/launcher/ui/pages/global/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp index 8bbed9643..90540247e 100644 --- a/launcher/ui/pages/global/LauncherPage.cpp +++ b/launcher/ui/pages/global/LauncherPage.cpp @@ -65,6 +65,15 @@ enum InstSortMode { Sort_LastLaunch }; +enum ShortcutCreationMode { + // Create a shortcut in the applications + Shortcut_OnlyApplications, + // Create a shortcut in both locations + Shortcut_Both, + // Create a shortcut on the desktop + Shortcut_OnlyDesktop +}; + LauncherPage::LauncherPage(QWidget* parent) : QWidget(parent), ui(new Ui::LauncherPage) { ui->setupUi(this); @@ -254,6 +263,19 @@ void LauncherPage::applySettings() s->set("ModMetadataDisabled", ui->metadataDisableBtn->isChecked()); s->set("ModDependenciesDisabled", ui->dependenciesDisableBtn->isChecked()); s->set("SkipModpackUpdatePrompt", ui->skipModpackUpdatePromptBtn->isChecked()); + + auto shortcutMode = (ShortcutCreationMode) ui->createShortcutActionComboBox->currentIndex(); + switch (shortcutMode) { + case Shortcut_OnlyApplications: + s->set("ShortcutCreationMode", "Applications"); + break; + case Shortcut_Both: + s->set("ShortcutCreationMode", "Both"); + break; + case Shortcut_OnlyDesktop: + s->set("ShortcutCreationMode", "Desktop"); + break; + } } void LauncherPage::loadSettings() { @@ -319,6 +341,19 @@ void LauncherPage::loadSettings() ui->metadataWarningLabel->setHidden(!ui->metadataDisableBtn->isChecked()); ui->dependenciesDisableBtn->setChecked(s->get("ModDependenciesDisabled").toBool()); ui->skipModpackUpdatePromptBtn->setChecked(s->get("SkipModpackUpdatePrompt").toBool()); + + QString shortcutModeStr = s->get("ShortcutCreationMode").toString(); + ShortcutCreationMode shortcutMode = Shortcut_OnlyDesktop; + if(shortcutModeStr == "Applications") { + shortcutMode = Shortcut_OnlyApplications; + } else if(shortcutModeStr == "Desktop") { + // Guess we don't need that, but it's here for completeness + shortcutMode = Shortcut_OnlyDesktop; + } else if(shortcutModeStr == "Both") { + shortcutMode = Shortcut_Both; + } + + ui->createShortcutActionComboBox->setCurrentIndex(shortcutMode); } void LauncherPage::refreshFontPreview() diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index 3cba468ff..1e08d8266 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -282,27 +282,37 @@ Miscellaneous - - + + - 1 + 0 - - + + + + Seconds to wait until the requests are terminated + - Number of concurrent tasks + Timeout for HTTP requests - - + + 1 + + + + s + + + @@ -317,28 +327,44 @@ - - - - 0 + + + + Number of concurrent tasks - - - - Seconds to wait until the requests are terminated + + + + 1 + + + + - Timeout for HTTP requests + Create shortcut action - - - - s - + + + + + Applications only + + + + + Applications & Desktop + + + + + Desktop only + + From 59efca764c03450717f8544d96902624bcd6cad0 Mon Sep 17 00:00:00 2001 From: sshcrack <34072808+sshcrack@users.noreply.github.com> Date: Tue, 22 Oct 2024 21:20:22 +0200 Subject: [PATCH 003/695] removed creation of shortcuts for flatpak / appimage users Signed-off-by: sshcrack <34072808+sshcrack@users.noreply.github.com> --- launcher/ui/MainWindow.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 0961a5c4e..8979667f0 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -1633,6 +1633,7 @@ void MainWindow::on_actionCreateInstanceShortcut_triggered() #endif args.append({ "--launch", m_selectedInstance->id() }); + bool userDefinedShortcutPath = !shortcutFilePath.isEmpty(); if (shortcutFilePath.isEmpty()) shortcutFilePath = FS::PathCombine(shortcutDirPath, FS::RemoveInvalidFilenameChars(m_selectedInstance->name())); if (!FS::createShortcut(shortcutFilePath, appPath, args, m_selectedInstance->name(), iconPath)) { @@ -1640,7 +1641,11 @@ void MainWindow::on_actionCreateInstanceShortcut_triggered() iconFile.remove(); #endif QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create instance shortcut!")); + return; } + + if(userDefinedShortcutPath) + break; } #if not defined(Q_OS_MACOS) QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance!")); From b182a888aa3559ab3c0e8034533cc63040f84c34 Mon Sep 17 00:00:00 2001 From: sshcrack <34072808+sshcrack@users.noreply.github.com> Date: Fri, 25 Oct 2024 10:39:03 +0200 Subject: [PATCH 004/695] revert changes to settings and used menu for shortcuts Signed-off-by: sshcrack <34072808+sshcrack@users.noreply.github.com> --- launcher/Application.cpp | 3 - launcher/ui/MainWindow.cpp | 276 ++++++++++++---------- launcher/ui/MainWindow.h | 9 +- launcher/ui/MainWindow.ui | 166 ++++++------- launcher/ui/pages/global/LauncherPage.cpp | 37 +-- launcher/ui/pages/global/LauncherPage.ui | 72 ++---- 6 files changed, 256 insertions(+), 307 deletions(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 8714799ff..ea749ca4c 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -701,9 +701,6 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_settings->registerSetting("InstSortMode", "Name"); m_settings->registerSetting("SelectedInstance", QString()); - // Shortcut creation - m_settings->registerSetting("ShortcutCreationMode", "Desktop"); - // Window state and geometry m_settings->registerSetting("MainWindowState", ""); m_settings->registerSetting("MainWindowGeometry", ""); diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 8979667f0..511055b07 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -208,6 +208,13 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi exportInstanceMenu->addAction(ui->actionExportInstanceMrPack); exportInstanceMenu->addAction(ui->actionExportInstanceFlamePack); ui->actionExportInstance->setMenu(exportInstanceMenu); + + auto shortcutInstanceMenu = new QMenu(this); + shortcutInstanceMenu->addAction(ui->actionCreateInstanceShortcutDesktop); + shortcutInstanceMenu->addAction(ui->actionCreateInstanceShortcutApplications); + shortcutInstanceMenu->addAction(ui->actionCreateInstanceShortcutOther); + + ui->actionCreateInstanceShortcut->setMenu(shortcutInstanceMenu); } // hide, disable and show stuff @@ -235,6 +242,13 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi } ui->actionViewJavaFolder->setEnabled(BuildConfig.JAVA_DOWNLOADER_ENABLED); + +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) + bool isFlatpak = DesktopServices::isFlatpak(); + + ui->actionCreateInstanceShortcutDesktop->setEnabled(isFlatpak); + ui->actionCreateInstanceShortcutApplications->setEnabled(isFlatpak); +#endif } // add the toolbar toggles to the view menu @@ -1491,167 +1505,169 @@ void MainWindow::on_actionKillInstance_triggered() } } -void MainWindow::on_actionCreateInstanceShortcut_triggered() -{ - if (!m_selectedInstance) - return; - - std::vector paths; - QString mode = APPLICATION->settings()->get("ShortcutCreationMode").toString(); - if (mode == "Applications") { - paths.push_back(FS::getApplicationsDir()); - } else if (mode == "Both") { - paths.push_back(FS::getDesktopDir()); - paths.push_back(FS::getApplicationsDir()); - } else { - // Default to desktop - paths.push_back(FS::getDesktopDir()); - } +void MainWindow::createInstanceShortcut(QString shortcutFilePath) { - for (const QString& shortcutDirPath : paths) { - if (shortcutDirPath.isEmpty()) { - // TODO come up with an alternative solution (open "save file" dialog) - QMessageBox::critical(this, tr("Create instance shortcut"), tr("Couldn't find desktop?!")); - return; - } - - QString shortcutFilePath; - QString appPath = QApplication::applicationFilePath(); - QString iconPath; - QStringList args; + QString appPath = QApplication::applicationFilePath(); + QString iconPath; + QStringList args; #if defined(Q_OS_MACOS) - appPath = QApplication::applicationFilePath(); - if (appPath.startsWith("/private/var/")) { - QMessageBox::critical(this, tr("Create instance shortcut"), - tr("The launcher is in the folder it was extracted from, therefore it cannot create shortcuts.")); - return; - } + appPath = QApplication::applicationFilePath(); + if (appPath.startsWith("/private/var/")) { + QMessageBox::critical(this, tr("Create instance shortcut"), + tr("The launcher is in the folder it was extracted from, therefore it cannot create shortcuts.")); + return; + } - auto pIcon = APPLICATION->icons()->icon(m_selectedInstance->iconKey()); - if (pIcon == nullptr) { - pIcon = APPLICATION->icons()->icon("grass"); - } + auto pIcon = APPLICATION->icons()->icon(m_selectedInstance->iconKey()); + if (pIcon == nullptr) { + pIcon = APPLICATION->icons()->icon("grass"); + } - iconPath = FS::PathCombine(m_selectedInstance->instanceRoot(), "Icon.icns"); + iconPath = FS::PathCombine(m_selectedInstance->instanceRoot(), "Icon.icns"); - QFile iconFile(iconPath); - if (!iconFile.open(QFile::WriteOnly)) { - QMessageBox::critical(this, tr("Create instance Application"), tr("Failed to create icon for Application.")); - return; - } + QFile iconFile(iconPath); + if (!iconFile.open(QFile::WriteOnly)) { + QMessageBox::critical(this, tr("Create instance Application"), tr("Failed to create icon for Application.")); + return; + } - QIcon icon = pIcon->icon(); + QIcon icon = pIcon->icon(); - bool success = icon.pixmap(1024, 1024).save(iconPath, "ICNS"); - iconFile.close(); + bool success = icon.pixmap(1024, 1024).save(iconPath, "ICNS"); + iconFile.close(); - if (!success) { - iconFile.remove(); - QMessageBox::critical(this, tr("Create instance Application"), tr("Failed to create icon for Application.")); - return; - } + if (!success) { + iconFile.remove(); + QMessageBox::critical(this, tr("Create instance Application"), tr("Failed to create icon for Application.")); + return; + } #elif defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) - if (appPath.startsWith("/tmp/.mount_")) { - // AppImage! - appPath = QProcessEnvironment::systemEnvironment().value(QStringLiteral("APPIMAGE")); - if (appPath.isEmpty()) { - QMessageBox::critical(this, tr("Create instance shortcut"), - tr("Launcher is running as misconfigured AppImage? ($APPIMAGE environment variable is missing)")); - } else if (appPath.endsWith("/")) { - appPath.chop(1); - } + if (appPath.startsWith("/tmp/.mount_")) { + // AppImage! + appPath = QProcessEnvironment::systemEnvironment().value(QStringLiteral("APPIMAGE")); + if (appPath.isEmpty()) { + QMessageBox::critical(this, tr("Create instance shortcut"), + tr("Launcher is running as misconfigured AppImage? ($APPIMAGE environment variable is missing)")); + } else if (appPath.endsWith("/")) { + appPath.chop(1); } + } - auto icon = APPLICATION->icons()->icon(m_selectedInstance->iconKey()); - if (icon == nullptr) { - icon = APPLICATION->icons()->icon("grass"); - } + auto icon = APPLICATION->icons()->icon(m_selectedInstance->iconKey()); + if (icon == nullptr) { + icon = APPLICATION->icons()->icon("grass"); + } - iconPath = FS::PathCombine(m_selectedInstance->instanceRoot(), "icon.png"); + iconPath = FS::PathCombine(m_selectedInstance->instanceRoot(), "icon.png"); - QFile iconFile(iconPath); - if (!iconFile.open(QFile::WriteOnly)) { - QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut.")); - return; - } - bool success = icon->icon().pixmap(64, 64).save(&iconFile, "PNG"); - iconFile.close(); + QFile iconFile(iconPath); + if (!iconFile.open(QFile::WriteOnly)) { + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut.")); + return; + } + bool success = icon->icon().pixmap(64, 64).save(&iconFile, "PNG"); + iconFile.close(); - if (!success) { - iconFile.remove(); - QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut.")); - return; - } + if (!success) { + iconFile.remove(); + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut.")); + return; + } - if (DesktopServices::isFlatpak()) { - shortcutFilePath = FS::PathCombine(shortcutDirPath, FS::RemoveInvalidFilenameChars(m_selectedInstance->name()) + ".desktop"); - QFileDialog fileDialog; - // workaround to make sure the portal file dialog opens in the desktop directory - fileDialog.setDirectoryUrl(shortcutDirPath); - shortcutFilePath = - fileDialog.getSaveFileName(this, tr("Create Shortcut"), shortcutFilePath, tr("Desktop Entries") + " (*.desktop)"); - if (shortcutFilePath.isEmpty()) - return; // file dialog canceled by user - appPath = "flatpak"; - QString flatpakAppId = BuildConfig.LAUNCHER_DESKTOPFILENAME; - flatpakAppId.remove(".desktop"); - args.append({ "run", flatpakAppId }); - } + if (DesktopServices::isFlatpak()) { + appPath = "flatpak"; + QString flatpakAppId = BuildConfig.LAUNCHER_DESKTOPFILENAME; + flatpakAppId.remove(".desktop"); + args.append({ "run", flatpakAppId }); + } #elif defined(Q_OS_WIN) - auto icon = APPLICATION->icons()->icon(m_selectedInstance->iconKey()); - if (icon == nullptr) { - icon = APPLICATION->icons()->icon("grass"); - } + auto icon = APPLICATION->icons()->icon(m_selectedInstance->iconKey()); + if (icon == nullptr) { + icon = APPLICATION->icons()->icon("grass"); + } - iconPath = FS::PathCombine(m_selectedInstance->instanceRoot(), "icon.ico"); + iconPath = FS::PathCombine(m_selectedInstance->instanceRoot(), "icon.ico"); - // part of fix for weird bug involving the window icon being replaced - // dunno why it happens, but this 2-line fix seems to be enough, so w/e - auto appIcon = APPLICATION->getThemedIcon("logo"); + // part of fix for weird bug involving the window icon being replaced + // dunno why it happens, but this 2-line fix seems to be enough, so w/e + auto appIcon = APPLICATION->getThemedIcon("logo"); - QFile iconFile(iconPath); - if (!iconFile.open(QFile::WriteOnly)) { - QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut.")); - return; - } - bool success = icon->icon().pixmap(64, 64).save(&iconFile, "ICO"); - iconFile.close(); + QFile iconFile(iconPath); + if (!iconFile.open(QFile::WriteOnly)) { + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut.")); + return; + } + bool success = icon->icon().pixmap(64, 64).save(&iconFile, "ICO"); + iconFile.close(); - // restore original window icon - QGuiApplication::setWindowIcon(appIcon); + // restore original window icon + QGuiApplication::setWindowIcon(appIcon); - if (!success) { - iconFile.remove(); - QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut.")); - return; - } + if (!success) { + iconFile.remove(); + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut.")); + return; + } #else - QMessageBox::critical(this, tr("Create instance shortcut"), tr("Not supported on your platform!")); - return; + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Not supported on your platform!")); + return; #endif - args.append({ "--launch", m_selectedInstance->id() }); + args.append({ "--launch", m_selectedInstance->id() }); - bool userDefinedShortcutPath = !shortcutFilePath.isEmpty(); - if (shortcutFilePath.isEmpty()) - shortcutFilePath = FS::PathCombine(shortcutDirPath, FS::RemoveInvalidFilenameChars(m_selectedInstance->name())); - if (!FS::createShortcut(shortcutFilePath, appPath, args, m_selectedInstance->name(), iconPath)) { + if (!FS::createShortcut(std::move(shortcutFilePath), appPath, args, m_selectedInstance->name(), iconPath)) { #if not defined(Q_OS_MACOS) - iconFile.remove(); + iconFile.remove(); #endif - QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create instance shortcut!")); - return; - } - - if(userDefinedShortcutPath) - break; + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create instance shortcut!")); + return; } -#if not defined(Q_OS_MACOS) - QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance!")); +} + +void MainWindow::on_actionCreateInstanceShortcutOther_triggered() { + if (!m_selectedInstance) + return; + + QString defaultedDir = FS::getDesktopDir(); +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) + QString extension = ".desktop"; +#elif defined(Q_OS_WINDOWS) + QString extension = ".lnk"; #else - QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance!")); + QString extension = ""; #endif + + QString shortcutFilePath = FS::PathCombine(defaultedDir, FS::RemoveInvalidFilenameChars(m_selectedInstance->name()) + extension); + QFileDialog fileDialog; + // workaround to make sure the portal file dialog opens in the desktop directory + fileDialog.setDirectoryUrl(defaultedDir); + + shortcutFilePath = fileDialog.getSaveFileName(this, tr("Create Shortcut"), shortcutFilePath, tr("Desktop Entries") + " (*.desktop)"); + if (shortcutFilePath.isEmpty()) + return; // file dialog canceled by user + + createInstanceShortcut(shortcutFilePath); + QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance!")); +} + +void MainWindow::on_actionCreateInstanceShortcutDesktop_triggered() { + if (!m_selectedInstance) + return; + + QString shortcutFilePath = FS::PathCombine(FS::getDesktopDir(), FS::RemoveInvalidFilenameChars(m_selectedInstance->name())); + createInstanceShortcut(shortcutFilePath); + QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance on your desktop!")); +} + +void MainWindow::on_actionCreateInstanceShortcutApplications_triggered() +{ + if (!m_selectedInstance) + return; + + QString shortcutFilePath = FS::PathCombine(FS::getApplicationsDir(), FS::RemoveInvalidFilenameChars(m_selectedInstance->name())); + createInstanceShortcut(shortcutFilePath); + QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance in your applications folder!")); } void MainWindow::taskEnd() diff --git a/launcher/ui/MainWindow.h b/launcher/ui/MainWindow.h index 0e692eda7..bdd4a1890 100644 --- a/launcher/ui/MainWindow.h +++ b/launcher/ui/MainWindow.h @@ -165,7 +165,13 @@ class MainWindow : public QMainWindow { void on_actionEditInstance_triggered(); - void on_actionCreateInstanceShortcut_triggered(); + inline void on_actionCreateInstanceShortcut_triggered() { + on_actionCreateInstanceShortcutDesktop_triggered(); + }; + + void on_actionCreateInstanceShortcutDesktop_triggered(); + void on_actionCreateInstanceShortcutApplications_triggered(); + void on_actionCreateInstanceShortcutOther_triggered(); void taskEnd(); @@ -226,6 +232,7 @@ class MainWindow : public QMainWindow { void setSelectedInstanceById(const QString& id); void updateStatusCenter(); void setInstanceActionsEnabled(bool enabled); + void createInstanceShortcut(QString shortcutDirPath); void runModalTask(Task* task); void instanceFromInstanceTask(InstanceTask* task); diff --git a/launcher/ui/MainWindow.ui b/launcher/ui/MainWindow.ui index f20c34206..3fa30c97e 100644 --- a/launcher/ui/MainWindow.ui +++ b/launcher/ui/MainWindow.ui @@ -131,7 +131,7 @@ 0 0 800 - 27 + 22 @@ -235,8 +235,7 @@ - - .. + More news... @@ -250,8 +249,7 @@ true - - .. + &Meow @@ -286,8 +284,7 @@ - - .. + Add Instanc&e... @@ -298,8 +295,7 @@ - - .. + &Update... @@ -313,8 +309,7 @@ - - .. + Setti&ngs... @@ -328,8 +323,7 @@ - - .. + &Manage Accounts... @@ -337,8 +331,7 @@ - - .. + &Launch @@ -349,8 +342,7 @@ - - .. + &Kill @@ -364,8 +356,7 @@ - - .. + Rename @@ -376,8 +367,7 @@ - - .. + &Change Group... @@ -399,8 +389,7 @@ - - .. + &Edit... @@ -414,8 +403,7 @@ - - .. + &Folder @@ -426,8 +414,7 @@ - - .. + Dele&te @@ -441,8 +428,7 @@ - - .. + Cop&y... @@ -456,8 +442,7 @@ - - .. + E&xport... @@ -468,8 +453,7 @@ - - .. + Prism Launcher (zip) @@ -477,8 +461,7 @@ - - .. + Modrinth (mrpack) @@ -486,8 +469,7 @@ - - .. + CurseForge (zip) @@ -495,20 +477,18 @@ - - .. + Create Shortcut - Creates a shortcut on your desktop to launch the selected instance. + Creates a shortcut on a selected folder to launch the selected instance. - - .. + No accounts added! @@ -519,8 +499,7 @@ true - - .. + No Default Account @@ -531,8 +510,7 @@ - - .. + Close &Window @@ -546,8 +524,7 @@ - - .. + &Instances @@ -558,8 +535,7 @@ - - .. + Launcher &Root @@ -570,8 +546,7 @@ - - .. + &Central Mods @@ -582,8 +557,7 @@ - - .. + &Skins @@ -594,8 +568,7 @@ - - .. + Instance Icons @@ -606,8 +579,7 @@ - - .. + Logs @@ -623,8 +595,7 @@ - - .. + Report a Bug or Suggest a Feature @@ -635,8 +606,7 @@ - - .. + &Discord Guild @@ -647,8 +617,7 @@ - - .. + &Matrix Space @@ -659,8 +628,7 @@ - - .. + Sub&reddit @@ -671,8 +639,7 @@ - - .. + &About %1 @@ -686,8 +653,7 @@ - - .. + &Clear Metadata Cache @@ -698,8 +664,7 @@ - - .. + Install to &PATH @@ -710,8 +675,7 @@ - - .. + Folders @@ -722,8 +686,7 @@ - - .. + Help @@ -734,8 +697,7 @@ - - .. + Accounts @@ -743,8 +705,7 @@ - - .. + %1 &Help @@ -755,8 +716,7 @@ - - .. + &Widget Themes @@ -767,8 +727,7 @@ - - .. + I&con Theme @@ -779,8 +738,7 @@ - - .. + Cat Packs @@ -791,8 +749,7 @@ - - .. + Java @@ -801,6 +758,39 @@ Open the Java folder in a file browser. Only available if the built-in Java downloader is used. + + + Desktop + + + Creates an shortcut to this instance on your desktop + + + QAction::TextHeuristicRole + + + + + Applications + + + Create a shortcut of this instance on your start menu + + + QAction::TextHeuristicRole + + + + + Other... + + + Creates a shortcut in a folder selected by you + + + QAction::TextHeuristicRole + + diff --git a/launcher/ui/pages/global/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp index 90540247e..da4ba9023 100644 --- a/launcher/ui/pages/global/LauncherPage.cpp +++ b/launcher/ui/pages/global/LauncherPage.cpp @@ -65,15 +65,6 @@ enum InstSortMode { Sort_LastLaunch }; -enum ShortcutCreationMode { - // Create a shortcut in the applications - Shortcut_OnlyApplications, - // Create a shortcut in both locations - Shortcut_Both, - // Create a shortcut on the desktop - Shortcut_OnlyDesktop -}; - LauncherPage::LauncherPage(QWidget* parent) : QWidget(parent), ui(new Ui::LauncherPage) { ui->setupUi(this); @@ -263,19 +254,6 @@ void LauncherPage::applySettings() s->set("ModMetadataDisabled", ui->metadataDisableBtn->isChecked()); s->set("ModDependenciesDisabled", ui->dependenciesDisableBtn->isChecked()); s->set("SkipModpackUpdatePrompt", ui->skipModpackUpdatePromptBtn->isChecked()); - - auto shortcutMode = (ShortcutCreationMode) ui->createShortcutActionComboBox->currentIndex(); - switch (shortcutMode) { - case Shortcut_OnlyApplications: - s->set("ShortcutCreationMode", "Applications"); - break; - case Shortcut_Both: - s->set("ShortcutCreationMode", "Both"); - break; - case Shortcut_OnlyDesktop: - s->set("ShortcutCreationMode", "Desktop"); - break; - } } void LauncherPage::loadSettings() { @@ -341,19 +319,6 @@ void LauncherPage::loadSettings() ui->metadataWarningLabel->setHidden(!ui->metadataDisableBtn->isChecked()); ui->dependenciesDisableBtn->setChecked(s->get("ModDependenciesDisabled").toBool()); ui->skipModpackUpdatePromptBtn->setChecked(s->get("SkipModpackUpdatePrompt").toBool()); - - QString shortcutModeStr = s->get("ShortcutCreationMode").toString(); - ShortcutCreationMode shortcutMode = Shortcut_OnlyDesktop; - if(shortcutModeStr == "Applications") { - shortcutMode = Shortcut_OnlyApplications; - } else if(shortcutModeStr == "Desktop") { - // Guess we don't need that, but it's here for completeness - shortcutMode = Shortcut_OnlyDesktop; - } else if(shortcutModeStr == "Both") { - shortcutMode = Shortcut_Both; - } - - ui->createShortcutActionComboBox->setCurrentIndex(shortcutMode); } void LauncherPage::refreshFontPreview() @@ -404,4 +369,4 @@ void LauncherPage::refreshFontPreview() void LauncherPage::retranslate() { ui->retranslateUi(this); -} +} \ No newline at end of file diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index 1e08d8266..ff95bdfbb 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -282,37 +282,27 @@ Miscellaneous - - + + - 0 + 1 - - - - Seconds to wait until the requests are terminated - + + - Timeout for HTTP requests + Number of concurrent tasks - - + + 1 - - - - s - - - @@ -327,44 +317,28 @@ - - - - Number of concurrent tasks - - - - - + + - 1 + 0 - - + + + + Seconds to wait until the requests are terminated + - Create shortcut action + Timeout for HTTP requests - - - - - Applications only - - - - - Applications & Desktop - - - - - Desktop only - - + + + + s + @@ -694,4 +668,4 @@ - + \ No newline at end of file From ef2f865159b23dd5715e77c838d7e7b1e0c2a694 Mon Sep 17 00:00:00 2001 From: sshcrack <34072808+sshcrack@users.noreply.github.com> Date: Fri, 25 Oct 2024 11:45:50 +0200 Subject: [PATCH 005/695] add back folder checks / use specific extension Signed-off-by: sshcrack <34072808+sshcrack@users.noreply.github.com> --- launcher/ui/MainWindow.cpp | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 511055b07..599497224 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -1506,6 +1506,8 @@ void MainWindow::on_actionKillInstance_triggered() } void MainWindow::createInstanceShortcut(QString shortcutFilePath) { + if(!m_selectedInstance) + return; QString appPath = QApplication::applicationFilePath(); QString iconPath; @@ -1643,7 +1645,7 @@ void MainWindow::on_actionCreateInstanceShortcutOther_triggered() { // workaround to make sure the portal file dialog opens in the desktop directory fileDialog.setDirectoryUrl(defaultedDir); - shortcutFilePath = fileDialog.getSaveFileName(this, tr("Create Shortcut"), shortcutFilePath, tr("Desktop Entries") + " (*.desktop)"); + shortcutFilePath = fileDialog.getSaveFileName(this, tr("Create Shortcut"), shortcutFilePath, tr("Desktop Entries") + " (*." + extension + ")"); if (shortcutFilePath.isEmpty()) return; // file dialog canceled by user @@ -1655,6 +1657,12 @@ void MainWindow::on_actionCreateInstanceShortcutDesktop_triggered() { if (!m_selectedInstance) return; + QString desktopDir = FS::getDesktopDir(); + if (desktopDir.isEmpty()) { + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Couldn't find desktop?!")); + return; + } + QString shortcutFilePath = FS::PathCombine(FS::getDesktopDir(), FS::RemoveInvalidFilenameChars(m_selectedInstance->name())); createInstanceShortcut(shortcutFilePath); QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance on your desktop!")); @@ -1665,7 +1673,13 @@ void MainWindow::on_actionCreateInstanceShortcutApplications_triggered() if (!m_selectedInstance) return; - QString shortcutFilePath = FS::PathCombine(FS::getApplicationsDir(), FS::RemoveInvalidFilenameChars(m_selectedInstance->name())); + QString applicationsDir = FS::getApplicationsDir(); + if (applicationsDir.isEmpty()) { + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Couldn't find applications folder?!")); + return; + } + + QString shortcutFilePath = FS::PathCombine(applicationsDir, FS::RemoveInvalidFilenameChars(m_selectedInstance->name())); createInstanceShortcut(shortcutFilePath); QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance in your applications folder!")); } From f1048c2e0d7593069e59282708838a5a5359f951 Mon Sep 17 00:00:00 2001 From: sshcrack <34072808+sshcrack@users.noreply.github.com> Date: Fri, 25 Oct 2024 11:46:27 +0200 Subject: [PATCH 006/695] removed unnecessary macro Signed-off-by: sshcrack <34072808+sshcrack@users.noreply.github.com> --- launcher/ui/MainWindow.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 599497224..6b9daa398 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -243,12 +243,10 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi ui->actionViewJavaFolder->setEnabled(BuildConfig.JAVA_DOWNLOADER_ENABLED); -#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) bool isFlatpak = DesktopServices::isFlatpak(); ui->actionCreateInstanceShortcutDesktop->setEnabled(isFlatpak); ui->actionCreateInstanceShortcutApplications->setEnabled(isFlatpak); -#endif } // add the toolbar toggles to the view menu From b16e12c9af4288a853b1c0b013a7d406d8952be8 Mon Sep 17 00:00:00 2001 From: sshcrack <34072808+sshcrack@users.noreply.github.com> Date: Fri, 25 Oct 2024 11:49:08 +0200 Subject: [PATCH 007/695] remove dot Signed-off-by: sshcrack <34072808+sshcrack@users.noreply.github.com> --- launcher/ui/MainWindow.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 6b9daa398..37d4422e4 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -1643,7 +1643,7 @@ void MainWindow::on_actionCreateInstanceShortcutOther_triggered() { // workaround to make sure the portal file dialog opens in the desktop directory fileDialog.setDirectoryUrl(defaultedDir); - shortcutFilePath = fileDialog.getSaveFileName(this, tr("Create Shortcut"), shortcutFilePath, tr("Desktop Entries") + " (*." + extension + ")"); + shortcutFilePath = fileDialog.getSaveFileName(this, tr("Create Shortcut"), shortcutFilePath, tr("Desktop Entries") + " (*" + extension + ")"); if (shortcutFilePath.isEmpty()) return; // file dialog canceled by user From 43ccf18449fa7eeee0b1fa001c64b01c7d072ec3 Mon Sep 17 00:00:00 2001 From: sshcrack <34072808+sshcrack@users.noreply.github.com> Date: Fri, 25 Oct 2024 11:51:47 +0200 Subject: [PATCH 008/695] fix default action for flatpak Signed-off-by: sshcrack <34072808+sshcrack@users.noreply.github.com> --- launcher/ui/MainWindow.cpp | 10 ++++++++++ launcher/ui/MainWindow.h | 4 +--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 37d4422e4..e73a702f8 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -1651,6 +1651,16 @@ void MainWindow::on_actionCreateInstanceShortcutOther_triggered() { QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance!")); } +void MainWindow::on_actionCreateInstanceShortcut_triggered() { + if(!m_selectedInstance) + return; + + if(DesktopServices::isFlatpak()) + onactionCreateInstanceShortcutOther_triggered(); + else + on_actionCreateInstanceShortcutDesktop_triggered(); +} + void MainWindow::on_actionCreateInstanceShortcutDesktop_triggered() { if (!m_selectedInstance) return; diff --git a/launcher/ui/MainWindow.h b/launcher/ui/MainWindow.h index bdd4a1890..f3f2de730 100644 --- a/launcher/ui/MainWindow.h +++ b/launcher/ui/MainWindow.h @@ -165,9 +165,7 @@ class MainWindow : public QMainWindow { void on_actionEditInstance_triggered(); - inline void on_actionCreateInstanceShortcut_triggered() { - on_actionCreateInstanceShortcutDesktop_triggered(); - }; + void on_actionCreateInstanceShortcut_triggered(); void on_actionCreateInstanceShortcutDesktop_triggered(); void on_actionCreateInstanceShortcutApplications_triggered(); From cd3db28fceea38602979571aa254e7d7e6a34c2f Mon Sep 17 00:00:00 2001 From: sshcrack <34072808+sshcrack@users.noreply.github.com> Date: Fri, 25 Oct 2024 11:52:44 +0200 Subject: [PATCH 009/695] fixed typo Signed-off-by: sshcrack <34072808+sshcrack@users.noreply.github.com> --- launcher/ui/MainWindow.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index e73a702f8..535089343 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -1656,7 +1656,7 @@ void MainWindow::on_actionCreateInstanceShortcut_triggered() { return; if(DesktopServices::isFlatpak()) - onactionCreateInstanceShortcutOther_triggered(); + on_actionCreateInstanceShortcutOther_triggered(); else on_actionCreateInstanceShortcutDesktop_triggered(); } From 7c60f375f35cebbbaaa72555e8195096cd14e9aa Mon Sep 17 00:00:00 2001 From: sshcrack <34072808+sshcrack@users.noreply.github.com> Date: Fri, 25 Oct 2024 14:15:04 +0200 Subject: [PATCH 010/695] hide actions if not available Signed-off-by: sshcrack <34072808+sshcrack@users.noreply.github.com> --- launcher/ui/MainWindow.cpp | 97 +++++++++++++++++++++----------------- 1 file changed, 55 insertions(+), 42 deletions(-) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 535089343..b6b85bb62 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -209,12 +209,25 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi exportInstanceMenu->addAction(ui->actionExportInstanceFlamePack); ui->actionExportInstance->setMenu(exportInstanceMenu); - auto shortcutInstanceMenu = new QMenu(this); - shortcutInstanceMenu->addAction(ui->actionCreateInstanceShortcutDesktop); - shortcutInstanceMenu->addAction(ui->actionCreateInstanceShortcutApplications); - shortcutInstanceMenu->addAction(ui->actionCreateInstanceShortcutOther); + QList shortcutActions = { ui->actionCreateInstanceShortcutOther }; + if (!DesktopServices::isFlatpak()) { + QString desktopDir = FS::getDesktopDir(); + QString applicationDir = FS::getApplicationsDir(); - ui->actionCreateInstanceShortcut->setMenu(shortcutInstanceMenu); + if(!applicationDir.isEmpty()) + shortcutActions.push_front(ui->actionCreateInstanceShortcutApplications); + + if(!desktopDir.isEmpty()) + shortcutActions.push_front(ui->actionCreateInstanceShortcutDesktop); + } + + if(shortcutActions.length() > 1) { + auto shortcutInstanceMenu = new QMenu(this); + + for(auto action : shortcutActions) + shortcutInstanceMenu->addAction(action); + ui->actionCreateInstanceShortcut->setMenu(shortcutInstanceMenu); + } } // hide, disable and show stuff @@ -242,11 +255,6 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi } ui->actionViewJavaFolder->setEnabled(BuildConfig.JAVA_DOWNLOADER_ENABLED); - - bool isFlatpak = DesktopServices::isFlatpak(); - - ui->actionCreateInstanceShortcutDesktop->setEnabled(isFlatpak); - ui->actionCreateInstanceShortcutApplications->setEnabled(isFlatpak); } // add the toolbar toggles to the view menu @@ -1503,8 +1511,9 @@ void MainWindow::on_actionKillInstance_triggered() } } -void MainWindow::createInstanceShortcut(QString shortcutFilePath) { - if(!m_selectedInstance) +void MainWindow::createInstanceShortcut(QString shortcutFilePath) +{ + if (!m_selectedInstance) return; QString appPath = QApplication::applicationFilePath(); @@ -1625,55 +1634,59 @@ void MainWindow::createInstanceShortcut(QString shortcutFilePath) { } } -void MainWindow::on_actionCreateInstanceShortcutOther_triggered() { - if (!m_selectedInstance) - return; +void MainWindow::on_actionCreateInstanceShortcutOther_triggered() +{ + if (!m_selectedInstance) + return; - QString defaultedDir = FS::getDesktopDir(); + QString defaultedDir = FS::getDesktopDir(); #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) - QString extension = ".desktop"; + QString extension = ".desktop"; #elif defined(Q_OS_WINDOWS) - QString extension = ".lnk"; + QString extension = ".lnk"; #else - QString extension = ""; + QString extension = ""; #endif - QString shortcutFilePath = FS::PathCombine(defaultedDir, FS::RemoveInvalidFilenameChars(m_selectedInstance->name()) + extension); - QFileDialog fileDialog; - // workaround to make sure the portal file dialog opens in the desktop directory - fileDialog.setDirectoryUrl(defaultedDir); + QString shortcutFilePath = FS::PathCombine(defaultedDir, FS::RemoveInvalidFilenameChars(m_selectedInstance->name()) + extension); + QFileDialog fileDialog; + // workaround to make sure the portal file dialog opens in the desktop directory + fileDialog.setDirectoryUrl(defaultedDir); - shortcutFilePath = fileDialog.getSaveFileName(this, tr("Create Shortcut"), shortcutFilePath, tr("Desktop Entries") + " (*" + extension + ")"); - if (shortcutFilePath.isEmpty()) - return; // file dialog canceled by user + shortcutFilePath = + fileDialog.getSaveFileName(this, tr("Create Shortcut"), shortcutFilePath, tr("Desktop Entries") + " (*" + extension + ")"); + if (shortcutFilePath.isEmpty()) + return; // file dialog canceled by user - createInstanceShortcut(shortcutFilePath); - QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance!")); + createInstanceShortcut(shortcutFilePath); + QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance!")); } -void MainWindow::on_actionCreateInstanceShortcut_triggered() { - if(!m_selectedInstance) +void MainWindow::on_actionCreateInstanceShortcut_triggered() +{ + if (!m_selectedInstance) return; - if(DesktopServices::isFlatpak()) + if (DesktopServices::isFlatpak()) on_actionCreateInstanceShortcutOther_triggered(); else on_actionCreateInstanceShortcutDesktop_triggered(); } -void MainWindow::on_actionCreateInstanceShortcutDesktop_triggered() { - if (!m_selectedInstance) - return; +void MainWindow::on_actionCreateInstanceShortcutDesktop_triggered() +{ + if (!m_selectedInstance) + return; - QString desktopDir = FS::getDesktopDir(); - if (desktopDir.isEmpty()) { - QMessageBox::critical(this, tr("Create instance shortcut"), tr("Couldn't find desktop?!")); - return; - } + QString desktopDir = FS::getDesktopDir(); + if (desktopDir.isEmpty()) { + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Couldn't find desktop?!")); + return; + } - QString shortcutFilePath = FS::PathCombine(FS::getDesktopDir(), FS::RemoveInvalidFilenameChars(m_selectedInstance->name())); - createInstanceShortcut(shortcutFilePath); - QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance on your desktop!")); + QString shortcutFilePath = FS::PathCombine(FS::getDesktopDir(), FS::RemoveInvalidFilenameChars(m_selectedInstance->name())); + createInstanceShortcut(shortcutFilePath); + QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance on your desktop!")); } void MainWindow::on_actionCreateInstanceShortcutApplications_triggered() From 8bb35f5b0b517177cb9869ee9ba60265e010e7a6 Mon Sep 17 00:00:00 2001 From: sshcrack <34072808+sshcrack@users.noreply.github.com> Date: Sat, 7 Dec 2024 19:43:09 +0100 Subject: [PATCH 011/695] add macos support for shortcuts Signed-off-by: sshcrack <34072808+sshcrack@users.noreply.github.com> --- launcher/FileSystem.cpp | 11 +---------- launcher/ui/MainWindow.cpp | 12 ++++++++++++ 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp index 8f683a9fb..a02a0d642 100644 --- a/launcher/FileSystem.cpp +++ b/launcher/FileSystem.cpp @@ -931,16 +931,7 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri return false; } #if defined(Q_OS_MACOS) - // Create the Application - QDir applicationDirectory = - QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation) + "/" + BuildConfig.LAUNCHER_NAME + " Instances/"; - - if (!applicationDirectory.mkpath(".")) { - qWarning() << "Couldn't create application directory"; - return false; - } - - QDir application = applicationDirectory.path() + "/" + name + ".app/"; + QDir application = destination + ".app/"; if (application.exists()) { qWarning() << "Application already exists!"; diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index b6b85bb62..70659741e 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -1658,6 +1658,8 @@ void MainWindow::on_actionCreateInstanceShortcutOther_triggered() if (shortcutFilePath.isEmpty()) return; // file dialog canceled by user + if(shortcutFilePath.endsWith(extension)) + shortcutFilePath = shortcutFilePath.mid(0, shortcutFilePath.length() - extension.length()); createInstanceShortcut(shortcutFilePath); QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance!")); } @@ -1700,6 +1702,16 @@ void MainWindow::on_actionCreateInstanceShortcutApplications_triggered() return; } +#if defined(Q_OS_MACOS) || defined(Q_OS_WIN) + applicationsDir = FS::PathCombine(applicationsDir, BuildConfig.LAUNCHER_DISPLAYNAME + " Instances"); + + QDir applicationsDirQ(applicationsDir); + if (!applicationsDirQ.mkpath(".")) { + QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create instances folder in applications folder!")); + return; + } +#endif + QString shortcutFilePath = FS::PathCombine(applicationsDir, FS::RemoveInvalidFilenameChars(m_selectedInstance->name())); createInstanceShortcut(shortcutFilePath); QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance in your applications folder!")); From c4ba7fc40159f74a10b8cff272727b5af98ed5bd Mon Sep 17 00:00:00 2001 From: sshcrack <34072808+sshcrack@users.noreply.github.com> Date: Sat, 7 Dec 2024 20:06:52 +0100 Subject: [PATCH 012/695] add newline back Signed-off-by: sshcrack <34072808+sshcrack@users.noreply.github.com> --- launcher/ui/pages/global/LauncherPage.cpp | 2 +- launcher/ui/pages/global/LauncherPage.ui | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/launcher/ui/pages/global/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp index da4ba9023..8bbed9643 100644 --- a/launcher/ui/pages/global/LauncherPage.cpp +++ b/launcher/ui/pages/global/LauncherPage.cpp @@ -369,4 +369,4 @@ void LauncherPage::refreshFontPreview() void LauncherPage::retranslate() { ui->retranslateUi(this); -} \ No newline at end of file +} diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index ff95bdfbb..3cba468ff 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -668,4 +668,4 @@ - \ No newline at end of file + From 1cd4b4978908cf0a5745055638bea1c2b85c8acb Mon Sep 17 00:00:00 2001 From: sshcrack <34072808+sshcrack@users.noreply.github.com> Date: Sat, 7 Dec 2024 20:26:48 +0100 Subject: [PATCH 013/695] git is making me crazy at this point Signed-off-by: sshcrack <34072808+sshcrack@users.noreply.github.com> --- launcher/ui/pages/global/LauncherPage.cpp | 1 + launcher/ui/pages/global/LauncherPage.ui | 1 + 2 files changed, 2 insertions(+) diff --git a/launcher/ui/pages/global/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp index 8bbed9643..154dddba5 100644 --- a/launcher/ui/pages/global/LauncherPage.cpp +++ b/launcher/ui/pages/global/LauncherPage.cpp @@ -370,3 +370,4 @@ void LauncherPage::retranslate() { ui->retranslateUi(this); } + diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index 3cba468ff..acca897c4 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -669,3 +669,4 @@ + From c06f83507cde69b7b8dca1aada5a89419cf7050c Mon Sep 17 00:00:00 2001 From: sshcrack <34072808+sshcrack@users.noreply.github.com> Date: Sat, 7 Dec 2024 20:29:09 +0100 Subject: [PATCH 014/695] now its too much newlines?? Signed-off-by: sshcrack <34072808+sshcrack@users.noreply.github.com> --- launcher/ui/pages/global/LauncherPage.cpp | 1 - launcher/ui/pages/global/LauncherPage.ui | 1 - 2 files changed, 2 deletions(-) diff --git a/launcher/ui/pages/global/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp index 154dddba5..8bbed9643 100644 --- a/launcher/ui/pages/global/LauncherPage.cpp +++ b/launcher/ui/pages/global/LauncherPage.cpp @@ -370,4 +370,3 @@ void LauncherPage::retranslate() { ui->retranslateUi(this); } - diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index acca897c4..3cba468ff 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -669,4 +669,3 @@ - From c9471d083b18de96c3f5095ac64367a7724bf2db Mon Sep 17 00:00:00 2001 From: Trial97 Date: Sat, 1 Feb 2025 20:25:57 +0200 Subject: [PATCH 015/695] change java on modpack update Signed-off-by: Trial97 --- launcher/BaseInstance.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/launcher/BaseInstance.cpp b/launcher/BaseInstance.cpp index ccfd0b847..6c1c1a574 100644 --- a/launcher/BaseInstance.cpp +++ b/launcher/BaseInstance.cpp @@ -44,6 +44,7 @@ #include #include +#include "Application.h" #include "settings/INISettingsObject.h" #include "settings/OverrideSetting.h" #include "settings/Setting.h" @@ -174,6 +175,12 @@ void BaseInstance::copyManagedPack(BaseInstance& other) m_settings->set("ManagedPackName", other.getManagedPackName()); m_settings->set("ManagedPackVersionID", other.getManagedPackVersionID()); m_settings->set("ManagedPackVersionName", other.getManagedPackVersionName()); + + if (APPLICATION->settings()->get("AutomaticJavaSwitch").toBool() && m_settings->get("AutomaticJava").toBool() && + m_settings->get("OverrideJavaLocation").toBool()) { + m_settings->set("OverrideJavaLocation", false); + m_settings->set("JavaPath", ""); + } } int BaseInstance::getConsoleMaxLines() const From 58100328613bc7e1280ce0981f6a3ce66a34a2be Mon Sep 17 00:00:00 2001 From: Trial97 Date: Wed, 12 Feb 2025 19:04:48 +0200 Subject: [PATCH 016/695] Fix icon removal in icon picker Signed-off-by: Trial97 --- launcher/icons/IconList.cpp | 6 ++++-- launcher/ui/dialogs/IconPickerDialog.cpp | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/launcher/icons/IconList.cpp b/launcher/icons/IconList.cpp index f4022e0fb..bdf57acaa 100644 --- a/launcher/icons/IconList.cpp +++ b/launcher/icons/IconList.cpp @@ -166,7 +166,8 @@ void IconList::directoryChanged(const QString& path) for (const MMCIcon& it : m_icons) { if (!it.has(IconType::FileBased)) continue; - currentSet.insert(it.m_images[IconType::FileBased].filename); + QFileInfo icon(it.getFilePath()); + currentSet.insert(icon.absoluteFilePath()); } QSet toRemove = currentSet - newSet; QSet toAdd = newSet - currentSet; @@ -174,7 +175,8 @@ void IconList::directoryChanged(const QString& path) for (const QString& removedPath : toRemove) { qDebug() << "Removing icon " << removedPath; QFileInfo removedFile(removedPath); - QString key = m_dir.relativeFilePath(removedFile.absoluteFilePath()); + QString relativePath = m_dir.relativeFilePath(removedFile.absoluteFilePath()); + QString key = QFileInfo(relativePath).completeBaseName(); int idx = getIconIndex(key); if (idx == -1) diff --git a/launcher/ui/dialogs/IconPickerDialog.cpp b/launcher/ui/dialogs/IconPickerDialog.cpp index b6e928a3d..8f53995f9 100644 --- a/launcher/ui/dialogs/IconPickerDialog.cpp +++ b/launcher/ui/dialogs/IconPickerDialog.cpp @@ -58,7 +58,7 @@ IconPickerDialog::IconPickerDialog(QWidget* parent) : QDialog(parent), ui(new Ui contentsWidget->setTextElideMode(Qt::ElideRight); contentsWidget->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); contentsWidget->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - contentsWidget->setItemDelegate(new ListViewDelegate()); + contentsWidget->setItemDelegate(new ListViewDelegate(contentsWidget)); // contentsWidget->setAcceptDrops(true); contentsWidget->setDropIndicatorShown(true); From a501441e6ee6a4bba3bbf9c0847b6ee9c0007197 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Mon, 17 Mar 2025 21:32:37 +0000 Subject: [PATCH 017/695] Paint project item backgrounds with native style Signed-off-by: TheKodeToad --- launcher/ui/widgets/ProjectItem.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/launcher/ui/widgets/ProjectItem.cpp b/launcher/ui/widgets/ProjectItem.cpp index 6946df41f..f313b58e8 100644 --- a/launcher/ui/widgets/ProjectItem.cpp +++ b/launcher/ui/widgets/ProjectItem.cpp @@ -2,6 +2,7 @@ #include "Common.h" +#include #include #include @@ -16,12 +17,12 @@ void ProjectItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& o auto rect = opt.rect; - if (opt.state & QStyle::State_Selected) { - painter->fillRect(rect, opt.palette.highlight()); + const QStyle* style = opt.widget == nullptr ? QApplication::style() : opt.widget->style(); + + style->drawPrimitive(QStyle::PE_PanelItemViewItem, &opt, painter, opt.widget); + + if (option.state & QStyle::State_Selected) painter->setPen(opt.palette.highlightedText().color()); - } else if (opt.state & QStyle::State_MouseOver) { - painter->fillRect(rect, opt.palette.window()); - } // The default icon size will be a square (and height is usually the lower value). auto icon_width = rect.height(), icon_height = rect.height(); From 5f70335a07f8502f49a965f93f77fff49fd1bf1f Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Tue, 18 Mar 2025 19:44:13 +0000 Subject: [PATCH 018/695] Render checkbox in project items Signed-off-by: TheKodeToad --- .../ui/pages/modplatform/ResourceModel.cpp | 2 + launcher/ui/widgets/ProjectItem.cpp | 46 +++++++++++-------- 2 files changed, 29 insertions(+), 19 deletions(-) diff --git a/launcher/ui/pages/modplatform/ResourceModel.cpp b/launcher/ui/pages/modplatform/ResourceModel.cpp index 6b8309fb7..b571ead0a 100644 --- a/launcher/ui/pages/modplatform/ResourceModel.cpp +++ b/launcher/ui/pages/modplatform/ResourceModel.cpp @@ -84,6 +84,8 @@ auto ResourceModel::data(const QModelIndex& index, int role) const -> QVariant return pack->description; case UserDataTypes::SELECTED: return pack->isAnyVersionSelected(); + case Qt::CheckStateRole: + return pack->isAnyVersionSelected() ? Qt::Checked : Qt::Unchecked; case UserDataTypes::INSTALLED: return this->isPackInstalled(pack); default: diff --git a/launcher/ui/widgets/ProjectItem.cpp b/launcher/ui/widgets/ProjectItem.cpp index f313b58e8..141a5191b 100644 --- a/launcher/ui/widgets/ProjectItem.cpp +++ b/launcher/ui/widgets/ProjectItem.cpp @@ -1,10 +1,11 @@ #include "ProjectItem.h" -#include "Common.h" - #include + +#include #include #include +#include "Common.h" ProjectItemDelegate::ProjectItemDelegate(QWidget* parent) : QStyledItemDelegate(parent) {} @@ -24,6 +25,27 @@ void ProjectItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& o if (option.state & QStyle::State_Selected) painter->setPen(opt.palette.highlightedText().color()); + if (opt.features & QStyleOptionViewItem::HasCheckIndicator) { + // 5px will be the typical margin with 48px icon size + // we don't want the checkbox to be all over the place + rect.translate(5, 0); + + QStyleOptionViewItem checkboxOpt = opt; + + checkboxOpt.state &= ~QStyle::State_HasFocus; + + if (checkboxOpt.checkState == Qt::Checked) + checkboxOpt.state |= QStyle::State_On; + + QRect checkboxRect = style->subElementRect(QStyle::SE_ItemViewItemCheckIndicator, &checkboxOpt, opt.widget); + checkboxOpt.rect = + QRect(rect.x(), rect.y() + (rect.height() / 2 - checkboxRect.height() / 2), checkboxRect.width(), checkboxRect.height()); + + style->drawPrimitive(QStyle::PE_IndicatorItemViewItemCheck, &checkboxOpt, painter, opt.widget); + + rect.setX(rect.x() + checkboxRect.width()); + } + // The default icon size will be a square (and height is usually the lower value). auto icon_width = rect.height(), icon_height = rect.height(); int icon_x_margin = (rect.height() - icon_width) / 2; @@ -43,6 +65,9 @@ void ProjectItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& o int x = rect.x() + icon_x_margin; int y = rect.y() + icon_y_margin; + if (opt.features & QStyleOptionViewItem::HasCheckIndicator) + rect.translate(icon_x_margin / 2, 0); + // Prevent 'scaling null pixmap' warnings if (icon_width > 0 && icon_height > 0) opt.icon.paint(painter, x, y, icon_width, icon_height); @@ -60,23 +85,6 @@ void ProjectItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& o painter->save(); auto font = opt.font; - if (index.data(UserDataTypes::SELECTED).toBool()) { - // Set nice font - font.setBold(true); - font.setUnderline(true); - } - if (index.data(UserDataTypes::INSTALLED).toBool()) { - auto hRect = opt.rect; - hRect.setX(hRect.x() + 1); - hRect.setY(hRect.y() + 1); - hRect.setHeight(hRect.height() - 2); - hRect.setWidth(hRect.width() - 2); - // Set nice font - font.setItalic(true); - font.setOverline(true); - painter->drawRect(hRect); - } - font.setPointSize(font.pointSize() + 2); painter->setFont(font); From 8577f58fe3b807c504656fdbaac2c64016e91f59 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Tue, 18 Mar 2025 20:44:01 +0000 Subject: [PATCH 019/695] Remove UserDataTypes::SELECTED Signed-off-by: TheKodeToad --- launcher/ui/pages/modplatform/ResourceModel.cpp | 3 --- launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp | 2 -- launcher/ui/pages/modplatform/flame/FlameModel.cpp | 2 -- launcher/ui/pages/modplatform/import_ftb/ListModel.cpp | 2 -- launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp | 2 -- launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp | 2 -- launcher/ui/pages/modplatform/technic/TechnicModel.cpp | 2 -- launcher/ui/widgets/ProjectItem.h | 1 - 8 files changed, 16 deletions(-) diff --git a/launcher/ui/pages/modplatform/ResourceModel.cpp b/launcher/ui/pages/modplatform/ResourceModel.cpp index b571ead0a..7fcb6edb1 100644 --- a/launcher/ui/pages/modplatform/ResourceModel.cpp +++ b/launcher/ui/pages/modplatform/ResourceModel.cpp @@ -82,8 +82,6 @@ auto ResourceModel::data(const QModelIndex& index, int role) const -> QVariant return pack->name; case UserDataTypes::DESCRIPTION: return pack->description; - case UserDataTypes::SELECTED: - return pack->isAnyVersionSelected(); case Qt::CheckStateRole: return pack->isAnyVersionSelected() ? Qt::Checked : Qt::Unchecked; case UserDataTypes::INSTALLED: @@ -105,7 +103,6 @@ QHash ResourceModel::roleNames() const roles[Qt::UserRole] = "pack"; roles[UserDataTypes::TITLE] = "title"; roles[UserDataTypes::DESCRIPTION] = "description"; - roles[UserDataTypes::SELECTED] = "selected"; roles[UserDataTypes::INSTALLED] = "installed"; return roles; diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp index f116ca915..e381f2a16 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp @@ -82,8 +82,6 @@ QVariant ListModel::data(const QModelIndex& index, int role) const return pack.name; case UserDataTypes::DESCRIPTION: return pack.description; - case UserDataTypes::SELECTED: - return false; case UserDataTypes::INSTALLED: return false; default: diff --git a/launcher/ui/pages/modplatform/flame/FlameModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModel.cpp index 18a2adc49..d501bf9f4 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModel.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModel.cpp @@ -65,8 +65,6 @@ QVariant ListModel::data(const QModelIndex& index, int role) const return pack.name; case UserDataTypes::DESCRIPTION: return pack.description; - case UserDataTypes::SELECTED: - return false; case UserDataTypes::INSTALLED: return false; default: diff --git a/launcher/ui/pages/modplatform/import_ftb/ListModel.cpp b/launcher/ui/pages/modplatform/import_ftb/ListModel.cpp index f3c737977..eaec76a04 100644 --- a/launcher/ui/pages/modplatform/import_ftb/ListModel.cpp +++ b/launcher/ui/pages/modplatform/import_ftb/ListModel.cpp @@ -128,8 +128,6 @@ QVariant ListModel::data(const QModelIndex& index, int role) const return pack.name; case UserDataTypes::DESCRIPTION: return tr("Minecraft %1").arg(pack.mcVersion); - case UserDataTypes::SELECTED: - return false; case UserDataTypes::INSTALLED: return false; default: diff --git a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp index 98922123c..4ddc6da5d 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp +++ b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp @@ -195,8 +195,6 @@ QVariant ListModel::data(const QModelIndex& index, int role) const return pack.name; case UserDataTypes::DESCRIPTION: return pack.description; - case UserDataTypes::SELECTED: - return false; case UserDataTypes::INSTALLED: return false; default: diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index 416c69d28..4681b1a7f 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -106,8 +106,6 @@ auto ModpackListModel::data(const QModelIndex& index, int role) const -> QVarian return pack.name; case UserDataTypes::DESCRIPTION: return pack.description; - case UserDataTypes::SELECTED: - return false; case UserDataTypes::INSTALLED: return false; default: diff --git a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp index f7e7f4433..c689ab0d2 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp @@ -89,8 +89,6 @@ QVariant Technic::ListModel::data(const QModelIndex& index, int role) const return pack.name; case UserDataTypes::DESCRIPTION: return pack.description; - case UserDataTypes::SELECTED: - return false; case UserDataTypes::INSTALLED: return false; default: diff --git a/launcher/ui/widgets/ProjectItem.h b/launcher/ui/widgets/ProjectItem.h index c3d0dce70..96e62af12 100644 --- a/launcher/ui/widgets/ProjectItem.h +++ b/launcher/ui/widgets/ProjectItem.h @@ -6,7 +6,6 @@ enum UserDataTypes { TITLE = 257, // QString DESCRIPTION = 258, // QString - SELECTED = 259, // bool INSTALLED = 260 // bool }; From 900579eea6f0732239b5dd1c7907fb8cb7ee3f7e Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Wed, 19 Mar 2025 01:17:25 +0000 Subject: [PATCH 020/695] Handle checkbox toggle Signed-off-by: TheKodeToad --- launcher/ui/pages/modplatform/ModModel.cpp | 4 +- launcher/ui/pages/modplatform/ModModel.h | 4 +- launcher/ui/pages/modplatform/ModPage.cpp | 1 - launcher/ui/pages/modplatform/ModPage.h | 2 +- .../ui/pages/modplatform/ResourceModel.cpp | 6 +- launcher/ui/pages/modplatform/ResourceModel.h | 14 +-- .../pages/modplatform/ResourcePackModel.cpp | 4 +- .../ui/pages/modplatform/ResourcePackModel.h | 4 +- .../ui/pages/modplatform/ResourcePackPage.cpp | 4 +- .../ui/pages/modplatform/ResourcePackPage.h | 2 +- .../ui/pages/modplatform/ResourcePage.cpp | 113 +++++++++++++----- launcher/ui/pages/modplatform/ResourcePage.h | 7 +- .../ui/pages/modplatform/ShaderPackModel.cpp | 4 +- .../ui/pages/modplatform/ShaderPackModel.h | 4 +- .../ui/pages/modplatform/ShaderPackPage.cpp | 5 +- .../ui/pages/modplatform/ShaderPackPage.h | 2 +- .../ui/pages/modplatform/TexturePackModel.cpp | 2 +- .../ui/pages/modplatform/TexturePackModel.h | 2 +- .../ui/pages/modplatform/TexturePackPage.h | 7 +- .../modplatform/flame/FlameResourceModels.cpp | 2 +- .../modplatform/flame/FlameResourceModels.h | 2 +- launcher/ui/widgets/ProjectItem.cpp | 70 ++++++++--- launcher/ui/widgets/ProjectItem.h | 10 +- tests/ResourceModel_test.cpp | 4 +- 24 files changed, 185 insertions(+), 94 deletions(-) diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp index cfc262b62..6e98a88bc 100644 --- a/launcher/ui/pages/modplatform/ModModel.cpp +++ b/launcher/ui/pages/modplatform/ModModel.cpp @@ -44,7 +44,7 @@ ResourceAPI::SearchArgs ModModel::createSearchArguments() }; } -ResourceAPI::VersionSearchArgs ModModel::createVersionsArguments(QModelIndex& entry) +ResourceAPI::VersionSearchArgs ModModel::createVersionsArguments(const QModelIndex& entry) { auto& pack = *m_packs[entry.row()]; auto profile = static_cast(m_base_instance).getPackProfile(); @@ -62,7 +62,7 @@ ResourceAPI::VersionSearchArgs ModModel::createVersionsArguments(QModelIndex& en return { pack, versions, loaders }; } -ResourceAPI::ProjectInfoArgs ModModel::createInfoArguments(QModelIndex& entry) +ResourceAPI::ProjectInfoArgs ModModel::createInfoArguments(const QModelIndex& entry) { auto& pack = *m_packs[entry.row()]; return { pack }; diff --git a/launcher/ui/pages/modplatform/ModModel.h b/launcher/ui/pages/modplatform/ModModel.h index 5c994f373..bb9255cd0 100644 --- a/launcher/ui/pages/modplatform/ModModel.h +++ b/launcher/ui/pages/modplatform/ModModel.h @@ -39,8 +39,8 @@ class ModModel : public ResourceModel { public slots: ResourceAPI::SearchArgs createSearchArguments() override; - ResourceAPI::VersionSearchArgs createVersionsArguments(QModelIndex&) override; - ResourceAPI::ProjectInfoArgs createInfoArguments(QModelIndex&) override; + ResourceAPI::VersionSearchArgs createVersionsArguments(const QModelIndex&) override; + ResourceAPI::ProjectInfoArgs createInfoArguments(const QModelIndex&) override; protected: auto documentToArray(QJsonDocument& obj) const -> QJsonArray override = 0; diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index f0cc2df54..8b4919015 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -59,7 +59,6 @@ namespace ResourceDownload { ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance& instance) : ResourcePage(dialog, instance) { connect(m_ui->resourceFilterButton, &QPushButton::clicked, this, &ModPage::filterMods); - connect(m_ui->packView, &QListView::doubleClicked, this, &ModPage::onResourceSelected); } void ModPage::setFilterWidget(unique_qobject_ptr& widget) diff --git a/launcher/ui/pages/modplatform/ModPage.h b/launcher/ui/pages/modplatform/ModPage.h index 5c9a82303..397b77d94 100644 --- a/launcher/ui/pages/modplatform/ModPage.h +++ b/launcher/ui/pages/modplatform/ModPage.h @@ -35,7 +35,7 @@ class ModPage : public ResourcePage { page->setFilterWidget(filter_widget); model->setFilter(page->getFilter()); - connect(model, &ResourceModel::versionListUpdated, page, &ResourcePage::updateVersionList); + connect(model, &ResourceModel::versionListUpdated, page, &ResourcePage::versionListUpdated); connect(model, &ResourceModel::projectInfoUpdated, page, &ResourcePage::updateUi); return page; diff --git a/launcher/ui/pages/modplatform/ResourceModel.cpp b/launcher/ui/pages/modplatform/ResourceModel.cpp index 7fcb6edb1..8e3be5e8f 100644 --- a/launcher/ui/pages/modplatform/ResourceModel.cpp +++ b/launcher/ui/pages/modplatform/ResourceModel.cpp @@ -192,7 +192,7 @@ void ResourceModel::search() runSearchJob(job); } -void ResourceModel::loadEntry(QModelIndex& entry) +void ResourceModel::loadEntry(const QModelIndex& entry) { auto const& pack = m_packs[entry.row()]; @@ -503,7 +503,7 @@ void ResourceModel::versionRequestSucceeded(QJsonDocument& doc, ModPlatform::Ind return; } - emit versionListUpdated(); + emit versionListUpdated(index); } void ResourceModel::infoRequestSucceeded(QJsonDocument& doc, ModPlatform::IndexedPack& pack, const QModelIndex& index) @@ -530,7 +530,7 @@ void ResourceModel::infoRequestSucceeded(QJsonDocument& doc, ModPlatform::Indexe return; } - emit projectInfoUpdated(); + emit projectInfoUpdated(index); } void ResourceModel::addPack(ModPlatform::IndexedPack::Ptr pack, diff --git a/launcher/ui/pages/modplatform/ResourceModel.h b/launcher/ui/pages/modplatform/ResourceModel.h index 4c7ea33a0..3f1e633ec 100644 --- a/launcher/ui/pages/modplatform/ResourceModel.h +++ b/launcher/ui/pages/modplatform/ResourceModel.h @@ -80,17 +80,17 @@ class ResourceModel : public QAbstractListModel { virtual ResourceAPI::SearchArgs createSearchArguments() = 0; virtual ResourceAPI::SearchCallbacks createSearchCallbacks() { return {}; } - virtual ResourceAPI::VersionSearchArgs createVersionsArguments(QModelIndex&) = 0; - virtual ResourceAPI::VersionSearchCallbacks createVersionsCallbacks(QModelIndex&) { return {}; } + virtual ResourceAPI::VersionSearchArgs createVersionsArguments(const QModelIndex&) = 0; + virtual ResourceAPI::VersionSearchCallbacks createVersionsCallbacks(const QModelIndex&) { return {}; } - virtual ResourceAPI::ProjectInfoArgs createInfoArguments(QModelIndex&) = 0; - virtual ResourceAPI::ProjectInfoCallbacks createInfoCallbacks(QModelIndex&) { return {}; } + virtual ResourceAPI::ProjectInfoArgs createInfoArguments(const QModelIndex&) = 0; + virtual ResourceAPI::ProjectInfoCallbacks createInfoCallbacks(const QModelIndex&) { return {}; } /** Requests the API for more entries. */ virtual void search(); /** Applies any processing / extra requests needed to fully load the specified entry's information. */ - virtual void loadEntry(QModelIndex&); + virtual void loadEntry(const QModelIndex&); /** Schedule a refresh, clearing the current state. */ void refresh(); @@ -170,8 +170,8 @@ class ResourceModel : public QAbstractListModel { void infoRequestSucceeded(QJsonDocument&, ModPlatform::IndexedPack&, const QModelIndex&); signals: - void versionListUpdated(); - void projectInfoUpdated(); + void versionListUpdated(const QModelIndex& index); + void projectInfoUpdated(const QModelIndex& index); }; } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/ResourcePackModel.cpp b/launcher/ui/pages/modplatform/ResourcePackModel.cpp index d436f320f..0de980ed8 100644 --- a/launcher/ui/pages/modplatform/ResourcePackModel.cpp +++ b/launcher/ui/pages/modplatform/ResourcePackModel.cpp @@ -20,13 +20,13 @@ ResourceAPI::SearchArgs ResourcePackResourceModel::createSearchArguments() return { ModPlatform::ResourceType::RESOURCE_PACK, m_next_search_offset, m_search_term, sort }; } -ResourceAPI::VersionSearchArgs ResourcePackResourceModel::createVersionsArguments(QModelIndex& entry) +ResourceAPI::VersionSearchArgs ResourcePackResourceModel::createVersionsArguments(const QModelIndex& entry) { auto& pack = m_packs[entry.row()]; return { *pack }; } -ResourceAPI::ProjectInfoArgs ResourcePackResourceModel::createInfoArguments(QModelIndex& entry) +ResourceAPI::ProjectInfoArgs ResourcePackResourceModel::createInfoArguments(const QModelIndex& entry) { auto& pack = m_packs[entry.row()]; return { *pack }; diff --git a/launcher/ui/pages/modplatform/ResourcePackModel.h b/launcher/ui/pages/modplatform/ResourcePackModel.h index e2b4a1957..4f00808e8 100644 --- a/launcher/ui/pages/modplatform/ResourcePackModel.h +++ b/launcher/ui/pages/modplatform/ResourcePackModel.h @@ -31,8 +31,8 @@ class ResourcePackResourceModel : public ResourceModel { public slots: ResourceAPI::SearchArgs createSearchArguments() override; - ResourceAPI::VersionSearchArgs createVersionsArguments(QModelIndex&) override; - ResourceAPI::ProjectInfoArgs createInfoArguments(QModelIndex&) override; + ResourceAPI::VersionSearchArgs createVersionsArguments(const QModelIndex&) override; + ResourceAPI::ProjectInfoArgs createInfoArguments(const QModelIndex&) override; protected: const BaseInstance& m_base_instance; diff --git a/launcher/ui/pages/modplatform/ResourcePackPage.cpp b/launcher/ui/pages/modplatform/ResourcePackPage.cpp index 99039476e..8a7ed2720 100644 --- a/launcher/ui/pages/modplatform/ResourcePackPage.cpp +++ b/launcher/ui/pages/modplatform/ResourcePackPage.cpp @@ -14,9 +14,7 @@ namespace ResourceDownload { ResourcePackResourcePage::ResourcePackResourcePage(ResourceDownloadDialog* dialog, BaseInstance& instance) : ResourcePage(dialog, instance) -{ - connect(m_ui->packView, &QListView::doubleClicked, this, &ResourcePackResourcePage::onResourceSelected); -} +{} /******** Callbacks to events in the UI (set up in the derived classes) ********/ diff --git a/launcher/ui/pages/modplatform/ResourcePackPage.h b/launcher/ui/pages/modplatform/ResourcePackPage.h index 440d91ab0..3f925fd23 100644 --- a/launcher/ui/pages/modplatform/ResourcePackPage.h +++ b/launcher/ui/pages/modplatform/ResourcePackPage.h @@ -25,7 +25,7 @@ class ResourcePackResourcePage : public ResourcePage { auto page = new T(dialog, instance); auto model = static_cast(page->getModel()); - connect(model, &ResourceModel::versionListUpdated, page, &ResourcePage::updateVersionList); + connect(model, &ResourceModel::versionListUpdated, page, &ResourcePage::versionListUpdated); connect(model, &ResourceModel::projectInfoUpdated, page, &ResourcePage::updateUi); return page; diff --git a/launcher/ui/pages/modplatform/ResourcePage.cpp b/launcher/ui/pages/modplatform/ResourcePage.cpp index 2dd5ccf0f..407684f8b 100644 --- a/launcher/ui/pages/modplatform/ResourcePage.cpp +++ b/launcher/ui/pages/modplatform/ResourcePage.cpp @@ -78,10 +78,14 @@ ResourcePage::ResourcePage(ResourceDownloadDialog* parent, BaseInstance& base_in m_ui->verticalLayout->insertWidget(1, &m_fetchProgress); - m_ui->packView->setItemDelegate(new ProjectItemDelegate(this)); + auto delegate = new ProjectItemDelegate(this); + m_ui->packView->setItemDelegate(delegate); m_ui->packView->installEventFilter(this); connect(m_ui->packDescription, &QTextBrowser::anchorClicked, this, &ResourcePage::openUrl); + + connect(m_ui->packView, &QListView::doubleClicked, this, &ResourcePage::onToggle); + connect(delegate, &ProjectItemDelegate::checkboxClicked, this, &ResourcePage::onToggle); } ResourcePage::~ResourcePage() @@ -177,8 +181,11 @@ ModPlatform::IndexedPack::Ptr ResourcePage::getCurrentPack() const return m_model->data(m_ui->packView->currentIndex(), Qt::UserRole).value(); } -void ResourcePage::updateUi() +void ResourcePage::updateUi(const QModelIndex& index) { + if (index != m_ui->packView->currentIndex()) + return; + auto current_pack = getCurrentPack(); if (!current_pack) { m_ui->packDescription->setHtml({}); @@ -268,39 +275,48 @@ void ResourcePage::updateSelectionButton() } } -void ResourcePage::updateVersionList() +void ResourcePage::versionListUpdated(const QModelIndex& index) { - auto current_pack = getCurrentPack(); + if (index == m_ui->packView->currentIndex()) { + auto current_pack = getCurrentPack(); - m_ui->versionSelectionBox->blockSignals(true); - m_ui->versionSelectionBox->clear(); - m_ui->versionSelectionBox->blockSignals(false); + m_ui->versionSelectionBox->blockSignals(true); + m_ui->versionSelectionBox->clear(); + m_ui->versionSelectionBox->blockSignals(false); - if (current_pack) { - auto installedVersion = m_model->getInstalledPackVersion(current_pack); + if (current_pack) { + auto installedVersion = m_model->getInstalledPackVersion(current_pack); - for (int i = 0; i < current_pack->versions.size(); i++) { - auto& version = current_pack->versions[i]; - if (!m_model->checkVersionFilters(version)) - continue; + for (int i = 0; i < current_pack->versions.size(); i++) { + auto& version = current_pack->versions[i]; + if (!m_model->checkVersionFilters(version)) + continue; - auto versionText = version.version; - if (version.version_type.isValid()) { - versionText += QString(" [%1]").arg(version.version_type.toString()); - } - if (version.fileId == installedVersion) { - versionText += tr(" [installed]", "Mod version select"); - } + auto versionText = version.version; + if (version.version_type.isValid()) { + versionText += QString(" [%1]").arg(version.version_type.toString()); + } + if (version.fileId == installedVersion) { + versionText += tr(" [installed]", "Mod version select"); + } - m_ui->versionSelectionBox->addItem(versionText, QVariant(i)); + m_ui->versionSelectionBox->addItem(versionText, QVariant(i)); + } + } + if (m_ui->versionSelectionBox->count() == 0) { + m_ui->versionSelectionBox->addItem(tr("No valid version found."), QVariant(-1)); + m_ui->resourceSelectionButton->setText(tr("Cannot select invalid version :(")); } - } - if (m_ui->versionSelectionBox->count() == 0) { - m_ui->versionSelectionBox->addItem(tr("No valid version found."), QVariant(-1)); - m_ui->resourceSelectionButton->setText(tr("Cannot select invalid version :(")); - } - updateSelectionButton(); + if (m_enableQueue.contains(index.row())) { + m_enableQueue.remove(index.row()); + onToggle(index); + } else + updateSelectionButton(); + } else if (m_enableQueue.contains(index.row())) { + m_enableQueue.remove(index.row()); + onToggle(index); + } } void ResourcePage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelIndex prev) @@ -318,16 +334,20 @@ void ResourcePage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelI request_load = true; } else { - updateVersionList(); + versionListUpdated(curr); } if (current_pack && !current_pack->extraDataLoaded) request_load = true; + // we are already requesting this + if (m_enableQueue.contains(curr.row())) + request_load = false; + if (request_load) m_model->loadEntry(curr); - updateUi(); + updateUi(curr); } void ResourcePage::onVersionSelectionChanged(int index) @@ -385,6 +405,41 @@ void ResourcePage::onResourceSelected() m_ui->packView->repaint(); } +void ResourcePage::onToggle(const QModelIndex& index) +{ + const bool is_selected = index == m_ui->packView->currentIndex(); + auto pack = m_model->data(index, Qt::UserRole).value(); + + if (pack->versionsLoaded) { + if (pack->isAnyVersionSelected()) + removeResourceFromDialog(pack->name); + else { + auto version = std::find_if(pack->versions.begin(), pack->versions.end(), [this](const ModPlatform::IndexedVersion& version) { + return m_model->checkVersionFilters(version); + }); + + if (version != pack->versions.end()) + addResourceToDialog(pack, *version); + } + + if (is_selected) + updateSelectionButton(); + + // force update + QVariant variant; + variant.setValue(pack); + m_model->setData(index, variant, Qt::UserRole); + } else { + // the model is just 1 dimensional so this is fine + m_enableQueue.insert(index.row()); + + // we can't be sure that this hasn't already been requested... + // but this does the job well enough and there's not much point preventing edgecases + if (!is_selected) + m_model->loadEntry(index); + } +} + void ResourcePage::openUrl(const QUrl& url) { // do not allow other url schemes for security reasons diff --git a/launcher/ui/pages/modplatform/ResourcePage.h b/launcher/ui/pages/modplatform/ResourcePage.h index 09c512df4..6a44dcf6d 100644 --- a/launcher/ui/pages/modplatform/ResourcePage.h +++ b/launcher/ui/pages/modplatform/ResourcePage.h @@ -71,9 +71,9 @@ class ResourcePage : public QWidget, public BasePage { void addSortings(); public slots: - virtual void updateUi(); + virtual void updateUi(const QModelIndex& index); virtual void updateSelectionButton(); - virtual void updateVersionList(); + virtual void versionListUpdated(const QModelIndex& index); void addResourceToDialog(ModPlatform::IndexedPack::Ptr, ModPlatform::IndexedVersion&); void removeResourceFromDialog(const QString& pack_name); @@ -91,6 +91,7 @@ class ResourcePage : public QWidget, public BasePage { void onSelectionChanged(QModelIndex first, QModelIndex second); void onVersionSelectionChanged(int index); void onResourceSelected(); + void onToggle(const QModelIndex& index); // NOTE: Can't use [[nodiscard]] here because of https://bugreports.qt.io/browse/QTBUG-58628 on Qt 5.12 @@ -115,6 +116,8 @@ class ResourcePage : public QWidget, public BasePage { QTimer m_searchTimer; bool m_doNotJumpToMod = false; + + QSet m_enableQueue; }; } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/ShaderPackModel.cpp b/launcher/ui/pages/modplatform/ShaderPackModel.cpp index 8c913657a..efc6bfaf9 100644 --- a/launcher/ui/pages/modplatform/ShaderPackModel.cpp +++ b/launcher/ui/pages/modplatform/ShaderPackModel.cpp @@ -20,13 +20,13 @@ ResourceAPI::SearchArgs ShaderPackResourceModel::createSearchArguments() return { ModPlatform::ResourceType::SHADER_PACK, m_next_search_offset, m_search_term, sort }; } -ResourceAPI::VersionSearchArgs ShaderPackResourceModel::createVersionsArguments(QModelIndex& entry) +ResourceAPI::VersionSearchArgs ShaderPackResourceModel::createVersionsArguments(const QModelIndex& entry) { auto& pack = m_packs[entry.row()]; return { *pack }; } -ResourceAPI::ProjectInfoArgs ShaderPackResourceModel::createInfoArguments(QModelIndex& entry) +ResourceAPI::ProjectInfoArgs ShaderPackResourceModel::createInfoArguments(const QModelIndex& entry) { auto& pack = m_packs[entry.row()]; return { *pack }; diff --git a/launcher/ui/pages/modplatform/ShaderPackModel.h b/launcher/ui/pages/modplatform/ShaderPackModel.h index f3c695e9f..5bb9e58b1 100644 --- a/launcher/ui/pages/modplatform/ShaderPackModel.h +++ b/launcher/ui/pages/modplatform/ShaderPackModel.h @@ -31,8 +31,8 @@ class ShaderPackResourceModel : public ResourceModel { public slots: ResourceAPI::SearchArgs createSearchArguments() override; - ResourceAPI::VersionSearchArgs createVersionsArguments(QModelIndex&) override; - ResourceAPI::ProjectInfoArgs createInfoArguments(QModelIndex&) override; + ResourceAPI::VersionSearchArgs createVersionsArguments(const QModelIndex&) override; + ResourceAPI::ProjectInfoArgs createInfoArguments(const QModelIndex&) override; protected: const BaseInstance& m_base_instance; diff --git a/launcher/ui/pages/modplatform/ShaderPackPage.cpp b/launcher/ui/pages/modplatform/ShaderPackPage.cpp index 08acf361a..ace07db0e 100644 --- a/launcher/ui/pages/modplatform/ShaderPackPage.cpp +++ b/launcher/ui/pages/modplatform/ShaderPackPage.cpp @@ -15,10 +15,7 @@ namespace ResourceDownload { -ShaderPackResourcePage::ShaderPackResourcePage(ShaderPackDownloadDialog* dialog, BaseInstance& instance) : ResourcePage(dialog, instance) -{ - connect(m_ui->packView, &QListView::doubleClicked, this, &ShaderPackResourcePage::onResourceSelected); -} +ShaderPackResourcePage::ShaderPackResourcePage(ShaderPackDownloadDialog* dialog, BaseInstance& instance) : ResourcePage(dialog, instance) {} /******** Callbacks to events in the UI (set up in the derived classes) ********/ diff --git a/launcher/ui/pages/modplatform/ShaderPackPage.h b/launcher/ui/pages/modplatform/ShaderPackPage.h index 4b92c33dc..68fdf6699 100644 --- a/launcher/ui/pages/modplatform/ShaderPackPage.h +++ b/launcher/ui/pages/modplatform/ShaderPackPage.h @@ -25,7 +25,7 @@ class ShaderPackResourcePage : public ResourcePage { auto page = new T(dialog, instance); auto model = static_cast(page->getModel()); - connect(model, &ResourceModel::versionListUpdated, page, &ResourcePage::updateVersionList); + connect(model, &ResourceModel::versionListUpdated, page, &ResourcePage::versionListUpdated); connect(model, &ResourceModel::projectInfoUpdated, page, &ResourcePage::updateUi); return page; diff --git a/launcher/ui/pages/modplatform/TexturePackModel.cpp b/launcher/ui/pages/modplatform/TexturePackModel.cpp index cb4cafd41..d56f9334b 100644 --- a/launcher/ui/pages/modplatform/TexturePackModel.cpp +++ b/launcher/ui/pages/modplatform/TexturePackModel.cpp @@ -70,7 +70,7 @@ ResourceAPI::SearchArgs TexturePackResourceModel::createSearchArguments() return args; } -ResourceAPI::VersionSearchArgs TexturePackResourceModel::createVersionsArguments(QModelIndex& entry) +ResourceAPI::VersionSearchArgs TexturePackResourceModel::createVersionsArguments(const QModelIndex& entry) { auto args = ResourcePackResourceModel::createVersionsArguments(entry); if (!m_version_list->isLoaded()) { diff --git a/launcher/ui/pages/modplatform/TexturePackModel.h b/launcher/ui/pages/modplatform/TexturePackModel.h index 607a03be3..45b5734ee 100644 --- a/launcher/ui/pages/modplatform/TexturePackModel.h +++ b/launcher/ui/pages/modplatform/TexturePackModel.h @@ -18,7 +18,7 @@ class TexturePackResourceModel : public ResourcePackResourceModel { [[nodiscard]] inline ::Version maximumTexturePackVersion() const { return { "1.6" }; } ResourceAPI::SearchArgs createSearchArguments() override; - ResourceAPI::VersionSearchArgs createVersionsArguments(QModelIndex&) override; + ResourceAPI::VersionSearchArgs createVersionsArguments(const QModelIndex&) override; protected: Meta::VersionList::Ptr m_version_list; diff --git a/launcher/ui/pages/modplatform/TexturePackPage.h b/launcher/ui/pages/modplatform/TexturePackPage.h index 42aa921c5..393ccc21e 100644 --- a/launcher/ui/pages/modplatform/TexturePackPage.h +++ b/launcher/ui/pages/modplatform/TexturePackPage.h @@ -27,7 +27,7 @@ class TexturePackResourcePage : public ResourcePackResourcePage { auto page = new T(dialog, instance); auto model = static_cast(page->getModel()); - connect(model, &ResourceModel::versionListUpdated, page, &ResourcePage::updateVersionList); + connect(model, &ResourceModel::versionListUpdated, page, &ResourcePage::versionListUpdated); connect(model, &ResourceModel::projectInfoUpdated, page, &ResourcePage::updateUi); return page; @@ -39,10 +39,7 @@ class TexturePackResourcePage : public ResourcePackResourcePage { [[nodiscard]] inline QString resourceString() const override { return tr("texture pack"); } protected: - TexturePackResourcePage(TexturePackDownloadDialog* dialog, BaseInstance& instance) : ResourcePackResourcePage(dialog, instance) - { - connect(m_ui->packView, &QListView::doubleClicked, this, &TexturePackResourcePage::onResourceSelected); - } + TexturePackResourcePage(TexturePackDownloadDialog* dialog, BaseInstance& instance) : ResourcePackResourcePage(dialog, instance) {} }; } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp b/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp index ae4562be4..d2dc29dfd 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp @@ -122,7 +122,7 @@ ResourceAPI::SearchArgs FlameTexturePackModel::createSearchArguments() return args; } -ResourceAPI::VersionSearchArgs FlameTexturePackModel::createVersionsArguments(QModelIndex& entry) +ResourceAPI::VersionSearchArgs FlameTexturePackModel::createVersionsArguments(const QModelIndex& entry) { auto args = TexturePackResourceModel::createVersionsArguments(entry); diff --git a/launcher/ui/pages/modplatform/flame/FlameResourceModels.h b/launcher/ui/pages/modplatform/flame/FlameResourceModels.h index 458fd85d0..9b86a0944 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourceModels.h +++ b/launcher/ui/pages/modplatform/flame/FlameResourceModels.h @@ -69,7 +69,7 @@ class FlameTexturePackModel : public TexturePackResourceModel { void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override; ResourceAPI::SearchArgs createSearchArguments() override; - ResourceAPI::VersionSearchArgs createVersionsArguments(QModelIndex&) override; + ResourceAPI::VersionSearchArgs createVersionsArguments(const QModelIndex&) override; auto documentToArray(QJsonDocument& obj) const -> QJsonArray override; }; diff --git a/launcher/ui/widgets/ProjectItem.cpp b/launcher/ui/widgets/ProjectItem.cpp index 141a5191b..b20a3e013 100644 --- a/launcher/ui/widgets/ProjectItem.cpp +++ b/launcher/ui/widgets/ProjectItem.cpp @@ -16,34 +16,20 @@ void ProjectItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& o QStyleOptionViewItem opt(option); initStyleOption(&opt, index); - auto rect = opt.rect; - const QStyle* style = opt.widget == nullptr ? QApplication::style() : opt.widget->style(); + auto rect = opt.rect; + style->drawPrimitive(QStyle::PE_PanelItemViewItem, &opt, painter, opt.widget); if (option.state & QStyle::State_Selected) painter->setPen(opt.palette.highlightedText().color()); if (opt.features & QStyleOptionViewItem::HasCheckIndicator) { - // 5px will be the typical margin with 48px icon size - // we don't want the checkbox to be all over the place - rect.translate(5, 0); - - QStyleOptionViewItem checkboxOpt = opt; - - checkboxOpt.state &= ~QStyle::State_HasFocus; - - if (checkboxOpt.checkState == Qt::Checked) - checkboxOpt.state |= QStyle::State_On; - - QRect checkboxRect = style->subElementRect(QStyle::SE_ItemViewItemCheckIndicator, &checkboxOpt, opt.widget); - checkboxOpt.rect = - QRect(rect.x(), rect.y() + (rect.height() / 2 - checkboxRect.height() / 2), checkboxRect.width(), checkboxRect.height()); - + QStyleOptionViewItem checkboxOpt = makeCheckboxStyleOption(opt, style); style->drawPrimitive(QStyle::PE_IndicatorItemViewItemCheck, &checkboxOpt, painter, opt.widget); - rect.setX(rect.x() + checkboxRect.width()); + rect.setX(checkboxOpt.rect.right()); } // The default icon size will be a square (and height is usually the lower value). @@ -141,3 +127,51 @@ void ProjectItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& o painter->restore(); } + +bool ProjectItemDelegate::editorEvent(QEvent* event, + QAbstractItemModel* model, + const QStyleOptionViewItem& option, + const QModelIndex& index) +{ + if (!(event->type() == QEvent::MouseButtonRelease || event->type() == QEvent::MouseButtonPress || + event->type() == QEvent::MouseButtonDblClick)) + return false; + + auto mouseEvent = (QMouseEvent*)event; + + QStyleOptionViewItem opt(option); + initStyleOption(&opt, index); + + const QStyle* style = opt.widget == nullptr ? QApplication::style() : opt.widget->style(); + + const QStyleOptionViewItem checkboxOpt = makeCheckboxStyleOption(opt, style); + + if (!checkboxOpt.rect.contains(mouseEvent->x(), mouseEvent->y())) + return false; + + // swallow other events + // (prevents item being selected or double click action triggering) + if (event->type() != QEvent::MouseButtonRelease) + return true; + + emit checkboxClicked(index); + return true; +} + +QStyleOptionViewItem ProjectItemDelegate::makeCheckboxStyleOption(const QStyleOptionViewItem& opt, const QStyle* style) const +{ + QStyleOptionViewItem checkboxOpt = opt; + + checkboxOpt.state &= ~QStyle::State_HasFocus; + + if (checkboxOpt.checkState == Qt::Checked) + checkboxOpt.state |= QStyle::State_On; + + QRect checkboxRect = style->subElementRect(QStyle::SE_ItemViewItemCheckIndicator, &checkboxOpt, opt.widget); + // 5px is the typical top margin for image + // we don't want the checkboxes to be all over the place :) + checkboxOpt.rect = QRect(opt.rect.x() + 5, opt.rect.y() + (opt.rect.height() / 2 - checkboxRect.height() / 2), checkboxRect.width(), + checkboxRect.height()); + + return checkboxOpt; +} diff --git a/launcher/ui/widgets/ProjectItem.h b/launcher/ui/widgets/ProjectItem.h index 96e62af12..068358ade 100644 --- a/launcher/ui/widgets/ProjectItem.h +++ b/launcher/ui/widgets/ProjectItem.h @@ -6,7 +6,7 @@ enum UserDataTypes { TITLE = 257, // QString DESCRIPTION = 258, // QString - INSTALLED = 260 // bool + INSTALLED = 259 // bool }; /** This is an item delegate composed of: @@ -21,4 +21,12 @@ class ProjectItemDelegate final : public QStyledItemDelegate { ProjectItemDelegate(QWidget* parent); void paint(QPainter*, const QStyleOptionViewItem&, const QModelIndex&) const override; + + bool editorEvent(QEvent* event, QAbstractItemModel* model, const QStyleOptionViewItem& option, const QModelIndex& index) override; + + signals: + void checkboxClicked(const QModelIndex& index); + + private: + QStyleOptionViewItem makeCheckboxStyleOption(const QStyleOptionViewItem& opt, const QStyle* style) const; }; diff --git a/tests/ResourceModel_test.cpp b/tests/ResourceModel_test.cpp index b589758aa..30bb99fb8 100644 --- a/tests/ResourceModel_test.cpp +++ b/tests/ResourceModel_test.cpp @@ -43,8 +43,8 @@ class DummyResourceModel : public ResourceModel { [[nodiscard]] auto metaEntryBase() const -> QString override { return ""; } ResourceAPI::SearchArgs createSearchArguments() override { return {}; } - ResourceAPI::VersionSearchArgs createVersionsArguments(QModelIndex&) override { return {}; } - ResourceAPI::ProjectInfoArgs createInfoArguments(QModelIndex&) override { return {}; } + ResourceAPI::VersionSearchArgs createVersionsArguments(const QModelIndex&) override { return {}; } + ResourceAPI::ProjectInfoArgs createInfoArguments(const QModelIndex&) override { return {}; } QJsonArray documentToArray(QJsonDocument& doc) const override { return doc.object().value("hits").toArray(); } From c6066fe6ecff2b2b374834894a7ab531ffa3b249 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Wed, 19 Mar 2025 01:41:56 +0000 Subject: [PATCH 021/695] Add warning dialog if there are no versions available Signed-off-by: TheKodeToad --- launcher/ui/pages/modplatform/ResourcePage.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/launcher/ui/pages/modplatform/ResourcePage.cpp b/launcher/ui/pages/modplatform/ResourcePage.cpp index 407684f8b..1f2261e2c 100644 --- a/launcher/ui/pages/modplatform/ResourcePage.cpp +++ b/launcher/ui/pages/modplatform/ResourcePage.cpp @@ -418,7 +418,14 @@ void ResourcePage::onToggle(const QModelIndex& index) return m_model->checkVersionFilters(version); }); - if (version != pack->versions.end()) + if (version == pack->versions.end()) { + auto errorMessage = new QMessageBox( + QMessageBox::Warning, tr("No versions available"), + tr("No versions for '%1' are available.\nThe author likely blocked third-party launchers.").arg(pack->name), + QMessageBox::Ok, this); + + errorMessage->open(); + } else addResourceToDialog(pack, *version); } From a5c62e657aa95b50992b63d590e66d1a0d685c7c Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Wed, 19 Mar 2025 01:43:43 +0000 Subject: [PATCH 022/695] Snek case Signed-off-by: TheKodeToad --- launcher/ui/pages/modplatform/ResourcePage.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/launcher/ui/pages/modplatform/ResourcePage.cpp b/launcher/ui/pages/modplatform/ResourcePage.cpp index 1f2261e2c..4e5de4933 100644 --- a/launcher/ui/pages/modplatform/ResourcePage.cpp +++ b/launcher/ui/pages/modplatform/ResourcePage.cpp @@ -407,7 +407,7 @@ void ResourcePage::onResourceSelected() void ResourcePage::onToggle(const QModelIndex& index) { - const bool is_selected = index == m_ui->packView->currentIndex(); + const bool isSelected = index == m_ui->packView->currentIndex(); auto pack = m_model->data(index, Qt::UserRole).value(); if (pack->versionsLoaded) { @@ -429,7 +429,7 @@ void ResourcePage::onToggle(const QModelIndex& index) addResourceToDialog(pack, *version); } - if (is_selected) + if (isSelected) updateSelectionButton(); // force update @@ -442,7 +442,7 @@ void ResourcePage::onToggle(const QModelIndex& index) // we can't be sure that this hasn't already been requested... // but this does the job well enough and there's not much point preventing edgecases - if (!is_selected) + if (!isSelected) m_model->loadEntry(index); } } From 75321722339ff6df854131623774a8e7420b6e76 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Wed, 19 Mar 2025 10:31:55 +0000 Subject: [PATCH 023/695] Clear enableQueue on model reset Signed-off-by: TheKodeToad --- launcher/ui/pages/modplatform/ModPage.h | 1 + launcher/ui/pages/modplatform/ResourcePackPage.h | 1 + launcher/ui/pages/modplatform/ResourcePage.cpp | 5 +++++ launcher/ui/pages/modplatform/ResourcePage.h | 2 ++ launcher/ui/pages/modplatform/ShaderPackPage.h | 1 + launcher/ui/pages/modplatform/TexturePackPage.h | 1 + 6 files changed, 11 insertions(+) diff --git a/launcher/ui/pages/modplatform/ModPage.h b/launcher/ui/pages/modplatform/ModPage.h index 397b77d94..47fe21e0f 100644 --- a/launcher/ui/pages/modplatform/ModPage.h +++ b/launcher/ui/pages/modplatform/ModPage.h @@ -37,6 +37,7 @@ class ModPage : public ResourcePage { connect(model, &ResourceModel::versionListUpdated, page, &ResourcePage::versionListUpdated); connect(model, &ResourceModel::projectInfoUpdated, page, &ResourcePage::updateUi); + connect(model, &QAbstractListModel::modelReset, page, &ResourcePage::modelReset); return page; } diff --git a/launcher/ui/pages/modplatform/ResourcePackPage.h b/launcher/ui/pages/modplatform/ResourcePackPage.h index 3f925fd23..8d967f73a 100644 --- a/launcher/ui/pages/modplatform/ResourcePackPage.h +++ b/launcher/ui/pages/modplatform/ResourcePackPage.h @@ -27,6 +27,7 @@ class ResourcePackResourcePage : public ResourcePage { connect(model, &ResourceModel::versionListUpdated, page, &ResourcePage::versionListUpdated); connect(model, &ResourceModel::projectInfoUpdated, page, &ResourcePage::updateUi); + connect(model, &QAbstractListModel::modelReset, page, &ResourcePage::modelReset); return page; } diff --git a/launcher/ui/pages/modplatform/ResourcePage.cpp b/launcher/ui/pages/modplatform/ResourcePage.cpp index 4e5de4933..5acfec5da 100644 --- a/launcher/ui/pages/modplatform/ResourcePage.cpp +++ b/launcher/ui/pages/modplatform/ResourcePage.cpp @@ -374,6 +374,11 @@ void ResourcePage::addResourceToPage(ModPlatform::IndexedPack::Ptr pack, m_model->addPack(pack, ver, base_model, is_indexed); } +void ResourcePage::modelReset() +{ + m_enableQueue.clear(); +} + void ResourcePage::removeResourceFromPage(const QString& name) { m_model->removePack(name); diff --git a/launcher/ui/pages/modplatform/ResourcePage.h b/launcher/ui/pages/modplatform/ResourcePage.h index 6a44dcf6d..23309333b 100644 --- a/launcher/ui/pages/modplatform/ResourcePage.h +++ b/launcher/ui/pages/modplatform/ResourcePage.h @@ -80,6 +80,8 @@ class ResourcePage : public QWidget, public BasePage { virtual void removeResourceFromPage(const QString& name); virtual void addResourceToPage(ModPlatform::IndexedPack::Ptr, ModPlatform::IndexedVersion&, std::shared_ptr); + virtual void modelReset(); + QList selectedPacks() { return m_model->selectedPacks(); } bool hasSelectedPacks() { return !(m_model->selectedPacks().isEmpty()); } diff --git a/launcher/ui/pages/modplatform/ShaderPackPage.h b/launcher/ui/pages/modplatform/ShaderPackPage.h index 68fdf6699..d436e218a 100644 --- a/launcher/ui/pages/modplatform/ShaderPackPage.h +++ b/launcher/ui/pages/modplatform/ShaderPackPage.h @@ -27,6 +27,7 @@ class ShaderPackResourcePage : public ResourcePage { connect(model, &ResourceModel::versionListUpdated, page, &ResourcePage::versionListUpdated); connect(model, &ResourceModel::projectInfoUpdated, page, &ResourcePage::updateUi); + connect(model, &QAbstractListModel::modelReset, page, &ResourcePage::modelReset); return page; } diff --git a/launcher/ui/pages/modplatform/TexturePackPage.h b/launcher/ui/pages/modplatform/TexturePackPage.h index 393ccc21e..27fd8bcfc 100644 --- a/launcher/ui/pages/modplatform/TexturePackPage.h +++ b/launcher/ui/pages/modplatform/TexturePackPage.h @@ -29,6 +29,7 @@ class TexturePackResourcePage : public ResourcePackResourcePage { connect(model, &ResourceModel::versionListUpdated, page, &ResourcePage::versionListUpdated); connect(model, &ResourceModel::projectInfoUpdated, page, &ResourcePage::updateUi); + connect(model, &QAbstractListModel::modelReset, page, &ResourcePage::modelReset); return page; } From 0f3ac57fddc7a364e7313759cc3784ce01fa4818 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Wed, 19 Mar 2025 10:42:38 +0000 Subject: [PATCH 024/695] Trigger onToggle instead of onResourceSelected when pressing enter Signed-off-by: TheKodeToad --- launcher/ui/pages/modplatform/ResourcePage.cpp | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/launcher/ui/pages/modplatform/ResourcePage.cpp b/launcher/ui/pages/modplatform/ResourcePage.cpp index 5acfec5da..07d296d48 100644 --- a/launcher/ui/pages/modplatform/ResourcePage.cpp +++ b/launcher/ui/pages/modplatform/ResourcePage.cpp @@ -132,13 +132,9 @@ auto ResourcePage::eventFilter(QObject* watched, QEvent* event) -> bool m_searchTimer.start(350); } } else if (watched == m_ui->packView) { + // stop the event from going to the confirm button if (keyEvent->key() == Qt::Key_Return) { - onResourceSelected(); - - // To have the 'select mod' button outlined instead of the 'review and confirm' one - m_ui->resourceSelectionButton->setFocus(Qt::FocusReason::ShortcutFocusReason); - m_ui->packView->setFocus(Qt::FocusReason::NoFocusReason); - + onToggle(m_ui->packView->currentIndex()); keyEvent->accept(); return true; } From 6c44a3f6dfb629682a3741336d1a83faa973250c Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Wed, 19 Mar 2025 10:46:32 +0000 Subject: [PATCH 025/695] onToggle -> onResourceToggle Signed-off-by: TheKodeToad --- launcher/ui/pages/modplatform/ResourcePage.cpp | 12 ++++++------ launcher/ui/pages/modplatform/ResourcePage.h | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/launcher/ui/pages/modplatform/ResourcePage.cpp b/launcher/ui/pages/modplatform/ResourcePage.cpp index 07d296d48..153392075 100644 --- a/launcher/ui/pages/modplatform/ResourcePage.cpp +++ b/launcher/ui/pages/modplatform/ResourcePage.cpp @@ -84,8 +84,8 @@ ResourcePage::ResourcePage(ResourceDownloadDialog* parent, BaseInstance& base_in connect(m_ui->packDescription, &QTextBrowser::anchorClicked, this, &ResourcePage::openUrl); - connect(m_ui->packView, &QListView::doubleClicked, this, &ResourcePage::onToggle); - connect(delegate, &ProjectItemDelegate::checkboxClicked, this, &ResourcePage::onToggle); + connect(m_ui->packView, &QListView::doubleClicked, this, &ResourcePage::onResourceToggle); + connect(delegate, &ProjectItemDelegate::checkboxClicked, this, &ResourcePage::onResourceToggle); } ResourcePage::~ResourcePage() @@ -134,7 +134,7 @@ auto ResourcePage::eventFilter(QObject* watched, QEvent* event) -> bool } else if (watched == m_ui->packView) { // stop the event from going to the confirm button if (keyEvent->key() == Qt::Key_Return) { - onToggle(m_ui->packView->currentIndex()); + onResourceToggle(m_ui->packView->currentIndex()); keyEvent->accept(); return true; } @@ -306,12 +306,12 @@ void ResourcePage::versionListUpdated(const QModelIndex& index) if (m_enableQueue.contains(index.row())) { m_enableQueue.remove(index.row()); - onToggle(index); + onResourceToggle(index); } else updateSelectionButton(); } else if (m_enableQueue.contains(index.row())) { m_enableQueue.remove(index.row()); - onToggle(index); + onResourceToggle(index); } } @@ -406,7 +406,7 @@ void ResourcePage::onResourceSelected() m_ui->packView->repaint(); } -void ResourcePage::onToggle(const QModelIndex& index) +void ResourcePage::onResourceToggle(const QModelIndex& index) { const bool isSelected = index == m_ui->packView->currentIndex(); auto pack = m_model->data(index, Qt::UserRole).value(); diff --git a/launcher/ui/pages/modplatform/ResourcePage.h b/launcher/ui/pages/modplatform/ResourcePage.h index 23309333b..055db441a 100644 --- a/launcher/ui/pages/modplatform/ResourcePage.h +++ b/launcher/ui/pages/modplatform/ResourcePage.h @@ -93,7 +93,7 @@ class ResourcePage : public QWidget, public BasePage { void onSelectionChanged(QModelIndex first, QModelIndex second); void onVersionSelectionChanged(int index); void onResourceSelected(); - void onToggle(const QModelIndex& index); + void onResourceToggle(const QModelIndex& index); // NOTE: Can't use [[nodiscard]] here because of https://bugreports.qt.io/browse/QTBUG-58628 on Qt 5.12 From 6dc16c48a2b1c6056fa06762f5d4a34602adaab5 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Wed, 19 Mar 2025 10:56:07 +0000 Subject: [PATCH 026/695] Append [installed] to installed mods Signed-off-by: TheKodeToad --- launcher/ui/widgets/ProjectItem.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/launcher/ui/widgets/ProjectItem.cpp b/launcher/ui/widgets/ProjectItem.cpp index b20a3e013..d1634591f 100644 --- a/launcher/ui/widgets/ProjectItem.cpp +++ b/launcher/ui/widgets/ProjectItem.cpp @@ -68,6 +68,9 @@ void ProjectItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& o { // Title painting auto title = index.data(UserDataTypes::TITLE).toString(); + if (index.data(UserDataTypes::INSTALLED).toBool()) + title += " [installed]"; + painter->save(); auto font = opt.font; From 9f768f76bbf309a80685f5870c5d881cfb7900df Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Wed, 19 Mar 2025 11:00:51 +0000 Subject: [PATCH 027/695] Translate installed indicator Signed-off-by: TheKodeToad --- launcher/ui/widgets/ProjectItem.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/widgets/ProjectItem.cpp b/launcher/ui/widgets/ProjectItem.cpp index d1634591f..1224b853b 100644 --- a/launcher/ui/widgets/ProjectItem.cpp +++ b/launcher/ui/widgets/ProjectItem.cpp @@ -69,7 +69,7 @@ void ProjectItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& o auto title = index.data(UserDataTypes::TITLE).toString(); if (index.data(UserDataTypes::INSTALLED).toBool()) - title += " [installed]"; + title = tr("%1 [installed]").arg(title); painter->save(); From 5832fb8b95b8853c66ddea114e3b9f3a888e287b Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Wed, 19 Mar 2025 11:09:25 +0000 Subject: [PATCH 028/695] Implement middle click Signed-off-by: TheKodeToad --- launcher/ui/pages/modplatform/ResourcePage.cpp | 8 ++++++++ launcher/ui/widgets/ProjectItem.cpp | 3 +++ 2 files changed, 11 insertions(+) diff --git a/launcher/ui/pages/modplatform/ResourcePage.cpp b/launcher/ui/pages/modplatform/ResourcePage.cpp index 153392075..ea8e8d5e9 100644 --- a/launcher/ui/pages/modplatform/ResourcePage.cpp +++ b/launcher/ui/pages/modplatform/ResourcePage.cpp @@ -81,6 +81,7 @@ ResourcePage::ResourcePage(ResourceDownloadDialog* parent, BaseInstance& base_in auto delegate = new ProjectItemDelegate(this); m_ui->packView->setItemDelegate(delegate); m_ui->packView->installEventFilter(this); + m_ui->packView->viewport()->installEventFilter(this); connect(m_ui->packDescription, &QTextBrowser::anchorClicked, this, &ResourcePage::openUrl); @@ -139,6 +140,13 @@ auto ResourcePage::eventFilter(QObject* watched, QEvent* event) -> bool return true; } } + } else if (watched == m_ui->packView->viewport() && event->type() == QEvent::MouseButtonPress) { + auto* mouseEvent = static_cast(event); + + if (mouseEvent->button() == Qt::MiddleButton) { + onResourceToggle(m_ui->packView->indexAt(mouseEvent->pos())); + return true; + } } return QWidget::eventFilter(watched, event); diff --git a/launcher/ui/widgets/ProjectItem.cpp b/launcher/ui/widgets/ProjectItem.cpp index 1224b853b..fee743c23 100644 --- a/launcher/ui/widgets/ProjectItem.cpp +++ b/launcher/ui/widgets/ProjectItem.cpp @@ -142,6 +142,9 @@ bool ProjectItemDelegate::editorEvent(QEvent* event, auto mouseEvent = (QMouseEvent*)event; + if (mouseEvent->button() != Qt::LeftButton) + return false; + QStyleOptionViewItem opt(option); initStyleOption(&opt, index); From e89f96e9e9087c6b49cceb8e7e710d85ca46d68d Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Wed, 19 Mar 2025 11:48:31 +0000 Subject: [PATCH 029/695] Hack for broken windowsvista Signed-off-by: TheKodeToad --- launcher/ui/widgets/ProjectItem.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/widgets/ProjectItem.cpp b/launcher/ui/widgets/ProjectItem.cpp index fee743c23..c11939b00 100644 --- a/launcher/ui/widgets/ProjectItem.cpp +++ b/launcher/ui/widgets/ProjectItem.cpp @@ -22,7 +22,7 @@ void ProjectItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& o style->drawPrimitive(QStyle::PE_PanelItemViewItem, &opt, painter, opt.widget); - if (option.state & QStyle::State_Selected) + if (option.state & QStyle::State_Selected && style->objectName() != "windowsvista") painter->setPen(opt.palette.highlightedText().color()); if (opt.features & QStyleOptionViewItem::HasCheckIndicator) { From a108b5e9eb05ee0072fc02d2ab0ef482025a2820 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Wed, 19 Mar 2025 12:35:24 +0000 Subject: [PATCH 030/695] Correctly set objectName for HintOverrideProxyStyle Signed-off-by: TheKodeToad --- launcher/ui/themes/HintOverrideProxyStyle.cpp | 4 ++++ launcher/ui/themes/HintOverrideProxyStyle.h | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/launcher/ui/themes/HintOverrideProxyStyle.cpp b/launcher/ui/themes/HintOverrideProxyStyle.cpp index 80e821349..f31969fce 100644 --- a/launcher/ui/themes/HintOverrideProxyStyle.cpp +++ b/launcher/ui/themes/HintOverrideProxyStyle.cpp @@ -18,6 +18,10 @@ #include "HintOverrideProxyStyle.h" +HintOverrideProxyStyle::HintOverrideProxyStyle(QStyle* style) : QProxyStyle(style) { + setObjectName(style->objectName()); +} + int HintOverrideProxyStyle::styleHint(QStyle::StyleHint hint, const QStyleOption* option, const QWidget* widget, diff --git a/launcher/ui/themes/HintOverrideProxyStyle.h b/launcher/ui/themes/HintOverrideProxyStyle.h index 09b296018..e9c489d09 100644 --- a/launcher/ui/themes/HintOverrideProxyStyle.h +++ b/launcher/ui/themes/HintOverrideProxyStyle.h @@ -25,7 +25,7 @@ class HintOverrideProxyStyle : public QProxyStyle { Q_OBJECT public: - HintOverrideProxyStyle(QStyle* style) : QProxyStyle(style) {} + explicit HintOverrideProxyStyle(QStyle* style); int styleHint(QStyle::StyleHint hint, const QStyleOption* option = nullptr, From 947ca679520d4656a9002aaed1ac63acfbc59f42 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Wed, 19 Mar 2025 13:32:14 +0000 Subject: [PATCH 031/695] Fix windows 9x and possibly other styles Signed-off-by: TheKodeToad --- launcher/ui/widgets/ProjectItem.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/launcher/ui/widgets/ProjectItem.cpp b/launcher/ui/widgets/ProjectItem.cpp index c11939b00..950c5fe0a 100644 --- a/launcher/ui/widgets/ProjectItem.cpp +++ b/launcher/ui/widgets/ProjectItem.cpp @@ -172,6 +172,8 @@ QStyleOptionViewItem ProjectItemDelegate::makeCheckboxStyleOption(const QStyleOp if (checkboxOpt.checkState == Qt::Checked) checkboxOpt.state |= QStyle::State_On; + else + checkboxOpt.state |= QStyle::State_Off; QRect checkboxRect = style->subElementRect(QStyle::SE_ItemViewItemCheckIndicator, &checkboxOpt, opt.widget); // 5px is the typical top margin for image From 7dd159aac086b07cbe434d5bd3a8918708a47327 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Fri, 21 Mar 2025 01:39:30 +0000 Subject: [PATCH 032/695] Rework Launcher as General category Signed-off-by: TheKodeToad --- launcher/ui/pages/global/LauncherPage.cpp | 98 +-- launcher/ui/pages/global/LauncherPage.h | 21 +- launcher/ui/pages/global/LauncherPage.ui | 998 ++++++++++------------ 3 files changed, 466 insertions(+), 651 deletions(-) diff --git a/launcher/ui/pages/global/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp index 04ee01b00..36d404660 100644 --- a/launcher/ui/pages/global/LauncherPage.cpp +++ b/launcher/ui/pages/global/LauncherPage.cpp @@ -72,24 +72,14 @@ LauncherPage::LauncherPage(QWidget* parent) : QWidget(parent), ui(new Ui::Launch ui->sortingModeGroup->setId(ui->sortByNameBtn, Sort_Name); ui->sortingModeGroup->setId(ui->sortLastLaunchedBtn, Sort_LastLaunch); - defaultFormat = new QTextCharFormat(ui->fontPreview->currentCharFormat()); - - m_languageModel = APPLICATION->translations(); loadSettings(); ui->updateSettingsBox->setHidden(!APPLICATION->updater()); - - connect(ui->fontSizeBox, QOverload::of(&QSpinBox::valueChanged), this, &LauncherPage::refreshFontPreview); - connect(ui->consoleFont, &QFontComboBox::currentFontChanged, this, &LauncherPage::refreshFontPreview); - connect(ui->themeCustomizationWidget, &ThemeCustomizationWidget::currentWidgetThemeChanged, this, &LauncherPage::refreshFontPreview); - - connect(ui->themeCustomizationWidget, &ThemeCustomizationWidget::currentCatChanged, APPLICATION, &Application::currentCatChanged); } LauncherPage::~LauncherPage() { delete ui; - delete defaultFormat; } bool LauncherPage::apply() @@ -194,9 +184,9 @@ void LauncherPage::on_skinsDirBrowseBtn_clicked() } } -void LauncherPage::on_metadataDisableBtn_clicked() +void LauncherPage::on_metadataEnableBtn_clicked() { - ui->metadataWarningLabel->setHidden(!ui->metadataDisableBtn->isChecked()); + ui->metadataWarningLabel->setHidden(ui->metadataEnableBtn->isChecked()); } void LauncherPage::applySettings() @@ -217,9 +207,6 @@ void LauncherPage::applySettings() s->set("RequestTimeout", ui->timeoutSecondsSpinBox->value()); // Console settings - QString consoleFontFamily = ui->consoleFont->currentFont().family(); - s->set("ConsoleFont", consoleFontFamily); - s->set("ConsoleFontSize", ui->fontSizeBox->value()); s->set("ConsoleMaxLines", ui->lineLimitSpinBox->value()); s->set("ConsoleOverflowStop", ui->checkStopLogging->checkState() != Qt::Unchecked); @@ -245,13 +232,10 @@ void LauncherPage::applySettings() break; } - // Cat - s->set("CatOpacity", ui->catOpacitySpinBox->value()); - // Mods - s->set("ModMetadataDisabled", ui->metadataDisableBtn->isChecked()); - s->set("ModDependenciesDisabled", ui->dependenciesDisableBtn->isChecked()); - s->set("SkipModpackUpdatePrompt", ui->skipModpackUpdatePromptBtn->isChecked()); + s->set("ModMetadataDisabled", !ui->metadataEnableBtn->isChecked()); + s->set("ModDependenciesDisabled", !ui->dependenciesEnableBtn->isChecked()); + s->set("SkipModpackUpdatePrompt", !ui->modpackUpdatePromptBtn->isChecked()); } void LauncherPage::loadSettings() { @@ -262,11 +246,6 @@ void LauncherPage::loadSettings() ui->updateIntervalSpinBox->setValue(APPLICATION->updater()->getUpdateCheckInterval() / 3600); } - // Toolbar/menu bar settings (not applicable if native menu bar is present) - ui->toolsBox->setEnabled(!QMenuBar().isNativeMenuBar()); -#ifdef Q_OS_MACOS - ui->toolsBox->setVisible(!QMenuBar().isNativeMenuBar()); -#endif ui->preferMenuBarCheckBox->setChecked(s->get("MenuBarInsteadOfToolBar").toBool()); ui->numberOfConcurrentTasksSpinBox->setValue(s->get("NumberOfConcurrentTasks").toInt()); @@ -275,17 +254,6 @@ void LauncherPage::loadSettings() ui->timeoutSecondsSpinBox->setValue(s->get("RequestTimeout").toInt()); // Console settings - QString fontFamily = APPLICATION->settings()->get("ConsoleFont").toString(); - QFont consoleFont(fontFamily); - ui->consoleFont->setCurrentFont(consoleFont); - - bool conversionOk = true; - int fontSize = APPLICATION->settings()->get("ConsoleFontSize").toInt(&conversionOk); - if (!conversionOk) { - fontSize = 11; - } - ui->fontSizeBox->setValue(fontSize); - refreshFontPreview(); ui->lineLimitSpinBox->setValue(s->get("ConsoleMaxLines").toInt()); ui->checkStopLogging->setChecked(s->get("ConsoleOverflowStop").toBool()); @@ -307,59 +275,11 @@ void LauncherPage::loadSettings() ui->sortByNameBtn->setChecked(true); } - // Cat - ui->catOpacitySpinBox->setValue(s->get("CatOpacity").toInt()); - // Mods - ui->metadataDisableBtn->setChecked(s->get("ModMetadataDisabled").toBool()); - ui->metadataWarningLabel->setHidden(!ui->metadataDisableBtn->isChecked()); - ui->dependenciesDisableBtn->setChecked(s->get("ModDependenciesDisabled").toBool()); - ui->skipModpackUpdatePromptBtn->setChecked(s->get("SkipModpackUpdatePrompt").toBool()); -} - -void LauncherPage::refreshFontPreview() -{ - const LogColors& colors = APPLICATION->themeManager()->getLogColors(); - - int fontSize = ui->fontSizeBox->value(); - QString fontFamily = ui->consoleFont->currentFont().family(); - ui->fontPreview->clear(); - defaultFormat->setFont(QFont(fontFamily, fontSize)); - - auto print = [this, colors](const QString& message, MessageLevel::Enum level) { - QTextCharFormat format(*defaultFormat); - - QColor bg = colors.background.value(level); - QColor fg = colors.foreground.value(level); - - if (bg.isValid()) - format.setBackground(bg); - - if (fg.isValid()) - format.setForeground(fg); - - // append a paragraph/line - auto workCursor = ui->fontPreview->textCursor(); - workCursor.movePosition(QTextCursor::End); - workCursor.insertText(message, format); - workCursor.insertBlock(); - }; - - print(QString("%1 version: %2 (%3)\n") - .arg(BuildConfig.LAUNCHER_DISPLAYNAME, BuildConfig.printableVersionString(), BuildConfig.BUILD_PLATFORM), - MessageLevel::Launcher); - - QDate today = QDate::currentDate(); - - if (today.month() == 10 && today.day() == 31) - print(tr("[Test/ERROR] OOoooOOOoooo! A spooky error!"), MessageLevel::Error); - else - print(tr("[Test/ERROR] A spooky error!"), MessageLevel::Error); - - print(tr("[Test/INFO] A harmless message..."), MessageLevel::Info); - print(tr("[Test/WARN] A not so spooky warning."), MessageLevel::Warning); - print(tr("[Test/DEBUG] A secret debugging message..."), MessageLevel::Debug); - print(tr("[Test/FATAL] A terrifying fatal error!"), MessageLevel::Fatal); + ui->metadataEnableBtn->setChecked(!s->get("ModMetadataDisabled").toBool()); + ui->metadataWarningLabel->setHidden(ui->metadataEnableBtn->isChecked()); + ui->dependenciesEnableBtn->setChecked(!s->get("ModDependenciesDisabled").toBool()); + ui->modpackUpdatePromptBtn->setChecked(!s->get("SkipModpackUpdatePrompt").toBool()); } void LauncherPage::retranslate() diff --git a/launcher/ui/pages/global/LauncherPage.h b/launcher/ui/pages/global/LauncherPage.h index 02f371b04..d76c84b63 100644 --- a/launcher/ui/pages/global/LauncherPage.h +++ b/launcher/ui/pages/global/LauncherPage.h @@ -57,8 +57,8 @@ class LauncherPage : public QWidget, public BasePage { explicit LauncherPage(QWidget* parent = 0); ~LauncherPage(); - QString displayName() const override { return tr("Launcher"); } - QIcon icon() const override { return APPLICATION->getThemedIcon("launcher"); } + QString displayName() const override { return tr("General"); } + QIcon icon() const override { return APPLICATION->getThemedIcon("settings"); } QString id() const override { return "launcher-settings"; } QString helpPage() const override { return "Launcher-settings"; } bool apply() override; @@ -75,23 +75,8 @@ class LauncherPage : public QWidget, public BasePage { void on_downloadsDirBrowseBtn_clicked(); void on_javaDirBrowseBtn_clicked(); void on_skinsDirBrowseBtn_clicked(); - void on_metadataDisableBtn_clicked(); - - /*! - * Updates the font preview - */ - void refreshFontPreview(); + void on_metadataEnableBtn_clicked(); private: Ui::LauncherPage* ui; - - /*! - * Stores the currently selected update channel. - */ - QString m_currentUpdateChannel; - - // default format for the font preview... - QTextCharFormat* defaultFormat; - - std::shared_ptr m_languageModel; }; diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index 31c878f3e..f4d329c44 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -6,8 +6,8 @@ 0 0 - 511 - 726 + 600 + 700 @@ -16,403 +16,45 @@ 0 - - - 0 - - - 0 - - - 0 - - - 0 - + - - - + + + Qt::ScrollBarAsNeeded - - QTabWidget::Rounded + + true - - 0 - - - - Features - - - - - - Qt::ScrollBarAsNeeded - - - true - - - - - 0 - 0 - 473 - 770 - - - - - - - Update Settings - - - - - - Check for updates automatically - - - - - - - - - Update interval - - - - - - - Set it to 0 to only check on launch - - - h - - - 0 - - - 99999999 - - - - - - - - - - - - Folders - - - - - - &Downloads: - - - downloadsDirTextBox - - - - - - - Browse - - - - - - - - - - - - - &Skins: - - - skinsDirTextBox - - - - - - - &Icons: - - - iconsDirTextBox - - - - - - - - - When enabled, in addition to the downloads folder, its sub folders will also be searched when looking for resources (e.g. when looking for blocked mods on CurseForge). - - - Check downloads folder recursively - - - - - - - When enabled, it will move blocked resources instead of copying them. - - - Move blocked resources - - - - - - - - - - - - &Java: - - - javaDirTextBox - - - - - - - &Mods: - - - modsDirTextBox - - - - - - - - - - - - - - - - Browse - - - - - - - Browse - - - - - - - Browse - - - - - - - I&nstances: - - - instDirTextBox - - - - - - - Browse - - - - - - - Browse - - - - - - - - - - Mods - - - - - - Disable using metadata provided by mod providers (like Modrinth or CurseForge) for mods. - - - Disable using metadata for mods - - - - - - - <html><head/><body><p><span style=" font-weight:600; color:#f5c211;">Warning</span><span style=" color:#f5c211;">: Disabling mod metadata may also disable some QoL features, such as mod updating!</span></p></body></html> - - - true - - - - - - - Disable the automatic detection, installation, and updating of mod dependencies. - - - Disable automatic mod dependency management - - - - - - - When creating a new modpack instance, do not suggest updating existing instances instead. - - - Skip modpack update prompt - - - - - - - - - - Miscellaneous - - - - - - 1 - - - - - - - Number of concurrent tasks - - - - - - - 1 - - - - - - - Number of concurrent downloads - - - - - - - Number of manual retries - - - - - - - 0 - - - - - - - Seconds to wait until the requests are terminated - - - Timeout for HTTP requests - - - - - - - s - - - - - - - - - - Qt::Vertical - - - - 0 - 0 - - - - - - - - - - - - - User Interface - - + + + + 0 + 0 + 563 + 1293 + + + - + true - Instance view sorting mode + User Interface - + - + - &By last launched + Instance Sorting + + + + + + + By &name sortingModeGroup @@ -420,158 +62,276 @@ - + - By &name + &By last launched sortingModeGroup + + + + Qt::Horizontal + + + + + + + The menubar is more friendly for keyboard-driven interaction. + + + &Replace toolbar with menubar + + + - + - Theme + Updater - + + + + + Check for updates automatically + + + - + + + Update checking interval + + + + + + + + + Set it to 0 to only check on launch + + + h + + + 0 + + + 168 + + + + + + + Qt::Horizontal + + + + 0 + 0 + + + + + - + - Cat + Folders - - - - - Set the cat's opacity. 0% is fully transparent and 100% is fully opaque. + + + + + &Downloads + + + downloadsDirTextBox + + + + - Opacity + Browse - - - - + + + + + + + + + + &Skins + + + skinsDirTextBox + + + + + + + &Icons + + + iconsDirTextBox + + + + + + + + + + + + + &Java - - % + + javaDirTextBox - - 100 + + + + + + &Mods - - 0 + + modsDirTextBox + + + + + + - - - Qt::Horizontal + + + + + + Browse + + + + + + + Browse + + + + + + + Browse + + + + + + + I&nstances - - - 40 - 20 - + + instDirTextBox - + + + + + + Browse + + + + + + + Browse + + - - - - 0 - 0 - - + - Tools + Mod Management - + - + - The menubar is more friendly for keyboard-driven interaction. + When enabled, in addition to the downloads folder, its sub folders will also be searched when looking for resources (e.g. when looking for blocked mods on CurseForge). - &Replace toolbar with menubar + Check &subfolders for blocked mods - - - - - - - Qt::Vertical - - - - 0 - 0 - - - - - - - - - Console - - - - - - &History limit - - - - + + + + When enabled, it will move blocked resources instead of copying them. + - &Stop logging when log overflows + Move blocked mods instead of copying them - - - - - 0 - 0 - + + + + Disable using metadata provided by mod providers (like Modrinth or CurseForge) for mods. - - lines + + Keep track of mod metadata - - 10000 + + + + + + <html><head/><body><p><span style=" font-weight:600; color:#f5c211;">Warning</span><span style=" color:#f5c211;">: Disabling mod metadata may also disable some QoL features, such as mod updating!</span></p></body></html> - - 1000000 + + true - - 10000 + + + + + + Disable the automatic detection, installation, and updating of mod dependencies. - - 100000 + + Install dependencies automatically @@ -579,81 +339,244 @@ - - - - 0 - 0 - + + + Modpacks + + + + + When creating a new modpack instance, do not suggest updating existing instances instead. + + + Ask whether to update an existing instance when installing modpacks + + + + + + + + - Console &font + Console - - - - - - 0 - 0 - + + + + + Log History &Limit - - Qt::ScrollBarAsNeeded + + lineLimitSpinBox - - false + + + + + + + + + 0 + 0 + + + + lines + + + 10000 + + + 1000000 + + + 10000 + + + 100000 + + + + + + + Qt::Horizontal + + + + + + + + + &Stop logging when log overflows - - Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + + + Tasks + + + + + + Number of concurrent tasks - - - - - 0 - 0 - + + + + + + 1 + + + + + + + Qt::Horizontal + + + + 0 + 0 + + + + + + + + + + Number of concurrent downloads - - - - 5 + + + + + + 1 + + + + + + + Qt::Horizontal + + + + 0 + 0 + + + + + + + + + + Number of manual retries - - 16 + + + + + + + + 0 + + + + + + + Qt::Horizontal + + + + 0 + 0 + + + + + + + + + + Seconds to wait until the requests are terminated - - 11 + + Timeout for HTTP requests + + + + + + s + + + + + + + Qt::Horizontal + + + + 0 + 0 + + + + + + + + + + Qt::Vertical + + + + 0 + 0 + + + + - - - ThemeCustomizationWidget - QWidget -
ui/widgets/ThemeCustomizationWidget.h
- 1 -
-
- tabWidget scrollArea autoUpdateCheckBox - updateIntervalSpinBox instDirTextBox instDirBrowseBtn modsDirTextBox @@ -666,27 +589,14 @@ skinsDirBrowseBtn downloadsDirTextBox downloadsDirBrowseBtn - downloadsDirWatchRecursiveCheckBox - metadataDisableBtn - dependenciesDisableBtn - skipModpackUpdatePromptBtn - numberOfConcurrentTasksSpinBox - numberOfConcurrentDownloadsSpinBox - numberOfManualRetriesSpinBox - timeoutSecondsSpinBox + metadataEnableBtn + dependenciesEnableBtn sortLastLaunchedBtn sortByNameBtn - catOpacitySpinBox - preferMenuBarCheckBox - lineLimitSpinBox - checkStopLogging - consoleFont - fontSizeBox - fontPreview - \ No newline at end of file + From dced39cab06ad6e4726adf0ce1dc539a94ea3e83 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Fri, 21 Mar 2025 11:04:05 +0000 Subject: [PATCH 033/695] Add appearance page Signed-off-by: TheKodeToad --- launcher/Application.cpp | 2 + launcher/CMakeLists.txt | 5 +- launcher/ui/pages/global/AppearancePage.cpp | 104 +++++++ launcher/ui/pages/global/AppearancePage.h | 75 +++++ launcher/ui/pages/global/AppearancePage.ui | 286 ++++++++++++++++++ .../ui/widgets/ThemeCustomizationWidget.ui | 277 +++++++---------- 6 files changed, 579 insertions(+), 170 deletions(-) create mode 100644 launcher/ui/pages/global/AppearancePage.cpp create mode 100644 launcher/ui/pages/global/AppearancePage.h create mode 100644 launcher/ui/pages/global/AppearancePage.ui diff --git a/launcher/Application.cpp b/launcher/Application.cpp index c3477d331..b1cad5ad4 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -137,6 +137,7 @@ #if defined(Q_OS_LINUX) #include +#include #endif #if defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) @@ -798,6 +799,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) { m_globalSettingsProvider = std::make_shared(tr("Settings")); m_globalSettingsProvider->addPage(); + m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 0e4204cfb..0c07ca618 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -924,7 +924,7 @@ SET(LAUNCHER_SOURCES ui/pages/instance/McResolver.h ui/pages/instance/ServerPingTask.cpp ui/pages/instance/ServerPingTask.h - + # GUI - global settings pages ui/pages/global/AccountListPage.cpp ui/pages/global/AccountListPage.h @@ -937,6 +937,8 @@ SET(LAUNCHER_SOURCES ui/pages/global/MinecraftPage.h ui/pages/global/LauncherPage.cpp ui/pages/global/LauncherPage.h + ui/pages/global/AppearancePage.cpp + ui/pages/global/AppearancePage.h ui/pages/global/ProxyPage.cpp ui/pages/global/ProxyPage.h ui/pages/global/APIPage.cpp @@ -1177,6 +1179,7 @@ qt_wrap_ui(LAUNCHER_UI ui/pages/global/AccountListPage.ui ui/pages/global/JavaPage.ui ui/pages/global/LauncherPage.ui + ui/pages/global/AppearancePage.ui ui/pages/global/APIPage.ui ui/pages/global/ProxyPage.ui ui/pages/global/ExternalToolsPage.ui diff --git a/launcher/ui/pages/global/AppearancePage.cpp b/launcher/ui/pages/global/AppearancePage.cpp new file mode 100644 index 000000000..7b6c893c7 --- /dev/null +++ b/launcher/ui/pages/global/AppearancePage.cpp @@ -0,0 +1,104 @@ +#include "AppearancePage.h" +#include "ui_AppearancePage.h" + +#include +#include +#include + +AppearancePage::AppearancePage(QWidget* parent) : QWidget(parent), m_ui(new Ui::AppearancePage) +{ + m_ui->setupUi(this); + + defaultFormat = new QTextCharFormat(m_ui->fontPreview->currentCharFormat()); + + loadSettings(); + connect(m_ui->fontSizeBox, QOverload::of(&QSpinBox::valueChanged), this, &AppearancePage::updateFontPreview); + connect(m_ui->consoleFont, &QFontComboBox::currentFontChanged, this, &AppearancePage::updateFontPreview); + connect(m_ui->themeCustomizationWidget, &ThemeCustomizationWidget::currentWidgetThemeChanged, this, &AppearancePage::updateFontPreview); + connect(m_ui->themeCustomizationWidget, &ThemeCustomizationWidget::currentCatChanged, APPLICATION, &Application::currentCatChanged); +} + +AppearancePage::~AppearancePage() +{ + delete m_ui; +} + +bool AppearancePage::apply() +{ + applySettings(); + return true; +} + +void AppearancePage::retranslate() +{ + m_ui->retranslateUi(this); +} + +void AppearancePage::applySettings() +{ + SettingsObjectPtr settings = APPLICATION->settings(); + QString consoleFontFamily = m_ui->consoleFont->currentFont().family(); + settings->set("ConsoleFont", consoleFontFamily); + settings->set("ConsoleFontSize", m_ui->fontSizeBox->value()); +} + +void AppearancePage::loadSettings() +{ + QString fontFamily = APPLICATION->settings()->get("ConsoleFont").toString(); + QFont consoleFont(fontFamily); + m_ui->consoleFont->setCurrentFont(consoleFont); + + bool conversionOk = true; + int fontSize = APPLICATION->settings()->get("ConsoleFontSize").toInt(&conversionOk); + if (!conversionOk) { + fontSize = 11; + } + m_ui->fontSizeBox->setValue(fontSize); + + updateFontPreview(); +} + +void AppearancePage::updateFontPreview() +{ + const LogColors& colors = APPLICATION->themeManager()->getLogColors(); + + int fontSize = m_ui->fontSizeBox->value(); + QString fontFamily = m_ui->consoleFont->currentFont().family(); + m_ui->fontPreview->clear(); + defaultFormat->setFont(QFont(fontFamily, fontSize)); + + auto print = [this, colors](const QString& message, MessageLevel::Enum level) { + QTextCharFormat format(*defaultFormat); + + QColor bg = colors.background.value(level); + QColor fg = colors.foreground.value(level); + + if (bg.isValid()) + format.setBackground(bg); + + if (fg.isValid()) + format.setForeground(fg); + + // append a paragraph/line + auto workCursor = m_ui->fontPreview->textCursor(); + workCursor.movePosition(QTextCursor::End); + workCursor.insertText(message, format); + workCursor.insertBlock(); + }; + + print(QString("%1 version: %2 (%3)\n") + .arg(BuildConfig.LAUNCHER_DISPLAYNAME, BuildConfig.printableVersionString(), BuildConfig.BUILD_PLATFORM), + MessageLevel::Launcher); + + QDate today = QDate::currentDate(); + + if (today.month() == 10 && today.day() == 31) + print(tr("[Test/ERROR] OOoooOOOoooo! A spooky error!"), MessageLevel::Error); + else + print(tr("[Test/ERROR] A spooky error!"), MessageLevel::Error); + + print(tr("[Test/INFO] A harmless message..."), MessageLevel::Info); + print(tr("[Test/WARN] A not so spooky warning."), MessageLevel::Warning); + print(tr("[Test/DEBUG] A secret debugging message..."), MessageLevel::Debug); + print(tr("[Test/FATAL] A terrifying fatal error!"), MessageLevel::Fatal); +} diff --git a/launcher/ui/pages/global/AppearancePage.h b/launcher/ui/pages/global/AppearancePage.h new file mode 100644 index 000000000..fe7cb3da7 --- /dev/null +++ b/launcher/ui/pages/global/AppearancePage.h @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - 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 . + * + * 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 "java/JavaChecker.h" +#include "ui/pages/BasePage.h" + +class QTextCharFormat; +class SettingsObject; + +namespace Ui { +class AppearancePage; +} + +class AppearancePage : public QWidget, public BasePage { + Q_OBJECT + + public: + explicit AppearancePage(QWidget* parent = 0); + ~AppearancePage(); + + QString displayName() const override { return tr("Appearance"); } + QIcon icon() const override { return APPLICATION->getThemedIcon("resourcepacks"); } + QString id() const override { return "appearance-settings"; } + QString helpPage() const override { return "Launcher-settings"; } + bool apply() override; + void retranslate() override; + + private: + void applySettings(); + void loadSettings(); + void updateFontPreview(); + + private: + Ui::AppearancePage* m_ui; + QTextCharFormat* defaultFormat; +}; diff --git a/launcher/ui/pages/global/AppearancePage.ui b/launcher/ui/pages/global/AppearancePage.ui new file mode 100644 index 000000000..d3ddfdb20 --- /dev/null +++ b/launcher/ui/pages/global/AppearancePage.ui @@ -0,0 +1,286 @@ + + + AppearancePage + + + + 0 + 0 + 600 + 700 + + + + Form + + + + + + Theme + + + + + + + + + + + + + 0 + 0 + + + + &Fonts + + + + + + + 0 + 0 + + + + + + + + 5 + + + 16 + + + 11 + + + + + + + Monospace Font + + + + + + + + + + Preview + + + + + + Icons + + + + + + + + + + + + + .. + + + true + + + + + + + + + + + .. + + + true + + + + + + + + + + + .. + + + true + + + + + + + + + + + .. + + + true + + + + + + + + + + + .. + + + true + + + + + + + + + + + .. + + + true + + + + + + + + + + + .. + + + true + + + + + + + + + + + .. + + + true + + + + + + + + + + + .. + + + true + + + + + + + + + + + .. + + + true + + + + + + + Qt::Horizontal + + + + + + + + + Qt::Horizontal + + + + + + + Console + + + + + + + + 0 + 0 + + + + Qt::ScrollBarAsNeeded + + + false + + + Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + + + + + ThemeCustomizationWidget + QWidget +
ui/widgets/ThemeCustomizationWidget.h
+ 1 +
+
+ + +
diff --git a/launcher/ui/widgets/ThemeCustomizationWidget.ui b/launcher/ui/widgets/ThemeCustomizationWidget.ui index 1faa45c4f..ed05c4292 100644 --- a/launcher/ui/widgets/ThemeCustomizationWidget.ui +++ b/launcher/ui/widgets/ThemeCustomizationWidget.ui @@ -13,180 +13,125 @@ Form - - - QLayout::SetMinimumSize - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - - &Icons - - - iconsComboBox - - - - - - - - - - 0 - 0 - - - - Qt::StrongFocus - - - - - - - View icon themes folder. - - - - - - - - - true - - - - - - - - - &Widgets - - - widgetStyleComboBox - - - - - - - - - - 0 - 0 - - - - Qt::StrongFocus - - - - - - - View widget themes folder. - - - - - - - - - true - - - - - - - - - The cat appears in the background and is not shown by default. It is only made visible when pressing the Cat button in the Toolbar. - - - C&at - - - backgroundCatComboBox - - - - - - - - - - 0 - 0 - - - - Qt::StrongFocus - - - The cat appears in the background and is not shown by default. It is only made visible when pressing the Cat button in the Toolbar. - - - - - - - View cat packs folder. - - - - - - - - - true - - - - - - + + + + + View icon themes folder. + + + Open Folder + + + + + + + The cat appears in the background and is not shown by default. It is only made visible when pressing the Cat button in the Toolbar. + + + C&at + + + backgroundCatComboBox + + + + + + + View cat packs folder. + + + Open Folder + + + + + + + &Icons + + + iconsComboBox + + - - + + + + + 0 + 0 + + + + Qt::StrongFocus + + + + + + + &Widgets + + + widgetStyleComboBox + + + + + + + View widget themes folder. + + + Open Folder + + + + + + + + 0 + 0 + + + + Qt::StrongFocus + + + The cat appears in the background and is not shown by default. It is only made visible when pressing the Cat button in the Toolbar. + + + + + + + + 0 + 0 + + + + Qt::StrongFocus + + + + + Qt::Horizontal - - - 40 - 20 - - - Refresh all + Refresh All @@ -195,12 +140,6 @@ Qt::Horizontal - - - 40 - 20 - - From 414ad1340d716b60ddbab077ca8f0c2414efd5bc Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Fri, 21 Mar 2025 15:30:05 +0000 Subject: [PATCH 034/695] Rework API page design and rename to services Signed-off-by: TheKodeToad --- launcher/ui/pages/global/APIPage.h | 2 +- launcher/ui/pages/global/APIPage.ui | 236 +++++++++++++--------------- 2 files changed, 106 insertions(+), 132 deletions(-) diff --git a/launcher/ui/pages/global/APIPage.h b/launcher/ui/pages/global/APIPage.h index d4ed92900..9252a9ab3 100644 --- a/launcher/ui/pages/global/APIPage.h +++ b/launcher/ui/pages/global/APIPage.h @@ -53,7 +53,7 @@ class APIPage : public QWidget, public BasePage { explicit APIPage(QWidget* parent = 0); ~APIPage(); - QString displayName() const override { return tr("APIs"); } + QString displayName() const override { return tr("Services"); } QIcon icon() const override { return APPLICATION->getThemedIcon("worlds"); } QString id() const override { return "apis"; } QString helpPage() const override { return "APIs"; } diff --git a/launcher/ui/pages/global/APIPage.ui b/launcher/ui/pages/global/APIPage.ui index 05c256bb2..ab4bdf83e 100644 --- a/launcher/ui/pages/global/APIPage.ui +++ b/launcher/ui/pages/global/APIPage.ui @@ -10,29 +10,50 @@ 620
- - - 0 - - - 0 - - - 0 - - - 0 - + - - - 0 + + + true - - - Services - - + + + + 0 + -342 + 804 + 946 + + + + + + + + 0 + 0 + + + + User Agent + + + + + + + + + Enter a custom User Agent here. The special string $LAUNCHER_VER will be replaced with the version of the launcher. + + + true + + + + + + @@ -92,9 +113,9 @@ - + - You can set this to a third-party metadata server to use patched libraries or other hacks. + Base URL Qt::RichText @@ -114,7 +135,7 @@ - Enter a custom URL for meta here. + You can set this to a third-party metadata server to use patched libraries or other hacks. Qt::RichText @@ -130,26 +151,6 @@ - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - API Keys - - @@ -157,9 +158,9 @@ - + - Note: you probably don't need to set this if logging in via Microsoft Authentication already works. + &Client ID Qt::RichText @@ -167,6 +168,12 @@ true + + true + + + msaClientID + @@ -177,9 +184,9 @@ - + - Enter a custom client ID for Microsoft Authentication here. + Note: you probably don't need to set this if logging in via Microsoft Authentication already works. Qt::RichText @@ -187,9 +194,6 @@ true - - true - @@ -201,23 +205,23 @@ true - &Modrinth API + &Modrinth - - - - <html><head/><body><p>Note: you only need to set this to access private data. Read the <a href="https://docs.modrinth.com/api/#authentication">documentation</a> for more information.</p></body></html> - - + + + true + + (None) + - - + + - Enter a custom API token for Modrinth here. + &API Token Qt::RichText @@ -228,15 +232,21 @@ true + + modrinthToken + - - - + + + + <html><head/><body><p>Note: you only need to set this to access private data. Read the <a href="https://docs.modrinth.com/api/#authentication">documentation</a> for more information.</p></body></html> + + true - - (None) + + true @@ -249,20 +259,23 @@ true - &CurseForge Core API + &CurseForge - - - - Note: you probably don't need to set this if CurseForge already works. + + + + true + + + (Default) - - + + - Enter a custom API Key for CurseForge here. + API &Key Qt::RichText @@ -273,15 +286,18 @@ true + + flameKey + - - - - true + + + + Note: you probably don't need to set this if CurseForge already works. - - (Default) + + true @@ -291,13 +307,13 @@ - Technic Client ID + Technic - + - <html><head/><body><p>Note: you only need to set this to access private data.</p></body></html> + GUID Client ID @@ -309,9 +325,12 @@ - + - Enter a custom GUID client ID for Technic here. + <html><head/><body><p>Note: you only need to set this to access private data.</p></body></html> + + + true @@ -319,61 +338,16 @@ - + Qt::Vertical - - 20 - 40 - - - - - - - - - Miscellaneous - - - - - 0 0 - - User Agent - - - - - - - - - Enter a custom User Agent here. The special string $LAUNCHER_VER will be replaced with the version of the launcher. - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - From 06b5ac9a25fdcc29c5555ed64a8dbaf8360f3128 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Fri, 21 Mar 2025 15:31:46 +0000 Subject: [PATCH 035/695] Add icons to appearance page Signed-off-by: TheKodeToad --- .../resources/breeze_dark/breeze_dark.qrc | 1 + .../breeze_dark/scalable/appearance.svg | 13 + .../resources/breeze_light/breeze_light.qrc | 1 + .../breeze_light/scalable/appearance.svg | 13 + launcher/resources/flat/flat.qrc | 1 + .../resources/flat/scalable/appearance.svg | 1 + launcher/resources/flat_white/flat_white.qrc | 1 + .../flat_white/scalable/appearance.svg | 1 + launcher/resources/multimc/multimc.qrc | 5 +- .../resources/multimc/scalable/appearance.svg | 2440 +++++++++++++++++ launcher/resources/pe_blue/pe_blue.qrc | 1 + .../resources/pe_blue/scalable/appearance.svg | 70 + launcher/resources/pe_colored/pe_colored.qrc | 1 + .../pe_colored/scalable/appearance.svg | 75 + launcher/resources/pe_dark/pe_dark.qrc | 1 + .../resources/pe_dark/scalable/appearance.svg | 69 + launcher/resources/pe_light/pe_light.qrc | 1 + .../pe_light/scalable/appearance.svg | 70 + launcher/ui/pages/global/AppearancePage.h | 2 +- launcher/ui/pages/global/AppearancePage.ui | 50 +- .../ui/widgets/ThemeCustomizationWidget.ui | 33 + 21 files changed, 2833 insertions(+), 17 deletions(-) create mode 100644 launcher/resources/breeze_dark/scalable/appearance.svg create mode 100644 launcher/resources/breeze_light/scalable/appearance.svg create mode 100644 launcher/resources/flat/scalable/appearance.svg create mode 100644 launcher/resources/flat_white/scalable/appearance.svg create mode 100644 launcher/resources/multimc/scalable/appearance.svg create mode 100644 launcher/resources/pe_blue/scalable/appearance.svg create mode 100644 launcher/resources/pe_colored/scalable/appearance.svg create mode 100644 launcher/resources/pe_dark/scalable/appearance.svg create mode 100644 launcher/resources/pe_light/scalable/appearance.svg diff --git a/launcher/resources/breeze_dark/breeze_dark.qrc b/launcher/resources/breeze_dark/breeze_dark.qrc index 61d82ec30..8fdfdeeb2 100644 --- a/launcher/resources/breeze_dark/breeze_dark.qrc +++ b/launcher/resources/breeze_dark/breeze_dark.qrc @@ -43,5 +43,6 @@ scalable/rename.svg scalable/launch.svg scalable/server.svg + scalable/appearance.svg diff --git a/launcher/resources/breeze_dark/scalable/appearance.svg b/launcher/resources/breeze_dark/scalable/appearance.svg new file mode 100644 index 000000000..93e6ffa76 --- /dev/null +++ b/launcher/resources/breeze_dark/scalable/appearance.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/launcher/resources/breeze_light/breeze_light.qrc b/launcher/resources/breeze_light/breeze_light.qrc index 2211c7188..6a7120fed 100644 --- a/launcher/resources/breeze_light/breeze_light.qrc +++ b/launcher/resources/breeze_light/breeze_light.qrc @@ -43,5 +43,6 @@ scalable/rename.svg scalable/launch.svg scalable/server.svg + scalable/appearance.svg diff --git a/launcher/resources/breeze_light/scalable/appearance.svg b/launcher/resources/breeze_light/scalable/appearance.svg new file mode 100644 index 000000000..6e6d64a79 --- /dev/null +++ b/launcher/resources/breeze_light/scalable/appearance.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/launcher/resources/flat/flat.qrc b/launcher/resources/flat/flat.qrc index 8876027da..b546faec0 100644 --- a/launcher/resources/flat/flat.qrc +++ b/launcher/resources/flat/flat.qrc @@ -49,5 +49,6 @@ scalable/rename.svg scalable/server.svg scalable/launch.svg + scalable/appearance.svg diff --git a/launcher/resources/flat/scalable/appearance.svg b/launcher/resources/flat/scalable/appearance.svg new file mode 100644 index 000000000..11dcb3f33 --- /dev/null +++ b/launcher/resources/flat/scalable/appearance.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/resources/flat_white/flat_white.qrc b/launcher/resources/flat_white/flat_white.qrc index 83b178cbf..c59bb2ba7 100644 --- a/launcher/resources/flat_white/flat_white.qrc +++ b/launcher/resources/flat_white/flat_white.qrc @@ -49,5 +49,6 @@ scalable/tag.svg scalable/launch.svg scalable/server.svg + scalable/appearance.svg diff --git a/launcher/resources/flat_white/scalable/appearance.svg b/launcher/resources/flat_white/scalable/appearance.svg new file mode 100644 index 000000000..b20d91f12 --- /dev/null +++ b/launcher/resources/flat_white/scalable/appearance.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launcher/resources/multimc/multimc.qrc b/launcher/resources/multimc/multimc.qrc index 25edd09e0..80b472917 100644 --- a/launcher/resources/multimc/multimc.qrc +++ b/launcher/resources/multimc/multimc.qrc @@ -247,7 +247,7 @@ scalable/matrix.svg - + scalable/discord.svg @@ -279,7 +279,7 @@ scalable/instances/fox.svg scalable/instances/bee.svg - + 32x32/instances/chicken_legacy.png 128x128/instances/chicken_legacy.png @@ -347,6 +347,7 @@ scalable/export.svg scalable/launch.svg scalable/server.svg + scalable/appearance.svg scalable/instances/quiltmc.svg scalable/instances/fabricmc.svg diff --git a/launcher/resources/multimc/scalable/appearance.svg b/launcher/resources/multimc/scalable/appearance.svg new file mode 100644 index 000000000..429670c36 --- /dev/null +++ b/launcher/resources/multimc/scalable/appearance.svg @@ -0,0 +1,2440 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + OK + + + + + + + + + + + + 22% + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + OK + + + + + + + + + + + + 22% + + + + + diff --git a/launcher/resources/pe_blue/pe_blue.qrc b/launcher/resources/pe_blue/pe_blue.qrc index 717d3972e..314fde1a8 100644 --- a/launcher/resources/pe_blue/pe_blue.qrc +++ b/launcher/resources/pe_blue/pe_blue.qrc @@ -41,5 +41,6 @@ scalable/launch.svg scalable/shortcut.svg scalable/server.svg + scalable/appearance.svg diff --git a/launcher/resources/pe_blue/scalable/appearance.svg b/launcher/resources/pe_blue/scalable/appearance.svg new file mode 100644 index 000000000..1d49d9d6f --- /dev/null +++ b/launcher/resources/pe_blue/scalable/appearance.svg @@ -0,0 +1,70 @@ + + + + diff --git a/launcher/resources/pe_colored/pe_colored.qrc b/launcher/resources/pe_colored/pe_colored.qrc index 023c81e74..484342534 100644 --- a/launcher/resources/pe_colored/pe_colored.qrc +++ b/launcher/resources/pe_colored/pe_colored.qrc @@ -41,5 +41,6 @@ scalable/launch.svg scalable/shortcut.svg scalable/server.svg + scalable/appearance.svg diff --git a/launcher/resources/pe_colored/scalable/appearance.svg b/launcher/resources/pe_colored/scalable/appearance.svg new file mode 100644 index 000000000..ac9cc258a --- /dev/null +++ b/launcher/resources/pe_colored/scalable/appearance.svg @@ -0,0 +1,75 @@ + + + + diff --git a/launcher/resources/pe_dark/pe_dark.qrc b/launcher/resources/pe_dark/pe_dark.qrc index c97fb469c..06ba97df7 100644 --- a/launcher/resources/pe_dark/pe_dark.qrc +++ b/launcher/resources/pe_dark/pe_dark.qrc @@ -41,5 +41,6 @@ scalable/launch.svg scalable/shortcut.svg scalable/server.svg + scalable/appearance.svg diff --git a/launcher/resources/pe_dark/scalable/appearance.svg b/launcher/resources/pe_dark/scalable/appearance.svg new file mode 100644 index 000000000..b50372fba --- /dev/null +++ b/launcher/resources/pe_dark/scalable/appearance.svg @@ -0,0 +1,69 @@ + + + + diff --git a/launcher/resources/pe_light/pe_light.qrc b/launcher/resources/pe_light/pe_light.qrc index b590dd2c6..a1081bcab 100644 --- a/launcher/resources/pe_light/pe_light.qrc +++ b/launcher/resources/pe_light/pe_light.qrc @@ -41,5 +41,6 @@ scalable/launch.svg scalable/shortcut.svg scalable/server.svg + scalable/appearance.svg diff --git a/launcher/resources/pe_light/scalable/appearance.svg b/launcher/resources/pe_light/scalable/appearance.svg new file mode 100644 index 000000000..4f000f452 --- /dev/null +++ b/launcher/resources/pe_light/scalable/appearance.svg @@ -0,0 +1,70 @@ + + + + diff --git a/launcher/ui/pages/global/AppearancePage.h b/launcher/ui/pages/global/AppearancePage.h index fe7cb3da7..2686f52e2 100644 --- a/launcher/ui/pages/global/AppearancePage.h +++ b/launcher/ui/pages/global/AppearancePage.h @@ -58,7 +58,7 @@ class AppearancePage : public QWidget, public BasePage { ~AppearancePage(); QString displayName() const override { return tr("Appearance"); } - QIcon icon() const override { return APPLICATION->getThemedIcon("resourcepacks"); } + QIcon icon() const override { return APPLICATION->getThemedIcon("appearance"); } QString id() const override { return "appearance-settings"; } QString helpPage() const override { return "Launcher-settings"; } bool apply() override; diff --git a/launcher/ui/pages/global/AppearancePage.ui b/launcher/ui/pages/global/AppearancePage.ui index d3ddfdb20..676c0659c 100644 --- a/launcher/ui/pages/global/AppearancePage.ui +++ b/launcher/ui/pages/global/AppearancePage.ui @@ -77,17 +77,13 @@ Preview - - - - Icons - - - + + Qt::NoFocus + @@ -102,6 +98,9 @@ + + Qt::NoFocus + @@ -116,6 +115,9 @@ + + Qt::NoFocus + @@ -130,6 +132,9 @@ + + Qt::NoFocus + @@ -144,6 +149,9 @@ + + Qt::NoFocus + @@ -158,6 +166,9 @@ + + Qt::NoFocus + @@ -172,6 +183,9 @@ + + Qt::NoFocus + @@ -186,6 +200,9 @@ + + Qt::NoFocus + @@ -200,6 +217,9 @@ + + Qt::NoFocus + @@ -214,6 +234,9 @@ + + Qt::NoFocus + @@ -231,6 +254,12 @@ Qt::Horizontal + + + 0 + 0 + + @@ -242,13 +271,6 @@
- - - - Console - - - diff --git a/launcher/ui/widgets/ThemeCustomizationWidget.ui b/launcher/ui/widgets/ThemeCustomizationWidget.ui index ed05c4292..4ac5706dd 100644 --- a/launcher/ui/widgets/ThemeCustomizationWidget.ui +++ b/launcher/ui/widgets/ThemeCustomizationWidget.ui @@ -14,6 +14,18 @@ Form + + 0 + + + 0 + + + 0 + + + 0 + @@ -126,6 +138,12 @@ Qt::Horizontal + + + 0 + 0 + + @@ -140,12 +158,27 @@ Qt::Horizontal + + + 0 + 0 + +
+ + iconsComboBox + iconsFolder + widgetStyleComboBox + widgetStyleFolder + backgroundCatComboBox + catPackFolder + refreshButton + From 99eeef40d9e7c9b80e77f6f2bc20a31d0ad3c531 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Sat, 22 Mar 2025 13:58:41 +0000 Subject: [PATCH 036/695] Improve appearance page more Signed-off-by: TheKodeToad --- launcher/ui/pages/global/AppearancePage.cpp | 150 +++- launcher/ui/pages/global/AppearancePage.h | 9 +- launcher/ui/pages/global/AppearancePage.ui | 663 ++++++++++++------ launcher/ui/pages/global/LauncherPage.ui | 440 ++++++------ launcher/ui/themes/CatPack.cpp | 6 +- launcher/ui/themes/CatPack.h | 18 +- launcher/ui/themes/ITheme.h | 1 + launcher/ui/themes/IconTheme.cpp | 17 - launcher/ui/themes/IconTheme.h | 8 +- .../ui/widgets/ThemeCustomizationWidget.cpp | 34 - .../ui/widgets/ThemeCustomizationWidget.h | 2 - .../ui/widgets/ThemeCustomizationWidget.ui | 145 ++-- 12 files changed, 902 insertions(+), 591 deletions(-) diff --git a/launcher/ui/pages/global/AppearancePage.cpp b/launcher/ui/pages/global/AppearancePage.cpp index 7b6c893c7..e9e4ad2b9 100644 --- a/launcher/ui/pages/global/AppearancePage.cpp +++ b/launcher/ui/pages/global/AppearancePage.cpp @@ -1,21 +1,41 @@ #include "AppearancePage.h" #include "ui_AppearancePage.h" -#include -#include -#include +#include +#include +#include "BuildConfig.h" +#include "ui/themes/ITheme.h" +#include "ui/themes/ThemeManager.h" AppearancePage::AppearancePage(QWidget* parent) : QWidget(parent), m_ui(new Ui::AppearancePage) { m_ui->setupUi(this); - defaultFormat = new QTextCharFormat(m_ui->fontPreview->currentCharFormat()); + m_ui->catPreview->setGraphicsEffect(new QGraphicsOpacityEffect(this)); + + defaultFormat = new QTextCharFormat(m_ui->consolePreview->currentCharFormat()); loadSettings(); - connect(m_ui->fontSizeBox, QOverload::of(&QSpinBox::valueChanged), this, &AppearancePage::updateFontPreview); - connect(m_ui->consoleFont, &QFontComboBox::currentFontChanged, this, &AppearancePage::updateFontPreview); - connect(m_ui->themeCustomizationWidget, &ThemeCustomizationWidget::currentWidgetThemeChanged, this, &AppearancePage::updateFontPreview); - connect(m_ui->themeCustomizationWidget, &ThemeCustomizationWidget::currentCatChanged, APPLICATION, &Application::currentCatChanged); + loadThemeSettings(); + + updateConsolePreview(); + updateCatPreview(); + + connect(m_ui->fontSizeBox, QOverload::of(&QSpinBox::valueChanged), this, &AppearancePage::updateConsolePreview); + connect(m_ui->consoleFont, &QFontComboBox::currentFontChanged, this, &AppearancePage::updateConsolePreview); + + connect(m_ui->iconsComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &AppearancePage::applyIconTheme); + connect(m_ui->widgetStyleComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &AppearancePage::applyWidgetTheme); + connect(m_ui->catPackComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &AppearancePage::applyCatTheme); + connect(m_ui->catOpacitySlider, &QAbstractSlider::valueChanged, this, &AppearancePage::updateCatPreview); + + connect(m_ui->iconsFolder, &QPushButton::clicked, this, + [] { DesktopServices::openPath(APPLICATION->themeManager()->getIconThemesFolder().path()); }); + connect(m_ui->widgetStyleFolder, &QPushButton::clicked, this, + [] { DesktopServices::openPath(APPLICATION->themeManager()->getApplicationThemesFolder().path()); }); + connect(m_ui->catPackFolder, &QPushButton::clicked, this, + [] { DesktopServices::openPath(APPLICATION->themeManager()->getCatPacksFolder().path()); }); + connect(m_ui->reloadThemesButton, &QPushButton::pressed, this, &AppearancePage::loadThemeSettings); } AppearancePage::~AppearancePage() @@ -40,6 +60,7 @@ void AppearancePage::applySettings() QString consoleFontFamily = m_ui->consoleFont->currentFont().family(); settings->set("ConsoleFont", consoleFontFamily); settings->set("ConsoleFontSize", m_ui->fontSizeBox->value()); + settings->set("CatOpacity", m_ui->catOpacitySlider->value()); } void AppearancePage::loadSettings() @@ -55,16 +76,111 @@ void AppearancePage::loadSettings() } m_ui->fontSizeBox->setValue(fontSize); - updateFontPreview(); + m_ui->catOpacitySlider->setValue(APPLICATION->settings()->get("CatOpacity").toInt()); +} + +void AppearancePage::applyIconTheme(int index) +{ + auto settings = APPLICATION->settings(); + auto originalIconTheme = settings->get("IconTheme").toString(); + auto newIconTheme = m_ui->iconsComboBox->itemData(index).toString(); + if (originalIconTheme != newIconTheme) { + settings->set("IconTheme", newIconTheme); + APPLICATION->themeManager()->applyCurrentlySelectedTheme(); + } +} + +void AppearancePage::applyWidgetTheme(int index) +{ + auto settings = APPLICATION->settings(); + auto originalAppTheme = settings->get("ApplicationTheme").toString(); + auto newAppTheme = m_ui->widgetStyleComboBox->itemData(index).toString(); + if (originalAppTheme != newAppTheme) { + settings->set("ApplicationTheme", newAppTheme); + APPLICATION->themeManager()->applyCurrentlySelectedTheme(); + } + + updateConsolePreview(); +} + +void AppearancePage::applyCatTheme(int index) +{ + auto settings = APPLICATION->settings(); + auto originalCat = settings->get("BackgroundCat").toString(); + auto newCat = m_ui->catPackComboBox->itemData(index).toString(); + if (originalCat != newCat) { + settings->set("BackgroundCat", newCat); + } + + APPLICATION->currentCatChanged(index); + updateCatPreview(); +} + +void AppearancePage::loadThemeSettings() +{ + APPLICATION->themeManager()->refresh(); + + m_ui->iconsComboBox->blockSignals(true); + m_ui->widgetStyleComboBox->blockSignals(true); + m_ui->catPackComboBox->blockSignals(true); + + m_ui->iconsComboBox->clear(); + m_ui->widgetStyleComboBox->clear(); + m_ui->catPackComboBox->clear(); + + const SettingsObjectPtr settings = APPLICATION->settings(); + + const QString currentIconTheme = settings->get("IconTheme").toString(); + const auto iconThemes = APPLICATION->themeManager()->getValidIconThemes(); + + for (int i = 0; i < iconThemes.count(); ++i) { + const IconTheme* theme = iconThemes[i]; + + QIcon iconForComboBox = QIcon(theme->path() + "/scalable/settings"); + m_ui->iconsComboBox->addItem(iconForComboBox, theme->name(), theme->id()); + + if (currentIconTheme == theme->id()) + m_ui->iconsComboBox->setCurrentIndex(i); + } + + const QString currentTheme = settings->get("ApplicationTheme").toString(); + auto themes = APPLICATION->themeManager()->getValidApplicationThemes(); + for (int i = 0; i < themes.count(); ++i) { + ITheme* theme = themes[i]; + + m_ui->widgetStyleComboBox->addItem(theme->name(), theme->id()); + + if (!theme->tooltip().isEmpty()) + m_ui->widgetStyleComboBox->setItemData(i, theme->tooltip(), Qt::ToolTipRole); + + if (currentTheme == theme->id()) + m_ui->widgetStyleComboBox->setCurrentIndex(i); + } + + const QString currentCat = settings->get("BackgroundCat").toString(); + const auto cats = APPLICATION->themeManager()->getValidCatPacks(); + for (int i = 0; i < cats.count(); ++i) { + const CatPack* cat = cats[i]; + + QIcon catIcon = QIcon(QString("%1").arg(cat->path())); + m_ui->catPackComboBox->addItem(catIcon, cat->name(), cat->id()); + + if (currentCat == cat->id()) + m_ui->catPackComboBox->setCurrentIndex(i); + } + + m_ui->iconsComboBox->blockSignals(false); + m_ui->widgetStyleComboBox->blockSignals(false); + m_ui->catPackComboBox->blockSignals(false); } -void AppearancePage::updateFontPreview() +void AppearancePage::updateConsolePreview() { const LogColors& colors = APPLICATION->themeManager()->getLogColors(); int fontSize = m_ui->fontSizeBox->value(); QString fontFamily = m_ui->consoleFont->currentFont().family(); - m_ui->fontPreview->clear(); + m_ui->consolePreview->clear(); defaultFormat->setFont(QFont(fontFamily, fontSize)); auto print = [this, colors](const QString& message, MessageLevel::Enum level) { @@ -80,7 +196,7 @@ void AppearancePage::updateFontPreview() format.setForeground(fg); // append a paragraph/line - auto workCursor = m_ui->fontPreview->textCursor(); + auto workCursor = m_ui->consolePreview->textCursor(); workCursor.movePosition(QTextCursor::End); workCursor.insertText(message, format); workCursor.insertBlock(); @@ -102,3 +218,13 @@ void AppearancePage::updateFontPreview() print(tr("[Test/DEBUG] A secret debugging message..."), MessageLevel::Debug); print(tr("[Test/FATAL] A terrifying fatal error!"), MessageLevel::Fatal); } + +void AppearancePage::updateCatPreview() +{ + QIcon catPackIcon(APPLICATION->themeManager()->getCatPack()); + m_ui->catPreview->setIcon(catPackIcon); + + auto effect = dynamic_cast(m_ui->catPreview->graphicsEffect()); + if (effect) + effect->setOpacity(m_ui->catOpacitySlider->value() / 100.0); +} diff --git a/launcher/ui/pages/global/AppearancePage.h b/launcher/ui/pages/global/AppearancePage.h index 2686f52e2..f964d5f7c 100644 --- a/launcher/ui/pages/global/AppearancePage.h +++ b/launcher/ui/pages/global/AppearancePage.h @@ -67,7 +67,14 @@ class AppearancePage : public QWidget, public BasePage { private: void applySettings(); void loadSettings(); - void updateFontPreview(); + + void applyIconTheme(int index); + void applyWidgetTheme(int index); + void applyCatTheme(int index); + void loadThemeSettings(); + + void updateConsolePreview(); + void updateCatPreview(); private: Ui::AppearancePage* m_ui; diff --git a/launcher/ui/pages/global/AppearancePage.ui b/launcher/ui/pages/global/AppearancePage.ui index 676c0659c..ad2aba7e8 100644 --- a/launcher/ui/pages/global/AppearancePage.ui +++ b/launcher/ui/pages/global/AppearancePage.ui @@ -10,239 +10,287 @@ 700 + + + 300 + 0 + + Form - + - + - Theme + - - - - - - - - - - - - 0 - 0 - + + false - - &Fonts - - - - - - - 0 - 0 - - - - - - - - 5 - - - 16 - - - 11 - - - - - - - Monospace Font - - - - - - - - - - Preview - - + - - - - - Qt::NoFocus + + + + + View cat packs folder. - - - - - .. - - - true + Open Folder - - - - Qt::NoFocus + + + + View widget themes folder. - - - - - .. - - - true + Open Folder - - - - Qt::NoFocus + + + + View icon themes folder. - - - - - .. - - - true + Open Folder - - - - Qt::NoFocus - + + - + &Cat Pack - - - .. - - - true + + catPackComboBox - - - - Qt::NoFocus - - - - - - - .. + + + + + + + + 0 + 0 + - - true + + Qt::StrongFocus - - - - Qt::NoFocus - - - + + + + + 0 + 0 + - - - .. - - - true + + Qt::StrongFocus - - - - Qt::NoFocus + + + + + 0 + 0 + - - - - - .. - - - true + Reload All - - - - Qt::NoFocus - + + - - - - - .. + Theme - - true + + widgetStyleComboBox - - - - Qt::NoFocus - + + - + &Icons - - - .. - - - true + + iconsComboBox + + + + + + Qt::Horizontal + + + + + + + Console Font + + + + + + + + 300 + 16777215 + + + + + + + + + + 5 + + + 16 + + + 11 + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 0 + 6 + + + + + + + + Cat Opacity + + + + + + + + 300 + 16777215 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + 100 + + + Qt::Horizontal + + + 5 + + + + + + + Transparent + + + + + + + true + + + Opaque + + + + + + + + + + + + + Preview + + + + - + + + + 0 + 0 + + Qt::NoFocus - - - .. + + + 128 + 256 + true @@ -250,59 +298,234 @@ - - - Qt::Horizontal - - - - 0 - 0 - - - + + + + + + + Qt::NoFocus + + + + + + + .. + + + true + + + + + + + Qt::NoFocus + + + + + + + .. + + + true + + + + + + + Qt::NoFocus + + + + + + + .. + + + true + + + + + + + Qt::NoFocus + + + + + + + .. + + + true + + + + + + + Qt::NoFocus + + + + + + + .. + + + true + + + + + + + Qt::NoFocus + + + + + + + .. + + + true + + + + + + + Qt::NoFocus + + + + + + + .. + + + true + + + + + + + Qt::NoFocus + + + + + + + .. + + + true + + + + + + + Qt::NoFocus + + + + + + + .. + + + true + + + + + + + Qt::NoFocus + + + + + + + .. + + + true + + + + + + + Qt::Horizontal + + + + 0 + 0 + + + + + + + + + + + 0 + 0 + + + + Qt::ScrollBarAsNeeded + + + false + + + Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + - - - - Qt::Horizontal - - - - - - - - 0 - 0 - - - - Qt::ScrollBarAsNeeded - - - false - - - Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - ThemeCustomizationWidget - QWidget -
ui/widgets/ThemeCustomizationWidget.h
- 1 -
-
+ + widgetStyleComboBox + widgetStyleFolder + iconsComboBox + iconsFolder + catPackComboBox + catPackFolder + consoleFont + fontSizeBox + catOpacitySlider + consolePreview +
diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index f4d329c44..da2f6d5f2 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -31,7 +31,7 @@ 0 0 563 - 1293 + 1336 @@ -72,11 +72,20 @@ - + - Qt::Horizontal + Qt::Vertical - + + QSizePolicy::Fixed + + + + 0 + 6 + + + @@ -104,6 +113,22 @@ + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 0 + 6 + + + + @@ -112,37 +137,26 @@ - - - - - Set it to 0 to only check on launch - - - h - - - 0 - - - 168 - - - - - - - Qt::Horizontal - - - - 0 - 0 - - - - - + + + + 0 + 0 + + + + Set it to 0 to only check on launch + + + h + + + 0 + + + 168 + + @@ -163,29 +177,13 @@ - - + + Browse - - - - - - - - - - &Skins - - - skinsDirTextBox - - - @@ -196,19 +194,10 @@ - - - - - - - - + + - &Java - - - javaDirTextBox + Browse @@ -222,36 +211,29 @@ - - - - - - - - - - - + + Browse - - - - Browse - - + + - - + + Browse + + + + + + @@ -262,13 +244,39 @@ - - + + + + &Java + + + javaDirTextBox + + + + + + + + Browse + + + + + + + &Skins + + + skinsDirTextBox + + + @@ -276,13 +284,16 @@ + + +
- Mod Management + Mods @@ -350,7 +361,7 @@ When creating a new modpack instance, do not suggest updating existing instances instead. - Ask whether to update an existing instance when installing modpacks + Suggest to update an existing instance @@ -374,40 +385,45 @@ - - - - - - 0 - 0 - - - - lines - - - 10000 - - - 1000000 - - - 10000 - - - 100000 - - - - - - - Qt::Horizontal - - - - + + + + 0 + 0 + + + + lines + + + 10000 + + + 1000000 + + + 10000 + + + 100000 + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 0 + 6 + + + @@ -433,28 +449,33 @@ - - - - - 1 - - - - - - - Qt::Horizontal - - - - 0 - 0 - - - - - + + + + 0 + 0 + + + + 1 + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 0 + 6 + + + @@ -464,28 +485,33 @@ - - - - - 1 - - - - - - - Qt::Horizontal - - - - 0 - 0 - - - - - + + + + 0 + 0 + + + + 1 + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 0 + 6 + + + @@ -495,28 +521,33 @@ - - - - - 0 - - - - - - - Qt::Horizontal - - - - 0 - 0 - - - - - + + + + 0 + 0 + + + + 0 + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 0 + 6 + + + @@ -529,28 +560,17 @@ - - - - - s - - - - - - - Qt::Horizontal - - - - 0 - 0 - - - - - + + + + 0 + 0 + + + + s + + diff --git a/launcher/ui/themes/CatPack.cpp b/launcher/ui/themes/CatPack.cpp index 85eb85a18..416b86139 100644 --- a/launcher/ui/themes/CatPack.cpp +++ b/launcher/ui/themes/CatPack.cpp @@ -43,7 +43,7 @@ #include "FileSystem.h" #include "Json.h" -QString BasicCatPack::path() +QString BasicCatPack::path() const { const auto now = QDate::currentDate(); const auto birthday = QDate(now.year(), 11, 1); @@ -100,12 +100,12 @@ QDate ensureDay(int year, int month, int day) return QDate(year, month, day); } -QString JsonCatPack::path() +QString JsonCatPack::path() const { return path(QDate::currentDate()); } -QString JsonCatPack::path(QDate now) +QString JsonCatPack::path(QDate now) const { for (auto var : m_variants) { QDate startDate = ensureDay(now.year(), var.startTime.month, var.startTime.day); diff --git a/launcher/ui/themes/CatPack.h b/launcher/ui/themes/CatPack.h index 5a13d0cef..e0e34f86e 100644 --- a/launcher/ui/themes/CatPack.h +++ b/launcher/ui/themes/CatPack.h @@ -43,18 +43,18 @@ class CatPack { public: virtual ~CatPack() {} - virtual QString id() = 0; - virtual QString name() = 0; - virtual QString path() = 0; + virtual QString id() const = 0; + virtual QString name() const = 0; + virtual QString path() const = 0; }; class BasicCatPack : public CatPack { public: BasicCatPack(QString id, QString name) : m_id(id), m_name(name) {} BasicCatPack(QString id) : BasicCatPack(id, id) {} - virtual QString id() override { return m_id; } - virtual QString name() override { return m_name; } - virtual QString path() override; + virtual QString id() const override { return m_id; } + virtual QString name() const override { return m_name; } + virtual QString path() const override; protected: QString m_id; @@ -65,7 +65,7 @@ class FileCatPack : public BasicCatPack { public: FileCatPack(QString id, QFileInfo& fileInfo) : BasicCatPack(id), m_path(fileInfo.absoluteFilePath()) {} FileCatPack(QFileInfo& fileInfo) : FileCatPack(fileInfo.baseName(), fileInfo) {} - virtual QString path() { return m_path; } + virtual QString path() const { return m_path; } private: QString m_path; @@ -83,8 +83,8 @@ class JsonCatPack : public BasicCatPack { PartialDate endTime; }; JsonCatPack(QFileInfo& manifestInfo); - virtual QString path() override; - QString path(QDate now); + virtual QString path() const override; + QString path(QDate now) const; private: QString m_default_path; diff --git a/launcher/ui/themes/ITheme.h b/launcher/ui/themes/ITheme.h index 7dc5fc64a..a3dd14d09 100644 --- a/launcher/ui/themes/ITheme.h +++ b/launcher/ui/themes/ITheme.h @@ -47,6 +47,7 @@ struct LogColors { }; // TODO: rename to Theme; this is not an interface as it contains method implementations +// TODO: make methods const class ITheme { public: virtual ~ITheme() {} diff --git a/launcher/ui/themes/IconTheme.cpp b/launcher/ui/themes/IconTheme.cpp index 4bd889854..6415c5148 100644 --- a/launcher/ui/themes/IconTheme.cpp +++ b/launcher/ui/themes/IconTheme.cpp @@ -21,8 +21,6 @@ #include #include -IconTheme::IconTheme(const QString& id, const QString& path) : m_id(id), m_path(path) {} - bool IconTheme::load() { const QString path = m_path + "/index.theme"; @@ -36,18 +34,3 @@ bool IconTheme::load() settings.endGroup(); return !m_name.isNull(); } - -QString IconTheme::id() -{ - return m_id; -} - -QString IconTheme::path() -{ - return m_path; -} - -QString IconTheme::name() -{ - return m_name; -} diff --git a/launcher/ui/themes/IconTheme.h b/launcher/ui/themes/IconTheme.h index 4e466c6ae..f49e39289 100644 --- a/launcher/ui/themes/IconTheme.h +++ b/launcher/ui/themes/IconTheme.h @@ -22,13 +22,13 @@ class IconTheme { public: - IconTheme(const QString& id, const QString& path); + IconTheme(const QString& id, const QString& path) : m_id(id), m_path(path) {} IconTheme() = default; bool load(); - QString id(); - QString path(); - QString name(); + QString id() const { return m_id; } + QString path() const { return m_path; } + QString name() const { return m_name; } private: QString m_id; diff --git a/launcher/ui/widgets/ThemeCustomizationWidget.cpp b/launcher/ui/widgets/ThemeCustomizationWidget.cpp index 097678b8d..b9412a10a 100644 --- a/launcher/ui/widgets/ThemeCustomizationWidget.cpp +++ b/launcher/ui/widgets/ThemeCustomizationWidget.cpp @@ -49,40 +49,6 @@ ThemeCustomizationWidget::~ThemeCustomizationWidget() delete ui; } -/// -/// The layout was not quite right, so currently this just disables the UI elements, which should be hidden instead -/// TODO FIXME -/// -/// Original Method One: -/// ui->iconsComboBox->setVisible(features& ThemeFields::ICONS); -/// ui->iconsLabel->setVisible(features& ThemeFields::ICONS); -/// ui->widgetStyleComboBox->setVisible(features& ThemeFields::WIDGETS); -/// ui->widgetThemeLabel->setVisible(features& ThemeFields::WIDGETS); -/// ui->backgroundCatComboBox->setVisible(features& ThemeFields::CAT); -/// ui->backgroundCatLabel->setVisible(features& ThemeFields::CAT); -/// -/// original Method Two: -/// if (!(features & ThemeFields::ICONS)) { -/// ui->formLayout->setRowVisible(0, false); -/// } -/// if (!(features & ThemeFields::WIDGETS)) { -/// ui->formLayout->setRowVisible(1, false); -/// } -/// if (!(features & ThemeFields::CAT)) { -/// ui->formLayout->setRowVisible(2, false); -/// } -/// -/// -void ThemeCustomizationWidget::showFeatures(ThemeFields features) -{ - ui->iconsComboBox->setEnabled(features & ThemeFields::ICONS); - ui->iconsLabel->setEnabled(features & ThemeFields::ICONS); - ui->widgetStyleComboBox->setEnabled(features & ThemeFields::WIDGETS); - ui->widgetStyleLabel->setEnabled(features & ThemeFields::WIDGETS); - ui->backgroundCatComboBox->setEnabled(features & ThemeFields::CAT); - ui->backgroundCatLabel->setEnabled(features & ThemeFields::CAT); -} - void ThemeCustomizationWidget::applyIconTheme(int index) { auto settings = APPLICATION->settings(); diff --git a/launcher/ui/widgets/ThemeCustomizationWidget.h b/launcher/ui/widgets/ThemeCustomizationWidget.h index 6977b8495..d5b160f3f 100644 --- a/launcher/ui/widgets/ThemeCustomizationWidget.h +++ b/launcher/ui/widgets/ThemeCustomizationWidget.h @@ -33,8 +33,6 @@ class ThemeCustomizationWidget : public QWidget { explicit ThemeCustomizationWidget(QWidget* parent = nullptr); ~ThemeCustomizationWidget() override; - void showFeatures(ThemeFields features); - void applySettings(); void loadSettings(); diff --git a/launcher/ui/widgets/ThemeCustomizationWidget.ui b/launcher/ui/widgets/ThemeCustomizationWidget.ui index 4ac5706dd..3e2808a48 100644 --- a/launcher/ui/widgets/ThemeCustomizationWidget.ui +++ b/launcher/ui/widgets/ThemeCustomizationWidget.ui @@ -7,68 +7,13 @@ 0 0 400 - 191 + 168 Form - - 0 - - - 0 - - - 0 - - - 0 - - - - - View icon themes folder. - - - Open Folder - - - - - - - The cat appears in the background and is not shown by default. It is only made visible when pressing the Cat button in the Toolbar. - - - C&at - - - backgroundCatComboBox - - - - - - - View cat packs folder. - - - Open Folder - - - - - - - &Icons - - - iconsComboBox - - - @@ -102,8 +47,8 @@ - - + + 0 @@ -113,25 +58,19 @@ Qt::StrongFocus - - The cat appears in the background and is not shown by default. It is only made visible when pressing the Cat button in the Toolbar. - - - - - - 0 - 0 - + + + + View icon themes folder. - - Qt::StrongFocus + + Open Folder - + @@ -146,13 +85,6 @@ - - - - Refresh All - - - @@ -168,6 +100,62 @@ + + + + The cat appears in the background and is not shown by default. It is only made visible when pressing the Cat button in the Toolbar. + + + C&at + + + backgroundCatComboBox + + + + + + + &Icons + + + iconsComboBox + + + + + + + View cat packs folder. + + + Open Folder + + + + + + + + 0 + 0 + + + + Qt::StrongFocus + + + The cat appears in the background and is not shown by default. It is only made visible when pressing the Cat button in the Toolbar. + + + + + + + Refresh All + + + @@ -177,7 +165,6 @@ widgetStyleFolder backgroundCatComboBox catPackFolder - refreshButton From dd3a4023c92116b31299614c5a8dbd0a4ee94fba Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Sat, 22 Mar 2025 16:03:23 +0000 Subject: [PATCH 037/695] Move cat to right and use QPushButtons for browse Signed-off-by: TheKodeToad --- launcher/ui/pages/global/AppearancePage.ui | 439 ++++++++++----------- launcher/ui/pages/global/LauncherPage.ui | 106 ++--- 2 files changed, 271 insertions(+), 274 deletions(-) diff --git a/launcher/ui/pages/global/AppearancePage.ui b/launcher/ui/pages/global/AppearancePage.ui index ad2aba7e8..455bf4c1f 100644 --- a/launcher/ui/pages/global/AppearancePage.ui +++ b/launcher/ui/pages/global/AppearancePage.ui @@ -19,9 +19,9 @@ Form - + - + @@ -135,13 +135,15 @@ - - - - Qt::Horizontal - - - + + + + + + + + + @@ -271,7 +273,213 @@ - + + + + + Qt::NoFocus + + + + + + + .. + + + true + + + + + + + Qt::NoFocus + + + + + + + .. + + + true + + + + + + + Qt::NoFocus + + + + + + + .. + + + true + + + + + + + Qt::NoFocus + + + + + + + .. + + + true + + + + + + + Qt::NoFocus + + + + + + + .. + + + true + + + + + + + Qt::NoFocus + + + + + + + .. + + + true + + + + + + + Qt::NoFocus + + + + + + + .. + + + true + + + + + + + Qt::NoFocus + + + + + + + .. + + + true + + + + + + + Qt::NoFocus + + + + + + + .. + + + true + + + + + + + Qt::NoFocus + + + + + + + .. + + + true + + + + + + + Qt::Horizontal + + + + 0 + 0 + + + + + + + + + + + + + 0 + 0 + + + + Qt::ScrollBarAsNeeded + + + false + + + Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + @@ -297,216 +505,6 @@ - - - - - - - - Qt::NoFocus - - - - - - - .. - - - true - - - - - - - Qt::NoFocus - - - - - - - .. - - - true - - - - - - - Qt::NoFocus - - - - - - - .. - - - true - - - - - - - Qt::NoFocus - - - - - - - .. - - - true - - - - - - - Qt::NoFocus - - - - - - - .. - - - true - - - - - - - Qt::NoFocus - - - - - - - .. - - - true - - - - - - - Qt::NoFocus - - - - - - - .. - - - true - - - - - - - Qt::NoFocus - - - - - - - .. - - - true - - - - - - - Qt::NoFocus - - - - - - - .. - - - true - - - - - - - Qt::NoFocus - - - - - - - .. - - - true - - - - - - - Qt::Horizontal - - - - 0 - 0 - - - - - - - - - - - 0 - 0 - - - - Qt::ScrollBarAsNeeded - - - false - - - Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - @@ -524,7 +522,6 @@ consoleFont fontSizeBox catOpacitySlider - consolePreview diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index da2f6d5f2..24678549f 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -167,22 +167,18 @@ Folders - - + + - &Downloads + &Mods - downloadsDirTextBox + modsDirTextBox - - - - Browse - - + + @@ -194,46 +190,26 @@ - - - - Browse - - - - - + + - &Mods + &Downloads - modsDirTextBox + downloadsDirTextBox - - + + - Browse + &Skins - - - - - - - - - Browse + + skinsDirTextBox - - - - - - @@ -244,6 +220,15 @@ + + + + + + + + + @@ -254,38 +239,53 @@ - - + + - - + + + + + Browse - - + + + + Browse + + - - + + - &Skins + Browse - - skinsDirTextBox + + + + + + Browse - + Browse - - + + + + Browse + + From 3a508bdc7821500f0d4f267286bd76d505380f2e Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Sat, 22 Mar 2025 16:48:29 +0000 Subject: [PATCH 038/695] Change slider behaviour to jumping directly when clicking Signed-off-by: TheKodeToad --- launcher/ui/pages/global/AppearancePage.ui | 3 --- launcher/ui/themes/HintOverrideProxyStyle.cpp | 6 ++++++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/launcher/ui/pages/global/AppearancePage.ui b/launcher/ui/pages/global/AppearancePage.ui index 455bf4c1f..5b159ff15 100644 --- a/launcher/ui/pages/global/AppearancePage.ui +++ b/launcher/ui/pages/global/AppearancePage.ui @@ -238,9 +238,6 @@ Qt::Horizontal - - 5 - diff --git a/launcher/ui/themes/HintOverrideProxyStyle.cpp b/launcher/ui/themes/HintOverrideProxyStyle.cpp index 80e821349..2567a35f4 100644 --- a/launcher/ui/themes/HintOverrideProxyStyle.cpp +++ b/launcher/ui/themes/HintOverrideProxyStyle.cpp @@ -26,5 +26,11 @@ int HintOverrideProxyStyle::styleHint(QStyle::StyleHint hint, if (hint == QStyle::SH_ItemView_ActivateItemOnSingleClick) return 0; + if (hint == QStyle::SH_Slider_AbsoluteSetButtons) + return Qt::LeftButton | Qt::MiddleButton; + + if (hint == QStyle::SH_Slider_PageSetButtons) + return Qt::RightButton; + return QProxyStyle::styleHint(hint, option, widget, returnData); } From a1baa5ff479bb227589509718c379462dfb74bfb Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Sat, 22 Mar 2025 17:00:02 +0000 Subject: [PATCH 039/695] Use OK and Cancel instead of Close in Settings dialog Signed-off-by: TheKodeToad --- launcher/ui/pagedialog/PageDialog.cpp | 22 +++++++++++----------- launcher/ui/pagedialog/PageDialog.h | 5 ++--- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/launcher/ui/pagedialog/PageDialog.cpp b/launcher/ui/pagedialog/PageDialog.cpp index d211cb4d3..275908efa 100644 --- a/launcher/ui/pagedialog/PageDialog.cpp +++ b/launcher/ui/pagedialog/PageDialog.cpp @@ -29,34 +29,34 @@ PageDialog::PageDialog(BasePageProvider* pageProvider, QString defaultId, QWidget* parent) : QDialog(parent) { setWindowTitle(pageProvider->dialogTitle()); - m_container = new PageContainer(pageProvider, defaultId, this); + m_container = new PageContainer(pageProvider, std::move(defaultId), this); - QVBoxLayout* mainLayout = new QVBoxLayout; + QVBoxLayout* mainLayout = new QVBoxLayout(this); mainLayout->addWidget(m_container); mainLayout->setSpacing(0); mainLayout->setContentsMargins(0, 0, 0, 0); setLayout(mainLayout); - QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Help | QDialogButtonBox::Close); - buttons->button(QDialogButtonBox::Close)->setDefault(true); - buttons->button(QDialogButtonBox::Close)->setText(tr("Close")); + QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Help | QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + buttons->button(QDialogButtonBox::Ok)->setDefault(true); buttons->button(QDialogButtonBox::Help)->setText(tr("Help")); buttons->setContentsMargins(6, 0, 6, 0); m_container->addButtons(buttons); - connect(buttons->button(QDialogButtonBox::Close), SIGNAL(clicked()), this, SLOT(close())); - connect(buttons->button(QDialogButtonBox::Help), SIGNAL(clicked()), m_container, SLOT(help())); + connect(buttons->button(QDialogButtonBox::Ok), &QPushButton::clicked, this, &PageDialog::applyAndClose); + connect(buttons->button(QDialogButtonBox::Cancel), &QPushButton::clicked, this, &PageDialog::reject); + connect(buttons->button(QDialogButtonBox::Help), &QPushButton::clicked, m_container, &PageContainer::help); restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("PagedGeometry").toByteArray())); } - -void PageDialog::closeEvent(QCloseEvent* event) +void PageDialog::applyAndClose() { - qDebug() << "Paged dialog close requested"; + qDebug() << "Paged dialog apply and close requested"; if (m_container->prepareToClose()) { qDebug() << "Paged dialog close approved"; APPLICATION->settings()->set("PagedGeometry", saveGeometry().toBase64()); qDebug() << "Paged dialog geometry saved"; - QDialog::closeEvent(event); + close(); } + } diff --git a/launcher/ui/pagedialog/PageDialog.h b/launcher/ui/pagedialog/PageDialog.h index aa50bc5e1..337cfd3a3 100644 --- a/launcher/ui/pagedialog/PageDialog.h +++ b/launcher/ui/pagedialog/PageDialog.h @@ -25,9 +25,8 @@ class PageDialog : public QDialog { explicit PageDialog(BasePageProvider* pageProvider, QString defaultId = QString(), QWidget* parent = 0); virtual ~PageDialog() {} - private slots: - virtual void closeEvent(QCloseEvent* event); - private: + void applyAndClose(); + PageContainer* m_container; }; From 411161fe495c4dad2155acc8fdef86fe0ecc5a18 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Sat, 22 Mar 2025 22:31:57 +0000 Subject: [PATCH 040/695] Use same UI for appearance page and wizard Signed-off-by: TheKodeToad --- launcher/CMakeLists.txt | 10 +- launcher/ui/pages/global/AppearancePage.h | 32 +- launcher/ui/setupwizard/ThemeWizardPage.cpp | 31 -- launcher/ui/setupwizard/ThemeWizardPage.h | 27 +- launcher/ui/setupwizard/ThemeWizardPage.ui | 371 --------------- .../AppearanceWidget.cpp} | 153 ++++--- ...stomizationWidget.h => AppearanceWidget.h} | 41 +- .../AppearanceWidget.ui} | 426 +++++++++--------- .../ui/widgets/ThemeCustomizationWidget.cpp | 163 ------- 9 files changed, 363 insertions(+), 891 deletions(-) delete mode 100644 launcher/ui/setupwizard/ThemeWizardPage.ui rename launcher/ui/{pages/global/AppearancePage.cpp => widgets/AppearanceWidget.cpp} (59%) rename launcher/ui/widgets/{ThemeCustomizationWidget.h => AppearanceWidget.h} (59%) rename launcher/ui/{pages/global/AppearancePage.ui => widgets/AppearanceWidget.ui} (56%) delete mode 100644 launcher/ui/widgets/ThemeCustomizationWidget.cpp diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 0c07ca618..319f000cc 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -843,7 +843,6 @@ SET(LAUNCHER_SOURCES ui/setupwizard/LanguageWizardPage.h ui/setupwizard/PasteWizardPage.cpp ui/setupwizard/PasteWizardPage.h - ui/setupwizard/ThemeWizardPage.cpp ui/setupwizard/ThemeWizardPage.h ui/setupwizard/AutoJavaWizardPage.cpp ui/setupwizard/AutoJavaWizardPage.h @@ -937,7 +936,6 @@ SET(LAUNCHER_SOURCES ui/pages/global/MinecraftPage.h ui/pages/global/LauncherPage.cpp ui/pages/global/LauncherPage.h - ui/pages/global/AppearancePage.cpp ui/pages/global/AppearancePage.h ui/pages/global/ProxyPage.cpp ui/pages/global/ProxyPage.h @@ -1132,8 +1130,8 @@ SET(LAUNCHER_SOURCES ui/widgets/ProgressWidget.cpp ui/widgets/WideBar.h ui/widgets/WideBar.cpp - ui/widgets/ThemeCustomizationWidget.h - ui/widgets/ThemeCustomizationWidget.cpp + ui/widgets/AppearanceWidget.h + ui/widgets/AppearanceWidget.cpp ui/widgets/MinecraftSettingsWidget.h ui/widgets/MinecraftSettingsWidget.cpp ui/widgets/JavaSettingsWidget.h @@ -1175,11 +1173,9 @@ qt_wrap_ui(LAUNCHER_UI ui/setupwizard/PasteWizardPage.ui ui/setupwizard/AutoJavaWizardPage.ui ui/setupwizard/LoginWizardPage.ui - ui/setupwizard/ThemeWizardPage.ui ui/pages/global/AccountListPage.ui ui/pages/global/JavaPage.ui ui/pages/global/LauncherPage.ui - ui/pages/global/AppearancePage.ui ui/pages/global/APIPage.ui ui/pages/global/ProxyPage.ui ui/pages/global/ExternalToolsPage.ui @@ -1210,7 +1206,7 @@ qt_wrap_ui(LAUNCHER_UI ui/widgets/InfoFrame.ui ui/widgets/ModFilterWidget.ui ui/widgets/SubTaskProgressBar.ui - ui/widgets/ThemeCustomizationWidget.ui + ui/widgets/AppearanceWidget.ui ui/widgets/MinecraftSettingsWidget.ui ui/widgets/JavaSettingsWidget.ui ui/dialogs/CopyInstanceDialog.ui diff --git a/launcher/ui/pages/global/AppearancePage.h b/launcher/ui/pages/global/AppearancePage.h index f964d5f7c..bf58ebb53 100644 --- a/launcher/ui/pages/global/AppearancePage.h +++ b/launcher/ui/pages/global/AppearancePage.h @@ -40,43 +40,29 @@ #include #include +#include #include "java/JavaChecker.h" #include "ui/pages/BasePage.h" class QTextCharFormat; class SettingsObject; -namespace Ui { -class AppearancePage; -} - -class AppearancePage : public QWidget, public BasePage { +class AppearancePage : public AppearanceWidget, public BasePage { Q_OBJECT public: - explicit AppearancePage(QWidget* parent = 0); - ~AppearancePage(); + explicit AppearancePage(QWidget *parent = nullptr) : AppearanceWidget(false, parent) {} QString displayName() const override { return tr("Appearance"); } QIcon icon() const override { return APPLICATION->getThemedIcon("appearance"); } QString id() const override { return "appearance-settings"; } QString helpPage() const override { return "Launcher-settings"; } - bool apply() override; - void retranslate() override; - - private: - void applySettings(); - void loadSettings(); - - void applyIconTheme(int index); - void applyWidgetTheme(int index); - void applyCatTheme(int index); - void loadThemeSettings(); - void updateConsolePreview(); - void updateCatPreview(); + bool apply() override + { + applySettings(); + return true; + } - private: - Ui::AppearancePage* m_ui; - QTextCharFormat* defaultFormat; + void retranslate() override { retranslateUi(); } }; diff --git a/launcher/ui/setupwizard/ThemeWizardPage.cpp b/launcher/ui/setupwizard/ThemeWizardPage.cpp index fe11ed9ae..c97037f9f 100644 --- a/launcher/ui/setupwizard/ThemeWizardPage.cpp +++ b/launcher/ui/setupwizard/ThemeWizardPage.cpp @@ -34,37 +34,6 @@ ThemeWizardPage::ThemeWizardPage(QWidget* parent) : BaseWizardPage(parent), ui(n updateIcons(); updateCat(); } - -ThemeWizardPage::~ThemeWizardPage() -{ - delete ui; -} - -void ThemeWizardPage::updateIcons() -{ - qDebug() << "Setting Icons"; - ui->previewIconButton0->setIcon(APPLICATION->getThemedIcon("new")); - ui->previewIconButton1->setIcon(APPLICATION->getThemedIcon("centralmods")); - ui->previewIconButton2->setIcon(APPLICATION->getThemedIcon("viewfolder")); - ui->previewIconButton3->setIcon(APPLICATION->getThemedIcon("launch")); - ui->previewIconButton4->setIcon(APPLICATION->getThemedIcon("copy")); - ui->previewIconButton5->setIcon(APPLICATION->getThemedIcon("export")); - ui->previewIconButton6->setIcon(APPLICATION->getThemedIcon("delete")); - ui->previewIconButton7->setIcon(APPLICATION->getThemedIcon("about")); - ui->previewIconButton8->setIcon(APPLICATION->getThemedIcon("settings")); - ui->previewIconButton9->setIcon(APPLICATION->getThemedIcon("cat")); - update(); - repaint(); - parentWidget()->update(); -} - -void ThemeWizardPage::updateCat() -{ - qDebug() << "Setting Cat"; - ui->catImagePreviewButton->setIcon(QIcon(QString(R"(%1)").arg(APPLICATION->themeManager()->getCatPack()))); -} - -void ThemeWizardPage::retranslate() { ui->retranslateUi(this); } diff --git a/launcher/ui/setupwizard/ThemeWizardPage.h b/launcher/ui/setupwizard/ThemeWizardPage.h index f3d40b6d8..8ea438398 100644 --- a/launcher/ui/setupwizard/ThemeWizardPage.h +++ b/launcher/ui/setupwizard/ThemeWizardPage.h @@ -17,27 +17,30 @@ */ #pragma once +#include +#include #include #include "BaseWizardPage.h" -namespace Ui { -class ThemeWizardPage; -} - class ThemeWizardPage : public BaseWizardPage { Q_OBJECT public: - explicit ThemeWizardPage(QWidget* parent = nullptr); - ~ThemeWizardPage(); + ThemeWizardPage(QWidget* parent = nullptr) : BaseWizardPage(parent) + { + auto layout = new QVBoxLayout(this); + layout->addWidget(&widget); + layout->addSpacerItem(new QSpacerItem(0, 0, QSizePolicy::Minimum, QSizePolicy::Expanding)); + layout->setContentsMargins(0, 0, 0, 0); + setLayout(layout); - bool validatePage() override { return true; }; - void retranslate() override; + setTitle(tr("Appearance")); + setSubTitle(tr("Select theme and icons to use")); + } - private slots: - void updateIcons(); - void updateCat(); + bool validatePage() override { return true; }; + void retranslate() override { widget.retranslateUi(); } private: - Ui::ThemeWizardPage* ui; + AppearanceWidget widget{true}; }; diff --git a/launcher/ui/setupwizard/ThemeWizardPage.ui b/launcher/ui/setupwizard/ThemeWizardPage.ui deleted file mode 100644 index 01394ea40..000000000 --- a/launcher/ui/setupwizard/ThemeWizardPage.ui +++ /dev/null @@ -1,371 +0,0 @@ - - - ThemeWizardPage - - - - 0 - 0 - 510 - 552 - - - - WizardPage - - - - - - Select the Theme you wish to use - - - - - - - - 0 - 100 - - - - - - - - Hint: The cat appears in the background and is not shown by default. It is only made visible when pressing the Cat button in the Toolbar. - - - true - - - - - - - Qt::Horizontal - - - - - - - Preview: - - - - - - - - - - 0 - 0 - - - - - 30 - 30 - - - - - .. - - - false - - - true - - - - - - - - 0 - 0 - - - - - 30 - 30 - - - - - .. - - - false - - - true - - - - - - - - 0 - 0 - - - - - 30 - 30 - - - - - .. - - - false - - - true - - - - - - - - 0 - 0 - - - - - 30 - 30 - - - - - .. - - - false - - - true - - - - - - - - 0 - 0 - - - - - 30 - 30 - - - - - .. - - - false - - - true - - - - - - - - 0 - 0 - - - - - 30 - 30 - - - - - .. - - - false - - - true - - - - - - - - 0 - 0 - - - - - 30 - 30 - - - - - .. - - - false - - - true - - - - - - - - 0 - 0 - - - - - 30 - 30 - - - - - .. - - - false - - - true - - - - - - - - 0 - 0 - - - - - 30 - 30 - - - - - .. - - - false - - - true - - - - - - - - 0 - 0 - - - - - 30 - 30 - - - - - .. - - - false - - - true - - - - - - - - - - 0 - 256 - - - - The cat appears in the background and does not serve a purpose, it is purely visual. - - - - - - - 256 - 256 - - - - true - - - - - - - Qt::Vertical - - - - 20 - 193 - - - - - - - - - ThemeCustomizationWidget - QWidget -
ui/widgets/ThemeCustomizationWidget.h
-
-
- - -
diff --git a/launcher/ui/pages/global/AppearancePage.cpp b/launcher/ui/widgets/AppearanceWidget.cpp similarity index 59% rename from launcher/ui/pages/global/AppearancePage.cpp rename to launcher/ui/widgets/AppearanceWidget.cpp index e9e4ad2b9..f9475cbbd 100644 --- a/launcher/ui/pages/global/AppearancePage.cpp +++ b/launcher/ui/widgets/AppearanceWidget.cpp @@ -1,5 +1,41 @@ -#include "AppearancePage.h" -#include "ui_AppearancePage.h" +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2025 TheKodeToad + * Copyright (C) 2022 Tayou + * + * 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 "AppearanceWidget.h" +#include "ui_AppearanceWidget.h" #include #include @@ -7,27 +43,38 @@ #include "ui/themes/ITheme.h" #include "ui/themes/ThemeManager.h" -AppearancePage::AppearancePage(QWidget* parent) : QWidget(parent), m_ui(new Ui::AppearancePage) +AppearanceWidget::AppearanceWidget(bool themesOnly, QWidget* parent) + : QWidget(parent), m_ui(new Ui::AppearanceWidget), m_themesOnly(themesOnly) { m_ui->setupUi(this); m_ui->catPreview->setGraphicsEffect(new QGraphicsOpacityEffect(this)); - defaultFormat = new QTextCharFormat(m_ui->consolePreview->currentCharFormat()); - - loadSettings(); - loadThemeSettings(); - - updateConsolePreview(); - updateCatPreview(); + m_defaultFormat = new QTextCharFormat(m_ui->consolePreview->currentCharFormat()); + + if (themesOnly) { + m_ui->catPackLabel->hide(); + m_ui->catPackComboBox->hide(); + m_ui->catPackFolder->hide(); + m_ui->settingsBox->hide(); + m_ui->consolePreview->hide(); + m_ui->catPreview->hide(); + loadThemeSettings(); + } else { + loadSettings(); + loadThemeSettings(); + + updateConsolePreview(); + updateCatPreview(); + } - connect(m_ui->fontSizeBox, QOverload::of(&QSpinBox::valueChanged), this, &AppearancePage::updateConsolePreview); - connect(m_ui->consoleFont, &QFontComboBox::currentFontChanged, this, &AppearancePage::updateConsolePreview); + connect(m_ui->fontSizeBox, QOverload::of(&QSpinBox::valueChanged), this, &AppearanceWidget::updateConsolePreview); + connect(m_ui->consoleFont, &QFontComboBox::currentFontChanged, this, &AppearanceWidget::updateConsolePreview); - connect(m_ui->iconsComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &AppearancePage::applyIconTheme); - connect(m_ui->widgetStyleComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &AppearancePage::applyWidgetTheme); - connect(m_ui->catPackComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &AppearancePage::applyCatTheme); - connect(m_ui->catOpacitySlider, &QAbstractSlider::valueChanged, this, &AppearancePage::updateCatPreview); + connect(m_ui->iconsComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &AppearanceWidget::applyIconTheme); + connect(m_ui->widgetStyleComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &AppearanceWidget::applyWidgetTheme); + connect(m_ui->catPackComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &AppearanceWidget::applyCatTheme); + connect(m_ui->catOpacitySlider, &QAbstractSlider::valueChanged, this, &AppearanceWidget::updateCatPreview); connect(m_ui->iconsFolder, &QPushButton::clicked, this, [] { DesktopServices::openPath(APPLICATION->themeManager()->getIconThemesFolder().path()); }); @@ -35,26 +82,15 @@ AppearancePage::AppearancePage(QWidget* parent) : QWidget(parent), m_ui(new Ui:: [] { DesktopServices::openPath(APPLICATION->themeManager()->getApplicationThemesFolder().path()); }); connect(m_ui->catPackFolder, &QPushButton::clicked, this, [] { DesktopServices::openPath(APPLICATION->themeManager()->getCatPacksFolder().path()); }); - connect(m_ui->reloadThemesButton, &QPushButton::pressed, this, &AppearancePage::loadThemeSettings); + connect(m_ui->reloadThemesButton, &QPushButton::pressed, this, &AppearanceWidget::loadThemeSettings); } -AppearancePage::~AppearancePage() +AppearanceWidget::~AppearanceWidget() { delete m_ui; } -bool AppearancePage::apply() -{ - applySettings(); - return true; -} - -void AppearancePage::retranslate() -{ - m_ui->retranslateUi(this); -} - -void AppearancePage::applySettings() +void AppearanceWidget::applySettings() { SettingsObjectPtr settings = APPLICATION->settings(); QString consoleFontFamily = m_ui->consoleFont->currentFont().family(); @@ -63,7 +99,7 @@ void AppearancePage::applySettings() settings->set("CatOpacity", m_ui->catOpacitySlider->value()); } -void AppearancePage::loadSettings() +void AppearanceWidget::loadSettings() { QString fontFamily = APPLICATION->settings()->get("ConsoleFont").toString(); QFont consoleFont(fontFamily); @@ -79,7 +115,12 @@ void AppearancePage::loadSettings() m_ui->catOpacitySlider->setValue(APPLICATION->settings()->get("CatOpacity").toInt()); } -void AppearancePage::applyIconTheme(int index) +void AppearanceWidget::retranslateUi() +{ + m_ui->retranslateUi(this); +} + +void AppearanceWidget::applyIconTheme(int index) { auto settings = APPLICATION->settings(); auto originalIconTheme = settings->get("IconTheme").toString(); @@ -90,7 +131,7 @@ void AppearancePage::applyIconTheme(int index) } } -void AppearancePage::applyWidgetTheme(int index) +void AppearanceWidget::applyWidgetTheme(int index) { auto settings = APPLICATION->settings(); auto originalAppTheme = settings->get("ApplicationTheme").toString(); @@ -103,7 +144,7 @@ void AppearancePage::applyWidgetTheme(int index) updateConsolePreview(); } -void AppearancePage::applyCatTheme(int index) +void AppearanceWidget::applyCatTheme(int index) { auto settings = APPLICATION->settings(); auto originalCat = settings->get("BackgroundCat").toString(); @@ -116,7 +157,7 @@ void AppearancePage::applyCatTheme(int index) updateCatPreview(); } -void AppearancePage::loadThemeSettings() +void AppearanceWidget::loadThemeSettings() { APPLICATION->themeManager()->refresh(); @@ -157,16 +198,18 @@ void AppearancePage::loadThemeSettings() m_ui->widgetStyleComboBox->setCurrentIndex(i); } - const QString currentCat = settings->get("BackgroundCat").toString(); - const auto cats = APPLICATION->themeManager()->getValidCatPacks(); - for (int i = 0; i < cats.count(); ++i) { - const CatPack* cat = cats[i]; + if (!m_themesOnly) { + const QString currentCat = settings->get("BackgroundCat").toString(); + const auto cats = APPLICATION->themeManager()->getValidCatPacks(); + for (int i = 0; i < cats.count(); ++i) { + const CatPack* cat = cats[i]; - QIcon catIcon = QIcon(QString("%1").arg(cat->path())); - m_ui->catPackComboBox->addItem(catIcon, cat->name(), cat->id()); + QIcon catIcon = QIcon(QString("%1").arg(cat->path())); + m_ui->catPackComboBox->addItem(catIcon, cat->name(), cat->id()); - if (currentCat == cat->id()) - m_ui->catPackComboBox->setCurrentIndex(i); + if (currentCat == cat->id()) + m_ui->catPackComboBox->setCurrentIndex(i); + } } m_ui->iconsComboBox->blockSignals(false); @@ -174,17 +217,17 @@ void AppearancePage::loadThemeSettings() m_ui->catPackComboBox->blockSignals(false); } -void AppearancePage::updateConsolePreview() +void AppearanceWidget::updateConsolePreview() { const LogColors& colors = APPLICATION->themeManager()->getLogColors(); int fontSize = m_ui->fontSizeBox->value(); QString fontFamily = m_ui->consoleFont->currentFont().family(); m_ui->consolePreview->clear(); - defaultFormat->setFont(QFont(fontFamily, fontSize)); + m_defaultFormat->setFont(QFont(fontFamily, fontSize)); auto print = [this, colors](const QString& message, MessageLevel::Enum level) { - QTextCharFormat format(*defaultFormat); + QTextCharFormat format(*m_defaultFormat); QColor bg = colors.background.value(level); QColor fg = colors.foreground.value(level); @@ -202,24 +245,24 @@ void AppearancePage::updateConsolePreview() workCursor.insertBlock(); }; - print(QString("%1 version: %2 (%3)\n") - .arg(BuildConfig.LAUNCHER_DISPLAYNAME, BuildConfig.printableVersionString(), BuildConfig.BUILD_PLATFORM), + print(QString("%1 version: %2\n") + .arg(BuildConfig.LAUNCHER_DISPLAYNAME, BuildConfig.printableVersionString()), MessageLevel::Launcher); QDate today = QDate::currentDate(); if (today.month() == 10 && today.day() == 31) - print(tr("[Test/ERROR] OOoooOOOoooo! A spooky error!"), MessageLevel::Error); + print(tr("[ERROR] OOoooOOOoooo! A spooky error!"), MessageLevel::Error); else - print(tr("[Test/ERROR] A spooky error!"), MessageLevel::Error); + print(tr("[ERROR] A spooky error!"), MessageLevel::Error); - print(tr("[Test/INFO] A harmless message..."), MessageLevel::Info); - print(tr("[Test/WARN] A not so spooky warning."), MessageLevel::Warning); - print(tr("[Test/DEBUG] A secret debugging message..."), MessageLevel::Debug); - print(tr("[Test/FATAL] A terrifying fatal error!"), MessageLevel::Fatal); + print(tr("[INFO] A harmless message..."), MessageLevel::Info); + print(tr("[WARN] A not so spooky warning."), MessageLevel::Warning); + print(tr("[DEBUG] A secret debugging message..."), MessageLevel::Debug); + print(tr("[FATAL] A terrifying fatal error!"), MessageLevel::Fatal); } -void AppearancePage::updateCatPreview() +void AppearanceWidget::updateCatPreview() { QIcon catPackIcon(APPLICATION->themeManager()->getCatPack()); m_ui->catPreview->setIcon(catPackIcon); diff --git a/launcher/ui/widgets/ThemeCustomizationWidget.h b/launcher/ui/widgets/AppearanceWidget.h similarity index 59% rename from launcher/ui/widgets/ThemeCustomizationWidget.h rename to launcher/ui/widgets/AppearanceWidget.h index d5b160f3f..3bc663676 100644 --- a/launcher/ui/widgets/ThemeCustomizationWidget.h +++ b/launcher/ui/widgets/AppearanceWidget.h @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher + * Copyright (C) 2025 TheKodeToad * Copyright (C) 2022 Tayou * * This program is free software: you can redistribute it and/or modify @@ -15,40 +16,46 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ + #pragma once -#include -#include "translations/TranslationsModel.h" +#include +#include + +#include +#include +#include "java/JavaChecker.h" +#include "ui/pages/BasePage.h" -enum ThemeFields { NONE = 0b0000, ICONS = 0b0001, WIDGETS = 0b0010, CAT = 0b0100 }; +class QTextCharFormat; +class SettingsObject; namespace Ui { -class ThemeCustomizationWidget; +class AppearanceWidget; } -class ThemeCustomizationWidget : public QWidget { +class AppearanceWidget : public QWidget { Q_OBJECT public: - explicit ThemeCustomizationWidget(QWidget* parent = nullptr); - ~ThemeCustomizationWidget() override; + explicit AppearanceWidget(bool simple, QWidget* parent = 0); + virtual ~AppearanceWidget(); + public: void applySettings(); - void loadSettings(); - void retranslate(); + void retranslateUi(); - private slots: + private: void applyIconTheme(int index); void applyWidgetTheme(int index); void applyCatTheme(int index); - void refresh(); + void loadThemeSettings(); - signals: - int currentIconThemeChanged(int index); - int currentWidgetThemeChanged(int index); - int currentCatChanged(int index); + void updateConsolePreview(); + void updateCatPreview(); - private: - Ui::ThemeCustomizationWidget* ui; + Ui::AppearanceWidget* m_ui; + QTextCharFormat* m_defaultFormat; + bool m_themesOnly; }; diff --git a/launcher/ui/pages/global/AppearancePage.ui b/launcher/ui/widgets/AppearanceWidget.ui similarity index 56% rename from launcher/ui/pages/global/AppearancePage.ui rename to launcher/ui/widgets/AppearanceWidget.ui index 5b159ff15..61e1167f9 100644 --- a/launcher/ui/pages/global/AppearancePage.ui +++ b/launcher/ui/widgets/AppearanceWidget.ui @@ -1,7 +1,7 @@ - AppearancePage - + AppearanceWidget + 0 @@ -16,10 +16,7 @@ 0 - - Form - - + @@ -269,214 +266,8 @@ Preview - - - - - - Qt::NoFocus - - - - - - - .. - - - true - - - - - - - Qt::NoFocus - - - - - - - .. - - - true - - - - - - - Qt::NoFocus - - - - - - - .. - - - true - - - - - - - Qt::NoFocus - - - - - - - .. - - - true - - - - - - - Qt::NoFocus - - - - - - - .. - - - true - - - - - - - Qt::NoFocus - - - - - - - .. - - - true - - - - - - - Qt::NoFocus - - - - - - - .. - - - true - - - - - - - Qt::NoFocus - - - - - - - .. - - - true - - - - - - - Qt::NoFocus - - - - - - - .. - - - true - - - - - - - Qt::NoFocus - - - - - - - .. - - - true - - - - - - - Qt::Horizontal - - - - 0 - 0 - - - - - - - - - - - 0 - 0 - - - - Qt::ScrollBarAsNeeded - - - false - - - Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - @@ -502,6 +293,216 @@ + + + + + + + + Qt::NoFocus + + + + + + + .. + + + true + + + + + + + Qt::NoFocus + + + + + + + .. + + + true + + + + + + + Qt::NoFocus + + + + + + + .. + + + true + + + + + + + Qt::NoFocus + + + + + + + .. + + + true + + + + + + + Qt::NoFocus + + + + + + + .. + + + true + + + + + + + Qt::NoFocus + + + + + + + .. + + + true + + + + + + + Qt::NoFocus + + + + + + + .. + + + true + + + + + + + Qt::NoFocus + + + + + + + .. + + + true + + + + + + + Qt::NoFocus + + + + + + + .. + + + true + + + + + + + Qt::NoFocus + + + + + + + .. + + + true + + + + + + + Qt::Horizontal + + + + 0 + 0 + + + + + + + + + + + 0 + 0 + + + + Qt::ScrollBarAsNeeded + + + false + + + Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + @@ -516,6 +517,7 @@ iconsFolder catPackComboBox catPackFolder + reloadThemesButton consoleFont fontSizeBox catOpacitySlider diff --git a/launcher/ui/widgets/ThemeCustomizationWidget.cpp b/launcher/ui/widgets/ThemeCustomizationWidget.cpp deleted file mode 100644 index b9412a10a..000000000 --- a/launcher/ui/widgets/ThemeCustomizationWidget.cpp +++ /dev/null @@ -1,163 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (C) 2024 Tayou - * - * 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 "ThemeCustomizationWidget.h" -#include "ui_ThemeCustomizationWidget.h" - -#include "Application.h" -#include "DesktopServices.h" -#include "ui/themes/ITheme.h" -#include "ui/themes/ThemeManager.h" - -ThemeCustomizationWidget::ThemeCustomizationWidget(QWidget* parent) : QWidget(parent), ui(new Ui::ThemeCustomizationWidget) -{ - ui->setupUi(this); - loadSettings(); - ThemeCustomizationWidget::refresh(); - - connect(ui->iconsComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &ThemeCustomizationWidget::applyIconTheme); - connect(ui->widgetStyleComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, - &ThemeCustomizationWidget::applyWidgetTheme); - connect(ui->backgroundCatComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &ThemeCustomizationWidget::applyCatTheme); - - connect(ui->iconsFolder, &QPushButton::clicked, this, - [] { DesktopServices::openPath(APPLICATION->themeManager()->getIconThemesFolder().path()); }); - connect(ui->widgetStyleFolder, &QPushButton::clicked, this, - [] { DesktopServices::openPath(APPLICATION->themeManager()->getApplicationThemesFolder().path()); }); - connect(ui->catPackFolder, &QPushButton::clicked, this, - [] { DesktopServices::openPath(APPLICATION->themeManager()->getCatPacksFolder().path()); }); - - connect(ui->refreshButton, &QPushButton::clicked, this, &ThemeCustomizationWidget::refresh); -} - -ThemeCustomizationWidget::~ThemeCustomizationWidget() -{ - delete ui; -} - -void ThemeCustomizationWidget::applyIconTheme(int index) -{ - auto settings = APPLICATION->settings(); - auto originalIconTheme = settings->get("IconTheme").toString(); - auto newIconTheme = ui->iconsComboBox->itemData(index).toString(); - if (originalIconTheme != newIconTheme) { - settings->set("IconTheme", newIconTheme); - APPLICATION->themeManager()->applyCurrentlySelectedTheme(); - } - - emit currentIconThemeChanged(index); -} - -void ThemeCustomizationWidget::applyWidgetTheme(int index) -{ - auto settings = APPLICATION->settings(); - auto originalAppTheme = settings->get("ApplicationTheme").toString(); - auto newAppTheme = ui->widgetStyleComboBox->itemData(index).toString(); - if (originalAppTheme != newAppTheme) { - settings->set("ApplicationTheme", newAppTheme); - APPLICATION->themeManager()->applyCurrentlySelectedTheme(); - } - - emit currentWidgetThemeChanged(index); -} - -void ThemeCustomizationWidget::applyCatTheme(int index) -{ - auto settings = APPLICATION->settings(); - auto originalCat = settings->get("BackgroundCat").toString(); - auto newCat = ui->backgroundCatComboBox->itemData(index).toString(); - if (originalCat != newCat) { - settings->set("BackgroundCat", newCat); - } - - emit currentCatChanged(index); -} - -void ThemeCustomizationWidget::applySettings() -{ - applyIconTheme(ui->iconsComboBox->currentIndex()); - applyWidgetTheme(ui->widgetStyleComboBox->currentIndex()); - applyCatTheme(ui->backgroundCatComboBox->currentIndex()); -} -void ThemeCustomizationWidget::loadSettings() -{ - auto settings = APPLICATION->settings(); - - { - auto currentIconTheme = settings->get("IconTheme").toString(); - auto iconThemes = APPLICATION->themeManager()->getValidIconThemes(); - int idx = 0; - for (auto iconTheme : iconThemes) { - QIcon iconForComboBox = QIcon(iconTheme->path() + "/scalable/settings"); - ui->iconsComboBox->addItem(iconForComboBox, iconTheme->name(), iconTheme->id()); - if (currentIconTheme == iconTheme->id()) { - ui->iconsComboBox->setCurrentIndex(idx); - } - idx++; - } - } - - { - auto currentTheme = settings->get("ApplicationTheme").toString(); - auto themes = APPLICATION->themeManager()->getValidApplicationThemes(); - int idx = 0; - for (auto& theme : themes) { - ui->widgetStyleComboBox->addItem(theme->name(), theme->id()); - if (theme->tooltip() != "") { - int index = ui->widgetStyleComboBox->count() - 1; - ui->widgetStyleComboBox->setItemData(index, theme->tooltip(), Qt::ToolTipRole); - } - if (currentTheme == theme->id()) { - ui->widgetStyleComboBox->setCurrentIndex(idx); - } - idx++; - } - } - - auto cat = settings->get("BackgroundCat").toString(); - for (auto& catFromList : APPLICATION->themeManager()->getValidCatPacks()) { - QIcon catIcon = QIcon(QString("%1").arg(catFromList->path())); - ui->backgroundCatComboBox->addItem(catIcon, catFromList->name(), catFromList->id()); - if (cat == catFromList->id()) { - ui->backgroundCatComboBox->setCurrentIndex(ui->backgroundCatComboBox->count() - 1); - } - } -} - -void ThemeCustomizationWidget::retranslate() -{ - ui->retranslateUi(this); -} - -void ThemeCustomizationWidget::refresh() -{ - applySettings(); - disconnect(ui->iconsComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &ThemeCustomizationWidget::applyIconTheme); - disconnect(ui->widgetStyleComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, - &ThemeCustomizationWidget::applyWidgetTheme); - disconnect(ui->backgroundCatComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, - &ThemeCustomizationWidget::applyCatTheme); - APPLICATION->themeManager()->refresh(); - ui->iconsComboBox->clear(); - ui->widgetStyleComboBox->clear(); - ui->backgroundCatComboBox->clear(); - loadSettings(); - connect(ui->iconsComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &ThemeCustomizationWidget::applyIconTheme); - connect(ui->widgetStyleComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, - &ThemeCustomizationWidget::applyWidgetTheme); - connect(ui->backgroundCatComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &ThemeCustomizationWidget::applyCatTheme); -}; \ No newline at end of file From 8852b5823ba269529a4105504ee54fdd6ee5329a Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Sun, 23 Mar 2025 13:04:14 +0000 Subject: [PATCH 041/695] Rework tools page Signed-off-by: TheKodeToad --- launcher/tools/JVisualVM.cpp | 4 +- launcher/tools/JVisualVM.h | 2 +- .../ui/pages/global/ExternalToolsPage.cpp | 11 +- launcher/ui/pages/global/ExternalToolsPage.h | 2 +- launcher/ui/pages/global/ExternalToolsPage.ui | 244 ++++++++++++------ launcher/ui/pages/global/LauncherPage.ui | 102 ++++---- 6 files changed, 219 insertions(+), 146 deletions(-) diff --git a/launcher/tools/JVisualVM.cpp b/launcher/tools/JVisualVM.cpp index 4da4e1e54..0cae8e37b 100644 --- a/launcher/tools/JVisualVM.cpp +++ b/launcher/tools/JVisualVM.cpp @@ -24,7 +24,7 @@ JVisualVM::JVisualVM(SettingsObjectPtr settings, InstancePtr instance, QObject* void JVisualVM::profilerStarted() { - emit readyToLaunch(tr("JVisualVM started")); + emit readyToLaunch(tr("VisualVM started")); } void JVisualVM::profilerFinished([[maybe_unused]] int exit, QProcess::ExitStatus status) @@ -82,7 +82,7 @@ bool JVisualVMFactory::check(const QString& path, QString* error) } QFileInfo finfo(path); if (!finfo.isExecutable() || !finfo.fileName().contains("visualvm")) { - *error = QObject::tr("Invalid path to JVisualVM"); + *error = QObject::tr("Invalid path to VisualVM"); return false; } return true; diff --git a/launcher/tools/JVisualVM.h b/launcher/tools/JVisualVM.h index 2828119a1..c152aecdb 100644 --- a/launcher/tools/JVisualVM.h +++ b/launcher/tools/JVisualVM.h @@ -4,7 +4,7 @@ class JVisualVMFactory : public BaseProfilerFactory { public: - QString name() const override { return "JVisualVM"; } + QString name() const override { return "VisualVM"; } void registerSettings(SettingsObjectPtr settings) override; BaseExternalTool* createTool(InstancePtr instance, QObject* parent = 0) override; bool check(QString* error) override; diff --git a/launcher/ui/pages/global/ExternalToolsPage.cpp b/launcher/ui/pages/global/ExternalToolsPage.cpp index 33e9c5388..4110bd5ad 100644 --- a/launcher/ui/pages/global/ExternalToolsPage.cpp +++ b/launcher/ui/pages/global/ExternalToolsPage.cpp @@ -50,7 +50,6 @@ ExternalToolsPage::ExternalToolsPage(QWidget* parent) : QWidget(parent), ui(new Ui::ExternalToolsPage) { ui->setupUi(this); - ui->tabWidget->tabBar()->hide(); ui->jsonEditorTextBox->setClearButtonEnabled(true); @@ -128,13 +127,13 @@ void ExternalToolsPage::on_jvisualvmPathBtn_clicked() QString raw_dir = ui->jvisualvmPathEdit->text(); QString error; do { - raw_dir = QFileDialog::getOpenFileName(this, tr("JVisualVM Executable"), raw_dir); + raw_dir = QFileDialog::getOpenFileName(this, tr("VisualVM Executable"), raw_dir); if (raw_dir.isEmpty()) { break; } QString cooked_dir = FS::NormalizePath(raw_dir); if (!APPLICATION->profilers()["jvisualvm"]->check(cooked_dir, &error)) { - QMessageBox::critical(this, tr("Error"), tr("Error while checking JVisualVM install:\n%1").arg(error)); + QMessageBox::critical(this, tr("Error"), tr("Error while checking VisualVM install:\n%1").arg(error)); continue; } else { ui->jvisualvmPathEdit->setText(cooked_dir); @@ -146,9 +145,9 @@ void ExternalToolsPage::on_jvisualvmCheckBtn_clicked() { QString error; if (!APPLICATION->profilers()["jvisualvm"]->check(ui->jvisualvmPathEdit->text(), &error)) { - QMessageBox::critical(this, tr("Error"), tr("Error while checking JVisualVM install:\n%1").arg(error)); + QMessageBox::critical(this, tr("Error"), tr("Error while checking VisualVM install:\n%1").arg(error)); } else { - QMessageBox::information(this, tr("OK"), tr("JVisualVM setup seems to be OK")); + QMessageBox::information(this, tr("OK"), tr("VisualVM setup seems to be OK")); } } @@ -187,7 +186,7 @@ void ExternalToolsPage::on_mceditCheckBtn_clicked() void ExternalToolsPage::on_jsonEditorBrowseBtn_clicked() { - QString raw_file = QFileDialog::getOpenFileName(this, tr("JSON Editor"), + QString raw_file = QFileDialog::getOpenFileName(this, tr("Text Editor"), ui->jsonEditorTextBox->text().isEmpty() #if defined(Q_OS_LINUX) ? QString("/usr/bin") diff --git a/launcher/ui/pages/global/ExternalToolsPage.h b/launcher/ui/pages/global/ExternalToolsPage.h index 7248f0c99..377488ccf 100644 --- a/launcher/ui/pages/global/ExternalToolsPage.h +++ b/launcher/ui/pages/global/ExternalToolsPage.h @@ -51,7 +51,7 @@ class ExternalToolsPage : public QWidget, public BasePage { explicit ExternalToolsPage(QWidget* parent = 0); ~ExternalToolsPage(); - QString displayName() const override { return tr("External Tools"); } + QString displayName() const override { return tr("Tools"); } QIcon icon() const override { auto icon = APPLICATION->getThemedIcon("externaltools"); diff --git a/launcher/ui/pages/global/ExternalToolsPage.ui b/launcher/ui/pages/global/ExternalToolsPage.ui index 47c77842a..37eb7de3e 100644 --- a/launcher/ui/pages/global/ExternalToolsPage.ui +++ b/launcher/ui/pages/global/ExternalToolsPage.ui @@ -7,45 +7,48 @@ 0 0 673 - 751 + 823 - - 0 - - - 0 - - - 0 - - - 0 - - - - 0 + + + true - - - Tab 1 - - + + + + 0 + 0 + 653 + 803 + + + - + - J&Profiler + &Editors - + - + + + &Text Editor + + + jsonEditorTextBox + + + + + - + - + Browse @@ -54,35 +57,52 @@ - + - Check + Used to edit component JSON files. - + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 0 + 6 + + + + + + - <html><head/><body><p><a href="https://www.ej-technologies.com/products/jprofiler/overview.html">https://www.ej-technologies.com/products/jprofiler/overview.html</a></p></body></html> + &MCEdit + + + mceditPathEdit - - - - - - - J&VisualVM - - - + - + - + + + Check + + + + + Browse @@ -91,16 +111,9 @@ - - - Check - - - - - + - <html><head/><body><p><a href="https://visualvm.github.io/">https://visualvm.github.io/</a></p></body></html> + <html><head/><body><p><a href="https://www.mcedit.net/">MCEdit Website</a> - Used as world editor in the instance Worlds menu.</p></body></html> @@ -108,18 +121,61 @@ - + - &MCEdit + &Profilers - + - + + + Profilers are accessible through the Launch dropdown menu. + + + jsonEditorTextBox + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 0 + 6 + + + + + + + + J&Profiler + + + jprofilerPathEdit + + + + + - + - + + + Check + + + + + Browse @@ -128,45 +184,63 @@ - + - Check + <html><head/><body><p><a href="https://www.ej-technologies.com/products/jprofiler/overview.html">JProfiler Website</a></p></body></html> - - - <html><head/><body><p><a href="https://www.mcedit.net/">https://www.mcedit.net/</a></p></body></html> + + + Qt::Vertical - - - - - - - - - External Editors (leave empty for system default) - - - - + + QSizePolicy::Fixed + + + + 0 + 6 + + + - - + + - &Text Editor: + &VisualVM - jsonEditorTextBox + jvisualvmPathEdit - - + + + + + + + + + Check + + + + + + + Browse + + + + + + + - Browse + <html><head/><body><p><a href="https://visualvm.github.io/">VisualVM Website</a></p></body></html> @@ -180,8 +254,8 @@ - 20 - 216 + 0 + 0 diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index 24678549f..d29ee31be 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -167,29 +167,29 @@ Folders - - + + - &Mods + &Java - modsDirTextBox + javaDirTextBox - - - - - + + - &Icons + I&nstances - iconsDirTextBox + instDirTextBox + + + @@ -200,93 +200,93 @@ - - - - &Skins - - - skinsDirTextBox - - - - - + + - I&nstances + &Icons - instDirTextBox + iconsDirTextBox - - - - - - - - + + - &Java - - - javaDirTextBox + Browse + + + + + + + Browse + + + - - + + - Browse + &Mods + + + modsDirTextBox - - + + Browse - - + + - Browse + &Skins + + + skinsDirTextBox - - + + Browse - - + + Browse - - + + Browse + + + From 8732e17db9fefab9a272eb30699f62fea965ebfb Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Sun, 23 Mar 2025 13:50:08 +0000 Subject: [PATCH 042/695] Improve appearance icons for simple theme Signed-off-by: TheKodeToad --- .../resources/pe_blue/scalable/appearance.svg | 31 ++++++++---------- .../pe_colored/scalable/appearance.svg | 32 ++++++++----------- .../resources/pe_dark/scalable/appearance.svg | 28 +++++++--------- .../pe_light/scalable/appearance.svg | 30 ++++++++--------- 4 files changed, 52 insertions(+), 69 deletions(-) diff --git a/launcher/resources/pe_blue/scalable/appearance.svg b/launcher/resources/pe_blue/scalable/appearance.svg index 1d49d9d6f..9323ec02d 100644 --- a/launcher/resources/pe_blue/scalable/appearance.svg +++ b/launcher/resources/pe_blue/scalable/appearance.svg @@ -21,11 +21,11 @@ inkscape:pageopacity="0.0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" - inkscape:zoom="8" - inkscape:cx="17.375" - inkscape:cy="18.5" + inkscape:zoom="11.313708" + inkscape:cx="4.9497475" + inkscape:cy="37.388271" inkscape:window-width="1920" - inkscape:window-height="1027" + inkscape:window-height="1011" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" @@ -54,17 +54,12 @@ height="4" x="1.4899759" y="7.1611009" /> + id="g2657" + transform="rotate(31.454004,7.3789217,28.015625)"> diff --git a/launcher/resources/pe_colored/scalable/appearance.svg b/launcher/resources/pe_colored/scalable/appearance.svg index ac9cc258a..88c1eaf26 100644 --- a/launcher/resources/pe_colored/scalable/appearance.svg +++ b/launcher/resources/pe_colored/scalable/appearance.svg @@ -21,16 +21,17 @@ inkscape:pageopacity="0.0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" - inkscape:zoom="32" - inkscape:cx="7.59375" - inkscape:cy="16.59375" + inkscape:zoom="16" + inkscape:cx="-7.8125" + inkscape:cy="17.6875" inkscape:window-width="1920" - inkscape:window-height="1027" + inkscape:window-height="1011" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" inkscape:current-layer="g7954" - showgrid="false" /> + style="fill:#c1272d;fill-opacity:1;stroke-width:14.5284" + d="M 8.413071,2.4595861 A 2.5,8.0000002 0 0 0 6.3465633,10.337319 2.5,8.0000002 0 0 0 7.4458626,16.960111 C 7.7356137,16.430277 8.2119852,16.01524 8.9151067,15.96976 9.5063364,15.93152 9.9629895,16.336614 10.250228,16.958042 A 2.5,8.0000002 0 0 0 11.346521,10.337343 2.5,8.0000002 0 0 0 8.8459304,2.3382251 2.5,8.0000002 0 0 0 8.413071,2.4595861 Z" /> diff --git a/launcher/resources/pe_dark/scalable/appearance.svg b/launcher/resources/pe_dark/scalable/appearance.svg index b50372fba..24b7283e4 100644 --- a/launcher/resources/pe_dark/scalable/appearance.svg +++ b/launcher/resources/pe_dark/scalable/appearance.svg @@ -22,10 +22,10 @@ inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" inkscape:zoom="8" - inkscape:cx="17.375" + inkscape:cx="17.5" inkscape:cy="18.5" inkscape:window-width="1920" - inkscape:window-height="1027" + inkscape:window-height="1011" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" @@ -53,17 +53,13 @@ height="4" x="1.4899759" y="7.1611009" /> + id="g2657" + transform="rotate(31.454004,7.3789216,28.015625)" + style="fill:#666666;fill-opacity:1"> diff --git a/launcher/resources/pe_light/scalable/appearance.svg b/launcher/resources/pe_light/scalable/appearance.svg index 4f000f452..61b2f3422 100644 --- a/launcher/resources/pe_light/scalable/appearance.svg +++ b/launcher/resources/pe_light/scalable/appearance.svg @@ -21,11 +21,11 @@ inkscape:pageopacity="0.0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" - inkscape:zoom="8" - inkscape:cx="17.375" - inkscape:cy="18.5" + inkscape:zoom="16" + inkscape:cx="16.875" + inkscape:cy="15.4375" inkscape:window-width="1920" - inkscape:window-height="1027" + inkscape:window-height="1011" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" @@ -54,17 +54,13 @@ height="4" x="1.4899759" y="7.1611009" /> + style="fill:#ffffff;fill-opacity:1;stroke-width:14.5284" + d="M 8.413071,2.4595861 A 2.5,8.0000002 0 0 0 6.3465633,10.337319 2.5,8.0000002 0 0 0 7.4458626,16.960111 C 7.7356137,16.430277 8.2119852,16.01524 8.9151067,15.96976 9.5063364,15.93152 9.9629895,16.336614 10.250228,16.958042 A 2.5,8.0000002 0 0 0 11.346521,10.337343 2.5,8.0000002 0 0 0 8.8459304,2.3382251 2.5,8.0000002 0 0 0 8.413071,2.4595861 Z" /> From adf2ed0849094761a33aa10ff45f616550968067 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Sun, 23 Mar 2025 15:22:17 +0000 Subject: [PATCH 043/695] Only fire apply event when settings are actually applied Signed-off-by: TheKodeToad --- launcher/Application.cpp | 2 +- launcher/Application.h | 2 +- launcher/ui/InstanceWindow.cpp | 2 +- launcher/ui/MainWindow.cpp | 2 +- launcher/ui/pagedialog/PageDialog.cpp | 18 ++++++++++++------ launcher/ui/pagedialog/PageDialog.h | 10 ++++++++-- .../ui/pages/instance/InstanceSettingsPage.h | 2 +- 7 files changed, 25 insertions(+), 13 deletions(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index b1cad5ad4..0f7d85b6b 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -1564,9 +1564,9 @@ void Application::ShowGlobalSettings(class QWidget* parent, QString open_page) { SettingsObject::Lock lock(APPLICATION->settings()); PageDialog dlg(m_globalSettingsProvider.get(), open_page, parent); + connect(&dlg, &PageDialog::applied, this, &Application::globalSettingsApplied); dlg.exec(); } - emit globalSettingsClosed(); } MainWindow* Application::showMainWindow(bool minimized) diff --git a/launcher/Application.h b/launcher/Application.h index 12f41509c..63432f7e5 100644 --- a/launcher/Application.h +++ b/launcher/Application.h @@ -197,7 +197,7 @@ class Application : public QApplication { signals: void updateAllowedChanged(bool status); void globalSettingsAboutToOpen(); - void globalSettingsClosed(); + void globalSettingsApplied(); int currentCatChanged(int index); void oauthReplyRecieved(QVariantMap); diff --git a/launcher/ui/InstanceWindow.cpp b/launcher/ui/InstanceWindow.cpp index bf83a56c9..2f156e125 100644 --- a/launcher/ui/InstanceWindow.cpp +++ b/launcher/ui/InstanceWindow.cpp @@ -111,7 +111,7 @@ InstanceWindow::InstanceWindow(InstancePtr instance, QWidget* parent) : QMainWin m_container->addButtons(horizontalLayout); connect(m_instance.get(), &BaseInstance::profilerChanged, this, &InstanceWindow::updateButtons); - connect(APPLICATION, &Application::globalSettingsClosed, this, &InstanceWindow::updateButtons); + connect(APPLICATION, &Application::globalSettingsApplied, this, &InstanceWindow::updateButtons); } // restore window state diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index a9473ac15..8b41f838c 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -351,7 +351,7 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi connect(APPLICATION->instances().get(), &InstanceList::instanceSelectRequest, this, &MainWindow::instanceSelectRequest); // When the global settings page closes, we want to know about it and update our state - connect(APPLICATION, &Application::globalSettingsClosed, this, &MainWindow::globalSettingsClosed); + connect(APPLICATION, &Application::globalSettingsApplied, this, &MainWindow::globalSettingsClosed); m_statusLeft = new QLabel(tr("No instance selected"), this); m_statusCenter = new QLabel(tr("Total playtime: 0s"), this); diff --git a/launcher/ui/pagedialog/PageDialog.cpp b/launcher/ui/pagedialog/PageDialog.cpp index 275908efa..bfa9ebdb0 100644 --- a/launcher/ui/pagedialog/PageDialog.cpp +++ b/launcher/ui/pagedialog/PageDialog.cpp @@ -43,20 +43,26 @@ PageDialog::PageDialog(BasePageProvider* pageProvider, QString defaultId, QWidge buttons->setContentsMargins(6, 0, 6, 0); m_container->addButtons(buttons); - connect(buttons->button(QDialogButtonBox::Ok), &QPushButton::clicked, this, &PageDialog::applyAndClose); + connect(buttons->button(QDialogButtonBox::Ok), &QPushButton::clicked, this, &PageDialog::accept); connect(buttons->button(QDialogButtonBox::Cancel), &QPushButton::clicked, this, &PageDialog::reject); connect(buttons->button(QDialogButtonBox::Help), &QPushButton::clicked, m_container, &PageContainer::help); + connect(this, &QDialog::accepted, this, &PageDialog::onAccepted); + connect(this, &QDialog::rejected, this, &PageDialog::storeGeometry); + restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("PagedGeometry").toByteArray())); } -void PageDialog::applyAndClose() +void PageDialog::onAccepted() { - qDebug() << "Paged dialog apply and close requested"; + qDebug() << "Paged dialog accepted"; if (m_container->prepareToClose()) { qDebug() << "Paged dialog close approved"; - APPLICATION->settings()->set("PagedGeometry", saveGeometry().toBase64()); - qDebug() << "Paged dialog geometry saved"; - close(); + emit applied(); } +} +void PageDialog::storeGeometry() +{ + APPLICATION->settings()->set("PagedGeometry", saveGeometry().toBase64()); + qDebug() << "Paged dialog geometry saved"; } diff --git a/launcher/ui/pagedialog/PageDialog.h b/launcher/ui/pagedialog/PageDialog.h index 337cfd3a3..d4af862f3 100644 --- a/launcher/ui/pagedialog/PageDialog.h +++ b/launcher/ui/pagedialog/PageDialog.h @@ -25,8 +25,14 @@ class PageDialog : public QDialog { explicit PageDialog(BasePageProvider* pageProvider, QString defaultId = QString(), QWidget* parent = 0); virtual ~PageDialog() {} - private: - void applyAndClose(); + signals: + void applied(); + + private slots: + void onAccepted(); + void storeGeometry(); + + private: PageContainer* m_container; }; diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.h b/launcher/ui/pages/instance/InstanceSettingsPage.h index fa1dce3dc..2549c1084 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.h +++ b/launcher/ui/pages/instance/InstanceSettingsPage.h @@ -48,7 +48,7 @@ class InstanceSettingsPage : public MinecraftSettingsWidget, public BasePage { explicit InstanceSettingsPage(MinecraftInstancePtr instance, QWidget* parent = nullptr) : MinecraftSettingsWidget(std::move(instance), parent) { connect(APPLICATION, &Application::globalSettingsAboutToOpen, this, &InstanceSettingsPage::saveSettings); - connect(APPLICATION, &Application::globalSettingsClosed, this, &InstanceSettingsPage::loadSettings); + connect(APPLICATION, &Application::globalSettingsApplied, this, &InstanceSettingsPage::loadSettings); } ~InstanceSettingsPage() override {} QString displayName() const override { return tr("Settings"); } From 3922136f6d7036144cf493899c4eaecee53c9d45 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Mon, 24 Mar 2025 00:29:07 +0000 Subject: [PATCH 044/695] Rework Minecraft and Java settings Signed-off-by: TheKodeToad --- launcher/Application.cpp | 1 + launcher/JavaCommon.cpp | 2 +- launcher/ui/pages/global/JavaPage.ui | 49 +- launcher/ui/widgets/AppearanceWidget.ui | 5 +- launcher/ui/widgets/EnvironmentVariables.ui | 76 +-- launcher/ui/widgets/JavaSettingsWidget.cpp | 99 ++- launcher/ui/widgets/JavaSettingsWidget.h | 5 +- launcher/ui/widgets/JavaSettingsWidget.ui | 622 ++++++++++++++---- .../ui/widgets/MinecraftSettingsWidget.ui | 196 ++++-- 9 files changed, 760 insertions(+), 295 deletions(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 0f7d85b6b..054d27f8a 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -665,6 +665,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_settings->registerSetting("AutomaticJavaSwitch", defaultEnableAutoJava); m_settings->registerSetting("AutomaticJavaDownload", defaultEnableAutoJava); m_settings->registerSetting("UserAskedAboutAutomaticJavaDownload", false); + m_settings->registerSetting("AdvancedJavaMemoryControl", false); // Legacy settings m_settings->registerSetting("OnlineFixes", false); diff --git a/launcher/JavaCommon.cpp b/launcher/JavaCommon.cpp index 188edb943..fc78f5f26 100644 --- a/launcher/JavaCommon.cpp +++ b/launcher/JavaCommon.cpp @@ -93,7 +93,7 @@ void JavaCommon::javaBinaryWasBad(QWidget* parent, const JavaChecker::Result& re { QString text; text += QObject::tr( - "The specified Java binary didn't work.
You should use the auto-detect feature, " + "The specified Java binary didn't work.
You should press 'Detect', " "or set the path to the Java executable.
"); CustomMessageBox::selectable(parent, QObject::tr("Java test failure"), text, QMessageBox::Warning)->show(); } diff --git a/launcher/ui/pages/global/JavaPage.ui b/launcher/ui/pages/global/JavaPage.ui index a4b2ac203..25c641cb5 100644 --- a/launcher/ui/pages/global/JavaPage.ui +++ b/launcher/ui/pages/global/JavaPage.ui @@ -32,7 +32,7 @@ - 0 + 1 @@ -50,7 +50,7 @@ 0 0 535 - 610 + 606 @@ -73,19 +73,9 @@ Downloaded Java Versions - + - - - - 0 - 0 - - - - - - + @@ -101,14 +91,14 @@ - + - Qt::Vertical + Qt::Horizontal - 20 - 40 + 40 + 20 @@ -122,22 +112,19 @@ + + + + + 0 + 0 + + + + - - - - Qt::Vertical - - - - 20 - 40 - - - -
diff --git a/launcher/ui/widgets/AppearanceWidget.ui b/launcher/ui/widgets/AppearanceWidget.ui index 61e1167f9..d646cb9e0 100644 --- a/launcher/ui/widgets/AppearanceWidget.ui +++ b/launcher/ui/widgets/AppearanceWidget.ui @@ -239,6 +239,9 @@
+ + false + Transparent @@ -247,7 +250,7 @@ - true + false Opaque diff --git a/launcher/ui/widgets/EnvironmentVariables.ui b/launcher/ui/widgets/EnvironmentVariables.ui index ded5b2ded..828626d12 100644 --- a/launcher/ui/widgets/EnvironmentVariables.ui +++ b/launcher/ui/widgets/EnvironmentVariables.ui @@ -35,6 +35,44 @@ true + + + + + + &Add + + + + + + + &Remove + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + &Clear + + + + + @@ -67,44 +105,6 @@ - - - - - - &Add - - - - - - - &Remove - - - - - - - &Clear - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - diff --git a/launcher/ui/widgets/JavaSettingsWidget.cpp b/launcher/ui/widgets/JavaSettingsWidget.cpp index a255168e9..200d81db8 100644 --- a/launcher/ui/widgets/JavaSettingsWidget.cpp +++ b/launcher/ui/widgets/JavaSettingsWidget.cpp @@ -53,6 +53,11 @@ #include "ui_JavaSettingsWidget.h" +static QString formatGiBLabel(int value) +{ + return QObject::tr("%1 GiB").arg(value / 1024.0, 0, 'f', 1); +} + JavaSettingsWidget::JavaSettingsWidget(InstancePtr instance, QWidget* parent) : QWidget(parent), m_instance(std::move(instance)), m_ui(new Ui::JavaSettingsWidget) { @@ -101,11 +106,50 @@ JavaSettingsWidget::JavaSettingsWidget(InstancePtr instance, QWidget* parent) connect(m_ui->javaDetectBtn, &QPushButton::clicked, this, &JavaSettingsWidget::onJavaAutodetect); connect(m_ui->javaBrowseBtn, &QPushButton::clicked, this, &JavaSettingsWidget::onJavaBrowse); - connect(m_ui->maxMemSpinBox, QOverload::of(&QSpinBox::valueChanged), this, &JavaSettingsWidget::updateThresholds); - connect(m_ui->minMemSpinBox, QOverload::of(&QSpinBox::valueChanged), this, &JavaSettingsWidget::updateThresholds); + connect(m_ui->minMemSpinBox, QOverload::of(&QSpinBox::valueChanged), m_ui->minMemSlider, [this](int value) { + m_ui->minMemSlider->blockSignals(true); + m_ui->minMemSlider->setValue(value); + m_ui->minMemSlider->blockSignals(false); + }); + connect(m_ui->maxMemSpinBox, QOverload::of(&QSpinBox::valueChanged), m_ui->maxMemSlider, [this](int value) { + m_ui->maxMemSlider->blockSignals(true); + m_ui->maxMemSlider->setValue(value); + m_ui->maxMemSlider->blockSignals(false); + }); + + connect(m_ui->minMemSlider, &QAbstractSlider::valueChanged, m_ui->minMemSpinBox, QOverload::of(&QSpinBox::setValue)); + connect(m_ui->maxMemSlider, &QAbstractSlider::valueChanged, m_ui->maxMemSpinBox, QOverload::of(&QSpinBox::setValue)); + + connect(m_ui->minMemSpinBox, &QAbstractSpinBox::editingFinished, this, &JavaSettingsWidget::finishAdjustingMinMemory); + connect(m_ui->maxMemSpinBox, &QAbstractSpinBox::editingFinished, this, &JavaSettingsWidget::finishAdjustingMaxMemory); + connect(m_ui->minMemSlider, &QAbstractSlider::valueChanged, this, &JavaSettingsWidget::finishAdjustingMinMemory); + connect(m_ui->maxMemSlider, &QAbstractSlider::valueChanged, this, &JavaSettingsWidget::finishAdjustingMaxMemory); + + connect(m_ui->maxMemSpinBox, QOverload::of(&QSpinBox::valueChanged), this, &JavaSettingsWidget::onMemoryChange); + connect(m_ui->minMemSpinBox, QOverload::of(&QSpinBox::valueChanged), this, &JavaSettingsWidget::onMemoryChange); + + int maxSystemMemory = (Sys::getSystemRam() / Sys::mebibyte) - 1; + m_ui->minMemSlider->setMaximum(maxSystemMemory - 1); + m_ui->maxMemSlider->setMaximum(maxSystemMemory - 1); + m_ui->minMemMaxValueHint->setText(formatGiBLabel(maxSystemMemory - 1)); + m_ui->maxMemMaxValueHint->setText(formatGiBLabel(maxSystemMemory - 1)); + + SettingsObjectPtr settings = APPLICATION->settings(); + + enableAdvancedMemoryControl(settings->get("AdvancedJavaMemoryControl").toBool()); + + connect(m_ui->memorySimpleButton, &QPushButton::clicked, this, [this, settings] () { + enableAdvancedMemoryControl(false); + settings->set("AdvancedJavaMemoryControl", false); + }); + + connect(m_ui->memoryAdvancedButton, &QPushButton::clicked, this, [this, settings] () { + enableAdvancedMemoryControl(true); + settings->set("AdvancedJavaMemoryControl", true); + }); loadSettings(); - updateThresholds(); + onMemoryChange(); } JavaSettingsWidget::~JavaSettingsWidget() @@ -283,32 +327,43 @@ void JavaSettingsWidget::onJavaAutodetect() } } } -void JavaSettingsWidget::updateThresholds() + +void JavaSettingsWidget::onMemoryChange() { auto sysMiB = Sys::getSystemRam() / Sys::mebibyte; unsigned int maxMem = m_ui->maxMemSpinBox->value(); - unsigned int minMem = m_ui->minMemSpinBox->value(); - - QString iconName; if (maxMem >= sysMiB) { - iconName = "status-bad"; - m_ui->labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation exceeds your system memory capacity.")); + m_ui->labelMaxMemNotice->setText(QString("%1").arg(tr("Your maximum memory allocation exceeds your system memory capacity."))); + m_ui->labelMaxMemNotice->show(); } else if (maxMem > (sysMiB * 0.9)) { - iconName = "status-yellow"; - m_ui->labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation approaches your system memory capacity.")); - } else if (maxMem < minMem) { - iconName = "status-yellow"; - m_ui->labelMaxMemIcon->setToolTip(tr("Your maximum memory allocation is smaller than the minimum value")); + // TODO: where is this colour from + m_ui->labelMaxMemNotice->setText(QString("%1") + .arg(tr("Your maximum memory allocation is close to your system memory capacity."))); + m_ui->labelMaxMemNotice->show(); } else { - iconName = "status-good"; - m_ui->labelMaxMemIcon->setToolTip(""); + m_ui->labelMaxMemNotice->hide(); } - { - auto height = m_ui->labelMaxMemIcon->fontInfo().pixelSize(); - QIcon icon = APPLICATION->getThemedIcon(iconName); - QPixmap pix = icon.pixmap(height, height); - m_ui->labelMaxMemIcon->setPixmap(pix); - } + m_ui->minMemGBLabel->setText(formatGiBLabel(m_ui->minMemSlider->value())); + m_ui->maxMemGBLabel->setText(formatGiBLabel(m_ui->maxMemSlider->value())); +} + +void JavaSettingsWidget::finishAdjustingMinMemory() +{ + if (m_ui->minMemSpinBox->value() > m_ui->maxMemSpinBox->value()) + m_ui->maxMemSpinBox->setValue(m_ui->minMemSpinBox->value()); +} + +void JavaSettingsWidget::finishAdjustingMaxMemory() +{ + if (m_ui->maxMemSpinBox->value() < m_ui->minMemSpinBox->value()) + m_ui->minMemSpinBox->setValue(m_ui->maxMemSpinBox->value()); +} + +void JavaSettingsWidget::enableAdvancedMemoryControl(bool enabled) { + m_ui->memorySimpleButton->setChecked(!enabled); + m_ui->memoryAdvancedButton->setChecked(enabled); + m_ui->memorySimple->setVisible(!enabled); + m_ui->memoryAdvanced->setVisible(enabled); } diff --git a/launcher/ui/widgets/JavaSettingsWidget.h b/launcher/ui/widgets/JavaSettingsWidget.h index 21a71fb8b..ed38d63f8 100644 --- a/launcher/ui/widgets/JavaSettingsWidget.h +++ b/launcher/ui/widgets/JavaSettingsWidget.h @@ -59,7 +59,10 @@ class JavaSettingsWidget : public QWidget { void onJavaBrowse(); void onJavaAutodetect(); void onJavaTest(); - void updateThresholds(); + void onMemoryChange(); + void finishAdjustingMinMemory(); + void finishAdjustingMaxMemory(); + void enableAdvancedMemoryControl(bool enabled); private: InstancePtr m_instance; diff --git a/launcher/ui/widgets/JavaSettingsWidget.ui b/launcher/ui/widgets/JavaSettingsWidget.ui index 15ce88f0c..0e44980bf 100644 --- a/launcher/ui/widgets/JavaSettingsWidget.ui +++ b/launcher/ui/widgets/JavaSettingsWidget.ui @@ -7,13 +7,56 @@ 0 0 500 - 600 + 1123 Form + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + Test S&ettings + + + + + + + Open Java &Downloader + + + + + + + Qt::Horizontal + + + + 0 + 0 + + + + + + @@ -29,52 +72,50 @@ false - - - - Auto-&detect Java version - - - - + - - - - Browse - - - - - - - - - - - Download Java - - - - Auto-detect... + &Detect - + + + + 0 + 0 + + - Test + &Browse + + + + If enabled, the launcher will not check if an instance is compatible with the selected Java version. + + + Skip Java compatibility checks + + + + + + + Auto-&detect Java version + + + @@ -95,13 +136,13 @@ - - - - If enabled, the launcher will not check if an instance is compatible with the selected Java version. - + + - Skip Java compatibility checks + Java &Executable + + + javaPathTextBox @@ -122,104 +163,422 @@ false - - - - - PermGen (Java 7 and earlier): - - - - - - - Minimum memory allocation: - - + + + + + + + Simple + + + true + + + false + + + + + + + Advanced + + + true + + + false + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + - - - - The amount of memory available to store loaded Java classes. - - - MiB - - - 4 - - - 999999999 - - - 8 - - - 64 - + + + + + + + M&inimum Memory Usage + + + minMemSpinBox + + + + + + + 0 GiB + + + + + + + 8 + + + 8192 + + + 512 + + + 512 + + + true + + + Qt::Horizontal + + + QSlider::TicksBelow + + + 1024 + + + + + + + + + false + + + 0 GiB + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + false + + + 8 GiB + + + + + + + + + M&aximum Memory Usage + + + maxMemSpinBox + + + + + + + 0 GiB + + + + + + + 8 + + + 8192 + + + 512 + + + 512 + + + Qt::Horizontal + + + QSlider::TicksBelow + + + 1024 + + + + + + + + + false + + + 0 GiB + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + false + + + 8 GiB + + + + + + - - - - Maximum memory allocation: - + + + + + + + Minimum Memory Allocation + + + + + + + 0 + + + + + -Xm&s= + + + minMemSpinBox + + + + + + + + 0 + 0 + + + + The amount of memory Minecraft is started with. + + + M + + + 8 + + + 1048576 + + + 128 + + + 256 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Maximum Memory Allocation + + + + + + + 0 + + + + + -Xm&x= + + + maxMemSpinBox + + + + + + + + 0 + 0 + + + + The maximum amount of memory Minecraft is allowed to use. + + + M + + + 8 + + + 1048576 + + + 128 + + + 1024 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + PermGen Size + + + + + + + 0 + + + + + -XX:&PermSize= + + + permGenSpinBox + + + + + + + + 0 + 0 + + + + The amount of memory available to store loaded Java classes. + + + M + + + 4 + + + 999999999 + + + 8 + + + 64 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + - - + + - - - - Qt::AlignCenter - - - maxMemSpinBox - - - - - - - The maximum amount of memory Minecraft is allowed to use. - - - MiB - - - 8 - - - 1048576 - - - 128 - - - 1024 - - - - - - - The amount of memory Minecraft is started with. - - - MiB - - - 8 - - - 1048576 - - - 128 - - - 256 + Maximum Memory Notice @@ -251,17 +610,10 @@ javaPathTextBox - javaBrowseBtn - javaDownloadBtn - javaDetectBtn - javaTestBtn skipCompatibilityCheckBox skipWizardCheckBox autodetectJavaCheckBox autodownloadJavaCheckBox - minMemSpinBox - maxMemSpinBox - permGenSpinBox jvmArgsTextBox diff --git a/launcher/ui/widgets/MinecraftSettingsWidget.ui b/launcher/ui/widgets/MinecraftSettingsWidget.ui index daa065ac8..34fa9af80 100644 --- a/launcher/ui/widgets/MinecraftSettingsWidget.ui +++ b/launcher/ui/widgets/MinecraftSettingsWidget.ui @@ -51,9 +51,6 @@ 0 - - Qt::ScrollBarAlwaysOff - true @@ -61,9 +58,9 @@ 0 - -253 + 0 610 - 550 + 610 @@ -100,23 +97,73 @@ - - - - - Window height: + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 0 + 6 + + + + + + + + &Window Size + + + windowWidthSpinBox + + + + + + + + + + 0 + 0 + + + + + + + 1 + + + 65536 + + + 480 - - + + - Window width: + × - + + + + 0 + 0 + + + + + 1 @@ -131,18 +178,25 @@ - - - - 1 + + + + pixels - - 65536 + + + + + + Qt::Horizontal - - 480 + + + 40 + 20 + - + @@ -298,7 +352,7 @@ 0 0 624 - 297 + 291 @@ -313,9 +367,6 @@ - - Qt::ScrollBarAlwaysOff - true @@ -323,9 +374,9 @@ 0 - -101 + 0 610 - 398 + 501 @@ -369,7 +420,7 @@ false - + Use system installation of OpenAL @@ -386,6 +437,13 @@ + + + + false + + + @@ -393,14 +451,7 @@ - - - - false - - - - + &OpenAL library path @@ -410,8 +461,24 @@ - - + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 0 + 6 + + + + + + false @@ -513,7 +580,7 @@ 0 0 624 - 297 + 291 @@ -530,24 +597,27 @@ - - - - - - 0 - 0 - - - - Account: - - - - - - - + + + + 0 + 0 + + + + Account + + + + + + + + 0 + 0 + + + @@ -567,7 +637,7 @@ - Server address: + Server address @@ -647,10 +717,7 @@ openGlobalSettingsButton settingsTabs - scrollArea maximizedCheckBox - windowWidthSpinBox - windowHeightSpinBox showGameTime recordGameTime showGlobalGameTime @@ -664,9 +731,7 @@ scrollArea_2 onlineFixes useNativeGLFWCheck - lineEditGLFWPath useNativeOpenALCheck - lineEditOpenALPath perfomanceGroupBox enableFeralGamemodeCheck enableMangoHud @@ -674,7 +739,6 @@ useZink scrollArea_3 instanceAccountGroupBox - instanceAccountSelector serverJoinGroupBox serverJoinAddressButton serverJoinAddress From 2ab08f01b13a2467161a8fe268cefd75fa602d9a Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Mon, 24 Mar 2025 00:30:23 +0000 Subject: [PATCH 045/695] Merge mods & modpack settings Signed-off-by: TheKodeToad --- launcher/ui/pages/global/LauncherPage.ui | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index d29ee31be..64609be99 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -29,9 +29,9 @@ 0 - 0 - 563 - 1336 + -329 + 566 + 1296 @@ -293,7 +293,7 @@ - Mods + Mods and Modpacks @@ -346,15 +346,6 @@ - - - - - - - Modpacks - - From ad8818a13491d1ac12d2d5920c7123881f9d1f83 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Mon, 24 Mar 2025 00:36:03 +0000 Subject: [PATCH 046/695] Improve proxy page Signed-off-by: TheKodeToad --- launcher/ui/pages/global/ProxyPage.ui | 32 +++++++++++++-------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/launcher/ui/pages/global/ProxyPage.ui b/launcher/ui/pages/global/ProxyPage.ui index 91ba46b3d..3f794ea61 100644 --- a/launcher/ui/pages/global/ProxyPage.ui +++ b/launcher/ui/pages/global/ProxyPage.ui @@ -54,14 +54,14 @@ Type - + Uses your system's default proxy settings. - &Default + S&ystem Settings proxyGroup @@ -142,23 +142,23 @@ - - + + - &Username: + Note: Proxy username and password are stored in plain text inside the launcher's configuration file! - - proxyUserEdit + + true - - + + - &Password: + &Username: - proxyPassEdit + proxyUserEdit @@ -169,13 +169,13 @@ - - + + - Note: Proxy username and password are stored in plain text inside the launcher's configuration file! + &Password: - - true + + proxyPassEdit From 0434cc648a79a62f583fea8df9c8ce0f06991f58 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Mon, 24 Mar 2025 00:36:44 +0000 Subject: [PATCH 047/695] Rearrange settings categories Signed-off-by: TheKodeToad --- launcher/Application.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 054d27f8a..3c12cfe0d 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -800,14 +800,14 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) { m_globalSettingsProvider = std::make_shared(tr("Settings")); m_globalSettingsProvider->addPage(); + m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); - m_globalSettingsProvider->addPage(); - m_globalSettingsProvider->addPage(); - m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); + m_globalSettingsProvider->addPage(); + m_globalSettingsProvider->addPage(); } PixmapCache::setInstance(new PixmapCache(this)); From 72b90cf7de504478b7170ac73b4727055a9c14f9 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Mon, 24 Mar 2025 01:07:40 +0000 Subject: [PATCH 048/695] gay bowser Signed-off-by: TheKodeToad --- launcher/Application.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 3c12cfe0d..88a613040 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -57,6 +57,7 @@ #include "ui/instanceview/AccessibleInstanceView.h" #include "ui/pages/BasePageProvider.h" +#include "ui/pages/global/AppearancePage.h" #include "ui/pages/global/APIPage.h" #include "ui/pages/global/AccountListPage.h" #include "ui/pages/global/ExternalToolsPage.h" @@ -137,7 +138,6 @@ #if defined(Q_OS_LINUX) #include -#include #endif #if defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) From c5e766512e0b60041c02c7fb47468e808bf95913 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Mon, 24 Mar 2025 01:21:52 +0000 Subject: [PATCH 049/695] Improve update interval setting Signed-off-by: TheKodeToad --- launcher/ui/pages/global/LauncherPage.ui | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index 64609be99..b358765a3 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -29,7 +29,7 @@ 0 - -329 + 0 566 1296 @@ -132,7 +132,7 @@ - Update checking interval + How Often? @@ -145,10 +145,16 @@ - Set it to 0 to only check on launch + Set to 0 to only check on launch + + + On Launch - h + hours + + + Every 0 From 0768623e1edbb4d67e42b22c5b462c9c890fad6b Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Mon, 24 Mar 2025 01:33:23 +0000 Subject: [PATCH 050/695] Select General tab by default on Java page Signed-off-by: TheKodeToad --- 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 25c641cb5..bc5e9523f 100644 --- a/launcher/ui/pages/global/JavaPage.ui +++ b/launcher/ui/pages/global/JavaPage.ui @@ -32,7 +32,7 @@ - 1 + 0 From 26f9850462ca88a90fe7c0e9c82e2f78f5e681cd Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Mon, 24 Mar 2025 01:48:15 +0000 Subject: [PATCH 051/695] fix memory leak Signed-off-by: TheKodeToad --- launcher/ui/widgets/AppearanceWidget.cpp | 6 +++--- launcher/ui/widgets/AppearanceWidget.h | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/launcher/ui/widgets/AppearanceWidget.cpp b/launcher/ui/widgets/AppearanceWidget.cpp index f9475cbbd..731b72727 100644 --- a/launcher/ui/widgets/AppearanceWidget.cpp +++ b/launcher/ui/widgets/AppearanceWidget.cpp @@ -50,7 +50,7 @@ AppearanceWidget::AppearanceWidget(bool themesOnly, QWidget* parent) m_ui->catPreview->setGraphicsEffect(new QGraphicsOpacityEffect(this)); - m_defaultFormat = new QTextCharFormat(m_ui->consolePreview->currentCharFormat()); + m_defaultFormat = QTextCharFormat(m_ui->consolePreview->currentCharFormat()); if (themesOnly) { m_ui->catPackLabel->hide(); @@ -224,10 +224,10 @@ void AppearanceWidget::updateConsolePreview() int fontSize = m_ui->fontSizeBox->value(); QString fontFamily = m_ui->consoleFont->currentFont().family(); m_ui->consolePreview->clear(); - m_defaultFormat->setFont(QFont(fontFamily, fontSize)); + m_defaultFormat.setFont(QFont(fontFamily, fontSize)); auto print = [this, colors](const QString& message, MessageLevel::Enum level) { - QTextCharFormat format(*m_defaultFormat); + QTextCharFormat format(m_defaultFormat); QColor bg = colors.background.value(level); QColor fg = colors.foreground.value(level); diff --git a/launcher/ui/widgets/AppearanceWidget.h b/launcher/ui/widgets/AppearanceWidget.h index 3bc663676..1fc89af3a 100644 --- a/launcher/ui/widgets/AppearanceWidget.h +++ b/launcher/ui/widgets/AppearanceWidget.h @@ -24,6 +24,7 @@ #include #include +#include #include "java/JavaChecker.h" #include "ui/pages/BasePage.h" @@ -56,6 +57,6 @@ class AppearanceWidget : public QWidget { void updateCatPreview(); Ui::AppearanceWidget* m_ui; - QTextCharFormat* m_defaultFormat; + QTextCharFormat m_defaultFormat; bool m_themesOnly; }; From e1e0a3d887bff6f85051c005e1d182b459176860 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Mon, 24 Mar 2025 01:49:43 +0000 Subject: [PATCH 052/695] Remove tabs from proxy page Signed-off-by: TheKodeToad --- launcher/ui/pages/global/ProxyPage.cpp | 1 - launcher/ui/pages/global/ProxyPage.ui | 335 ++++++++++++------------- 2 files changed, 156 insertions(+), 180 deletions(-) diff --git a/launcher/ui/pages/global/ProxyPage.cpp b/launcher/ui/pages/global/ProxyPage.cpp index 9caffcb37..979f07c6a 100644 --- a/launcher/ui/pages/global/ProxyPage.cpp +++ b/launcher/ui/pages/global/ProxyPage.cpp @@ -46,7 +46,6 @@ ProxyPage::ProxyPage(QWidget* parent) : QWidget(parent), ui(new Ui::ProxyPage) { ui->setupUi(this); - ui->tabWidget->tabBar()->hide(); loadSettings(); updateCheckboxStuff(); diff --git a/launcher/ui/pages/global/ProxyPage.ui b/launcher/ui/pages/global/ProxyPage.ui index 3f794ea61..830ae1ac8 100644 --- a/launcher/ui/pages/global/ProxyPage.ui +++ b/launcher/ui/pages/global/ProxyPage.ui @@ -17,188 +17,165 @@ - - 0 - - - 0 - - - 0 - - - 0 - - - - - - - - - - - This only applies to the launcher. Minecraft does not accept proxy settings. - - - Qt::AlignCenter - - - true - - - - - - - Type - - - - - - Uses your system's default proxy settings. - - - S&ystem Settings - - - proxyGroup - - - - - - - &None - - - proxyGroup - - - - - - - &SOCKS5 - - - proxyGroup - - - - - - - &HTTP - - - proxyGroup - - - - - - - - - - &Address and Port - - - - - - 127.0.0.1 - - - - - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - QAbstractSpinBox::PlusMinus - - - 65535 - - - 8080 - - - - - - - - - - Authentication - - - - - - - - - Note: Proxy username and password are stored in plain text inside the launcher's configuration file! - - - true - - - - - - - &Username: - - - proxyUserEdit - - - - - - - QLineEdit::Password - - - - - - - &Password: - - - proxyPassEdit - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - + + + This only applies to the launcher. Minecraft does not accept proxy settings. + + + Qt::AlignCenter + + + true + + + + + Type + + + + + + Uses your system's default proxy settings. + + + S&ystem Settings + + + proxyGroup + + + + + + + &None + + + proxyGroup + + + + + + + &SOCKS5 + + + proxyGroup + + + + + + + &HTTP + + + proxyGroup + + + + + + + + + + &Address and Port + + + + + + 127.0.0.1 + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + QAbstractSpinBox::PlusMinus + + + 65535 + + + 8080 + + + + + + + + + + Authentication + + + + + + + + + Note: Proxy username and password are stored in plain text inside the launcher's configuration file! + + + true + + + + + + + &Username: + + + proxyUserEdit + + + + + + + QLineEdit::Password + + + + + + + &Password: + + + proxyPassEdit + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + From 4e6bfde7239f83016c9c3f88e7c6991c267fc26b Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Mon, 24 Mar 2025 11:59:29 +0000 Subject: [PATCH 053/695] Revert sliders Signed-off-by: TheKodeToad --- launcher/Application.cpp | 1 - launcher/ui/pages/global/JavaPage.cpp | 2 +- launcher/ui/widgets/JavaSettingsWidget.cpp | 84 +--- launcher/ui/widgets/JavaSettingsWidget.h | 7 +- launcher/ui/widgets/JavaSettingsWidget.ui | 518 +++++---------------- 5 files changed, 127 insertions(+), 485 deletions(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 88a613040..ecd606f43 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -665,7 +665,6 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_settings->registerSetting("AutomaticJavaSwitch", defaultEnableAutoJava); m_settings->registerSetting("AutomaticJavaDownload", defaultEnableAutoJava); m_settings->registerSetting("UserAskedAboutAutomaticJavaDownload", false); - m_settings->registerSetting("AdvancedJavaMemoryControl", false); // Legacy settings m_settings->registerSetting("OnlineFixes", false); diff --git a/launcher/ui/pages/global/JavaPage.cpp b/launcher/ui/pages/global/JavaPage.cpp index b99d0c63e..6a44c9290 100644 --- a/launcher/ui/pages/global/JavaPage.cpp +++ b/launcher/ui/pages/global/JavaPage.cpp @@ -62,7 +62,7 @@ JavaPage::JavaPage(QWidget* parent) : QWidget(parent), ui(new Ui::JavaPage) { ui->setupUi(this); - + if (BuildConfig.JAVA_DOWNLOADER_ENABLED) { ui->managedJavaList->initialize(new JavaInstallList(this, true)); ui->managedJavaList->setResizeOn(2); diff --git a/launcher/ui/widgets/JavaSettingsWidget.cpp b/launcher/ui/widgets/JavaSettingsWidget.cpp index 200d81db8..bd48f3faa 100644 --- a/launcher/ui/widgets/JavaSettingsWidget.cpp +++ b/launcher/ui/widgets/JavaSettingsWidget.cpp @@ -53,11 +53,6 @@ #include "ui_JavaSettingsWidget.h" -static QString formatGiBLabel(int value) -{ - return QObject::tr("%1 GiB").arg(value / 1024.0, 0, 'f', 1); -} - JavaSettingsWidget::JavaSettingsWidget(InstancePtr instance, QWidget* parent) : QWidget(parent), m_instance(std::move(instance)), m_ui(new Ui::JavaSettingsWidget) { @@ -106,50 +101,11 @@ JavaSettingsWidget::JavaSettingsWidget(InstancePtr instance, QWidget* parent) connect(m_ui->javaDetectBtn, &QPushButton::clicked, this, &JavaSettingsWidget::onJavaAutodetect); connect(m_ui->javaBrowseBtn, &QPushButton::clicked, this, &JavaSettingsWidget::onJavaBrowse); - connect(m_ui->minMemSpinBox, QOverload::of(&QSpinBox::valueChanged), m_ui->minMemSlider, [this](int value) { - m_ui->minMemSlider->blockSignals(true); - m_ui->minMemSlider->setValue(value); - m_ui->minMemSlider->blockSignals(false); - }); - connect(m_ui->maxMemSpinBox, QOverload::of(&QSpinBox::valueChanged), m_ui->maxMemSlider, [this](int value) { - m_ui->maxMemSlider->blockSignals(true); - m_ui->maxMemSlider->setValue(value); - m_ui->maxMemSlider->blockSignals(false); - }); - - connect(m_ui->minMemSlider, &QAbstractSlider::valueChanged, m_ui->minMemSpinBox, QOverload::of(&QSpinBox::setValue)); - connect(m_ui->maxMemSlider, &QAbstractSlider::valueChanged, m_ui->maxMemSpinBox, QOverload::of(&QSpinBox::setValue)); - - connect(m_ui->minMemSpinBox, &QAbstractSpinBox::editingFinished, this, &JavaSettingsWidget::finishAdjustingMinMemory); - connect(m_ui->maxMemSpinBox, &QAbstractSpinBox::editingFinished, this, &JavaSettingsWidget::finishAdjustingMaxMemory); - connect(m_ui->minMemSlider, &QAbstractSlider::valueChanged, this, &JavaSettingsWidget::finishAdjustingMinMemory); - connect(m_ui->maxMemSlider, &QAbstractSlider::valueChanged, this, &JavaSettingsWidget::finishAdjustingMaxMemory); - - connect(m_ui->maxMemSpinBox, QOverload::of(&QSpinBox::valueChanged), this, &JavaSettingsWidget::onMemoryChange); - connect(m_ui->minMemSpinBox, QOverload::of(&QSpinBox::valueChanged), this, &JavaSettingsWidget::onMemoryChange); - - int maxSystemMemory = (Sys::getSystemRam() / Sys::mebibyte) - 1; - m_ui->minMemSlider->setMaximum(maxSystemMemory - 1); - m_ui->maxMemSlider->setMaximum(maxSystemMemory - 1); - m_ui->minMemMaxValueHint->setText(formatGiBLabel(maxSystemMemory - 1)); - m_ui->maxMemMaxValueHint->setText(formatGiBLabel(maxSystemMemory - 1)); - - SettingsObjectPtr settings = APPLICATION->settings(); - - enableAdvancedMemoryControl(settings->get("AdvancedJavaMemoryControl").toBool()); - - connect(m_ui->memorySimpleButton, &QPushButton::clicked, this, [this, settings] () { - enableAdvancedMemoryControl(false); - settings->set("AdvancedJavaMemoryControl", false); - }); - - connect(m_ui->memoryAdvancedButton, &QPushButton::clicked, this, [this, settings] () { - enableAdvancedMemoryControl(true); - settings->set("AdvancedJavaMemoryControl", true); - }); + connect(m_ui->maxMemSpinBox, QOverload::of(&QSpinBox::valueChanged), this, &JavaSettingsWidget::updateThresholds); + connect(m_ui->minMemSpinBox, QOverload::of(&QSpinBox::valueChanged), this, &JavaSettingsWidget::updateThresholds); loadSettings(); - onMemoryChange(); + updateThresholds(); } JavaSettingsWidget::~JavaSettingsWidget() @@ -327,43 +283,23 @@ void JavaSettingsWidget::onJavaAutodetect() } } } - -void JavaSettingsWidget::onMemoryChange() +void JavaSettingsWidget::updateThresholds() { auto sysMiB = Sys::getSystemRam() / Sys::mebibyte; unsigned int maxMem = m_ui->maxMemSpinBox->value(); + unsigned int minMem = m_ui->minMemSpinBox->value(); + + const QString warningColour(QStringLiteral("%1")); if (maxMem >= sysMiB) { m_ui->labelMaxMemNotice->setText(QString("%1").arg(tr("Your maximum memory allocation exceeds your system memory capacity."))); m_ui->labelMaxMemNotice->show(); } else if (maxMem > (sysMiB * 0.9)) { - // TODO: where is this colour from - m_ui->labelMaxMemNotice->setText(QString("%1") - .arg(tr("Your maximum memory allocation is close to your system memory capacity."))); + m_ui->labelMaxMemNotice->setText(warningColour.arg(tr("Your maximum memory allocation is close to your system memory capacity."))); m_ui->labelMaxMemNotice->show(); + } else if (maxMem < minMem) { + m_ui->labelMaxMemNotice->setText(warningColour.arg(tr("Your maximum memory allocation is below the minimum memory allocation."))) } else { m_ui->labelMaxMemNotice->hide(); } - - m_ui->minMemGBLabel->setText(formatGiBLabel(m_ui->minMemSlider->value())); - m_ui->maxMemGBLabel->setText(formatGiBLabel(m_ui->maxMemSlider->value())); -} - -void JavaSettingsWidget::finishAdjustingMinMemory() -{ - if (m_ui->minMemSpinBox->value() > m_ui->maxMemSpinBox->value()) - m_ui->maxMemSpinBox->setValue(m_ui->minMemSpinBox->value()); -} - -void JavaSettingsWidget::finishAdjustingMaxMemory() -{ - if (m_ui->maxMemSpinBox->value() < m_ui->minMemSpinBox->value()) - m_ui->minMemSpinBox->setValue(m_ui->maxMemSpinBox->value()); -} - -void JavaSettingsWidget::enableAdvancedMemoryControl(bool enabled) { - m_ui->memorySimpleButton->setChecked(!enabled); - m_ui->memoryAdvancedButton->setChecked(enabled); - m_ui->memorySimple->setVisible(!enabled); - m_ui->memoryAdvanced->setVisible(enabled); } diff --git a/launcher/ui/widgets/JavaSettingsWidget.h b/launcher/ui/widgets/JavaSettingsWidget.h index ed38d63f8..85266bc2b 100644 --- a/launcher/ui/widgets/JavaSettingsWidget.h +++ b/launcher/ui/widgets/JavaSettingsWidget.h @@ -59,13 +59,10 @@ class JavaSettingsWidget : public QWidget { void onJavaBrowse(); void onJavaAutodetect(); void onJavaTest(); - void onMemoryChange(); - void finishAdjustingMinMemory(); - void finishAdjustingMaxMemory(); - void enableAdvancedMemoryControl(bool enabled); + void updateThresholds(); private: InstancePtr m_instance; Ui::JavaSettingsWidget* m_ui; unique_qobject_ptr m_checker; -}; +}; \ No newline at end of file diff --git a/launcher/ui/widgets/JavaSettingsWidget.ui b/launcher/ui/widgets/JavaSettingsWidget.ui index 0e44980bf..5cc894b8e 100644 --- a/launcher/ui/widgets/JavaSettingsWidget.ui +++ b/launcher/ui/widgets/JavaSettingsWidget.ui @@ -165,420 +165,123 @@ - - - - - Simple - - - true - - - false - - - - - - - Advanced - - - true - - - false - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - + + + M&inimum Memory Usage (-Xms) + + + minMemSpinBox + + - - - - - - M&inimum Memory Usage - - - minMemSpinBox - - - - - - - 0 GiB - - - - - - - 8 - - - 8192 - - - 512 - - - 512 - - - true - - - Qt::Horizontal - - - QSlider::TicksBelow - - - 1024 - - - - - - - - - false - - - 0 GiB - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - false - - - 8 GiB - - - - - - - - - M&aximum Memory Usage - - - maxMemSpinBox - - - - - - - 0 GiB - - - - - - - 8 - - - 8192 - - - 512 - - - 512 - - - Qt::Horizontal - - - QSlider::TicksBelow - - - 1024 - - - - - - - - - false - - - 0 GiB - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - false - - - 8 GiB - - - - - - + + + + 0 + 0 + + + + The amount of memory Minecraft is started with. + + + MiB + + + 8 + + + 1048576 + + + 128 + + + 256 + - - - - - - Minimum Memory Allocation - - - - - - - 0 - - - - - -Xm&s= - - - minMemSpinBox - - - - - - - - 0 - 0 - - - - The amount of memory Minecraft is started with. - - - M - - - 8 - - - 1048576 - - - 128 - - - 256 - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - Maximum Memory Allocation - - - - - - - 0 - - - - - -Xm&x= - - - maxMemSpinBox - - - - - - - - 0 - 0 - - - - The maximum amount of memory Minecraft is allowed to use. - - - M - - - 8 - - - 1048576 - - - 128 - - - 1024 - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - PermGen Size - - - - - - - 0 - - - - - -XX:&PermSize= - - - permGenSpinBox - - - - - - - - 0 - 0 - - - - The amount of memory available to store loaded Java classes. - - - M - - - 4 - - - 999999999 - - - 8 - - - 64 - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - + + + Ma&ximum Memory Usage (-Xmx) + + + maxMemSpinBox + + + + + + + + 0 + 0 + + + + The maximum amount of memory Minecraft is allowed to use. + + + MiB + + + 8 + + + 1048576 + + + 128 + + + 1024 + + + + + + + &PermGen Size (-XX:PermSize) + + + permGenSpinBox + + + + + + + + 0 + 0 + + + + The amount of memory available to store loaded Java classes. + + + MiB + + + 4 + + + 999999999 + + + 8 + + + 64 + - Maximum Memory Notice + TextLabel @@ -609,11 +312,18 @@ + javaTestBtn + javaDownloadBtn javaPathTextBox + javaDetectBtn + javaBrowseBtn skipCompatibilityCheckBox skipWizardCheckBox autodetectJavaCheckBox autodownloadJavaCheckBox + minMemSpinBox + maxMemSpinBox + permGenSpinBox jvmArgsTextBox From a8ea072d36dbfcf0e67c9cedb605f57623d96a99 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Mon, 24 Mar 2025 13:23:53 +0000 Subject: [PATCH 054/695] Make Services page nicer Signed-off-by: TheKodeToad --- launcher/ui/pages/global/APIPage.ui | 195 +++++++++++---------- launcher/ui/widgets/JavaSettingsWidget.cpp | 2 +- 2 files changed, 105 insertions(+), 92 deletions(-) diff --git a/launcher/ui/pages/global/APIPage.ui b/launcher/ui/pages/global/APIPage.ui index ab4bdf83e..3280558ab 100644 --- a/launcher/ui/pages/global/APIPage.ui +++ b/launcher/ui/pages/global/APIPage.ui @@ -20,40 +20,12 @@ 0 - -342 - 804 - 946 + -270 + 807 + 870 - - - - - 0 - 0 - - - - User Agent - - - - - - - - - Enter a custom User Agent here. The special string $LAUNCHER_VER will be replaced with the version of the launcher. - - - true - - - - - - @@ -86,7 +58,7 @@ - + Use Default true @@ -128,7 +100,7 @@ - + Use Default @@ -151,16 +123,48 @@ + + + + + 0 + 0 + + + + User Agent + + + + + + Use Default + + + + + + + Enter a custom User Agent here. The special string $LAUNCHER_VER will be replaced with the version of the launcher. + + + true + + + + + + - &Microsoft Authentication + &API Keys - &Client ID + &Microsoft Authentation Qt::RichText @@ -179,7 +183,7 @@ - (Default) + Use Default @@ -196,32 +200,23 @@ - - - - - - - true - - - &Modrinth - - - - - - true + + + + QSizePolicy::Fixed - - (None) + + + 0 + 6 + - + - + - &API Token + Mod&rinth Qt::RichText @@ -237,7 +232,17 @@ - + + + + true + + + Use None + + + + <html><head/><body><p>Note: you only need to set this to access private data. Read the <a href="https://docs.modrinth.com/api/#authentication">documentation</a> for more information.</p></body></html> @@ -250,32 +255,23 @@ - - - - - - - true - - - &CurseForge - - - - - - true + + + + QSizePolicy::Fixed - - (Default) + + + 0 + 6 + - + - + - API &Key + &CurseForge Qt::RichText @@ -291,7 +287,17 @@ - + + + + true + + + Use Default + + + + Note: you probably don't need to set this if CurseForge already works. @@ -301,26 +307,33 @@ - - - - - - - Technic - - + + + + QSizePolicy::Fixed + + + + 0 + 6 + + + + - GUID Client ID + &Technic + + + technicClientID - (None) + Use Default diff --git a/launcher/ui/widgets/JavaSettingsWidget.cpp b/launcher/ui/widgets/JavaSettingsWidget.cpp index bd48f3faa..265385d03 100644 --- a/launcher/ui/widgets/JavaSettingsWidget.cpp +++ b/launcher/ui/widgets/JavaSettingsWidget.cpp @@ -298,7 +298,7 @@ void JavaSettingsWidget::updateThresholds() m_ui->labelMaxMemNotice->setText(warningColour.arg(tr("Your maximum memory allocation is close to your system memory capacity."))); m_ui->labelMaxMemNotice->show(); } else if (maxMem < minMem) { - m_ui->labelMaxMemNotice->setText(warningColour.arg(tr("Your maximum memory allocation is below the minimum memory allocation."))) + m_ui->labelMaxMemNotice->setText(warningColour.arg(tr("Your maximum memory allocation is below the minimum memory allocation."))); } else { m_ui->labelMaxMemNotice->hide(); } From 491b5e147370189ec8553ef5819ece448f9cd9e3 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Mon, 24 Mar 2025 16:34:45 +0000 Subject: [PATCH 055/695] Correctly show max memory notice for max < min Signed-off-by: TheKodeToad --- launcher/ui/widgets/AppearanceWidget.ui | 24 ++++++++++++++++++++++ launcher/ui/widgets/JavaSettingsWidget.cpp | 1 + 2 files changed, 25 insertions(+) diff --git a/launcher/ui/widgets/AppearanceWidget.ui b/launcher/ui/widgets/AppearanceWidget.ui index d646cb9e0..deb5a94d4 100644 --- a/launcher/ui/widgets/AppearanceWidget.ui +++ b/launcher/ui/widgets/AppearanceWidget.ui @@ -157,6 +157,18 @@ + + 0 + + + 0 + + + 0 + + + 0 + @@ -208,6 +220,18 @@ + + 0 + + + 0 + + + 0 + + + 0 + diff --git a/launcher/ui/widgets/JavaSettingsWidget.cpp b/launcher/ui/widgets/JavaSettingsWidget.cpp index 265385d03..f5435d16f 100644 --- a/launcher/ui/widgets/JavaSettingsWidget.cpp +++ b/launcher/ui/widgets/JavaSettingsWidget.cpp @@ -299,6 +299,7 @@ void JavaSettingsWidget::updateThresholds() m_ui->labelMaxMemNotice->show(); } else if (maxMem < minMem) { m_ui->labelMaxMemNotice->setText(warningColour.arg(tr("Your maximum memory allocation is below the minimum memory allocation."))); + m_ui->labelMaxMemNotice->show(); } else { m_ui->labelMaxMemNotice->hide(); } From 23a28fe762d09f4ea07b1b77def655cdf99072ee Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Mon, 24 Mar 2025 17:19:09 +0000 Subject: [PATCH 056/695] Fix tooltip inconsistency Signed-off-by: TheKodeToad --- launcher/ui/pages/global/LauncherPage.ui | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index b358765a3..b70cc6c1b 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -29,7 +29,7 @@ 0 - 0 + -616 566 1296 @@ -325,7 +325,7 @@ - Disable using metadata provided by mod providers (like Modrinth or CurseForge) for mods. + Store version information provided by mod providers (like Modrinth or CurseForge) for mods. Keep track of mod metadata @@ -345,7 +345,7 @@ - Disable the automatic detection, installation, and updating of mod dependencies. + Automatically detect, install, and update mod dependencies. Install dependencies automatically @@ -355,7 +355,7 @@ - When creating a new modpack instance, do not suggest updating existing instances instead. + When creating a new modpack instance, suggest updating an existing instance instead. Suggest to update an existing instance From e2b85a2e2bf07751b5084a5686baf85e1fff12b8 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Mon, 24 Mar 2025 17:24:18 +0000 Subject: [PATCH 057/695] Clang format Signed-off-by: TheKodeToad --- launcher/Application.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index ecd606f43..bf9fa32ed 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -57,9 +57,9 @@ #include "ui/instanceview/AccessibleInstanceView.h" #include "ui/pages/BasePageProvider.h" -#include "ui/pages/global/AppearancePage.h" #include "ui/pages/global/APIPage.h" #include "ui/pages/global/AccountListPage.h" +#include "ui/pages/global/AppearancePage.h" #include "ui/pages/global/ExternalToolsPage.h" #include "ui/pages/global/JavaPage.h" #include "ui/pages/global/LanguagePage.h" From 18ee1f897ad95ce6f61f9d14779985745b271498 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Tue, 25 Mar 2025 16:28:16 +0000 Subject: [PATCH 058/695] Global data packs Signed-off-by: TheKodeToad --- launcher/InstancePageProvider.h | 2 + launcher/minecraft/MinecraftInstance.cpp | 27 ++++- launcher/minecraft/MinecraftInstance.h | 4 + .../resources/breeze_dark/breeze_dark.qrc | 2 +- ...nvironment-variables.svg => datapacks.svg} | 0 .../resources/breeze_light/breeze_light.qrc | 2 +- ...nvironment-variables.svg => datapacks.svg} | 0 launcher/resources/flat/flat.qrc | 2 +- ...nvironment-variables.svg => datapacks.svg} | 0 launcher/resources/flat_white/flat_white.qrc | 2 +- ...nvironment-variables.svg => datapacks.svg} | 0 launcher/resources/multimc/multimc.qrc | 2 +- ...nvironment-variables.svg => datapacks.svg} | 0 launcher/resources/pe_blue/pe_blue.qrc | 2 +- ...nvironment-variables.svg => datapacks.svg} | 0 launcher/resources/pe_colored/pe_colored.qrc | 2 +- ...nvironment-variables.svg => datapacks.svg} | 0 launcher/resources/pe_dark/pe_dark.qrc | 2 +- ...nvironment-variables.svg => datapacks.svg} | 0 launcher/resources/pe_light/pe_light.qrc | 2 +- ...nvironment-variables.svg => datapacks.svg} | 0 launcher/ui/pages/instance/DataPackPage.cpp | 99 ++++++++++++++++++- launcher/ui/pages/instance/DataPackPage.h | 32 +++++- launcher/ui/pages/instance/WorldListPage.cpp | 5 +- .../ui/widgets/MinecraftSettingsWidget.cpp | 14 +++ .../ui/widgets/MinecraftSettingsWidget.ui | 77 +++++++++++++-- 26 files changed, 254 insertions(+), 24 deletions(-) rename launcher/resources/breeze_dark/scalable/{environment-variables.svg => datapacks.svg} (100%) rename launcher/resources/breeze_light/scalable/{environment-variables.svg => datapacks.svg} (100%) rename launcher/resources/flat/scalable/{environment-variables.svg => datapacks.svg} (100%) rename launcher/resources/flat_white/scalable/{environment-variables.svg => datapacks.svg} (100%) rename launcher/resources/multimc/scalable/{environment-variables.svg => datapacks.svg} (100%) rename launcher/resources/pe_blue/scalable/{environment-variables.svg => datapacks.svg} (100%) rename launcher/resources/pe_colored/scalable/{environment-variables.svg => datapacks.svg} (100%) rename launcher/resources/pe_dark/scalable/{environment-variables.svg => datapacks.svg} (100%) rename launcher/resources/pe_light/scalable/{environment-variables.svg => datapacks.svg} (100%) diff --git a/launcher/InstancePageProvider.h b/launcher/InstancePageProvider.h index 1d7c193f8..e4884d11f 100644 --- a/launcher/InstancePageProvider.h +++ b/launcher/InstancePageProvider.h @@ -1,5 +1,6 @@ #pragma once #include +#include #include "minecraft/MinecraftInstance.h" #include "ui/pages/BasePage.h" #include "ui/pages/BasePageProvider.h" @@ -36,6 +37,7 @@ class InstancePageProvider : protected QObject, public BasePageProvider { values.append(new CoreModFolderPage(onesix.get(), onesix->coreModList())); values.append(new NilModFolderPage(onesix.get(), onesix->nilModList())); values.append(new ResourcePackPage(onesix.get(), onesix->resourcePackList())); + values.append(new GlobalDataPackPage(onesix.get())); values.append(new TexturePackPage(onesix.get(), onesix->texturePackList())); values.append(new ShaderPackPage(onesix.get(), onesix->shaderPackList())); values.append(new NotesPage(onesix.get())); diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index d1780d497..ab4219627 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -250,6 +250,12 @@ void MinecraftInstance::loadSpecificSettings() m_settings->registerSetting("ExportAuthor", ""); m_settings->registerSetting("ExportOptionalFiles", true); + auto dataPacksEnabled = m_settings->registerSetting("GlobalDataPacksEnabled", false); + auto dataPacksPath = m_settings->registerSetting("GlobalDataPacksPath", ""); + + connect(dataPacksEnabled.get(), &Setting::SettingChanged, this, [this] { m_data_pack_list.reset(); }); + connect(dataPacksPath.get(), &Setting::SettingChanged, this, [this] { m_data_pack_list.reset(); }); + qDebug() << "Instance-type specific settings were loaded!"; setSpecificSettingsLoaded(true); @@ -392,6 +398,16 @@ QString MinecraftInstance::nilModsDir() const return FS::PathCombine(gameRoot(), "nilmods"); } +QString MinecraftInstance::dataPacksDir() +{ + QString relativePath = settings()->get("GlobalDataPacksPath").toString(); + + if (relativePath.isEmpty()) + relativePath = "datapacks"; + + return FS::PathCombine(gameRoot(), relativePath); +} + QString MinecraftInstance::resourcePacksDir() const { return FS::PathCombine(gameRoot(), "resourcepacks"); @@ -1303,9 +1319,18 @@ std::shared_ptr MinecraftInstance::shaderPackList() return m_shader_pack_list; } +std::shared_ptr MinecraftInstance::dataPackList() +{ + if (!m_data_pack_list && settings()->get("GlobalDataPacksEnabled").toBool()) { + bool isIndexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); + m_data_pack_list.reset(new DataPackFolderModel(dataPacksDir(), this, isIndexed, true)); + } + return m_data_pack_list; +} + QList> MinecraftInstance::resourceLists() { - return { loaderModList(), coreModList(), nilModList(), resourcePackList(), texturePackList(), shaderPackList() }; + return { loaderModList(), coreModList(), nilModList(), resourcePackList(), texturePackList(), shaderPackList(), dataPackList() }; } std::shared_ptr MinecraftInstance::worldList() diff --git a/launcher/minecraft/MinecraftInstance.h b/launcher/minecraft/MinecraftInstance.h index 5d9bb45ef..68f5d4f2a 100644 --- a/launcher/minecraft/MinecraftInstance.h +++ b/launcher/minecraft/MinecraftInstance.h @@ -36,6 +36,7 @@ #pragma once #include +#include #include #include #include "BaseInstance.h" @@ -80,6 +81,7 @@ class MinecraftInstance : public BaseInstance { QString modsRoot() const override; QString coreModsDir() const; QString nilModsDir() const; + QString dataPacksDir(); QString modsCacheLocation() const; QString libDir() const; QString worldDir() const; @@ -116,6 +118,7 @@ class MinecraftInstance : public BaseInstance { std::shared_ptr resourcePackList(); std::shared_ptr texturePackList(); std::shared_ptr shaderPackList(); + std::shared_ptr dataPackList(); QList> resourceLists(); std::shared_ptr worldList(); std::shared_ptr gameOptionsModel(); @@ -171,6 +174,7 @@ class MinecraftInstance : public BaseInstance { mutable std::shared_ptr m_resource_pack_list; mutable std::shared_ptr m_shader_pack_list; mutable std::shared_ptr m_texture_pack_list; + mutable std::shared_ptr m_data_pack_list; mutable std::shared_ptr m_world_list; mutable std::shared_ptr m_game_options; }; diff --git a/launcher/resources/breeze_dark/breeze_dark.qrc b/launcher/resources/breeze_dark/breeze_dark.qrc index 61d82ec30..8be5d117b 100644 --- a/launcher/resources/breeze_dark/breeze_dark.qrc +++ b/launcher/resources/breeze_dark/breeze_dark.qrc @@ -9,7 +9,7 @@ scalable/copy.svg scalable/coremods.svg scalable/custom-commands.svg - scalable/environment-variables.svg + scalable/datapacks.svg scalable/discord.svg scalable/externaltools.svg scalable/help.svg diff --git a/launcher/resources/breeze_dark/scalable/environment-variables.svg b/launcher/resources/breeze_dark/scalable/datapacks.svg similarity index 100% rename from launcher/resources/breeze_dark/scalable/environment-variables.svg rename to launcher/resources/breeze_dark/scalable/datapacks.svg diff --git a/launcher/resources/breeze_light/breeze_light.qrc b/launcher/resources/breeze_light/breeze_light.qrc index 2211c7188..291bcb508 100644 --- a/launcher/resources/breeze_light/breeze_light.qrc +++ b/launcher/resources/breeze_light/breeze_light.qrc @@ -9,7 +9,7 @@ scalable/copy.svg scalable/coremods.svg scalable/custom-commands.svg - scalable/environment-variables.svg + scalable/datapacks.svg scalable/discord.svg scalable/externaltools.svg scalable/help.svg diff --git a/launcher/resources/breeze_light/scalable/environment-variables.svg b/launcher/resources/breeze_light/scalable/datapacks.svg similarity index 100% rename from launcher/resources/breeze_light/scalable/environment-variables.svg rename to launcher/resources/breeze_light/scalable/datapacks.svg diff --git a/launcher/resources/flat/flat.qrc b/launcher/resources/flat/flat.qrc index 8876027da..9f88645c2 100644 --- a/launcher/resources/flat/flat.qrc +++ b/launcher/resources/flat/flat.qrc @@ -11,7 +11,7 @@ scalable/copy.svg scalable/coremods.svg scalable/custom-commands.svg - scalable/environment-variables.svg + scalable/datapacks.svg scalable/discord.svg scalable/externaltools.svg scalable/help.svg diff --git a/launcher/resources/flat/scalable/environment-variables.svg b/launcher/resources/flat/scalable/datapacks.svg similarity index 100% rename from launcher/resources/flat/scalable/environment-variables.svg rename to launcher/resources/flat/scalable/datapacks.svg diff --git a/launcher/resources/flat_white/flat_white.qrc b/launcher/resources/flat_white/flat_white.qrc index 83b178cbf..56aad62fa 100644 --- a/launcher/resources/flat_white/flat_white.qrc +++ b/launcher/resources/flat_white/flat_white.qrc @@ -11,7 +11,7 @@ scalable/copy.svg scalable/coremods.svg scalable/custom-commands.svg - scalable/environment-variables.svg + scalable/datapacks.svg scalable/discord.svg scalable/externaltools.svg scalable/help.svg diff --git a/launcher/resources/flat_white/scalable/environment-variables.svg b/launcher/resources/flat_white/scalable/datapacks.svg similarity index 100% rename from launcher/resources/flat_white/scalable/environment-variables.svg rename to launcher/resources/flat_white/scalable/datapacks.svg diff --git a/launcher/resources/multimc/multimc.qrc b/launcher/resources/multimc/multimc.qrc index 25edd09e0..853dda525 100644 --- a/launcher/resources/multimc/multimc.qrc +++ b/launcher/resources/multimc/multimc.qrc @@ -74,7 +74,7 @@ scalable/screenshots.svg scalable/custom-commands.svg - scalable/environment-variables.svg + scalable/datapacks.svg 16x16/cat.png diff --git a/launcher/resources/multimc/scalable/environment-variables.svg b/launcher/resources/multimc/scalable/datapacks.svg similarity index 100% rename from launcher/resources/multimc/scalable/environment-variables.svg rename to launcher/resources/multimc/scalable/datapacks.svg diff --git a/launcher/resources/pe_blue/pe_blue.qrc b/launcher/resources/pe_blue/pe_blue.qrc index 717d3972e..df3a71ab2 100644 --- a/launcher/resources/pe_blue/pe_blue.qrc +++ b/launcher/resources/pe_blue/pe_blue.qrc @@ -10,7 +10,7 @@ scalable/copy.svg scalable/coremods.svg scalable/custom-commands.svg - scalable/environment-variables.svg + scalable/datapacks.svg scalable/externaltools.svg scalable/help.svg scalable/instance-settings.svg diff --git a/launcher/resources/pe_blue/scalable/environment-variables.svg b/launcher/resources/pe_blue/scalable/datapacks.svg similarity index 100% rename from launcher/resources/pe_blue/scalable/environment-variables.svg rename to launcher/resources/pe_blue/scalable/datapacks.svg diff --git a/launcher/resources/pe_colored/pe_colored.qrc b/launcher/resources/pe_colored/pe_colored.qrc index 023c81e74..1a08f7e18 100644 --- a/launcher/resources/pe_colored/pe_colored.qrc +++ b/launcher/resources/pe_colored/pe_colored.qrc @@ -10,7 +10,7 @@ scalable/copy.svg scalable/coremods.svg scalable/custom-commands.svg - scalable/environment-variables.svg + scalable/datapacks.svg scalable/externaltools.svg scalable/help.svg scalable/instance-settings.svg diff --git a/launcher/resources/pe_colored/scalable/environment-variables.svg b/launcher/resources/pe_colored/scalable/datapacks.svg similarity index 100% rename from launcher/resources/pe_colored/scalable/environment-variables.svg rename to launcher/resources/pe_colored/scalable/datapacks.svg diff --git a/launcher/resources/pe_dark/pe_dark.qrc b/launcher/resources/pe_dark/pe_dark.qrc index c97fb469c..9d3d3d46c 100644 --- a/launcher/resources/pe_dark/pe_dark.qrc +++ b/launcher/resources/pe_dark/pe_dark.qrc @@ -10,7 +10,7 @@ scalable/copy.svg scalable/coremods.svg scalable/custom-commands.svg - scalable/environment-variables.svg + scalable/datapacks.svg scalable/externaltools.svg scalable/help.svg scalable/instance-settings.svg diff --git a/launcher/resources/pe_dark/scalable/environment-variables.svg b/launcher/resources/pe_dark/scalable/datapacks.svg similarity index 100% rename from launcher/resources/pe_dark/scalable/environment-variables.svg rename to launcher/resources/pe_dark/scalable/datapacks.svg diff --git a/launcher/resources/pe_light/pe_light.qrc b/launcher/resources/pe_light/pe_light.qrc index b590dd2c6..2775a0872 100644 --- a/launcher/resources/pe_light/pe_light.qrc +++ b/launcher/resources/pe_light/pe_light.qrc @@ -10,7 +10,7 @@ scalable/copy.svg scalable/coremods.svg scalable/custom-commands.svg - scalable/environment-variables.svg + scalable/datapacks.svg scalable/externaltools.svg scalable/help.svg scalable/instance-settings.svg diff --git a/launcher/resources/pe_light/scalable/environment-variables.svg b/launcher/resources/pe_light/scalable/datapacks.svg similarity index 100% rename from launcher/resources/pe_light/scalable/environment-variables.svg rename to launcher/resources/pe_light/scalable/datapacks.svg diff --git a/launcher/ui/pages/instance/DataPackPage.cpp b/launcher/ui/pages/instance/DataPackPage.cpp index 833a72301..ddc12d7b3 100644 --- a/launcher/ui/pages/instance/DataPackPage.cpp +++ b/launcher/ui/pages/instance/DataPackPage.cpp @@ -22,7 +22,7 @@ #include "ui/dialogs/ProgressDialog.h" #include "ui/dialogs/ResourceDownloadDialog.h" -DataPackPage::DataPackPage(MinecraftInstance* instance, std::shared_ptr model, QWidget* parent) +DataPackPage::DataPackPage(BaseInstance* instance, std::shared_ptr model, QWidget* parent) : ExternalResourcesPage(instance, model, parent) { ui->actionDownloadItem->setText(tr("Download packs")); @@ -30,7 +30,7 @@ DataPackPage::DataPackPage(MinecraftInstance* instance, std::shared_ptractionDownloadItem->setEnabled(true); connect(ui->actionDownloadItem, &QAction::triggered, this, &DataPackPage::downloadDataPacks); ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionDownloadItem); - + ui->actionViewConfigs->setVisible(false); } @@ -49,8 +49,7 @@ void DataPackPage::downloadDataPacks() ResourceDownload::DataPackDownloadDialog mdownload(this, std::static_pointer_cast(m_model), m_instance); if (mdownload.exec()) { - auto tasks = - new ConcurrentTask("Download Data Pack", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); + auto tasks = new ConcurrentTask("Download Data Pack", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); connect(tasks, &Task::failed, [this, tasks](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); tasks->deleteLater(); @@ -78,3 +77,95 @@ void DataPackPage::downloadDataPacks() m_model->update(); } } + +GlobalDataPackPage::GlobalDataPackPage(MinecraftInstance* instance, QWidget* parent) : QWidget(parent), m_instance(instance) +{ + auto layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + setLayout(layout); + + connect(instance->settings()->getSetting("GlobalDataPacksEnabled").get(), &Setting::SettingChanged, this, [this] { + updateContent(); + if (m_container != nullptr) + m_container->refreshContainer(); + }); + + connect(instance->settings()->getSetting("GlobalDataPacksPath").get(), &Setting::SettingChanged, this, + &GlobalDataPackPage::updateContent); +} + +QString GlobalDataPackPage::displayName() const +{ + if (m_underlyingPage == nullptr) + return {}; + + return m_underlyingPage->displayName(); +} + +QIcon GlobalDataPackPage::icon() const +{ + if (m_underlyingPage == nullptr) + return {}; + + return m_underlyingPage->icon(); +} + +QString GlobalDataPackPage::helpPage() const +{ + if (m_underlyingPage == nullptr) + return {}; + + return m_underlyingPage->helpPage(); +} + +bool GlobalDataPackPage::shouldDisplay() const +{ + return m_instance->settings()->get("GlobalDataPacksEnabled").toBool(); +} + +bool GlobalDataPackPage::apply() +{ + return m_underlyingPage != nullptr && m_underlyingPage->apply(); +} + +void GlobalDataPackPage::openedImpl() +{ + if (m_underlyingPage != nullptr) + m_underlyingPage->openedImpl(); +} + +void GlobalDataPackPage::closedImpl() +{ + if (m_underlyingPage != nullptr) + m_underlyingPage->closedImpl(); +} + +void GlobalDataPackPage::updateContent() +{ + if (m_underlyingPage != nullptr) { + if (m_container->selectedPage() == this) + m_underlyingPage->closedImpl(); + + m_underlyingPage->apply(); + + layout()->removeWidget(m_underlyingPage); + + delete m_underlyingPage; + m_underlyingPage = nullptr; + } + + if (shouldDisplay()) { + m_underlyingPage = new DataPackPage(m_instance, m_instance->dataPackList()); + + if (m_container->selectedPage() == this) + m_underlyingPage->openedImpl(); + + layout()->addWidget(m_underlyingPage); + } +} + +void GlobalDataPackPage::setParentContainer(BasePageContainer* container) +{ + BasePage::setParentContainer(container); + updateContent(); +} diff --git a/launcher/ui/pages/instance/DataPackPage.h b/launcher/ui/pages/instance/DataPackPage.h index 2ea284e31..781dd1092 100644 --- a/launcher/ui/pages/instance/DataPackPage.h +++ b/launcher/ui/pages/instance/DataPackPage.h @@ -25,9 +25,9 @@ class DataPackPage : public ExternalResourcesPage { Q_OBJECT public: - explicit DataPackPage(MinecraftInstance* instance, std::shared_ptr model, QWidget* parent = 0); + explicit DataPackPage(BaseInstance* instance, std::shared_ptr model, QWidget* parent = nullptr); - QString displayName() const override { return tr("Data packs"); } + QString displayName() const override { return QObject::tr("Data packs"); } QIcon icon() const override { return APPLICATION->getThemedIcon("datapacks"); } QString id() const override { return "datapacks"; } QString helpPage() const override { return "Data-packs"; } @@ -37,3 +37,31 @@ class DataPackPage : public ExternalResourcesPage { void updateFrame(const QModelIndex& current, const QModelIndex& previous) override; void downloadDataPacks(); }; + +/** + * Syncs DataPackPage with GlobalDataPacksPath and shows/hides based on GlobalDataPacksEnabled. + */ +class GlobalDataPackPage : public QWidget, public BasePage { + public: + explicit GlobalDataPackPage(MinecraftInstance* instance, QWidget* parent = nullptr); + + QString displayName() const override; + QIcon icon() const override; + QString id() const override { return "datapacks"; } + QString helpPage() const override; + + bool shouldDisplay() const override; + + bool apply() override; + void openedImpl() override; + void closedImpl() override; + + void setParentContainer(BasePageContainer *container) override; + + private: + void updateContent(); + QVBoxLayout* layout() { return static_cast(QWidget::layout()); } + + MinecraftInstance* m_instance; + DataPackPage* m_underlyingPage = nullptr; +}; \ No newline at end of file diff --git a/launcher/ui/pages/instance/WorldListPage.cpp b/launcher/ui/pages/instance/WorldListPage.cpp index 7cf4a22a5..96d16f681 100644 --- a/launcher/ui/pages/instance/WorldListPage.cpp +++ b/launcher/ui/pages/instance/WorldListPage.cpp @@ -238,8 +238,11 @@ void WorldListPage::on_actionData_Packs_triggered() static_cast(std::max(0.75 * window()->height(), 400.0))); dialog->restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("DataPackDownloadGeometry").toByteArray())); + bool isIndexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); + auto model = std::make_shared(folder, m_inst.get(), isIndexed, true); + auto layout = new QHBoxLayout(dialog); - auto page = new DataPackPage(m_inst.get(), std::make_shared(folder, m_inst.get(), true, true)); + auto page = new DataPackPage(m_inst.get(), std::move(model)); page->setParent(dialog); // HACK: many pages extend QMainWindow; setting the parent manually prevents them from creating a window. layout->addWidget(page); dialog->setLayout(layout); diff --git a/launcher/ui/widgets/MinecraftSettingsWidget.cpp b/launcher/ui/widgets/MinecraftSettingsWidget.cpp index cec7f267f..eefffb73f 100644 --- a/launcher/ui/widgets/MinecraftSettingsWidget.cpp +++ b/launcher/ui/widgets/MinecraftSettingsWidget.cpp @@ -58,6 +58,8 @@ MinecraftSettingsWidget::MinecraftSettingsWidget(MinecraftInstancePtr instance, } m_ui->openGlobalSettingsButton->setVisible(false); + + m_ui->globalDataPacksGroupBox->hide(); } else { m_javaSettings = new JavaSettingsWidget(m_instance, this); m_ui->javaScrollArea->setWidget(m_javaSettings); @@ -97,6 +99,11 @@ MinecraftSettingsWidget::MinecraftSettingsWidget(MinecraftInstancePtr instance, connect(m_ui->openGlobalSettingsButton, &QCommandLinkButton::clicked, this, &MinecraftSettingsWidget::openGlobalSettings); connect(m_ui->serverJoinAddressButton, &QAbstractButton::toggled, m_ui->serverJoinAddress, &QWidget::setEnabled); connect(m_ui->worldJoinButton, &QAbstractButton::toggled, m_ui->worldsCb, &QWidget::setEnabled); + + connect(m_ui->globalDataPacksGroupBox, &QGroupBox::toggled, this, + [this](bool value) { m_instance->settings()->set("GlobalDataPacksEnabled", value); }); + connect(m_ui->dataPacksPathEdit, &QLineEdit::editingFinished, this, + [this]() { m_instance->settings()->set("GlobalDataPacksPath", m_ui->dataPacksPathEdit->text()); }); } m_ui->maximizedWarning->hide(); @@ -231,6 +238,13 @@ void MinecraftSettingsWidget::loadSettings() m_ui->legacySettingsGroupBox->setChecked(settings->get("OverrideLegacySettings").toBool()); m_ui->onlineFixes->setChecked(settings->get("OnlineFixes").toBool()); + + m_ui->globalDataPacksGroupBox->blockSignals(true); + m_ui->dataPacksPathEdit->blockSignals(true); + m_ui->globalDataPacksGroupBox->setChecked(settings->get("GlobalDataPacksEnabled").toBool()); + m_ui->dataPacksPathEdit->setText(settings->get("GlobalDataPacksPath").toString()); + m_ui->globalDataPacksGroupBox->blockSignals(false); + m_ui->dataPacksPathEdit->blockSignals(false); } void MinecraftSettingsWidget::saveSettings() diff --git a/launcher/ui/widgets/MinecraftSettingsWidget.ui b/launcher/ui/widgets/MinecraftSettingsWidget.ui index daa065ac8..61d90255f 100644 --- a/launcher/ui/widgets/MinecraftSettingsWidget.ui +++ b/launcher/ui/widgets/MinecraftSettingsWidget.ui @@ -61,9 +61,9 @@ 0 - -253 + -166 610 - 550 + 768 @@ -149,6 +149,70 @@ + + + + &Global Data Packs + + + true + + + true + + + + + + Allows installing data packs across all worlds if an applicable mod is installed. +It is most likely you will need to change the path - please refer to the mod's website. + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 0 + 6 + + + + + + + + Folder Path + + + + + + + + + datapacks + + + + + + + Browse + + + + + + + + @@ -298,7 +362,7 @@ 0 0 624 - 297 + 291 @@ -323,9 +387,9 @@ 0 - -101 + 0 610 - 398 + 439 @@ -513,7 +577,7 @@ 0 0 624 - 297 + 291 @@ -647,7 +711,6 @@ openGlobalSettingsButton settingsTabs - scrollArea maximizedCheckBox windowWidthSpinBox windowHeightSpinBox From 481a1d222ccd897592953a712c18e94694980eb8 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Tue, 25 Mar 2025 16:31:41 +0000 Subject: [PATCH 059/695] Fix use after free If any page apply methods return false, the launcher breaks when opening an instance window for the second time Signed-off-by: TheKodeToad --- launcher/ui/pages/instance/DataPackPage.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/pages/instance/DataPackPage.cpp b/launcher/ui/pages/instance/DataPackPage.cpp index ddc12d7b3..3ca86850a 100644 --- a/launcher/ui/pages/instance/DataPackPage.cpp +++ b/launcher/ui/pages/instance/DataPackPage.cpp @@ -125,7 +125,7 @@ bool GlobalDataPackPage::shouldDisplay() const bool GlobalDataPackPage::apply() { - return m_underlyingPage != nullptr && m_underlyingPage->apply(); + return m_underlyingPage == nullptr || m_underlyingPage->apply(); } void GlobalDataPackPage::openedImpl() From 46c348a7b4f7d51bafffd4f3c364086b17c81b67 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Tue, 25 Mar 2025 17:18:55 +0000 Subject: [PATCH 060/695] Fix license header emails Signed-off-by: TheKodeToad --- launcher/ui/pages/modplatform/DataPackModel.cpp | 2 +- launcher/ui/pages/modplatform/DataPackModel.h | 2 +- launcher/ui/pages/modplatform/DataPackPage.cpp | 2 +- launcher/ui/pages/modplatform/DataPackPage.h | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/launcher/ui/pages/modplatform/DataPackModel.cpp b/launcher/ui/pages/modplatform/DataPackModel.cpp index c17703d3c..598fe3ca3 100644 --- a/launcher/ui/pages/modplatform/DataPackModel.cpp +++ b/launcher/ui/pages/modplatform/DataPackModel.cpp @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: 2023 flowln -// SPDX-FileCopyrightText: 2023 TheKodeToad +// SPDX-FileCopyrightText: 2023 TheKodeToad // // SPDX-License-Identifier: GPL-3.0-only diff --git a/launcher/ui/pages/modplatform/DataPackModel.h b/launcher/ui/pages/modplatform/DataPackModel.h index 4954b7350..54da2404c 100644 --- a/launcher/ui/pages/modplatform/DataPackModel.h +++ b/launcher/ui/pages/modplatform/DataPackModel.h @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: 2023 flowln -// SPDX-FileCopyrightText: 2023 TheKodeToad +// SPDX-FileCopyrightText: 2023 TheKodeToad // // SPDX-License-Identifier: GPL-3.0-only diff --git a/launcher/ui/pages/modplatform/DataPackPage.cpp b/launcher/ui/pages/modplatform/DataPackPage.cpp index 397a33e96..2b506ca67 100644 --- a/launcher/ui/pages/modplatform/DataPackPage.cpp +++ b/launcher/ui/pages/modplatform/DataPackPage.cpp @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: 2023 flowln -// SPDX-FileCopyrightText: 2023 TheKodeToad +// SPDX-FileCopyrightText: 2023 TheKodeToad // // SPDX-License-Identifier: GPL-3.0-only diff --git a/launcher/ui/pages/modplatform/DataPackPage.h b/launcher/ui/pages/modplatform/DataPackPage.h index 55ed205f8..2e622ebd4 100644 --- a/launcher/ui/pages/modplatform/DataPackPage.h +++ b/launcher/ui/pages/modplatform/DataPackPage.h @@ -1,5 +1,5 @@ // SPDX-FileCopyrightText: 2023 flowln -// SPDX-FileCopyrightText: 2023 TheKodeToad +// SPDX-FileCopyrightText: 2023 TheKodeToad // // SPDX-License-Identifier: GPL-3.0-only From 82978ee34d46c654c306ccb64a99efada9ad01c3 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Tue, 25 Mar 2025 19:23:35 +0000 Subject: [PATCH 061/695] Fix data pack filtering Signed-off-by: TheKodeToad --- launcher/modplatform/ModIndex.cpp | 4 +++- launcher/modplatform/ModIndex.h | 10 +++++++++- launcher/modplatform/flame/FlameAPI.h | 2 ++ launcher/modplatform/modrinth/ModrinthAPI.h | 12 ++++++------ launcher/ui/pages/modplatform/DataPackModel.cpp | 4 ++-- 5 files changed, 22 insertions(+), 10 deletions(-) diff --git a/launcher/modplatform/ModIndex.cpp b/launcher/modplatform/ModIndex.cpp index c3ecccf8e..380ff660f 100644 --- a/launcher/modplatform/ModIndex.cpp +++ b/launcher/modplatform/ModIndex.cpp @@ -122,11 +122,13 @@ auto getModLoaderAsString(ModLoaderType type) -> const QString case Cauldron: return "cauldron"; case LiteLoader: - return "liteloader"; + return "liteloader"; case Fabric: return "fabric"; case Quilt: return "quilt"; + case DataPack: + return "datapack"; default: break; } diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h index 1c8507f12..74024ba0e 100644 --- a/launcher/modplatform/ModIndex.h +++ b/launcher/modplatform/ModIndex.h @@ -30,7 +30,15 @@ class QIODevice; namespace ModPlatform { -enum ModLoaderType { NeoForge = 1 << 0, Forge = 1 << 1, Cauldron = 1 << 2, LiteLoader = 1 << 3, Fabric = 1 << 4, Quilt = 1 << 5 }; +enum ModLoaderType { + NeoForge = 1 << 0, + Forge = 1 << 1, + Cauldron = 1 << 2, + LiteLoader = 1 << 3, + Fabric = 1 << 4, + Quilt = 1 << 5, + DataPack = 1 << 6 +}; Q_DECLARE_FLAGS(ModLoaderTypes, ModLoaderType) QList modLoaderTypesToList(ModLoaderTypes flags); diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index 802c67a35..14e4effec 100644 --- a/launcher/modplatform/flame/FlameAPI.h +++ b/launcher/modplatform/flame/FlameAPI.h @@ -68,6 +68,8 @@ class FlameAPI : public NetworkResourceAPI { return 5; case ModPlatform::NeoForge: return 6; + case ModPlatform::DataPack: + break; // not supported } return 0; } diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index 26e2f423a..17b23723b 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -42,8 +42,8 @@ class ModrinthAPI : public NetworkResourceAPI { static auto getModLoaderStrings(const ModPlatform::ModLoaderTypes types) -> const QStringList { QStringList l; - for (auto loader : - { ModPlatform::NeoForge, ModPlatform::Forge, ModPlatform::Fabric, ModPlatform::Quilt, ModPlatform::LiteLoader }) { + for (auto loader : { ModPlatform::NeoForge, ModPlatform::Forge, ModPlatform::Fabric, ModPlatform::Quilt, ModPlatform::LiteLoader, + ModPlatform::DataPack }) { if (types & loader) { l << getModLoaderAsString(loader); } @@ -103,12 +103,13 @@ class ModrinthAPI : public NetworkResourceAPI { { switch (type) { case ModPlatform::ResourceType::MOD: - case ModPlatform::ResourceType::DATA_PACK: return "mod"; case ModPlatform::ResourceType::RESOURCE_PACK: return "resourcepack"; case ModPlatform::ResourceType::SHADER_PACK: return "shader"; + case ModPlatform::ResourceType::DATA_PACK: + return "datapack"; case ModPlatform::ResourceType::MODPACK: return "modpack"; default: @@ -127,8 +128,6 @@ class ModrinthAPI : public NetworkResourceAPI { facets_list.append(QString("[%1]").arg(getModLoaderFilters(args.loaders.value()))); if (args.versions.has_value() && !args.versions.value().empty()) facets_list.append(QString("[%1]").arg(getGameVersionsArray(args.versions.value()))); - if (args.type == ModPlatform::ResourceType::DATA_PACK) - facets_list.append("[\"categories:datapack\"]"); if (args.side.has_value()) { auto side = getSideFilters(args.side.value()); if (!side.isEmpty()) @@ -200,7 +199,8 @@ class ModrinthAPI : public NetworkResourceAPI { static inline auto validateModLoaders(ModPlatform::ModLoaderTypes loaders) -> bool { - return loaders & (ModPlatform::NeoForge | ModPlatform::Forge | ModPlatform::Fabric | ModPlatform::Quilt | ModPlatform::LiteLoader); + return loaders & (ModPlatform::NeoForge | ModPlatform::Forge | ModPlatform::Fabric | ModPlatform::Quilt | ModPlatform::LiteLoader | + ModPlatform::DataPack); } [[nodiscard]] std::optional getDependencyURL(DependencySearchArgs const& args) const override diff --git a/launcher/ui/pages/modplatform/DataPackModel.cpp b/launcher/ui/pages/modplatform/DataPackModel.cpp index 598fe3ca3..4b537cda9 100644 --- a/launcher/ui/pages/modplatform/DataPackModel.cpp +++ b/launcher/ui/pages/modplatform/DataPackModel.cpp @@ -18,13 +18,13 @@ DataPackResourceModel::DataPackResourceModel(BaseInstance const& base_inst, Reso ResourceAPI::SearchArgs DataPackResourceModel::createSearchArguments() { auto sort = getCurrentSortingMethodByIndex(); - return { ModPlatform::ResourceType::DATA_PACK, m_next_search_offset, m_search_term, sort }; + return { ModPlatform::ResourceType::DATA_PACK, m_next_search_offset, m_search_term, sort, ModPlatform::ModLoaderType::DataPack }; } ResourceAPI::VersionSearchArgs DataPackResourceModel::createVersionsArguments(QModelIndex& entry) { auto& pack = m_packs[entry.row()]; - return { *pack }; + return { *pack, {}, ModPlatform::ModLoaderType::DataPack }; } ResourceAPI::ProjectInfoArgs DataPackResourceModel::createInfoArguments(QModelIndex& entry) From ed96f2064bf75373d72baab6a3901cd0c52558c1 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Tue, 25 Mar 2025 23:23:49 +0000 Subject: [PATCH 062/695] Display selected count and add buttons to dialog Signed-off-by: TheKodeToad --- launcher/ui/pages/instance/DataPackPage.cpp | 3 +- launcher/ui/pages/instance/WorldListPage.cpp | 42 ++++++++++++++------ 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/launcher/ui/pages/instance/DataPackPage.cpp b/launcher/ui/pages/instance/DataPackPage.cpp index 3ca86850a..f99d618b0 100644 --- a/launcher/ui/pages/instance/DataPackPage.cpp +++ b/launcher/ui/pages/instance/DataPackPage.cpp @@ -156,6 +156,8 @@ void GlobalDataPackPage::updateContent() if (shouldDisplay()) { m_underlyingPage = new DataPackPage(m_instance, m_instance->dataPackList()); + m_underlyingPage->setParentContainer(m_container); + m_underlyingPage->updateExtraInfo = [this](QString id, QString value) { updateExtraInfo(std::move(id), std::move(value)); }; if (m_container->selectedPage() == this) m_underlyingPage->openedImpl(); @@ -163,7 +165,6 @@ void GlobalDataPackPage::updateContent() layout()->addWidget(m_underlyingPage); } } - void GlobalDataPackPage::setParentContainer(BasePageContainer* container) { BasePage::setParentContainer(container); diff --git a/launcher/ui/pages/instance/WorldListPage.cpp b/launcher/ui/pages/instance/WorldListPage.cpp index 96d16f681..f1e00a799 100644 --- a/launcher/ui/pages/instance/WorldListPage.cpp +++ b/launcher/ui/pages/instance/WorldListPage.cpp @@ -40,7 +40,9 @@ #include "ui/dialogs/CustomMessageBox.h" #include "ui_WorldListPage.h" +#include #include +#include #include #include #include @@ -49,6 +51,7 @@ #include #include #include +#include #include "FileSystem.h" #include "tools/MCEditTool.h" @@ -230,7 +233,7 @@ void WorldListPage::on_actionData_Packs_triggered() const QString fullPath = m_worlds->data(index, WorldList::FolderRole).toString(); const QString folder = FS::PathCombine(fullPath, "datapacks"); - auto dialog = new QDialog(window()); + auto dialog = new QDialog(this); dialog->setWindowTitle(tr("Data packs for %1").arg(m_worlds->data(index, WorldList::NameRole).toString())); dialog->setWindowModality(Qt::WindowModal); @@ -238,22 +241,35 @@ void WorldListPage::on_actionData_Packs_triggered() static_cast(std::max(0.75 * window()->height(), 400.0))); dialog->restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("DataPackDownloadGeometry").toByteArray())); - bool isIndexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); - auto model = std::make_shared(folder, m_inst.get(), isIndexed, true); + GenericPageProvider provider(dialog->windowTitle()); + + provider.addPageCreator([this, folder] { + bool isIndexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); + auto model = std::make_shared(folder, m_inst.get(), isIndexed, true); + return new DataPackPage(m_inst.get(), std::move(model)); + }); + + auto layout = new QVBoxLayout(dialog); + + auto focusStealer = new QPushButton(dialog); + layout->addWidget(focusStealer); + focusStealer->setDefault(true); + focusStealer->hide(); + + auto pageContainer = new PageContainer(&provider, {}, dialog); + pageContainer->hidePageList(); + layout->addWidget(pageContainer); + + auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Close | QDialogButtonBox::Help); + connect(buttonBox, &QDialogButtonBox::accepted, dialog, &QDialog::accept); + connect(buttonBox, &QDialogButtonBox::helpRequested, pageContainer, &PageContainer::help); + layout->addWidget(buttonBox); - auto layout = new QHBoxLayout(dialog); - auto page = new DataPackPage(m_inst.get(), std::move(model)); - page->setParent(dialog); // HACK: many pages extend QMainWindow; setting the parent manually prevents them from creating a window. - layout->addWidget(page); dialog->setLayout(layout); - connect(dialog, &QDialog::finished, this, [dialog, page] { - APPLICATION->settings()->set("DataPackDownloadGeometry", dialog->saveGeometry().toBase64()); - page->closed(); - }); + dialog->exec(); - dialog->show(); - page->opened(); + APPLICATION->settings()->set("DataPackDownloadGeometry", dialog->saveGeometry().toBase64()); } void WorldListPage::on_actionReset_Icon_triggered() From ea82d44aab908e830126b778124fc76a4ba45210 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Wed, 26 Mar 2025 00:30:00 +0000 Subject: [PATCH 063/695] Allow absolute path for global data packs Signed-off-by: TheKodeToad --- launcher/minecraft/MinecraftInstance.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index ab4219627..8cb0634a3 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -405,7 +405,7 @@ QString MinecraftInstance::dataPacksDir() if (relativePath.isEmpty()) relativePath = "datapacks"; - return FS::PathCombine(gameRoot(), relativePath); + return QDir(gameRoot()).filePath(relativePath); } QString MinecraftInstance::resourcePacksDir() const From 5bdc0b38710f329684bae3abac4dc6f6cc076cd8 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Wed, 26 Mar 2025 01:04:04 +0000 Subject: [PATCH 064/695] Implement browse for global data pack folder Signed-off-by: TheKodeToad --- .../ui/widgets/MinecraftSettingsWidget.cpp | 24 ++++++++++++++++++- launcher/ui/widgets/MinecraftSettingsWidget.h | 1 + 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/launcher/ui/widgets/MinecraftSettingsWidget.cpp b/launcher/ui/widgets/MinecraftSettingsWidget.cpp index eefffb73f..4cc30411e 100644 --- a/launcher/ui/widgets/MinecraftSettingsWidget.cpp +++ b/launcher/ui/widgets/MinecraftSettingsWidget.cpp @@ -36,6 +36,7 @@ */ #include "MinecraftSettingsWidget.h" +#include #include "Application.h" #include "BuildConfig.h" @@ -103,7 +104,8 @@ MinecraftSettingsWidget::MinecraftSettingsWidget(MinecraftInstancePtr instance, connect(m_ui->globalDataPacksGroupBox, &QGroupBox::toggled, this, [this](bool value) { m_instance->settings()->set("GlobalDataPacksEnabled", value); }); connect(m_ui->dataPacksPathEdit, &QLineEdit::editingFinished, this, - [this]() { m_instance->settings()->set("GlobalDataPacksPath", m_ui->dataPacksPathEdit->text()); }); + [this] { m_instance->settings()->set("GlobalDataPacksPath", m_ui->dataPacksPathEdit->text()); }); + connect(m_ui->dataPacksPathBrowse, &QPushButton::clicked, this, &MinecraftSettingsWidget::selectDataPacksFolder); } m_ui->maximizedWarning->hide(); @@ -472,3 +474,23 @@ bool MinecraftSettingsWidget::isQuickPlaySupported() { return m_instance->traits().contains("feature:is_quick_play_singleplayer"); } + +void MinecraftSettingsWidget::selectDataPacksFolder() +{ + QString path = QFileDialog::getExistingDirectory(this, tr("Select Global Data Packs Folder"), m_instance->instanceRoot()); + + if (path.isEmpty()) + return; + + // if it's inside the instance dir, set path relative to .minecraft + // (so that if it's directly in instance dir it will still lead with .. but more than two levels up are kept absolute) + + const QUrl instanceRootUrl = QUrl::fromLocalFile(m_instance->instanceRoot()); + const QUrl pathUrl = QUrl::fromLocalFile(path); + + if (instanceRootUrl.isParentOf(pathUrl)) + path = QDir(m_instance->gameRoot()).relativeFilePath(path); + + m_ui->dataPacksPathEdit->setText(path); + m_instance->settings()->set("GlobalDataPacksPath", path); +} diff --git a/launcher/ui/widgets/MinecraftSettingsWidget.h b/launcher/ui/widgets/MinecraftSettingsWidget.h index 86effb337..0f9e35b9c 100644 --- a/launcher/ui/widgets/MinecraftSettingsWidget.h +++ b/launcher/ui/widgets/MinecraftSettingsWidget.h @@ -56,6 +56,7 @@ class MinecraftSettingsWidget : public QWidget { void openGlobalSettings(); void updateAccountsMenu(const SettingsObject& settings); bool isQuickPlaySupported(); + void selectDataPacksFolder(); MinecraftInstancePtr m_instance; Ui::MinecraftSettingsWidget* m_ui; From e22930fabf8e621f249fa587272712d4f5128eb8 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Wed, 26 Mar 2025 01:08:05 +0000 Subject: [PATCH 065/695] Fix build Signed-off-by: TheKodeToad --- .../mod/tasks/LocalResourcePackParseTask.cpp | 192 ------------------ .../mod/tasks/LocalResourcePackParseTask.h | 36 ---- .../ui/widgets/MinecraftSettingsWidget.cpp | 4 +- tests/MetaComponentParse_test.cpp | 3 +- 4 files changed, 4 insertions(+), 231 deletions(-) delete mode 100644 launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp delete mode 100644 launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h diff --git a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp deleted file mode 100644 index db4b2e55c..000000000 --- a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp +++ /dev/null @@ -1,192 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - 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 . - */ - -#include "LocalResourcePackParseTask.h" - -#include "FileSystem.h" -#include "Json.h" -#include "minecraft/mod/tasks/LocalDataPackParseTask.h" - -#include -#include -#include - -#include - -namespace ResourcePackUtils { - -QString buildStyle(const QJsonObject& obj) -{ - QStringList styles; - if (auto color = Json::ensureString(obj, "color"); !color.isEmpty()) { - styles << QString("color: %1;").arg(color); - } - if (obj.contains("bold")) { - QString weight = "normal"; - if (Json::ensureBoolean(obj, "bold", false)) { - weight = "bold"; - } - styles << QString("font-weight: %1;").arg(weight); - } - if (obj.contains("italic")) { - QString style = "normal"; - if (Json::ensureBoolean(obj, "italic", false)) { - style = "italic"; - } - styles << QString("font-style: %1;").arg(style); - } - - return styles.isEmpty() ? "" : QString("style=\"%1\"").arg(styles.join(" ")); -} - -QString processComponent(const QJsonArray& value, bool strikethrough, bool underline) -{ - QString result; - for (auto current : value) - result += processComponent(current, strikethrough, underline); - return result; -} - -QString processComponent(const QJsonObject& obj, bool strikethrough, bool underline) -{ - underline = Json::ensureBoolean(obj, "underlined", underline); - strikethrough = Json::ensureBoolean(obj, "strikethrough", strikethrough); - - QString result = Json::ensureString(obj, "text"); - if (underline) { - result = QString("%1").arg(result); - } - if (strikethrough) { - result = QString("%1").arg(result); - } - // the extra needs to be a array - result += processComponent(Json::ensureArray(obj, "extra"), strikethrough, underline); - if (auto style = buildStyle(obj); !style.isEmpty()) { - result = QString("%2").arg(style, result); - } - if (obj.contains("clickEvent")) { - auto click_event = Json::ensureObject(obj, "clickEvent"); - auto action = Json::ensureString(click_event, "action"); - auto value = Json::ensureString(click_event, "value"); - if (action == "open_url" && !value.isEmpty()) { - result = QString("%2").arg(value, result); - } - } - return result; -} - -QString processComponent(const QJsonValue& value, bool strikethrough, bool underline) -{ - if (value.isString()) { - return value.toString(); - } - if (value.isBool()) { - return value.toBool() ? "true" : "false"; - } - if (value.isDouble()) { - return QString::number(value.toDouble()); - } - if (value.isArray()) { - return processComponent(value.toArray(), strikethrough, underline); - } - if (value.isObject()) { - return processComponent(value.toObject(), strikethrough, underline); - } - qWarning() << "Invalid component type!"; - return {}; -} - -bool processPackPNG(const ResourcePack* pack, QByteArray&& raw_data) -{ - auto img = QImage::fromData(raw_data); - if (!img.isNull()) { - pack->setImage(img); - } else { - qWarning() << "Failed to parse pack.png."; - return false; - } - return true; -} - -bool processPackPNG(const ResourcePack* pack) -{ - auto png_invalid = [&pack]() { - qWarning() << "Resource pack at" << pack->fileinfo().filePath() << "does not have a valid pack.png"; - return false; - }; - - switch (pack->type()) { - case ResourceType::FOLDER: { - QFileInfo image_file_info(FS::PathCombine(pack->fileinfo().filePath(), "pack.png")); - if (image_file_info.exists() && image_file_info.isFile()) { - QFile pack_png_file(image_file_info.filePath()); - if (!pack_png_file.open(QIODevice::ReadOnly)) - return png_invalid(); // can't open pack.png file - - auto data = pack_png_file.readAll(); - - bool pack_png_result = ResourcePackUtils::processPackPNG(pack, std::move(data)); - - pack_png_file.close(); - if (!pack_png_result) { - return png_invalid(); // pack.png invalid - } - } else { - return png_invalid(); // pack.png does not exists or is not a valid file. - } - return false; // not processed correctly; https://github.com/PrismLauncher/PrismLauncher/issues/1740 - } - case ResourceType::ZIPFILE: { - QuaZip zip(pack->fileinfo().filePath()); - if (!zip.open(QuaZip::mdUnzip)) - return false; // can't open zip file - - QuaZipFile file(&zip); - if (zip.setCurrentFile("pack.png")) { - if (!file.open(QIODevice::ReadOnly)) { - qCritical() << "Failed to open file in zip."; - zip.close(); - return png_invalid(); - } - - auto data = file.readAll(); - - bool pack_png_result = ResourcePackUtils::processPackPNG(pack, std::move(data)); - - file.close(); - if (!pack_png_result) { - return png_invalid(); // pack.png invalid - } - } else { - return png_invalid(); // could not set pack.mcmeta as current file. - } - return false; // not processed correctly; https://github.com/PrismLauncher/PrismLauncher/issues/1740 - } - default: - qWarning() << "Invalid type for resource pack parse task!"; - return false; - } -} - -bool validate(QFileInfo file) -{ - ResourcePack rp{ file }; - return DataPackUtils::process(&rp, DataPackUtils::ProcessingLevel::BasicInfoOnly) && rp.valid(); -} - -} // namespace ResourcePackUtils diff --git a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h deleted file mode 100644 index 6b4378aa6..000000000 --- a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h +++ /dev/null @@ -1,36 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - 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 . - */ - -#pragma once - -#include -#include - -#include "minecraft/mod/ResourcePack.h" - -namespace ResourcePackUtils { - -QString processComponent(const QJsonValue& value, bool strikethrough = false, bool underline = false); -bool processPackPNG(const ResourcePack* pack, QByteArray&& raw_data); - -/// processes ONLY the pack.png (rest of the pack may be invalid) -bool processPackPNG(const ResourcePack* pack); - -/** Checks whether a file is valid as a resource pack or not. */ -bool validate(QFileInfo file); -} // namespace ResourcePackUtils diff --git a/launcher/ui/widgets/MinecraftSettingsWidget.cpp b/launcher/ui/widgets/MinecraftSettingsWidget.cpp index 4cc30411e..b53c870f7 100644 --- a/launcher/ui/widgets/MinecraftSettingsWidget.cpp +++ b/launcher/ui/widgets/MinecraftSettingsWidget.cpp @@ -36,14 +36,14 @@ */ #include "MinecraftSettingsWidget.h" -#include +#include "ui_MinecraftSettingsWidget.h" +#include #include "Application.h" #include "BuildConfig.h" #include "minecraft/WorldList.h" #include "minecraft/auth/AccountList.h" #include "settings/Setting.h" -#include "ui_MinecraftSettingsWidget.h" MinecraftSettingsWidget::MinecraftSettingsWidget(MinecraftInstancePtr instance, QWidget* parent) : QWidget(parent), m_instance(std::move(instance)), m_ui(new Ui::MinecraftSettingsWidget) diff --git a/tests/MetaComponentParse_test.cpp b/tests/MetaComponentParse_test.cpp index 9979a9fa6..e8c1d3226 100644 --- a/tests/MetaComponentParse_test.cpp +++ b/tests/MetaComponentParse_test.cpp @@ -41,6 +41,7 @@ #include +#include #include class MetaComponentParseTest : public QObject { @@ -69,7 +70,7 @@ class MetaComponentParseTest : public QObject { QString expected = expected_json.toString(); - QString processed = ResourcePackUtils::processComponent(description_json); + QString processed = DataPackUtils::processComponent(description_json); QCOMPARE(processed, expected); } From 9c920fbad9ebdbc00cab4ed05740916d537d9d88 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Wed, 26 Mar 2025 01:10:10 +0000 Subject: [PATCH 066/695] Open FileDialog in game root Signed-off-by: TheKodeToad --- launcher/ui/widgets/MinecraftSettingsWidget.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/widgets/MinecraftSettingsWidget.cpp b/launcher/ui/widgets/MinecraftSettingsWidget.cpp index b53c870f7..79ffccce8 100644 --- a/launcher/ui/widgets/MinecraftSettingsWidget.cpp +++ b/launcher/ui/widgets/MinecraftSettingsWidget.cpp @@ -477,7 +477,7 @@ bool MinecraftSettingsWidget::isQuickPlaySupported() void MinecraftSettingsWidget::selectDataPacksFolder() { - QString path = QFileDialog::getExistingDirectory(this, tr("Select Global Data Packs Folder"), m_instance->instanceRoot()); + QString path = QFileDialog::getExistingDirectory(this, tr("Select Global Data Packs Folder"), m_instance->gameRoot()); if (path.isEmpty()) return; From 683df62980c91b72ca562bf823fa71636f2c5c1d Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Wed, 26 Mar 2025 01:30:06 +0000 Subject: [PATCH 067/695] Implement data pack updating Signed-off-by: TheKodeToad --- launcher/ui/pages/instance/DataPackPage.cpp | 183 +++++++++++++++++++- launcher/ui/pages/instance/DataPackPage.h | 7 + 2 files changed, 184 insertions(+), 6 deletions(-) diff --git a/launcher/ui/pages/instance/DataPackPage.cpp b/launcher/ui/pages/instance/DataPackPage.cpp index f99d618b0..b9f23bac0 100644 --- a/launcher/ui/pages/instance/DataPackPage.cpp +++ b/launcher/ui/pages/instance/DataPackPage.cpp @@ -17,28 +17,46 @@ */ #include "DataPackPage.h" +#include #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/ProgressDialog.h" #include "ui/dialogs/ResourceDownloadDialog.h" DataPackPage::DataPackPage(BaseInstance* instance, std::shared_ptr model, QWidget* parent) - : ExternalResourcesPage(instance, model, parent) + : ExternalResourcesPage(instance, model, parent), m_model(model) { - ui->actionDownloadItem->setText(tr("Download packs")); - ui->actionDownloadItem->setToolTip(tr("Download data packs from online platforms")); + ui->actionDownloadItem->setText(tr("Download Packs")); + ui->actionDownloadItem->setToolTip(tr("Download data packs from online mod platforms")); ui->actionDownloadItem->setEnabled(true); - connect(ui->actionDownloadItem, &QAction::triggered, this, &DataPackPage::downloadDataPacks); ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionDownloadItem); - ui->actionViewConfigs->setVisible(false); + connect(ui->actionDownloadItem, &QAction::triggered, this, &DataPackPage::downloadDataPacks); + + ui->actionUpdateItem->setToolTip(tr("Try to check or update all selected data packs (all data packs if none are selected)")); + connect(ui->actionUpdateItem, &QAction::triggered, this, &DataPackPage::updateDataPacks); + ui->actionsToolbar->insertActionBefore(ui->actionAddItem, ui->actionUpdateItem); + + auto updateMenu = new QMenu(this); + + auto update = updateMenu->addAction(ui->actionUpdateItem->text()); + connect(update, &QAction::triggered, this, &DataPackPage::updateDataPacks); + + updateMenu->addAction(ui->actionResetItemMetadata); + connect(ui->actionResetItemMetadata, &QAction::triggered, this, &DataPackPage::deleteDataPackMetadata); + + ui->actionUpdateItem->setMenu(updateMenu); + + ui->actionChangeVersion->setToolTip(tr("Change a data pack's version.")); + connect(ui->actionChangeVersion, &QAction::triggered, this, &DataPackPage::changeDataPackVersion); + ui->actionsToolbar->insertActionAfter(ui->actionUpdateItem, ui->actionChangeVersion); } void DataPackPage::updateFrame(const QModelIndex& current, [[maybe_unused]] const QModelIndex& previous) { auto sourceCurrent = m_filterModel->mapToSource(current); int row = sourceCurrent.row(); - auto& dp = static_cast(m_model->at(row)); + auto& dp = m_model->at(row); ui->frame->updateWithDataPack(dp); } @@ -78,6 +96,159 @@ void DataPackPage::downloadDataPacks() } } + +void DataPackPage::updateDataPacks() +{ + if (m_instance->typeName() != "Minecraft") + return; // this is a null instance or a legacy instance + + auto profile = static_cast(m_instance)->getPackProfile(); + if (APPLICATION->settings()->get("ModMetadataDisabled").toBool()) { + QMessageBox::critical(this, tr("Error"), tr("Data pack updates are unavailable when metadata is disabled!")); + return; + } + if (m_instance != nullptr && m_instance->isRunning()) { + auto response = CustomMessageBox::selectable( + this, tr("Confirm Update"), + tr("Updating data packs while the game is running may cause pack duplication and game crashes.\n" + "The old files may not be deleted as they are in use.\n" + "Are you sure you want to do this?"), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); + + if (response != QMessageBox::Yes) + return; + } + auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); + + auto mods_list = m_model->selectedResources(selection); + bool use_all = mods_list.empty(); + if (use_all) + mods_list = m_model->allResources(); + + ResourceUpdateDialog update_dialog(this, m_instance, m_model, mods_list, false, false); + update_dialog.checkCandidates(); + + if (update_dialog.aborted()) { + CustomMessageBox::selectable(this, tr("Aborted"), tr("The data pack updater was aborted!"), QMessageBox::Warning)->show(); + return; + } + if (update_dialog.noUpdates()) { + QString message{ tr("'%1' is up-to-date! :)").arg(mods_list.front()->name()) }; + if (mods_list.size() > 1) { + if (use_all) { + message = tr("All data packs are up-to-date! :)"); + } else { + message = tr("All selected data packs are up-to-date! :)"); + } + } + CustomMessageBox::selectable(this, tr("Update checker"), message)->exec(); + return; + } + + if (update_dialog.exec()) { + auto tasks = new ConcurrentTask("Download Data Packs", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); + connect(tasks, &Task::failed, [this, tasks](QString reason) { + CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); + tasks->deleteLater(); + }); + connect(tasks, &Task::aborted, [this, tasks]() { + CustomMessageBox::selectable(this, tr("Aborted"), tr("Download stopped by user."), QMessageBox::Information)->show(); + tasks->deleteLater(); + }); + connect(tasks, &Task::succeeded, [this, tasks]() { + QStringList warnings = tasks->warnings(); + if (warnings.count()) { + CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); + } + tasks->deleteLater(); + }); + + for (auto task : update_dialog.getTasks()) { + tasks->addTask(task); + } + + ProgressDialog loadDialog(this); + loadDialog.setSkipButton(true, tr("Abort")); + loadDialog.execWithTask(tasks); + + m_model->update(); + } +} + +void DataPackPage::deleteDataPackMetadata() +{ + auto selection = m_filterModel->mapSelectionToSource(ui->treeView->selectionModel()->selection()).indexes(); + auto selectionCount = m_model->selectedDataPacks(selection).length(); + if (selectionCount == 0) + return; + if (selectionCount > 1) { + auto response = CustomMessageBox::selectable(this, tr("Confirm Removal"), + tr("You are about to remove the metadata for %1 data packs.\n" + "Are you sure?") + .arg(selectionCount), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); + + if (response != QMessageBox::Yes) + return; + } + + m_model->deleteMetadata(selection); +} + +void DataPackPage::changeDataPackVersion() +{ + if (m_instance->typeName() != "Minecraft") + return; // this is a null instance or a legacy instance + + if (APPLICATION->settings()->get("ModMetadataDisabled").toBool()) { + QMessageBox::critical(this, tr("Error"), tr("Data pack updates are unavailable when metadata is disabled!")); + return; + } + + const QModelIndexList rows = ui->treeView->selectionModel()->selectedRows(); + + if (rows.count() != 1) + return; + + Resource& resource = m_model->at(m_filterModel->mapToSource(rows[0]).row()); + + if (resource.metadata() == nullptr) + return; + + ResourceDownload::DataPackDownloadDialog mdownload(this, m_model, m_instance); + mdownload.setResourceMetadata(resource.metadata()); + if (mdownload.exec()) { + auto tasks = new ConcurrentTask("Download Data Packs", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); + connect(tasks, &Task::failed, [this, tasks](QString reason) { + CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); + tasks->deleteLater(); + }); + connect(tasks, &Task::aborted, [this, tasks]() { + CustomMessageBox::selectable(this, tr("Aborted"), tr("Download stopped by user."), QMessageBox::Information)->show(); + tasks->deleteLater(); + }); + connect(tasks, &Task::succeeded, [this, tasks]() { + QStringList warnings = tasks->warnings(); + if (warnings.count()) + CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); + + tasks->deleteLater(); + }); + + for (auto& task : mdownload.getTasks()) { + tasks->addTask(task); + } + + ProgressDialog loadDialog(this); + loadDialog.setSkipButton(true, tr("Abort")); + loadDialog.execWithTask(tasks); + + m_model->update(); + } +} + GlobalDataPackPage::GlobalDataPackPage(MinecraftInstance* instance, QWidget* parent) : QWidget(parent), m_instance(instance) { auto layout = new QVBoxLayout(this); diff --git a/launcher/ui/pages/instance/DataPackPage.h b/launcher/ui/pages/instance/DataPackPage.h index 781dd1092..80eda1602 100644 --- a/launcher/ui/pages/instance/DataPackPage.h +++ b/launcher/ui/pages/instance/DataPackPage.h @@ -36,6 +36,13 @@ class DataPackPage : public ExternalResourcesPage { public slots: void updateFrame(const QModelIndex& current, const QModelIndex& previous) override; void downloadDataPacks(); + void updateDataPacks(); + void deleteDataPackMetadata(); + void changeDataPackVersion(); + + private: + std::shared_ptr m_model; + }; /** From 7bac7f7b2b6aa906fd09b831cb052475fc15f6ea Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Wed, 26 Mar 2025 01:32:17 +0000 Subject: [PATCH 068/695] Actually fix build? Signed-off-by: TheKodeToad --- tests/MetaComponentParse_test.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/MetaComponentParse_test.cpp b/tests/MetaComponentParse_test.cpp index e8c1d3226..c5c41388b 100644 --- a/tests/MetaComponentParse_test.cpp +++ b/tests/MetaComponentParse_test.cpp @@ -42,7 +42,6 @@ #include #include -#include class MetaComponentParseTest : public QObject { Q_OBJECT From 36ceaebcf0b6f74e5e8fc4d286ad6dbbebdbfa26 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Wed, 26 Mar 2025 01:50:05 +0000 Subject: [PATCH 069/695] pls compile Signed-off-by: TheKodeToad --- tests/ResourcePackParse_test.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/ResourcePackParse_test.cpp b/tests/ResourcePackParse_test.cpp index 17c0482fc..e9b5244ad 100644 --- a/tests/ResourcePackParse_test.cpp +++ b/tests/ResourcePackParse_test.cpp @@ -23,7 +23,6 @@ #include #include -#include class ResourcePackParseTest : public QObject { Q_OBJECT From 221365f05b9d816c49721789edd10fc1d3d1f6f0 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Wed, 26 Mar 2025 10:12:13 +0000 Subject: [PATCH 070/695] Make requested changes Signed-off-by: TheKodeToad --- launcher/minecraft/mod/DataPackFolderModel.cpp | 13 ++++++------- launcher/ui/pages/instance/ExternalResourcesPage.h | 1 - 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/launcher/minecraft/mod/DataPackFolderModel.cpp b/launcher/minecraft/mod/DataPackFolderModel.cpp index c94f61fc2..45cf1271f 100644 --- a/launcher/minecraft/mod/DataPackFolderModel.cpp +++ b/launcher/minecraft/mod/DataPackFolderModel.cpp @@ -47,7 +47,8 @@ #include "minecraft/mod/tasks/LocalDataPackParseTask.h" -DataPackFolderModel::DataPackFolderModel(const QString& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent) : ResourceFolderModel(QDir(dir), instance, is_indexed, create_dir, parent) +DataPackFolderModel::DataPackFolderModel(const QString& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent) + : ResourceFolderModel(QDir(dir), instance, is_indexed, create_dir, parent) { m_column_names = QStringList({ "Enable", "Image", "Name", "Pack Format", "Last Modified" }); m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Pack Format"), tr("Last Modified") }); @@ -123,12 +124,10 @@ QVariant DataPackFolderModel::data(const QModelIndex& index, int role) const } return {}; case Qt::CheckStateRole: - switch (column) { - case ActiveColumn: - return at(row).enabled() ? Qt::Checked : Qt::Unchecked; - default: - return {}; - } + if (column == ActiveColumn) + return at(row).enabled() ? Qt::Checked : Qt::Unchecked; + else + return {}; default: return {}; } diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.h b/launcher/ui/pages/instance/ExternalResourcesPage.h index d9077d7e6..00bb5d17d 100644 --- a/launcher/ui/pages/instance/ExternalResourcesPage.h +++ b/launcher/ui/pages/instance/ExternalResourcesPage.h @@ -1,6 +1,5 @@ #pragma once -#include #include #include From 1d1480f4706d6ae11cfc34ce92ba735891faa10b Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Wed, 26 Mar 2025 13:24:10 +0000 Subject: [PATCH 071/695] Filter for datapack loader in datapack update too Signed-off-by: TheKodeToad --- launcher/ui/dialogs/ResourceUpdateDialog.cpp | 15 ++++----------- launcher/ui/dialogs/ResourceUpdateDialog.h | 4 ++-- launcher/ui/pages/instance/DataPackPage.cpp | 17 ++++++++--------- launcher/ui/pages/instance/ModFolderPage.cpp | 2 +- launcher/ui/pages/instance/ResourcePackPage.cpp | 2 +- launcher/ui/pages/instance/ShaderPackPage.cpp | 2 +- launcher/ui/pages/instance/TexturePackPage.cpp | 2 +- 7 files changed, 18 insertions(+), 26 deletions(-) diff --git a/launcher/ui/dialogs/ResourceUpdateDialog.cpp b/launcher/ui/dialogs/ResourceUpdateDialog.cpp index 7e29e1192..f2b64a60f 100644 --- a/launcher/ui/dialogs/ResourceUpdateDialog.cpp +++ b/launcher/ui/dialogs/ResourceUpdateDialog.cpp @@ -32,17 +32,12 @@ static std::list mcVersions(BaseInstance* inst) return { static_cast(inst)->getPackProfile()->getComponent("net.minecraft")->getVersion() }; } -static QList mcLoadersList(BaseInstance* inst) -{ - return static_cast(inst)->getPackProfile()->getModLoadersList(); -} - ResourceUpdateDialog::ResourceUpdateDialog(QWidget* parent, BaseInstance* instance, const std::shared_ptr resource_model, QList& search_for, bool include_deps, - bool filter_loaders) + QList loadersList) : ReviewMessageBox(parent, tr("Confirm resources to update"), "") , m_parent(parent) , m_resource_model(resource_model) @@ -50,7 +45,7 @@ ResourceUpdateDialog::ResourceUpdateDialog(QWidget* parent, , m_second_try_metadata(new ConcurrentTask("Second Metadata Search", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt())) , m_instance(instance) , m_include_deps(include_deps) - , m_filter_loaders(filter_loaders) + , m_loadersList(std::move(loadersList)) { ReviewMessageBox::setGeometry(0, 0, 800, 600); @@ -89,12 +84,10 @@ void ResourceUpdateDialog::checkCandidates() } auto versions = mcVersions(m_instance); - auto loadersList = m_filter_loaders ? mcLoadersList(m_instance) : QList(); - SequentialTask check_task(tr("Checking for updates")); if (!m_modrinth_to_update.empty()) { - m_modrinth_check_task.reset(new ModrinthCheckUpdate(m_modrinth_to_update, versions, loadersList, m_resource_model)); + m_modrinth_check_task.reset(new ModrinthCheckUpdate(m_modrinth_to_update, versions, m_loadersList, m_resource_model)); connect(m_modrinth_check_task.get(), &CheckUpdateTask::checkFailed, this, [this](Resource* resource, QString reason, QUrl recover_url) { m_failed_check_update.append({ resource, reason, recover_url }); @@ -103,7 +96,7 @@ void ResourceUpdateDialog::checkCandidates() } if (!m_flame_to_update.empty()) { - m_flame_check_task.reset(new FlameCheckUpdate(m_flame_to_update, versions, loadersList, m_resource_model)); + m_flame_check_task.reset(new FlameCheckUpdate(m_flame_to_update, versions, m_loadersList, m_resource_model)); connect(m_flame_check_task.get(), &CheckUpdateTask::checkFailed, this, [this](Resource* resource, QString reason, QUrl recover_url) { m_failed_check_update.append({ resource, reason, recover_url }); diff --git a/launcher/ui/dialogs/ResourceUpdateDialog.h b/launcher/ui/dialogs/ResourceUpdateDialog.h index de1d845d2..aef11c90f 100644 --- a/launcher/ui/dialogs/ResourceUpdateDialog.h +++ b/launcher/ui/dialogs/ResourceUpdateDialog.h @@ -21,7 +21,7 @@ class ResourceUpdateDialog final : public ReviewMessageBox { std::shared_ptr resource_model, QList& search_for, bool include_deps, - bool filter_loaders); + QList loadersList = {}); void checkCandidates(); @@ -64,5 +64,5 @@ class ResourceUpdateDialog final : public ReviewMessageBox { bool m_no_updates = false; bool m_aborted = false; bool m_include_deps = false; - bool m_filter_loaders = false; + QList m_loadersList; }; diff --git a/launcher/ui/pages/instance/DataPackPage.cpp b/launcher/ui/pages/instance/DataPackPage.cpp index b9f23bac0..1723bc37f 100644 --- a/launcher/ui/pages/instance/DataPackPage.cpp +++ b/launcher/ui/pages/instance/DataPackPage.cpp @@ -96,7 +96,6 @@ void DataPackPage::downloadDataPacks() } } - void DataPackPage::updateDataPacks() { if (m_instance->typeName() != "Minecraft") @@ -108,13 +107,13 @@ void DataPackPage::updateDataPacks() return; } if (m_instance != nullptr && m_instance->isRunning()) { - auto response = CustomMessageBox::selectable( - this, tr("Confirm Update"), - tr("Updating data packs while the game is running may cause pack duplication and game crashes.\n" - "The old files may not be deleted as they are in use.\n" - "Are you sure you want to do this?"), - QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) - ->exec(); + auto response = + CustomMessageBox::selectable(this, tr("Confirm Update"), + tr("Updating data packs while the game is running may cause pack duplication and game crashes.\n" + "The old files may not be deleted as they are in use.\n" + "Are you sure you want to do this?"), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); if (response != QMessageBox::Yes) return; @@ -126,7 +125,7 @@ void DataPackPage::updateDataPacks() if (use_all) mods_list = m_model->allResources(); - ResourceUpdateDialog update_dialog(this, m_instance, m_model, mods_list, false, false); + ResourceUpdateDialog update_dialog(this, m_instance, m_model, mods_list, false, { ModPlatform::ModLoaderType::DataPack }); update_dialog.checkCandidates(); if (update_dialog.aborted()) { diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index 95507ac22..a4dc45942 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -210,7 +210,7 @@ void ModFolderPage::updateMods(bool includeDeps) if (use_all) mods_list = m_model->allResources(); - ResourceUpdateDialog update_dialog(this, m_instance, m_model, mods_list, includeDeps, true); + ResourceUpdateDialog update_dialog(this, m_instance, m_model, mods_list, includeDeps, profile->getModLoadersList()); update_dialog.checkCandidates(); if (update_dialog.aborted()) { diff --git a/launcher/ui/pages/instance/ResourcePackPage.cpp b/launcher/ui/pages/instance/ResourcePackPage.cpp index 79e677765..f9fbc6176 100644 --- a/launcher/ui/pages/instance/ResourcePackPage.cpp +++ b/launcher/ui/pages/instance/ResourcePackPage.cpp @@ -144,7 +144,7 @@ void ResourcePackPage::updateResourcePacks() if (use_all) mods_list = m_model->allResources(); - ResourceUpdateDialog update_dialog(this, m_instance, m_model, mods_list, false, false); + ResourceUpdateDialog update_dialog(this, m_instance, m_model, mods_list, false); update_dialog.checkCandidates(); if (update_dialog.aborted()) { diff --git a/launcher/ui/pages/instance/ShaderPackPage.cpp b/launcher/ui/pages/instance/ShaderPackPage.cpp index a287d3edf..00a17bfdf 100644 --- a/launcher/ui/pages/instance/ShaderPackPage.cpp +++ b/launcher/ui/pages/instance/ShaderPackPage.cpp @@ -141,7 +141,7 @@ void ShaderPackPage::updateShaderPacks() if (use_all) mods_list = m_model->allResources(); - ResourceUpdateDialog update_dialog(this, m_instance, m_model, mods_list, false, false); + ResourceUpdateDialog update_dialog(this, m_instance, m_model, mods_list, false); update_dialog.checkCandidates(); if (update_dialog.aborted()) { diff --git a/launcher/ui/pages/instance/TexturePackPage.cpp b/launcher/ui/pages/instance/TexturePackPage.cpp index fd1e0a2fc..ed74c90da 100644 --- a/launcher/ui/pages/instance/TexturePackPage.cpp +++ b/launcher/ui/pages/instance/TexturePackPage.cpp @@ -150,7 +150,7 @@ void TexturePackPage::updateTexturePacks() if (use_all) mods_list = m_model->allResources(); - ResourceUpdateDialog update_dialog(this, m_instance, m_model, mods_list, false, false); + ResourceUpdateDialog update_dialog(this, m_instance, m_model, mods_list, false); update_dialog.checkCandidates(); if (update_dialog.aborted()) { From 5ece4bae703e5dd6899077682b6edb25fde3dc5e Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Wed, 26 Mar 2025 16:11:53 +0000 Subject: [PATCH 072/695] Add CurseForge support Currently doesn't work. Will try another approach to modrinth filter. Signed-off-by: TheKodeToad --- launcher/modplatform/flame/FlameAPI.h | 5 ++- .../ui/dialogs/ResourceDownloadDialog.cpp | 9 +++-- .../modplatform/flame/FlameResourceModels.cpp | 28 +++++++++++++ .../modplatform/flame/FlameResourceModels.h | 19 +++++++++ .../modplatform/flame/FlameResourcePages.cpp | 39 +++++++++++++++++++ .../modplatform/flame/FlameResourcePages.h | 28 +++++++++++++ 6 files changed, 122 insertions(+), 6 deletions(-) diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index 14e4effec..f9b4c11a5 100644 --- a/launcher/modplatform/flame/FlameAPI.h +++ b/launcher/modplatform/flame/FlameAPI.h @@ -43,12 +43,13 @@ class FlameAPI : public NetworkResourceAPI { case ModPlatform::ResourceType::MOD: return 6; case ModPlatform::ResourceType::RESOURCE_PACK: - case ModPlatform::ResourceType::DATA_PACK: return 12; case ModPlatform::ResourceType::SHADER_PACK: return 6552; case ModPlatform::ResourceType::MODPACK: return 4471; + case ModPlatform::ResourceType::DATA_PACK: + return 6945; } } @@ -69,7 +70,7 @@ class FlameAPI : public NetworkResourceAPI { case ModPlatform::NeoForge: return 6; case ModPlatform::DataPack: - break; // not supported + break; // not supported } return 0; } diff --git a/launcher/ui/dialogs/ResourceDownloadDialog.cpp b/launcher/ui/dialogs/ResourceDownloadDialog.cpp index a9fa826ec..fe9ee7bdb 100644 --- a/launcher/ui/dialogs/ResourceDownloadDialog.cpp +++ b/launcher/ui/dialogs/ResourceDownloadDialog.cpp @@ -397,10 +397,9 @@ void ResourceDownloadDialog::setResourceMetadata(const std::shared_ptropenProject(meta->project_id); } - DataPackDownloadDialog::DataPackDownloadDialog(QWidget* parent, - const std::shared_ptr& data_packs, - BaseInstance* instance) + const std::shared_ptr& data_packs, + BaseInstance* instance) : ResourceDownloadDialog(parent, data_packs), m_instance(instance) { setWindowTitle(dialogTitle()); @@ -416,7 +415,9 @@ QList DataPackDownloadDialog::getPages() { QList pages; pages.append(ModrinthDataPackPage::create(this, *m_instance)); + if (APPLICATION->capabilities() & Application::SupportsFlame) + pages.append(FlameDataPackPage::create(this, *m_instance)); return pages; } - + } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp b/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp index fea1fc27a..5b254a675 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp @@ -170,4 +170,32 @@ auto FlameShaderPackModel::documentToArray(QJsonDocument& obj) const -> QJsonArr return Json::ensureArray(obj.object(), "data"); } +FlameDataPackModel::FlameDataPackModel(const BaseInstance& base) : DataPackResourceModel(base, new FlameAPI) {} + +void FlameDataPackModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) +{ + FlameMod::loadIndexedPack(m, obj); +} + +// We already deal with the URLs when initializing the pack, due to the API response's structure +void FlameDataPackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) +{ + FlameMod::loadBody(m, obj); +} + +void FlameDataPackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) +{ + FlameMod::loadIndexedPackVersions(m, arr); +} + +bool FlameDataPackModel::optedOut(const ModPlatform::IndexedVersion& ver) const +{ + return isOptedOut(ver); +} + +auto FlameDataPackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray +{ + return Json::ensureArray(obj.object(), "data"); +} + } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/flame/FlameResourceModels.h b/launcher/ui/pages/modplatform/flame/FlameResourceModels.h index 458fd85d0..b80217a16 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourceModels.h +++ b/launcher/ui/pages/modplatform/flame/FlameResourceModels.h @@ -93,4 +93,23 @@ class FlameShaderPackModel : public ShaderPackResourceModel { auto documentToArray(QJsonDocument& obj) const -> QJsonArray override; }; +class FlameDataPackModel : public DataPackResourceModel { + Q_OBJECT + + public: + FlameDataPackModel(const BaseInstance&); + ~FlameDataPackModel() override = default; + + bool optedOut(const ModPlatform::IndexedVersion& ver) const override; + + private: + [[nodiscard]] QString debugName() const override { return Flame::debugName() + " (Model)"; } + [[nodiscard]] QString metaEntryBase() const override { return Flame::metaEntryBase(); } + + void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; + void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override; + void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override; + auto documentToArray(QJsonDocument& obj) const -> QJsonArray override; +}; + } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp index 4e01f3a65..609d77608 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp @@ -152,6 +152,22 @@ void FlameTexturePackPage::openUrl(const QUrl& url) TexturePackResourcePage::openUrl(url); } +void FlameDataPackPage::openUrl(const QUrl& url) +{ + if (url.scheme().isEmpty()) { + QString query = url.query(QUrl::FullyDecoded); + + if (query.startsWith("remoteUrl=")) { + // attempt to resolve url from warning page + query.remove(0, 10); + DataPackResourcePage::openUrl({ QUrl::fromPercentEncoding(query.toUtf8()) }); // double decoding is necessary + return; + } + } + + DataPackResourcePage::openUrl(url); +} + FlameShaderPackPage::FlameShaderPackPage(ShaderPackDownloadDialog* dialog, BaseInstance& instance) : ShaderPackResourcePage(dialog, instance) { @@ -171,6 +187,25 @@ FlameShaderPackPage::FlameShaderPackPage(ShaderPackDownloadDialog* dialog, BaseI m_ui->packDescription->setMetaEntry(metaEntryBase()); } +FlameDataPackPage::FlameDataPackPage(DataPackDownloadDialog* dialog, BaseInstance& instance) + : DataPackResourcePage(dialog, instance) +{ + m_model = new FlameDataPackModel(instance); + m_ui->packView->setModel(m_model); + + addSortings(); + + // 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(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); + connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlameDataPackPage::onSelectionChanged); + connect(m_ui->versionSelectionBox, QOverload::of(&QComboBox::currentIndexChanged), this, + &FlameDataPackPage::onVersionSelectionChanged); + connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &FlameDataPackPage::onResourceSelected); + + m_ui->packDescription->setMetaEntry(metaEntryBase()); +} + void FlameShaderPackPage::openUrl(const QUrl& url) { if (url.scheme().isEmpty()) { @@ -206,6 +241,10 @@ auto FlameShaderPackPage::shouldDisplay() const -> bool { return true; } +auto FlameDataPackPage::shouldDisplay() const -> bool +{ + return true; +} unique_qobject_ptr FlameModPage::createFilterWidget() { diff --git a/launcher/ui/pages/modplatform/flame/FlameResourcePages.h b/launcher/ui/pages/modplatform/flame/FlameResourcePages.h index 052706549..306cdb4f3 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourcePages.h +++ b/launcher/ui/pages/modplatform/flame/FlameResourcePages.h @@ -39,6 +39,7 @@ #pragma once +#include #include "Application.h" #include "modplatform/ResourceAPI.h" @@ -180,4 +181,31 @@ class FlameShaderPackPage : public ShaderPackResourcePage { void openUrl(const QUrl& url) override; }; + + +class FlameDataPackPage : public DataPackResourcePage { + Q_OBJECT + + public: + static FlameDataPackPage* create(DataPackDownloadDialog* dialog, BaseInstance& instance) + { + return DataPackResourcePage::create(dialog, instance); + } + + FlameDataPackPage(DataPackDownloadDialog* dialog, BaseInstance& instance); + ~FlameDataPackPage() override = default; + + [[nodiscard]] bool shouldDisplay() const override; + + [[nodiscard]] inline auto displayName() const -> QString override { return Flame::displayName(); } + [[nodiscard]] inline auto icon() const -> QIcon override { return Flame::icon(); } + [[nodiscard]] inline auto id() const -> QString override { return Flame::id(); } + [[nodiscard]] inline auto debugName() const -> QString override { return Flame::debugName(); } + [[nodiscard]] inline auto metaEntryBase() const -> QString override { return Flame::metaEntryBase(); } + + [[nodiscard]] inline auto helpPage() const -> QString override { return ""; } + + void openUrl(const QUrl& url) override; +}; + } // namespace ResourceDownload From 9c942c68943afa47512a01d1c229c2bbdf7925b3 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Wed, 26 Mar 2025 23:35:40 +0000 Subject: [PATCH 073/695] Fix CurseForge support Signed-off-by: TheKodeToad --- launcher/modplatform/flame/FlameAPI.h | 10 +++++++--- launcher/modplatform/import_ftb/PackInstallTask.cpp | 2 ++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index f9b4c11a5..db96b6971 100644 --- a/launcher/modplatform/flame/FlameAPI.h +++ b/launcher/modplatform/flame/FlameAPI.h @@ -100,8 +100,12 @@ class FlameAPI : public NetworkResourceAPI { if (args.sorting.has_value()) get_arguments.append(QString("sortField=%1").arg(args.sorting.value().index)); get_arguments.append("sortOrder=desc"); - if (args.loaders.has_value() && args.loaders.value() != 0) - get_arguments.append(QString("modLoaderTypes=%1").arg(getModLoaderFilters(args.loaders.value()))); + if (args.loaders.has_value()) { + ModPlatform::ModLoaderTypes loaders = args.loaders.value(); + loaders &= ~ModPlatform::ModLoaderType::DataPack; + if (loaders != 0) + get_arguments.append(QString("modLoaderTypes=%1").arg(getModLoaderFilters(loaders))); + } if (args.categoryIds.has_value() && !args.categoryIds->empty()) get_arguments.append(QString("categoryIds=[%1]").arg(args.categoryIds->join(","))); @@ -119,7 +123,7 @@ class FlameAPI : public NetworkResourceAPI { if (args.mcVersions.has_value()) url += QString("&gameVersion=%1").arg(args.mcVersions.value().front().toString()); - if (args.loaders.has_value() && ModPlatform::hasSingleModLoaderSelected(args.loaders.value())) { + if (args.loaders.has_value() && args.loaders.value() != ModPlatform::ModLoaderType::DataPack && ModPlatform::hasSingleModLoaderSelected(args.loaders.value())) { int mappedModLoader = getMappedModLoader(static_cast(static_cast(args.loaders.value()))); url += QString("&modLoaderType=%1").arg(mappedModLoader); } diff --git a/launcher/modplatform/import_ftb/PackInstallTask.cpp b/launcher/modplatform/import_ftb/PackInstallTask.cpp index 8046300e1..7cb8b6ebc 100644 --- a/launcher/modplatform/import_ftb/PackInstallTask.cpp +++ b/launcher/modplatform/import_ftb/PackInstallTask.cpp @@ -89,6 +89,8 @@ void PackInstallTask::copySettings() break; case ModPlatform::LiteLoader: break; + case ModPlatform::DataPack: + break; } components->saveNow(); From ccef855f06614a51292741ce8875de1bf9f4b538 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Wed, 26 Mar 2025 23:41:47 +0000 Subject: [PATCH 074/695] Reset path when unchecked (still keeps it in UI in case you recheck) Signed-off-by: TheKodeToad --- launcher/ui/widgets/MinecraftSettingsWidget.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/launcher/ui/widgets/MinecraftSettingsWidget.cpp b/launcher/ui/widgets/MinecraftSettingsWidget.cpp index 79ffccce8..f369e393c 100644 --- a/launcher/ui/widgets/MinecraftSettingsWidget.cpp +++ b/launcher/ui/widgets/MinecraftSettingsWidget.cpp @@ -101,8 +101,11 @@ MinecraftSettingsWidget::MinecraftSettingsWidget(MinecraftInstancePtr instance, connect(m_ui->serverJoinAddressButton, &QAbstractButton::toggled, m_ui->serverJoinAddress, &QWidget::setEnabled); connect(m_ui->worldJoinButton, &QAbstractButton::toggled, m_ui->worldsCb, &QWidget::setEnabled); - connect(m_ui->globalDataPacksGroupBox, &QGroupBox::toggled, this, - [this](bool value) { m_instance->settings()->set("GlobalDataPacksEnabled", value); }); + connect(m_ui->globalDataPacksGroupBox, &QGroupBox::toggled, this, [this](bool value) { + m_instance->settings()->set("GlobalDataPacksEnabled", value); + if (!value) + m_instance->settings()->reset("GlobalDataPacksPath"); + }); connect(m_ui->dataPacksPathEdit, &QLineEdit::editingFinished, this, [this] { m_instance->settings()->set("GlobalDataPacksPath", m_ui->dataPacksPathEdit->text()); }); connect(m_ui->dataPacksPathBrowse, &QPushButton::clicked, this, &MinecraftSettingsWidget::selectDataPacksFolder); From 1e7ceafa5f318da0ec997d8c40b4b97fdc8bc816 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Wed, 26 Mar 2025 23:48:00 +0000 Subject: [PATCH 075/695] Auto-fix \ to / on Windows (for portability) Signed-off-by: TheKodeToad --- launcher/ui/widgets/MinecraftSettingsWidget.cpp | 11 +++++++++-- launcher/ui/widgets/MinecraftSettingsWidget.h | 1 + 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/launcher/ui/widgets/MinecraftSettingsWidget.cpp b/launcher/ui/widgets/MinecraftSettingsWidget.cpp index f369e393c..df7ed083b 100644 --- a/launcher/ui/widgets/MinecraftSettingsWidget.cpp +++ b/launcher/ui/widgets/MinecraftSettingsWidget.cpp @@ -106,8 +106,7 @@ MinecraftSettingsWidget::MinecraftSettingsWidget(MinecraftInstancePtr instance, if (!value) m_instance->settings()->reset("GlobalDataPacksPath"); }); - connect(m_ui->dataPacksPathEdit, &QLineEdit::editingFinished, this, - [this] { m_instance->settings()->set("GlobalDataPacksPath", m_ui->dataPacksPathEdit->text()); }); + connect(m_ui->dataPacksPathEdit, &QLineEdit::editingFinished, this, &MinecraftSettingsWidget::editedDataPacksPath); connect(m_ui->dataPacksPathBrowse, &QPushButton::clicked, this, &MinecraftSettingsWidget::selectDataPacksFolder); } @@ -478,6 +477,14 @@ bool MinecraftSettingsWidget::isQuickPlaySupported() return m_instance->traits().contains("feature:is_quick_play_singleplayer"); } +void MinecraftSettingsWidget::editedDataPacksPath() +{ + if (QDir::separator() != '/') + m_ui->dataPacksPathEdit->setText(m_ui->dataPacksPathEdit->text().replace(QDir::separator(), '/')); + + m_instance->settings()->set("GlobalDataPacksPath", m_ui->dataPacksPathEdit->text()); +} + void MinecraftSettingsWidget::selectDataPacksFolder() { QString path = QFileDialog::getExistingDirectory(this, tr("Select Global Data Packs Folder"), m_instance->gameRoot()); diff --git a/launcher/ui/widgets/MinecraftSettingsWidget.h b/launcher/ui/widgets/MinecraftSettingsWidget.h index 0f9e35b9c..002cd2d56 100644 --- a/launcher/ui/widgets/MinecraftSettingsWidget.h +++ b/launcher/ui/widgets/MinecraftSettingsWidget.h @@ -56,6 +56,7 @@ class MinecraftSettingsWidget : public QWidget { void openGlobalSettings(); void updateAccountsMenu(const SettingsObject& settings); bool isQuickPlaySupported(); + void editedDataPacksPath(); void selectDataPacksFolder(); MinecraftInstancePtr m_instance; From 910febeeb00295492cddff43c4fc212450d21e6a Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Thu, 27 Mar 2025 00:37:39 +0000 Subject: [PATCH 076/695] Try to make getchoo requested changes Signed-off-by: TheKodeToad --- launcher/ui/pages/global/JavaPage.ui | 115 ++++------ launcher/ui/widgets/CustomCommands.cpp | 9 +- launcher/ui/widgets/CustomCommands.ui | 54 +++-- launcher/ui/widgets/EnvironmentVariables.cpp | 10 +- launcher/ui/widgets/EnvironmentVariables.ui | 41 ++-- launcher/ui/widgets/JavaSettingsWidget.ui | 106 ++++----- .../ui/widgets/MinecraftSettingsWidget.cpp | 9 +- .../ui/widgets/MinecraftSettingsWidget.ui | 212 ++++++++---------- 8 files changed, 261 insertions(+), 295 deletions(-) diff --git a/launcher/ui/pages/global/JavaPage.ui b/launcher/ui/pages/global/JavaPage.ui index bc5e9523f..3b5d797d6 100644 --- a/launcher/ui/pages/global/JavaPage.ui +++ b/launcher/ui/pages/global/JavaPage.ui @@ -17,18 +17,6 @@ - - 0 - - - 0 - - - 0 - - - 0 - @@ -49,8 +37,8 @@ 0 0 - 535 - 606 + 523 + 594 @@ -65,64 +53,55 @@ - Management + Installations - - - Downloaded Java Versions + + + + + Download + + + + + + + Remove + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Refresh + + + + + + + + + + 0 + 0 + - - - - - - - Download - - - - - - - Remove - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Refresh - - - - - - - - - - 0 - 0 - - - - - diff --git a/launcher/ui/widgets/CustomCommands.cpp b/launcher/ui/widgets/CustomCommands.cpp index 9b98d7409..45bd0cbea 100644 --- a/launcher/ui/widgets/CustomCommands.cpp +++ b/launcher/ui/widgets/CustomCommands.cpp @@ -44,13 +44,14 @@ CustomCommands::~CustomCommands() CustomCommands::CustomCommands(QWidget* parent) : QWidget(parent), ui(new Ui::CustomCommands) { ui->setupUi(this); + connect(ui->overrideCheckBox, &QCheckBox::toggled, ui->customCommandsWidget, &QWidget::setEnabled); } void CustomCommands::initialize(bool checkable, bool checked, const QString& prelaunch, const QString& wrapper, const QString& postexit) { - ui->customCommandsGroupBox->setCheckable(checkable); + ui->overrideCheckBox->setVisible(checkable); if (checkable) { - ui->customCommandsGroupBox->setChecked(checked); + ui->overrideCheckBox->setChecked(checked); } ui->preLaunchCmdTextBox->setText(prelaunch); ui->wrapperCmdTextBox->setText(wrapper); @@ -64,9 +65,9 @@ void CustomCommands::retranslate() bool CustomCommands::checked() const { - if (!ui->customCommandsGroupBox->isCheckable()) + if (!ui->overrideCheckBox->isVisible()) return true; - return ui->customCommandsGroupBox->isChecked(); + return ui->overrideCheckBox->isChecked(); } QString CustomCommands::prelaunchCommand() const diff --git a/launcher/ui/widgets/CustomCommands.ui b/launcher/ui/widgets/CustomCommands.ui index b485c293e..53fca9419 100644 --- a/launcher/ui/widgets/CustomCommands.ui +++ b/launcher/ui/widgets/CustomCommands.ui @@ -24,20 +24,30 @@ 0 - - - true - - - &Custom Commands + + + Override &Global Settings - + true - - false + + + + + + true + + 0 + + + 0 + + + 0 + @@ -48,35 +58,35 @@ + + + - - + + - &Wrapper command: + P&ost-exit command: - wrapperCmdTextBox + labelPostExitCmd - - + + - - + + - P&ost-exit command: + &Wrapper command: - postExitCmdTextBox + wrapperCmdTextBox - - - diff --git a/launcher/ui/widgets/EnvironmentVariables.cpp b/launcher/ui/widgets/EnvironmentVariables.cpp index 633fc6122..653f0d23d 100644 --- a/launcher/ui/widgets/EnvironmentVariables.cpp +++ b/launcher/ui/widgets/EnvironmentVariables.cpp @@ -50,6 +50,8 @@ EnvironmentVariables::EnvironmentVariables(QWidget* parent) : QWidget(parent), u }); connect(ui->clear, &QPushButton::clicked, this, [this] { ui->list->clear(); }); + + connect(ui->overrideCheckBox, &QCheckBox::toggled, ui->settingsWidget, &QWidget::setEnabled); } EnvironmentVariables::~EnvironmentVariables() @@ -60,8 +62,8 @@ EnvironmentVariables::~EnvironmentVariables() void EnvironmentVariables::initialize(bool instance, bool override, const QMap& value) { // update widgets to settings - ui->groupBox->setCheckable(instance); - ui->groupBox->setChecked(override); + ui->overrideCheckBox->setVisible(instance); + ui->overrideCheckBox->setChecked(override); // populate ui->list->clear(); @@ -94,9 +96,9 @@ void EnvironmentVariables::retranslate() bool EnvironmentVariables::override() const { - if (!ui->groupBox->isCheckable()) + if (!ui->overrideCheckBox->isVisible()) return false; - return ui->groupBox->isChecked(); + return ui->overrideCheckBox->isChecked(); } QMap EnvironmentVariables::value() const diff --git a/launcher/ui/widgets/EnvironmentVariables.ui b/launcher/ui/widgets/EnvironmentVariables.ui index 828626d12..cc52b5d10 100644 --- a/launcher/ui/widgets/EnvironmentVariables.ui +++ b/launcher/ui/widgets/EnvironmentVariables.ui @@ -14,27 +14,34 @@ Form - - 0 - - - 0 - - - 0 - - - 0 - - - - &Environment Variables + + + Override &Global Settings - + true - + + + + + + true + + + + 0 + + + 0 + + + 0 + + + 0 + diff --git a/launcher/ui/widgets/JavaSettingsWidget.ui b/launcher/ui/widgets/JavaSettingsWidget.ui index 5cc894b8e..3094aadc8 100644 --- a/launcher/ui/widgets/JavaSettingsWidget.ui +++ b/launcher/ui/widgets/JavaSettingsWidget.ui @@ -26,37 +26,6 @@ 0 - - - - - - Test S&ettings - - - - - - - Open Java &Downloader - - - - - - - Qt::Horizontal - - - - 0 - 0 - - - - - - @@ -72,6 +41,16 @@ false + + + + Java &Executable + + + javaPathTextBox + + + @@ -99,23 +78,6 @@ - - - - If enabled, the launcher will not check if an instance is compatible with the selected Java version. - - - Skip Java compatibility checks - - - - - - - Auto-&detect Java version - - - @@ -136,16 +98,54 @@ - - + + - Java &Executable + Auto-&detect Java version - - javaPathTextBox + + + + + + If enabled, the launcher will not check if an instance is compatible with the selected Java version. + + + Skip Java compatibility checks + + + + + + Test S&ettings + + + + + + + Open Java &Downloader + + + + + + + Qt::Horizontal + + + + 0 + 0 + + + + + + diff --git a/launcher/ui/widgets/MinecraftSettingsWidget.cpp b/launcher/ui/widgets/MinecraftSettingsWidget.cpp index cec7f267f..2f22ae54a 100644 --- a/launcher/ui/widgets/MinecraftSettingsWidget.cpp +++ b/launcher/ui/widgets/MinecraftSettingsWidget.cpp @@ -50,14 +50,11 @@ MinecraftSettingsWidget::MinecraftSettingsWidget(MinecraftInstancePtr instance, m_ui->setupUi(this); if (m_instance == nullptr) { - for (int i = m_ui->settingsTabs->count() - 1; i >= 0; --i) { - const QString name = m_ui->settingsTabs->widget(i)->objectName(); - - if (name == "javaPage" || name == "launchPage") - m_ui->settingsTabs->removeTab(i); - } + m_ui->settingsTabs->removeTab(1); m_ui->openGlobalSettingsButton->setVisible(false); + m_ui->instanceAccountGroupBox->hide(); + m_ui->serverJoinGroupBox->hide(); } else { m_javaSettings = new JavaSettingsWidget(m_instance, this); m_ui->javaScrollArea->setWidget(m_javaSettings); diff --git a/launcher/ui/widgets/MinecraftSettingsWidget.ui b/launcher/ui/widgets/MinecraftSettingsWidget.ui index 34fa9af80..0edbeacb7 100644 --- a/launcher/ui/widgets/MinecraftSettingsWidget.ui +++ b/launcher/ui/widgets/MinecraftSettingsWidget.ui @@ -58,9 +58,9 @@ 0 - 0 - 610 - 610 + -125 + 603 + 786 @@ -203,6 +203,79 @@ + + + + &Override Default Account + + + true + + + false + + + + + + + 0 + 0 + + + + Account + + + + + + + + 0 + 0 + + + + + + + + + + + Enable &Auto-join + + + true + + + false + + + + + + Server address + + + + + + + + + + Singleplayer world + + + + + + + + + @@ -352,7 +425,7 @@ 0 0 624 - 291 + 287 @@ -375,8 +448,8 @@ 0 0 - 610 - 501 + 603 + 470 @@ -564,118 +637,6 @@ - - - Launch - - - - - - true - - - - - 0 - 0 - 624 - 291 - - - - - - - Override default &account - - - true - - - false - - - - - - - 0 - 0 - - - - Account - - - - - - - - 0 - 0 - - - - - - - - - - - Set a &target to join on launch - - - true - - - false - - - - - - Server address - - - - - - - - - - Singleplayer world - - - - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - Custom Commands @@ -691,6 +652,18 @@ Environment Variables + + 0 + + + 0 + + + 0 + + + 0 + @@ -737,9 +710,6 @@ enableMangoHud useDiscreteGpuCheck useZink - scrollArea_3 - instanceAccountGroupBox - serverJoinGroupBox serverJoinAddressButton serverJoinAddress worldJoinButton From 1ce343fb97d2ce88dc4033c976c1994d71278576 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Thu, 27 Mar 2025 00:48:56 +0000 Subject: [PATCH 077/695] Try to make Minecraft general page easier to understand - remove misc Signed-off-by: TheKodeToad --- .../ui/widgets/MinecraftSettingsWidget.cpp | 34 ++--- .../ui/widgets/MinecraftSettingsWidget.ui | 143 ++++++++---------- 2 files changed, 75 insertions(+), 102 deletions(-) diff --git a/launcher/ui/widgets/MinecraftSettingsWidget.cpp b/launcher/ui/widgets/MinecraftSettingsWidget.cpp index 2f22ae54a..07761207a 100644 --- a/launcher/ui/widgets/MinecraftSettingsWidget.cpp +++ b/launcher/ui/widgets/MinecraftSettingsWidget.cpp @@ -68,7 +68,6 @@ MinecraftSettingsWidget::MinecraftSettingsWidget(MinecraftInstancePtr instance, tr("Warning: The maximized option is " "not fully supported on this Minecraft version.")); - m_ui->miscellaneousSettingsBox->setCheckable(true); m_ui->consoleSettingsBox->setCheckable(true); m_ui->windowSizeGroupBox->setCheckable(true); m_ui->nativeWorkaroundsGroupBox->setCheckable(true); @@ -136,11 +135,14 @@ void MinecraftSettingsWidget::loadSettings() settings = APPLICATION->settings(); // Game Window - m_ui->windowSizeGroupBox->setChecked(m_instance == nullptr || settings->get("OverrideWindow").toBool()); + m_ui->windowSizeGroupBox->setChecked(m_instance == nullptr || settings->get("OverrideWindow").toBool() || + settings->get("OverrideMiscellaneous").toBool()); m_ui->windowSizeGroupBox->setChecked(settings->get("OverrideWindow").toBool()); m_ui->maximizedCheckBox->setChecked(settings->get("LaunchMaximized").toBool()); m_ui->windowWidthSpinBox->setValue(settings->get("MinecraftWinWidth").toInt()); m_ui->windowHeightSpinBox->setValue(settings->get("MinecraftWinHeight").toInt()); + m_ui->closeAfterLaunchCheck->setChecked(settings->get("CloseAfterLaunch").toBool()); + m_ui->quitAfterGameStopCheck->setChecked(settings->get("QuitAfterGameStop").toBool()); // Game Time m_ui->gameTimeGroupBox->setChecked(m_instance == nullptr || settings->get("OverrideGameTime").toBool()); @@ -155,11 +157,6 @@ void MinecraftSettingsWidget::loadSettings() m_ui->autoCloseConsoleCheck->setChecked(settings->get("AutoCloseConsole").toBool()); m_ui->showConsoleErrorCheck->setChecked(settings->get("ShowConsoleOnError").toBool()); - // Miscellaneous - m_ui->miscellaneousSettingsBox->setChecked(settings->get("OverrideMiscellaneous").toBool()); - m_ui->closeAfterLaunchCheck->setChecked(settings->get("CloseAfterLaunch").toBool()); - m_ui->quitAfterGameStopCheck->setChecked(settings->get("QuitAfterGameStop").toBool()); - if (m_javaSettings != nullptr) m_javaSettings->loadSettings(); @@ -242,19 +239,6 @@ void MinecraftSettingsWidget::saveSettings() { SettingsObject::Lock lock(settings); - // Miscellaneous - bool miscellaneous = m_instance == nullptr || m_ui->miscellaneousSettingsBox->isChecked(); - - if (m_instance != nullptr) - settings->set("OverrideMiscellaneous", miscellaneous); - - if (miscellaneous) { - settings->set("CloseAfterLaunch", m_ui->closeAfterLaunchCheck->isChecked()); - settings->set("QuitAfterGameStop", m_ui->quitAfterGameStopCheck->isChecked()); - } else { - settings->reset("CloseAfterLaunch"); - settings->reset("QuitAfterGameStop"); - } // Console bool console = m_instance == nullptr || m_ui->consoleSettingsBox->isChecked(); @@ -272,20 +256,26 @@ void MinecraftSettingsWidget::saveSettings() settings->reset("ShowConsoleOnError"); } - // Window Size + // Game Window bool window = m_instance == nullptr || m_ui->windowSizeGroupBox->isChecked(); - if (m_instance != nullptr) + if (m_instance != nullptr) { settings->set("OverrideWindow", window); + settings->set("OverrideMiscellaneous", window); + } if (window) { settings->set("LaunchMaximized", m_ui->maximizedCheckBox->isChecked()); settings->set("MinecraftWinWidth", m_ui->windowWidthSpinBox->value()); settings->set("MinecraftWinHeight", m_ui->windowHeightSpinBox->value()); + settings->set("CloseAfterLaunch", m_ui->closeAfterLaunchCheck->isChecked()); + settings->set("QuitAfterGameStop", m_ui->quitAfterGameStopCheck->isChecked()); } else { settings->reset("LaunchMaximized"); settings->reset("MinecraftWinWidth"); settings->reset("MinecraftWinHeight"); + settings->reset("CloseAfterLaunch"); + settings->reset("QuitAfterGameStop"); } // Custom Commands diff --git a/launcher/ui/widgets/MinecraftSettingsWidget.ui b/launcher/ui/widgets/MinecraftSettingsWidget.ui index 0edbeacb7..b71250c47 100644 --- a/launcher/ui/widgets/MinecraftSettingsWidget.ui +++ b/launcher/ui/widgets/MinecraftSettingsWidget.ui @@ -58,9 +58,9 @@ 0 - -125 + 0 603 - 786 + 744 @@ -200,6 +200,59 @@ + + + + Close the launcher after the game window opens + + + + + + + Quit the launcher after the game window closes + + + + + + + + + + true + + + &Console Window + + + false + + + false + + + + + + Show console window while the game is running + + + + + + + Automatically close console window when the game quits + + + + + + + Show console window when the game crashes + + + @@ -253,6 +306,13 @@ false + + + + Singleplayer world + + + @@ -263,14 +323,7 @@ - - - - Singleplayer world - - - - + @@ -322,74 +375,6 @@ - - - - true - - - &Console - - - false - - - false - - - - - - Show console while the game is running - - - - - - - Automatically close console when the game quits - - - - - - - Show console when the game crashes - - - - - - - - - - &Miscellaneous - - - false - - - false - - - - - - Close the launcher after game window opens - - - - - - - Quit the launcher after game window closes - - - - - - @@ -698,8 +683,6 @@ showConsoleCheck autoCloseConsoleCheck showConsoleErrorCheck - closeAfterLaunchCheck - quitAfterGameStopCheck javaScrollArea scrollArea_2 onlineFixes From 7e5178cf8117be59f350e5c89b6e210d9a5e6d5e Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Thu, 27 Mar 2025 01:08:26 +0000 Subject: [PATCH 078/695] Visual cleanup Signed-off-by: TheKodeToad --- launcher/ui/pages/global/APIPage.ui | 28 ++-- launcher/ui/widgets/CustomCommands.ui | 47 ++++-- launcher/ui/widgets/JavaSettingsWidget.ui | 50 +++++- .../ui/widgets/MinecraftSettingsWidget.cpp | 1 - .../ui/widgets/MinecraftSettingsWidget.ui | 149 +++++++++++------- 5 files changed, 187 insertions(+), 88 deletions(-) diff --git a/launcher/ui/pages/global/APIPage.ui b/launcher/ui/pages/global/APIPage.ui index 3280558ab..0b9dde764 100644 --- a/launcher/ui/pages/global/APIPage.ui +++ b/launcher/ui/pages/global/APIPage.ui @@ -20,9 +20,9 @@ 0 - -270 - 807 - 870 + 0 + 804 + 832 @@ -84,19 +84,6 @@ Meta&data Server - - - - Base URL - - - Qt::RichText - - - true - - - @@ -202,6 +189,9 @@ + + Qt::Vertical + QSizePolicy::Fixed @@ -257,6 +247,9 @@ + + Qt::Vertical + QSizePolicy::Fixed @@ -309,6 +302,9 @@ + + Qt::Vertical + QSizePolicy::Fixed diff --git a/launcher/ui/widgets/CustomCommands.ui b/launcher/ui/widgets/CustomCommands.ui index 53fca9419..cabd1372d 100644 --- a/launcher/ui/widgets/CustomCommands.ui +++ b/launcher/ui/widgets/CustomCommands.ui @@ -51,42 +51,71 @@ - &Pre-launch command: + &Pre-launch Command preLaunchCmdTextBox - - + + + + Qt::Vertical + + + + 0 + 6 + + + - + - + + + + - P&ost-exit command: + P&ost-exit Command labelPostExitCmd - + - + - &Wrapper command: + &Wrapper Command wrapperCmdTextBox + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 0 + 6 + + + + diff --git a/launcher/ui/widgets/JavaSettingsWidget.ui b/launcher/ui/widgets/JavaSettingsWidget.ui index 3094aadc8..366b8d6fc 100644 --- a/launcher/ui/widgets/JavaSettingsWidget.ui +++ b/launcher/ui/widgets/JavaSettingsWidget.ui @@ -202,6 +202,22 @@ + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 0 + 6 + + + + @@ -240,6 +256,22 @@ + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 0 + 6 + + + + @@ -278,10 +310,26 @@ + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 0 + 6 + + + + - TextLabel + Memory Notice diff --git a/launcher/ui/widgets/MinecraftSettingsWidget.cpp b/launcher/ui/widgets/MinecraftSettingsWidget.cpp index 07761207a..c3d342d42 100644 --- a/launcher/ui/widgets/MinecraftSettingsWidget.cpp +++ b/launcher/ui/widgets/MinecraftSettingsWidget.cpp @@ -137,7 +137,6 @@ void MinecraftSettingsWidget::loadSettings() // Game Window m_ui->windowSizeGroupBox->setChecked(m_instance == nullptr || settings->get("OverrideWindow").toBool() || settings->get("OverrideMiscellaneous").toBool()); - m_ui->windowSizeGroupBox->setChecked(settings->get("OverrideWindow").toBool()); m_ui->maximizedCheckBox->setChecked(settings->get("LaunchMaximized").toBool()); m_ui->windowWidthSpinBox->setValue(settings->get("MinecraftWinWidth").toInt()); m_ui->windowHeightSpinBox->setValue(settings->get("MinecraftWinHeight").toInt()); diff --git a/launcher/ui/widgets/MinecraftSettingsWidget.ui b/launcher/ui/widgets/MinecraftSettingsWidget.ui index b71250c47..4c317c15d 100644 --- a/launcher/ui/widgets/MinecraftSettingsWidget.ui +++ b/launcher/ui/widgets/MinecraftSettingsWidget.ui @@ -58,9 +58,9 @@ 0 - 0 + -130 603 - 744 + 812 @@ -200,17 +200,33 @@ + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 0 + 6 + + + + - Close the launcher after the game window opens + When the game window opens, hide the launcher - Quit the launcher after the game window closes + When the game window closes, quit the launcher @@ -235,21 +251,67 @@ - Show console window while the game is running + When the game is launched, show the console window + + + + + + + When the game crashes, show the console window - Automatically close console window when the game quits + When the game quits, hide the console window + + + + + + + true + + + Game &Time + + + false + + + false + + - + - Show console window when the game crashes + Show time spent &playing instances + + + + + + + &Record time spent playing instances + + + + + + + Show the &total time played across instances + + + + + + + Always show durations in &hours @@ -259,7 +321,7 @@ - &Override Default Account + Override &Default Account true @@ -297,7 +359,7 @@ - Enable &Auto-join + Enable Auto-&join true @@ -305,70 +367,38 @@ false - - - - - Singleplayer world - - - - + + Server address - - - - - - - - - - - - - true - - - Game &Time - - - false - - - false - - - - - - Show time spent &playing instances - - - - - - &Record time spent playing instances + + + + 200 + 16777215 + - + - Show the &total time played across instances + Singleplayer world - - - Always show durations in &hours + + + + 0 + 0 + @@ -682,7 +712,6 @@ showGameTimeWithoutDays showConsoleCheck autoCloseConsoleCheck - showConsoleErrorCheck javaScrollArea scrollArea_2 onlineFixes @@ -694,9 +723,7 @@ useDiscreteGpuCheck useZink serverJoinAddressButton - serverJoinAddress worldJoinButton - worldsCb From 3b393e8d615256bc02e390198e41b7ac6a249a81 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Thu, 27 Mar 2025 01:20:38 +0000 Subject: [PATCH 079/695] Fix Java padding Signed-off-by: TheKodeToad --- launcher/ui/pages/global/JavaPage.ui | 12 ++++++++++++ launcher/ui/widgets/JavaSettingsWidget.ui | 12 ------------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/launcher/ui/pages/global/JavaPage.ui b/launcher/ui/pages/global/JavaPage.ui index 3b5d797d6..6b350fe07 100644 --- a/launcher/ui/pages/global/JavaPage.ui +++ b/launcher/ui/pages/global/JavaPage.ui @@ -42,6 +42,18 @@ + + 0 + + + 0 + + + 0 + + + 0 + diff --git a/launcher/ui/widgets/JavaSettingsWidget.ui b/launcher/ui/widgets/JavaSettingsWidget.ui index 366b8d6fc..c34b07cfa 100644 --- a/launcher/ui/widgets/JavaSettingsWidget.ui +++ b/launcher/ui/widgets/JavaSettingsWidget.ui @@ -14,18 +14,6 @@ Form - - 0 - - - 0 - - - 0 - - - 0 - From 36a89b0839dda900565b65ef2ab00214c7d43ca5 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Sat, 29 Mar 2025 20:24:21 +0000 Subject: [PATCH 080/695] Fix CustomCommands spacing Signed-off-by: TheKodeToad --- launcher/ui/widgets/CustomCommands.ui | 3 +++ 1 file changed, 3 insertions(+) diff --git a/launcher/ui/widgets/CustomCommands.ui b/launcher/ui/widgets/CustomCommands.ui index cabd1372d..c1b4558a8 100644 --- a/launcher/ui/widgets/CustomCommands.ui +++ b/launcher/ui/widgets/CustomCommands.ui @@ -63,6 +63,9 @@ Qt::Vertical + + QSizePolicy::Fixed + 0 From b9a1fa36459c91d7aa4d6155f1ccced83214453f Mon Sep 17 00:00:00 2001 From: Soup <43444191+Soup-64@users.noreply.github.com> Date: Sat, 5 Apr 2025 22:26:58 -0400 Subject: [PATCH 081/695] Implement popup for metacache someone in the Discord ran into an issue somewhat related to the metacache button not working (folder in use err), so this warning makes it more obvious when this happens, though it would be better to find out why it ran into a process conflict Signed-off-by: Soup <43444191+Soup-64@users.noreply.github.com> --- launcher/FileSystem.cpp | 1 + launcher/net/HttpMetaCache.cpp | 6 ++++-- launcher/net/HttpMetaCache.h | 2 +- launcher/ui/MainWindow.cpp | 5 ++++- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp index 954e7936e..14b50768a 100644 --- a/launcher/FileSystem.cpp +++ b/launcher/FileSystem.cpp @@ -55,6 +55,7 @@ #include "DesktopServices.h" #include "PSaveFile.h" #include "StringUtils.h" +#include "ui/dialogs/CustomMessageBox.h" #if defined Q_OS_WIN32 #define NOMINMAX diff --git a/launcher/net/HttpMetaCache.cpp b/launcher/net/HttpMetaCache.cpp index 4985ad080..f57aad1ba 100644 --- a/launcher/net/HttpMetaCache.cpp +++ b/launcher/net/HttpMetaCache.cpp @@ -166,8 +166,9 @@ auto HttpMetaCache::evictEntry(MetaEntryPtr entry) -> bool return true; } -void HttpMetaCache::evictAll() +bool HttpMetaCache::evictAll() { + bool ret; for (QString& base : m_entries.keys()) { EntryMap& map = m_entries[base]; qCDebug(taskHttpMetaCacheLogC) << "Evicting base" << base; @@ -176,8 +177,9 @@ void HttpMetaCache::evictAll() qCWarning(taskHttpMetaCacheLogC) << "Unexpected missing cache entry" << entry->m_basePath; } map.entry_list.clear(); - FS::deletePath(map.base_path); + ret = FS::deletePath(map.base_path); } + return ret; } auto HttpMetaCache::staleEntry(QString base, QString resource_path) -> MetaEntryPtr diff --git a/launcher/net/HttpMetaCache.h b/launcher/net/HttpMetaCache.h index 036a8dd94..144012ae5 100644 --- a/launcher/net/HttpMetaCache.h +++ b/launcher/net/HttpMetaCache.h @@ -113,7 +113,7 @@ class HttpMetaCache : public QObject { // evict selected entry from cache auto evictEntry(MetaEntryPtr entry) -> bool; - void evictAll(); + bool evictAll(); void addBase(QString base, QString base_root); diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index ddf726373..2b43af2b8 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -1318,7 +1318,10 @@ void MainWindow::on_actionReportBug_triggered() void MainWindow::on_actionClearMetadata_triggered() { - APPLICATION->metacache()->evictAll(); + if(!APPLICATION->metacache()->evictAll()){ + CustomMessageBox::selectable(this, tr("Error"), tr("Metadata cache clear Failed!\n To clear the metadata cache manually, press Folders -> View Launcher Root Folder, and after closing the launcher delete the folder named \"meta\"\n"), QMessageBox::Warning)->show(); + } + APPLICATION->metacache()->SaveNow(); } From 0c90530f8859125ea762b6460e180c32096e8987 Mon Sep 17 00:00:00 2001 From: Soup <43444191+Soup-64@users.noreply.github.com> Date: Sun, 6 Apr 2025 15:27:49 -0400 Subject: [PATCH 082/695] cleanup Fix formatting and fix a typo in the return code check Signed-off-by: Soup <43444191+Soup-64@users.noreply.github.com> --- launcher/net/HttpMetaCache.cpp | 8 +++++--- launcher/net/HttpMetaCache.h | 2 +- launcher/ui/MainWindow.cpp | 9 +++++++-- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/launcher/net/HttpMetaCache.cpp b/launcher/net/HttpMetaCache.cpp index f57aad1ba..5a3a451b7 100644 --- a/launcher/net/HttpMetaCache.cpp +++ b/launcher/net/HttpMetaCache.cpp @@ -166,9 +166,10 @@ auto HttpMetaCache::evictEntry(MetaEntryPtr entry) -> bool return true; } -bool HttpMetaCache::evictAll() +//returns true on success, false otherwise +auto HttpMetaCache::evictAll() -> bool { - bool ret; + bool ret = true; for (QString& base : m_entries.keys()) { EntryMap& map = m_entries[base]; qCDebug(taskHttpMetaCacheLogC) << "Evicting base" << base; @@ -177,7 +178,8 @@ bool HttpMetaCache::evictAll() qCWarning(taskHttpMetaCacheLogC) << "Unexpected missing cache entry" << entry->m_basePath; } map.entry_list.clear(); - ret = FS::deletePath(map.base_path); + //AND all return codes together so the result is true iff all runs of deletePath() are true + ret &= FS::deletePath(map.base_path); } return ret; } diff --git a/launcher/net/HttpMetaCache.h b/launcher/net/HttpMetaCache.h index 144012ae5..5db41259f 100644 --- a/launcher/net/HttpMetaCache.h +++ b/launcher/net/HttpMetaCache.h @@ -113,7 +113,7 @@ class HttpMetaCache : public QObject { // evict selected entry from cache auto evictEntry(MetaEntryPtr entry) -> bool; - bool evictAll(); + auto evictAll() -> bool; void addBase(QString base, QString base_root); diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 2b43af2b8..887d89006 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -1318,8 +1318,13 @@ void MainWindow::on_actionReportBug_triggered() void MainWindow::on_actionClearMetadata_triggered() { - if(!APPLICATION->metacache()->evictAll()){ - CustomMessageBox::selectable(this, tr("Error"), tr("Metadata cache clear Failed!\n To clear the metadata cache manually, press Folders -> View Launcher Root Folder, and after closing the launcher delete the folder named \"meta\"\n"), QMessageBox::Warning)->show(); + //This if contains side effects! + if (!APPLICATION->metacache()->evictAll()) { + CustomMessageBox::selectable(this, tr("Error"), + tr("Metadata cache clear Failed!\nTo clear the metadata cache manually, press Folders -> View " + "Launcher Root Folder, and after closing the launcher delete the folder named \"meta\"\n"), + QMessageBox::Warning) + ->show(); } APPLICATION->metacache()->SaveNow(); From 25d7db207d3ddaca7d812754453c1e5385a3f80e Mon Sep 17 00:00:00 2001 From: Soup of the tomato kind <43444191+Soup-64@users.noreply.github.com> Date: Sun, 6 Apr 2025 16:15:02 -0400 Subject: [PATCH 083/695] Update FileSystem.cpp fix oopsie Signed-off-by: Soup of the tomato kind <43444191+Soup-64@users.noreply.github.com> --- launcher/FileSystem.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp index 14b50768a..954e7936e 100644 --- a/launcher/FileSystem.cpp +++ b/launcher/FileSystem.cpp @@ -55,7 +55,6 @@ #include "DesktopServices.h" #include "PSaveFile.h" #include "StringUtils.h" -#include "ui/dialogs/CustomMessageBox.h" #if defined Q_OS_WIN32 #define NOMINMAX From 9b3fa591d3d6d0d09a5a164bfcc6356ae74932a7 Mon Sep 17 00:00:00 2001 From: Soup of the tomato kind <43444191+Soup-64@users.noreply.github.com> Date: Sun, 6 Apr 2025 16:24:09 -0400 Subject: [PATCH 084/695] Update launcher/net/HttpMetaCache.h Co-authored-by: Alexandru Ionut Tripon Signed-off-by: Soup of the tomato kind <43444191+Soup-64@users.noreply.github.com> --- launcher/net/HttpMetaCache.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/net/HttpMetaCache.h b/launcher/net/HttpMetaCache.h index 5db41259f..144012ae5 100644 --- a/launcher/net/HttpMetaCache.h +++ b/launcher/net/HttpMetaCache.h @@ -113,7 +113,7 @@ class HttpMetaCache : public QObject { // evict selected entry from cache auto evictEntry(MetaEntryPtr entry) -> bool; - auto evictAll() -> bool; + bool evictAll(); void addBase(QString base, QString base_root); From b579cae5c28be2ef8342246be56655452ce141bb Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Tue, 8 Apr 2025 04:17:10 -0700 Subject: [PATCH 085/695] feat(server): start using semver for launcher Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- CMakeLists.txt | 13 +++---- buildconfig/BuildConfig.cpp.in | 36 +++++++------------ buildconfig/BuildConfig.h | 2 ++ .../updater/prismupdater/PrismUpdater.cpp | 12 ++++++- launcher/updater/prismupdater/PrismUpdater.h | 1 + program_info/win_install.nsi.in | 1 + 6 files changed, 35 insertions(+), 30 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 138049018..f80c675bf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -102,7 +102,7 @@ set(CMAKE_CXX_FLAGS_RELEASE "-O2 -D_FORTIFY_SOURCE=2 ${CMAKE_CXX_FLAGS_RELEASE}" # Export compile commands for debug builds if we can (useful in LSPs like clangd) # https://cmake.org/cmake/help/v3.31/variable/CMAKE_EXPORT_COMPILE_COMMANDS.html if(CMAKE_GENERATOR STREQUAL "Unix Makefiles" OR CMAKE_GENERATOR STREQUAL "Ninja" AND CMAKE_BUILD_TYPE STREQUAL "Debug") - set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + set(CMAKE_EXPORT_COMPILE_COMMANDS ON) endif() option(DEBUG_ADDRESS_SANITIZER "Enable Address Sanitizer in Debug builds" OFF) @@ -195,10 +195,11 @@ set(Launcher_FMLLIBS_BASE_URL "https://files.prismlauncher.org/fmllibs/" CACHE S ######## Set version numbers ######## set(Launcher_VERSION_MAJOR 10) set(Launcher_VERSION_MINOR 0) +set(Launcher_VERSION_PATCH 0) -set(Launcher_VERSION_NAME "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}") -set(Launcher_VERSION_NAME4 "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.0.0") -set(Launcher_VERSION_NAME4_COMMA "${Launcher_VERSION_MAJOR},${Launcher_VERSION_MINOR},0,0") +set(Launcher_VERSION_NAME "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.${Launcher_VERSION_PATCH}") +set(Launcher_VERSION_NAME4 "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.${Launcher_VERSION_PATCH}.0") +set(Launcher_VERSION_NAME4_COMMA "${Launcher_VERSION_MAJOR},${Launcher_VERSION_MINOR},${Launcher_VERSION_PATCH},0") # Build platform. set(Launcher_BUILD_PLATFORM "unknown" CACHE STRING "A short string identifying the platform that this build was built for. Only used to display in the about dialog.") @@ -242,7 +243,7 @@ set(Launcher_ENABLE_JAVA_DOWNLOADER_DEFAULT ON) # differing Linux/BSD/etc distributions. Downstream packagers should be explicitly opt-ing into this # feature if they know it will work with their distribution. if(UNIX AND NOT APPLE) - set(Launcher_ENABLE_JAVA_DOWNLOADER_DEFAULT OFF) + set(Launcher_ENABLE_JAVA_DOWNLOADER_DEFAULT OFF) endif() # Java downloader @@ -383,7 +384,7 @@ set(Launcher_ENABLE_UPDATER NO) set(Launcher_BUILD_UPDATER NO) if (NOT APPLE AND (NOT Launcher_UPDATER_GITHUB_REPO STREQUAL "" AND NOT Launcher_BUILD_ARTIFACT STREQUAL "")) - set(Launcher_BUILD_UPDATER YES) + set(Launcher_BUILD_UPDATER YES) endif() if(NOT (UNIX AND APPLE)) diff --git a/buildconfig/BuildConfig.cpp.in b/buildconfig/BuildConfig.cpp.in index 2124d02ae..6bebcb80e 100644 --- a/buildconfig/BuildConfig.cpp.in +++ b/buildconfig/BuildConfig.cpp.in @@ -34,8 +34,8 @@ */ #include -#include "BuildConfig.h" #include +#include "BuildConfig.h" const Config BuildConfig; @@ -58,6 +58,7 @@ Config::Config() // Version information VERSION_MAJOR = @Launcher_VERSION_MAJOR@; VERSION_MINOR = @Launcher_VERSION_MINOR@; + VERSION_PATCH = @Launcher_VERSION_PATCH@; BUILD_PLATFORM = "@Launcher_BUILD_PLATFORM@"; BUILD_ARTIFACT = "@Launcher_BUILD_ARTIFACT@"; @@ -74,14 +75,13 @@ Config::Config() MAC_SPARKLE_PUB_KEY = "@MACOSX_SPARKLE_UPDATE_PUBLIC_KEY@"; MAC_SPARKLE_APPCAST_URL = "@MACOSX_SPARKLE_UPDATE_FEED_URL@"; - if (!MAC_SPARKLE_PUB_KEY.isEmpty() && !MAC_SPARKLE_APPCAST_URL.isEmpty()) - { + if (!MAC_SPARKLE_PUB_KEY.isEmpty() && !MAC_SPARKLE_APPCAST_URL.isEmpty()) { UPDATER_ENABLED = true; - } else if(!UPDATER_GITHUB_REPO.isEmpty() && !BUILD_ARTIFACT.isEmpty()) { + } else if (!UPDATER_GITHUB_REPO.isEmpty() && !BUILD_ARTIFACT.isEmpty()) { UPDATER_ENABLED = true; } - #cmakedefine01 Launcher_ENABLE_JAVA_DOWNLOADER +#cmakedefine01 Launcher_ENABLE_JAVA_DOWNLOADER JAVA_DOWNLOADER_ENABLED = Launcher_ENABLE_JAVA_DOWNLOADER; GIT_COMMIT = "@Launcher_GIT_COMMIT@"; @@ -89,27 +89,19 @@ Config::Config() GIT_REFSPEC = "@Launcher_GIT_REFSPEC@"; // Assume that builds outside of Git repos are "stable" - if (GIT_REFSPEC == QStringLiteral("GITDIR-NOTFOUND") - || GIT_TAG == QStringLiteral("GITDIR-NOTFOUND") - || GIT_REFSPEC == QStringLiteral("") - || GIT_TAG == QStringLiteral("GIT-NOTFOUND")) - { + if (GIT_REFSPEC == QStringLiteral("GITDIR-NOTFOUND") || GIT_TAG == QStringLiteral("GITDIR-NOTFOUND") || + GIT_REFSPEC == QStringLiteral("") || GIT_TAG == QStringLiteral("GIT-NOTFOUND")) { GIT_REFSPEC = "refs/heads/stable"; GIT_TAG = versionString(); GIT_COMMIT = ""; } - if (GIT_REFSPEC.startsWith("refs/heads/")) - { + if (GIT_REFSPEC.startsWith("refs/heads/")) { VERSION_CHANNEL = GIT_REFSPEC; - VERSION_CHANNEL.remove("refs/heads/"); - } - else if (!GIT_COMMIT.isEmpty()) - { + VERSION_CHANNEL.remove("refs/heads/"); + } else if (!GIT_COMMIT.isEmpty()) { VERSION_CHANNEL = GIT_COMMIT.mid(0, 8); - } - else - { + } else { VERSION_CHANNEL = "unknown"; } @@ -136,7 +128,7 @@ Config::Config() QString Config::versionString() const { - return QString("%1.%2").arg(VERSION_MAJOR).arg(VERSION_MINOR); + return QString("%1.%2.%3").arg(VERSION_MAJOR).arg(VERSION_MINOR).arg(VERSION_PATCH); } QString Config::printableVersionString() const @@ -144,8 +136,7 @@ QString Config::printableVersionString() const QString vstr = versionString(); // If the build is not a main release, append the channel - if(VERSION_CHANNEL != "stable" && GIT_TAG != vstr) - { + if (VERSION_CHANNEL != "stable" && GIT_TAG != vstr) { vstr += "-" + VERSION_CHANNEL; } return vstr; @@ -162,4 +153,3 @@ QString Config::systemID() const { return QStringLiteral("%1 %2 %3").arg(COMPILER_TARGET_SYSTEM, COMPILER_TARGET_SYSTEM_VERSION, COMPILER_TARGET_SYSTEM_PROCESSOR); } - diff --git a/buildconfig/BuildConfig.h b/buildconfig/BuildConfig.h index 099d9b5ca..b59adcb57 100644 --- a/buildconfig/BuildConfig.h +++ b/buildconfig/BuildConfig.h @@ -59,6 +59,8 @@ class Config { int VERSION_MAJOR; /// The minor version number. int VERSION_MINOR; + /// The patch version number. + int VERSION_PATCH; /** * The version channel diff --git a/launcher/updater/prismupdater/PrismUpdater.cpp b/launcher/updater/prismupdater/PrismUpdater.cpp index 8bf8cb473..96172b0bc 100644 --- a/launcher/updater/prismupdater/PrismUpdater.cpp +++ b/launcher/updater/prismupdater/PrismUpdater.cpp @@ -298,6 +298,10 @@ PrismUpdaterApp::PrismUpdaterApp(int& argc, char** argv) : QApplication(argc, ar auto version_parts = version.split('.'); m_prismVersionMajor = version_parts.takeFirst().toInt(); m_prismVersionMinor = version_parts.takeFirst().toInt(); + if (!version_parts.isEmpty()) + m_prismVersionPatch = version_parts.takeFirst().toInt(); + else + m_prismVersionPatch = 0; } m_allowPreRelease = parser.isSet("pre-release"); @@ -556,6 +560,7 @@ void PrismUpdaterApp::run() m_prismVersion = BuildConfig.printableVersionString(); m_prismVersionMajor = BuildConfig.VERSION_MAJOR; m_prismVersionMinor = BuildConfig.VERSION_MINOR; + m_prismVersionPatch = BuildConfig.VERSION_PATCH; m_prsimVersionChannel = BuildConfig.VERSION_CHANNEL; m_prismGitCommit = BuildConfig.GIT_COMMIT; } @@ -564,6 +569,7 @@ void PrismUpdaterApp::run() qDebug() << "Executable reports as:" << m_prismBinaryName << "version:" << m_prismVersion; qDebug() << "Version major:" << m_prismVersionMajor; qDebug() << "Version minor:" << m_prismVersionMinor; + qDebug() << "Version minor:" << m_prismVersionPatch; qDebug() << "Version channel:" << m_prsimVersionChannel; qDebug() << "Git Commit:" << m_prismGitCommit; @@ -1277,6 +1283,10 @@ bool PrismUpdaterApp::loadPrismVersionFromExe(const QString& exe_path) return false; m_prismVersionMajor = version_parts.takeFirst().toInt(); m_prismVersionMinor = version_parts.takeFirst().toInt(); + if (!version_parts.isEmpty()) + m_prismVersionPatch = version_parts.takeFirst().toInt(); + else + m_prismVersionPatch = 0; m_prismGitCommit = lines.takeFirst().simplified(); return true; } @@ -1400,7 +1410,7 @@ GitHubRelease PrismUpdaterApp::getLatestRelease() bool PrismUpdaterApp::needUpdate(const GitHubRelease& release) { - auto current_ver = Version(QString("%1.%2").arg(QString::number(m_prismVersionMajor)).arg(QString::number(m_prismVersionMinor))); + auto current_ver = Version(QString("%1.%2.%3").arg(m_prismVersionMajor).arg(m_prismVersionMinor).arg(m_prismVersionPatch)); return current_ver < release.version; } diff --git a/launcher/updater/prismupdater/PrismUpdater.h b/launcher/updater/prismupdater/PrismUpdater.h index f3dd6e062..a904cbb6f 100644 --- a/launcher/updater/prismupdater/PrismUpdater.h +++ b/launcher/updater/prismupdater/PrismUpdater.h @@ -121,6 +121,7 @@ class PrismUpdaterApp : public QApplication { QString m_prismVersion; int m_prismVersionMajor = -1; int m_prismVersionMinor = -1; + int m_prismVersionPatch = -1; QString m_prsimVersionChannel; QString m_prismGitCommit; diff --git a/program_info/win_install.nsi.in b/program_info/win_install.nsi.in index 24f6ee4e8..dfdce2e1c 100644 --- a/program_info/win_install.nsi.in +++ b/program_info/win_install.nsi.in @@ -398,6 +398,7 @@ Section "@Launcher_DisplayName@" WriteRegStr HKCU Software\Classes\prismlauncher\shell\open\command "" '"$INSTDIR\@Launcher_APP_BINARY_NAME@.exe" "%1"' ; Write the uninstall keys for Windows + ; https://learn.microsoft.com/en-us/windows/win32/msi/uninstall-registry-key ${GetParameters} $R0 ${GetOptions} $R0 "/NoUninstaller" $R1 ${If} ${Errors} From cc69a59f368e3db90a1e63ff9025abcf4d3aaf90 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Fri, 11 Apr 2025 08:38:05 +0300 Subject: [PATCH 086/695] fix: crash when the instance window is closed before download dialog is open Signed-off-by: Trial97 --- launcher/ui/pages/instance/ModFolderPage.cpp | 19 +++++++++++-------- .../ui/pages/instance/ResourcePackPage.cpp | 18 +++++++++++------- launcher/ui/pages/instance/ShaderPackPage.cpp | 18 +++++++++++------- .../ui/pages/instance/TexturePackPage.cpp | 18 +++++++++++------- launcher/ui/widgets/ModFilterWidget.cpp | 1 - 5 files changed, 44 insertions(+), 30 deletions(-) diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index 95507ac22..026f0c140 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -145,9 +145,10 @@ void ModFolderPage::downloadMods() QMessageBox::critical(this, tr("Error"), tr("Please install a mod loader first!")); return; } - - ResourceDownload::ModDownloadDialog mdownload(this, m_model, m_instance); - if (mdownload.exec()) { + auto mdownload = new ResourceDownload::ModDownloadDialog(this, m_model, m_instance); + mdownload->setAttribute(Qt::WA_DeleteOnClose); + connect(this, &QObject::destroyed, mdownload, &QDialog::close); + if (mdownload->exec()) { auto tasks = new ConcurrentTask(tr("Download Mods"), APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); connect(tasks, &Task::failed, [this, tasks](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); @@ -165,7 +166,7 @@ void ModFolderPage::downloadMods() tasks->deleteLater(); }); - for (auto& task : mdownload.getTasks()) { + for (auto& task : mdownload->getTasks()) { tasks->addTask(task); } @@ -300,9 +301,11 @@ void ModFolderPage::changeModVersion() if (mods_list.length() != 1 || mods_list[0]->metadata() == nullptr) return; - ResourceDownload::ModDownloadDialog mdownload(this, m_model, m_instance); - mdownload.setResourceMetadata((*mods_list.begin())->metadata()); - if (mdownload.exec()) { + auto mdownload = new ResourceDownload::ModDownloadDialog(this, m_model, m_instance); + mdownload->setAttribute(Qt::WA_DeleteOnClose); + connect(this, &QObject::destroyed, mdownload, &QDialog::close); + mdownload->setResourceMetadata((*mods_list.begin())->metadata()); + if (mdownload->exec()) { auto tasks = new ConcurrentTask("Download Mods", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); connect(tasks, &Task::failed, [this, tasks](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); @@ -320,7 +323,7 @@ void ModFolderPage::changeModVersion() tasks->deleteLater(); }); - for (auto& task : mdownload.getTasks()) { + for (auto& task : mdownload->getTasks()) { tasks->addTask(task); } diff --git a/launcher/ui/pages/instance/ResourcePackPage.cpp b/launcher/ui/pages/instance/ResourcePackPage.cpp index 79e677765..ae5eb8fac 100644 --- a/launcher/ui/pages/instance/ResourcePackPage.cpp +++ b/launcher/ui/pages/instance/ResourcePackPage.cpp @@ -84,8 +84,10 @@ void ResourcePackPage::downloadResourcePacks() if (m_instance->typeName() != "Minecraft") return; // this is a null instance or a legacy instance - ResourceDownload::ResourcePackDownloadDialog mdownload(this, m_model, m_instance); - if (mdownload.exec()) { + auto mdownload = new ResourceDownload::ResourcePackDownloadDialog(this, m_model, m_instance); + mdownload->setAttribute(Qt::WA_DeleteOnClose); + connect(this, &QObject::destroyed, mdownload, &QDialog::close); + if (mdownload->exec()) { auto tasks = new ConcurrentTask("Download Resource Pack", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); connect(tasks, &Task::failed, [this, tasks](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); @@ -103,7 +105,7 @@ void ResourcePackPage::downloadResourcePacks() tasks->deleteLater(); }); - for (auto& task : mdownload.getTasks()) { + for (auto& task : mdownload->getTasks()) { tasks->addTask(task); } @@ -235,9 +237,11 @@ void ResourcePackPage::changeResourcePackVersion() if (resource.metadata() == nullptr) return; - ResourceDownload::ResourcePackDownloadDialog mdownload(this, m_model, m_instance); - mdownload.setResourceMetadata(resource.metadata()); - if (mdownload.exec()) { + auto mdownload = new ResourceDownload::ResourcePackDownloadDialog(this, m_model, m_instance); + mdownload->setAttribute(Qt::WA_DeleteOnClose); + connect(this, &QObject::destroyed, mdownload, &QDialog::close); + mdownload->setResourceMetadata(resource.metadata()); + if (mdownload->exec()) { auto tasks = new ConcurrentTask("Download Resource Packs", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); connect(tasks, &Task::failed, [this, tasks](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); @@ -255,7 +259,7 @@ void ResourcePackPage::changeResourcePackVersion() tasks->deleteLater(); }); - for (auto& task : mdownload.getTasks()) { + for (auto& task : mdownload->getTasks()) { tasks->addTask(task); } diff --git a/launcher/ui/pages/instance/ShaderPackPage.cpp b/launcher/ui/pages/instance/ShaderPackPage.cpp index a287d3edf..45bb02030 100644 --- a/launcher/ui/pages/instance/ShaderPackPage.cpp +++ b/launcher/ui/pages/instance/ShaderPackPage.cpp @@ -81,8 +81,10 @@ void ShaderPackPage::downloadShaderPack() if (m_instance->typeName() != "Minecraft") return; // this is a null instance or a legacy instance - ResourceDownload::ShaderPackDownloadDialog mdownload(this, m_model, m_instance); - if (mdownload.exec()) { + auto mdownload = new ResourceDownload::ShaderPackDownloadDialog(this, m_model, m_instance); + mdownload->setAttribute(Qt::WA_DeleteOnClose); + connect(this, &QObject::destroyed, mdownload, &QDialog::close); + if (mdownload->exec()) { auto tasks = new ConcurrentTask("Download Shader Packs", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); connect(tasks, &Task::failed, [this, tasks](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); @@ -100,7 +102,7 @@ void ShaderPackPage::downloadShaderPack() tasks->deleteLater(); }); - for (auto& task : mdownload.getTasks()) { + for (auto& task : mdownload->getTasks()) { tasks->addTask(task); } @@ -232,9 +234,11 @@ void ShaderPackPage::changeShaderPackVersion() if (resource.metadata() == nullptr) return; - ResourceDownload::ShaderPackDownloadDialog mdownload(this, m_model, m_instance); - mdownload.setResourceMetadata(resource.metadata()); - if (mdownload.exec()) { + auto mdownload = new ResourceDownload::ShaderPackDownloadDialog(this, m_model, m_instance); + mdownload->setAttribute(Qt::WA_DeleteOnClose); + connect(this, &QObject::destroyed, mdownload, &QDialog::close); + mdownload->setResourceMetadata(resource.metadata()); + if (mdownload->exec()) { auto tasks = new ConcurrentTask("Download Shader Packs", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); connect(tasks, &Task::failed, [this, tasks](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); @@ -252,7 +256,7 @@ void ShaderPackPage::changeShaderPackVersion() tasks->deleteLater(); }); - for (auto& task : mdownload.getTasks()) { + for (auto& task : mdownload->getTasks()) { tasks->addTask(task); } diff --git a/launcher/ui/pages/instance/TexturePackPage.cpp b/launcher/ui/pages/instance/TexturePackPage.cpp index fd1e0a2fc..6d000a486 100644 --- a/launcher/ui/pages/instance/TexturePackPage.cpp +++ b/launcher/ui/pages/instance/TexturePackPage.cpp @@ -90,8 +90,10 @@ void TexturePackPage::downloadTexturePacks() if (m_instance->typeName() != "Minecraft") return; // this is a null instance or a legacy instance - ResourceDownload::TexturePackDownloadDialog mdownload(this, m_model, m_instance); - if (mdownload.exec()) { + auto mdownload = new ResourceDownload::TexturePackDownloadDialog(this, m_model, m_instance); + mdownload->setAttribute(Qt::WA_DeleteOnClose); + connect(this, &QObject::destroyed, mdownload, &QDialog::close); + if (mdownload->exec()) { auto tasks = new ConcurrentTask("Download Texture Packs", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); connect(tasks, &Task::failed, [this, tasks](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); @@ -109,7 +111,7 @@ void TexturePackPage::downloadTexturePacks() tasks->deleteLater(); }); - for (auto& task : mdownload.getTasks()) { + for (auto& task : mdownload->getTasks()) { tasks->addTask(task); } @@ -241,9 +243,11 @@ void TexturePackPage::changeTexturePackVersion() if (resource.metadata() == nullptr) return; - ResourceDownload::TexturePackDownloadDialog mdownload(this, m_model, m_instance); - mdownload.setResourceMetadata(resource.metadata()); - if (mdownload.exec()) { + auto mdownload = new ResourceDownload::TexturePackDownloadDialog(this, m_model, m_instance); + mdownload->setAttribute(Qt::WA_DeleteOnClose); + connect(this, &QObject::destroyed, mdownload, &QDialog::close); + mdownload->setResourceMetadata(resource.metadata()); + if (mdownload->exec()) { auto tasks = new ConcurrentTask("Download Texture Packs", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); connect(tasks, &Task::failed, [this, tasks](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); @@ -261,7 +265,7 @@ void TexturePackPage::changeTexturePackVersion() tasks->deleteLater(); }); - for (auto& task : mdownload.getTasks()) { + for (auto& task : mdownload->getTasks()) { tasks->addTask(task); } diff --git a/launcher/ui/widgets/ModFilterWidget.cpp b/launcher/ui/widgets/ModFilterWidget.cpp index 0fda7933e..03522bc19 100644 --- a/launcher/ui/widgets/ModFilterWidget.cpp +++ b/launcher/ui/widgets/ModFilterWidget.cpp @@ -291,7 +291,6 @@ void ModFilterWidget::onSideFilterChanged() side = ""; } - m_filter_changed = side != m_filter->side; m_filter->side = side; if (m_filter_changed) From 8bb9b168fb996df9209e1e34be854235eda3d42a Mon Sep 17 00:00:00 2001 From: Yihe Li Date: Sat, 12 Apr 2025 01:59:07 +0800 Subject: [PATCH 087/695] Use explicit construction for QFile from QString Signed-off-by: Yihe Li --- launcher/FileSystem.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp index 5d3008aae..7189ca841 100644 --- a/launcher/FileSystem.cpp +++ b/launcher/FileSystem.cpp @@ -934,7 +934,7 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri QDir content = application.path() + "/Contents/"; QDir resources = content.path() + "/Resources/"; QDir binaryDir = content.path() + "/MacOS/"; - QFile info = content.path() + "/Info.plist"; + QFile info(content.path() + "/Info.plist"); if (!(content.mkpath(".") && resources.mkpath(".") && binaryDir.mkpath("."))) { qWarning() << "Couldn't create directories within application"; From 6812d137e67b323fdec32c091cb4f408f8f5874b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 13 Apr 2025 00:51:01 +0000 Subject: [PATCH 088/695] chore(nix): update lockfile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'nixpkgs': 'github:NixOS/nixpkgs/2c8d3f48d33929642c1c12cd243df4cc7d2ce434?narHash=sha256-F7n4%2BKOIfWrwoQjXrL2wD9RhFYLs2/GGe/MQY1sSdlE%3D' (2025-04-02) → 'github:NixOS/nixpkgs/2631b0b7abcea6e640ce31cd78ea58910d31e650?narHash=sha256-LWqduOgLHCFxiTNYi3Uj5Lgz0SR%2BXhw3kr/3Xd0GPTM%3D' (2025-04-12) --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 996d79e22..2d79b8335 100644 --- a/flake.lock +++ b/flake.lock @@ -18,11 +18,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1743583204, - "narHash": "sha256-F7n4+KOIfWrwoQjXrL2wD9RhFYLs2/GGe/MQY1sSdlE=", + "lastModified": 1744463964, + "narHash": "sha256-LWqduOgLHCFxiTNYi3Uj5Lgz0SR+Xhw3kr/3Xd0GPTM=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "2c8d3f48d33929642c1c12cd243df4cc7d2ce434", + "rev": "2631b0b7abcea6e640ce31cd78ea58910d31e650", "type": "github" }, "original": { From 7d4034cfa55275edc08cef2c6dd9d9f8f5d063d1 Mon Sep 17 00:00:00 2001 From: Kenneth Chew <79120643+kthchew@users.noreply.github.com> Date: Sun, 13 Apr 2025 00:50:19 -0400 Subject: [PATCH 089/695] Shorten LocalPeer socket names On most systems supporting Unix sockets, the maximum length of a socket name is quite low (e.g. on macOS 104 characters and on Linux 108). If the name is too long, the sockets will not work and thus sending messages to a running instance of the launcher will not work. Signed-off-by: Kenneth Chew <79120643+kthchew@users.noreply.github.com> --- launcher/Application.cpp | 17 ++++++++++++----- libraries/LocalPeer/src/LocalPeer.cpp | 8 ++++---- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 816f7b8ab..d773d9a1c 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -375,19 +375,20 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_peerInstance = new LocalPeer(this, appID); connect(m_peerInstance, &LocalPeer::messageReceived, this, &Application::messageReceived); if (m_peerInstance->isClient()) { + bool sentMessage = false; int timeout = 2000; if (m_instanceIdToLaunch.isEmpty()) { ApplicationMessage activate; activate.command = "activate"; - m_peerInstance->sendMessage(activate.serialize(), timeout); + sentMessage = m_peerInstance->sendMessage(activate.serialize(), timeout); if (!m_urlsToImport.isEmpty()) { for (auto url : m_urlsToImport) { ApplicationMessage import; import.command = "import"; import.args.insert("url", url.toString()); - m_peerInstance->sendMessage(import.serialize(), timeout); + sentMessage = m_peerInstance->sendMessage(import.serialize(), timeout); } } } else { @@ -407,10 +408,16 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) launch.args["offline_enabled"] = "true"; launch.args["offline_name"] = m_offlineName; } - m_peerInstance->sendMessage(launch.serialize(), timeout); + sentMessage = m_peerInstance->sendMessage(launch.serialize(), timeout); + } + if (sentMessage) { + m_status = Application::Succeeded; + return; + } else { + std::cerr << "Unable to redirect command to already running instance\n"; + // C function not Qt function - event loop not started yet + ::exit(1); } - m_status = Application::Succeeded; - return; } } diff --git a/libraries/LocalPeer/src/LocalPeer.cpp b/libraries/LocalPeer/src/LocalPeer.cpp index bd407042f..c1875bf98 100644 --- a/libraries/LocalPeer/src/LocalPeer.cpp +++ b/libraries/LocalPeer/src/LocalPeer.cpp @@ -76,7 +76,7 @@ ApplicationId ApplicationId::fromTraditionalApp() prefix.truncate(6); QByteArray idc = protoId.toUtf8(); quint16 idNum = qChecksum(idc.constData(), idc.size()); - auto socketName = QLatin1String("qtsingleapp-") + prefix + QLatin1Char('-') + QString::number(idNum, 16); + auto socketName = QLatin1String("pl") + prefix + QLatin1Char('-') + QString::number(idNum, 16).left(12); #if defined(Q_OS_WIN) if (!pProcessIdToSessionId) { QLibrary lib("kernel32"); @@ -98,12 +98,12 @@ ApplicationId ApplicationId::fromPathAndVersion(const QString& dataPath, const Q QCryptographicHash shasum(QCryptographicHash::Algorithm::Sha1); QString result = dataPath + QLatin1Char('-') + version; shasum.addData(result.toUtf8()); - return ApplicationId(QLatin1String("qtsingleapp-") + QString::fromLatin1(shasum.result().toHex())); + return ApplicationId(QLatin1String("pl") + QString::fromLatin1(shasum.result().toHex()).left(12)); } ApplicationId ApplicationId::fromCustomId(const QString& id) { - return ApplicationId(QLatin1String("qtsingleapp-") + id); + return ApplicationId(QLatin1String("pl") + id); } ApplicationId ApplicationId::fromRawString(const QString& id) @@ -139,7 +139,7 @@ bool LocalPeer::isClient() #if defined(Q_OS_UNIX) // ### Workaround if (!res && server->serverError() == QAbstractSocket::AddressInUseError) { - QFile::remove(QDir::cleanPath(QDir::tempPath()) + QLatin1Char('/') + socketName); + QLocalServer::removeServer(socketName); res = server->listen(socketName); } #endif From bd304eee947847b1d8630568933e5d95046d349c Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Sun, 13 Apr 2025 12:16:07 -0700 Subject: [PATCH 090/695] chore: use nix-shell over nix develop in .envrc (brakes less things) Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- .envrc | 2 +- flake.nix | 2 ++ shell.nix | 4 ++++ 3 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 shell.nix diff --git a/.envrc b/.envrc index 190b5b2b3..1d11c5354 100644 --- a/.envrc +++ b/.envrc @@ -1,2 +1,2 @@ -use flake +use nix watch_file nix/*.nix diff --git a/flake.nix b/flake.nix index fd3003bc4..594a82d91 100644 --- a/flake.nix +++ b/flake.nix @@ -132,6 +132,8 @@ { default = pkgs.mkShell { + name = "prism-launcher"; + inputsFrom = [ packages'.prismlauncher-unwrapped ]; packages = with pkgs; [ diff --git a/shell.nix b/shell.nix new file mode 100644 index 000000000..21bab1009 --- /dev/null +++ b/shell.nix @@ -0,0 +1,4 @@ +(import (fetchTarball { + url = "https://github.com/edolstra/flake-compat/archive/ff81ac966bb2cae68946d5ed5fc4994f96d0ffec.tar.gz"; + sha256 = "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU="; +}) { src = ./.; }).shellNix From 4ac6a0629b7c52aca9272cfbff8908d28b2b1763 Mon Sep 17 00:00:00 2001 From: Yihe Li Date: Tue, 15 Apr 2025 03:42:28 +0800 Subject: [PATCH 091/695] Use LogView to implement level highlighting for other logs Signed-off-by: Yihe Li --- launcher/InstancePageProvider.h | 2 +- launcher/ui/pages/instance/LogPage.cpp | 131 +++++++++---------- launcher/ui/pages/instance/LogPage.h | 14 +- launcher/ui/pages/instance/OtherLogsPage.cpp | 77 ++++++++--- launcher/ui/pages/instance/OtherLogsPage.h | 7 +- launcher/ui/pages/instance/OtherLogsPage.ui | 18 ++- 6 files changed, 156 insertions(+), 93 deletions(-) diff --git a/launcher/InstancePageProvider.h b/launcher/InstancePageProvider.h index 1d7c193f8..acc7fce58 100644 --- a/launcher/InstancePageProvider.h +++ b/launcher/InstancePageProvider.h @@ -46,7 +46,7 @@ class InstancePageProvider : protected QObject, public BasePageProvider { values.append(new InstanceSettingsPage(onesix)); auto logMatcher = inst->getLogFileMatcher(); if (logMatcher) { - values.append(new OtherLogsPage(inst->getLogFileRoot(), logMatcher)); + values.append(new OtherLogsPage(inst, logMatcher)); } return values; } diff --git a/launcher/ui/pages/instance/LogPage.cpp b/launcher/ui/pages/instance/LogPage.cpp index 4962f90ce..f050212b0 100644 --- a/launcher/ui/pages/instance/LogPage.cpp +++ b/launcher/ui/pages/instance/LogPage.cpp @@ -52,90 +52,81 @@ #include -class LogFormatProxyModel : public QIdentityProxyModel { - public: - LogFormatProxyModel(QObject* parent = nullptr) : QIdentityProxyModel(parent) {} - QVariant data(const QModelIndex& index, int role) const override - { - const LogColors& colors = APPLICATION->themeManager()->getLogColors(); +QVariant LogFormatProxyModel::data(const QModelIndex& index, int role) const +{ + const LogColors& colors = APPLICATION->themeManager()->getLogColors(); - switch (role) { - case Qt::FontRole: - return m_font; - case Qt::ForegroundRole: { - auto level = static_cast(QIdentityProxyModel::data(index, LogModel::LevelRole).toInt()); - QColor result = colors.foreground.value(level); + switch (role) { + case Qt::FontRole: + return m_font; + case Qt::ForegroundRole: { + auto level = static_cast(QIdentityProxyModel::data(index, LogModel::LevelRole).toInt()); + QColor result = colors.foreground.value(level); - if (result.isValid()) - return result; + if (result.isValid()) + return result; - break; - } - case Qt::BackgroundRole: { - auto level = static_cast(QIdentityProxyModel::data(index, LogModel::LevelRole).toInt()); - QColor result = colors.background.value(level); + break; + } + case Qt::BackgroundRole: { + auto level = static_cast(QIdentityProxyModel::data(index, LogModel::LevelRole).toInt()); + QColor result = colors.background.value(level); - if (result.isValid()) - return result; + if (result.isValid()) + return result; - break; - } + break; } - - return QIdentityProxyModel::data(index, role); } - void setFont(QFont font) { m_font = font; } + return QIdentityProxyModel::data(index, role); +} - QModelIndex find(const QModelIndex& start, const QString& value, bool reverse) const - { - QModelIndex parentIndex = parent(start); - auto compare = [this, start, parentIndex, value](int r) -> QModelIndex { - QModelIndex idx = index(r, start.column(), parentIndex); - if (!idx.isValid() || idx == start) { - return QModelIndex(); - } - QVariant v = data(idx, Qt::DisplayRole); - QString t = v.toString(); - if (t.contains(value, Qt::CaseInsensitive)) - return idx; +QModelIndex LogFormatProxyModel::find(const QModelIndex& start, const QString& value, bool reverse) const +{ + QModelIndex parentIndex = parent(start); + auto compare = [this, start, parentIndex, value](int r) -> QModelIndex { + QModelIndex idx = index(r, start.column(), parentIndex); + if (!idx.isValid() || idx == start) { return QModelIndex(); - }; - if (reverse) { - int from = start.row(); - int to = 0; - - for (int i = 0; i < 2; ++i) { - for (int r = from; (r >= to); --r) { - auto idx = compare(r); - if (idx.isValid()) - return idx; - } - // prepare for the next iteration - from = rowCount() - 1; - to = start.row(); + } + QVariant v = data(idx, Qt::DisplayRole); + QString t = v.toString(); + if (t.contains(value, Qt::CaseInsensitive)) + return idx; + return QModelIndex(); + }; + if (reverse) { + int from = start.row(); + int to = 0; + + for (int i = 0; i < 2; ++i) { + for (int r = from; (r >= to); --r) { + auto idx = compare(r); + if (idx.isValid()) + return idx; } - } else { - int from = start.row(); - int to = rowCount(parentIndex); - - for (int i = 0; i < 2; ++i) { - for (int r = from; (r < to); ++r) { - auto idx = compare(r); - if (idx.isValid()) - return idx; - } - // prepare for the next iteration - from = 0; - to = start.row(); + // prepare for the next iteration + from = rowCount() - 1; + to = start.row(); + } + } else { + int from = start.row(); + int to = rowCount(parentIndex); + + for (int i = 0; i < 2; ++i) { + for (int r = from; (r < to); ++r) { + auto idx = compare(r); + if (idx.isValid()) + return idx; } + // prepare for the next iteration + from = 0; + to = start.row(); } - return QModelIndex(); } - - private: - QFont m_font; -}; + return QModelIndex(); +} LogPage::LogPage(InstancePtr instance, QWidget* parent) : QWidget(parent), ui(new Ui::LogPage), m_instance(instance) { diff --git a/launcher/ui/pages/instance/LogPage.h b/launcher/ui/pages/instance/LogPage.h index 6c259891d..1295410ea 100644 --- a/launcher/ui/pages/instance/LogPage.h +++ b/launcher/ui/pages/instance/LogPage.h @@ -35,6 +35,7 @@ #pragma once +#include #include #include @@ -46,7 +47,18 @@ namespace Ui { class LogPage; } class QTextCharFormat; -class LogFormatProxyModel; + +class LogFormatProxyModel : public QIdentityProxyModel { + public: + LogFormatProxyModel(QObject* parent = nullptr) : QIdentityProxyModel(parent) {} + QVariant data(const QModelIndex& index, int role) const override; + QFont getFont() const { return m_font; } + void setFont(QFont font) { m_font = font; } + QModelIndex find(const QModelIndex& start, const QString& value, bool reverse) const; + + private: + QFont m_font; +}; class LogPage : public QWidget, public BasePage { Q_OBJECT diff --git a/launcher/ui/pages/instance/OtherLogsPage.cpp b/launcher/ui/pages/instance/OtherLogsPage.cpp index ed8ef68d9..6fa7cb4c1 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.cpp +++ b/launcher/ui/pages/instance/OtherLogsPage.cpp @@ -46,12 +46,38 @@ #include #include "RecursiveFileSystemWatcher.h" -OtherLogsPage::OtherLogsPage(QString path, IPathMatcher::Ptr fileFilter, QWidget* parent) - : QWidget(parent), ui(new Ui::OtherLogsPage), m_path(path), m_fileFilter(fileFilter), m_watcher(new RecursiveFileSystemWatcher(this)) +OtherLogsPage::OtherLogsPage(InstancePtr instance, IPathMatcher::Ptr fileFilter, QWidget* parent) + : QWidget(parent) + , ui(new Ui::OtherLogsPage) + , m_instance(instance) + , m_path(instance->getLogFileRoot()) + , m_fileFilter(fileFilter) + , m_watcher(new RecursiveFileSystemWatcher(this)) + , m_model(new LogModel()) { ui->setupUi(this); ui->tabWidget->tabBar()->hide(); + m_proxy = new LogFormatProxyModel(this); + + // set up fonts in the log proxy + { + QString fontFamily = APPLICATION->settings()->get("ConsoleFont").toString(); + bool conversionOk = false; + int fontSize = APPLICATION->settings()->get("ConsoleFontSize").toInt(&conversionOk); + if (!conversionOk) { + fontSize = 11; + } + m_proxy->setFont(QFont(fontFamily, fontSize)); + } + + ui->text->setModel(m_proxy); + + m_model->setMaxLines(m_instance->getConsoleMaxLines()); + m_model->setStopOnOverflow(m_instance->shouldStopOnConsoleOverflow()); + m_model->setOverflowMessage(tr("Cannot display this log since the log length surpassed %1 lines.").arg(m_model->getMaxLines())); + m_proxy->setSourceModel(m_model.get()); + m_watcher->setMatcher(fileFilter); m_watcher->setRootDir(QDir::current().absoluteFilePath(m_path)); @@ -139,14 +165,8 @@ void OtherLogsPage::on_btnReload_clicked() QMessageBox::critical(this, tr("Error"), tr("Unable to open %1 for reading: %2").arg(m_currentFile, file.errorString())); } else { auto setPlainText = [this](const QString& text) { - QString fontFamily = APPLICATION->settings()->get("ConsoleFont").toString(); - bool conversionOk = false; - int fontSize = APPLICATION->settings()->get("ConsoleFontSize").toInt(&conversionOk); - if (!conversionOk) { - fontSize = 11; - } QTextDocument* doc = ui->text->document(); - doc->setDefaultFont(QFont(fontFamily, fontSize)); + doc->setDefaultFont(m_proxy->getFont()); ui->text->setPlainText(text); }; auto showTooBig = [setPlainText, &file]() { @@ -173,7 +193,32 @@ void OtherLogsPage::on_btnReload_clicked() showTooBig(); return; } - setPlainText(content); + + // If the file is not too big for display, but too slow for syntax highlighting, just show content as plain text + if (content.size() >= 10000000ll || content.isEmpty()) { + setPlainText(content); + return; + } + + // Try to determine a level for each line + if (content.back() == '\n') + content = content.removeLast(); + for (auto& line : content.split('\n')) { + MessageLevel::Enum level = MessageLevel::Unknown; + + // if the launcher part set a log level, use it + auto innerLevel = MessageLevel::fromLine(line); + if (innerLevel != MessageLevel::Unknown) { + level = innerLevel; + } + + // If the level is still undetermined, guess level + if (level == MessageLevel::StdErr || level == MessageLevel::StdOut || level == MessageLevel::Unknown) { + level = m_instance->guessLevel(line, level); + } + + m_model->append(level, line); + } } } @@ -273,27 +318,21 @@ void OtherLogsPage::setControlsEnabled(const bool enabled) ui->btnClean->setEnabled(enabled); } -// FIXME: HACK, use LogView instead? -static void findNext(QPlainTextEdit* _this, const QString& what, bool reverse) -{ - _this->find(what, reverse ? QTextDocument::FindFlag::FindBackward : QTextDocument::FindFlag(0)); -} - void OtherLogsPage::on_findButton_clicked() { auto modifiers = QApplication::keyboardModifiers(); bool reverse = modifiers & Qt::ShiftModifier; - findNext(ui->text, ui->searchBar->text(), reverse); + ui->text->findNext(ui->searchBar->text(), reverse); } void OtherLogsPage::findNextActivated() { - findNext(ui->text, ui->searchBar->text(), false); + ui->text->findNext(ui->searchBar->text(), false); } void OtherLogsPage::findPreviousActivated() { - findNext(ui->text, ui->searchBar->text(), true); + ui->text->findNext(ui->searchBar->text(), true); } void OtherLogsPage::findActivated() diff --git a/launcher/ui/pages/instance/OtherLogsPage.h b/launcher/ui/pages/instance/OtherLogsPage.h index 85a3a2dbc..d65ed6456 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.h +++ b/launcher/ui/pages/instance/OtherLogsPage.h @@ -39,6 +39,7 @@ #include #include +#include "LogPage.h" #include "ui/pages/BasePage.h" namespace Ui { @@ -51,7 +52,7 @@ class OtherLogsPage : public QWidget, public BasePage { Q_OBJECT public: - explicit OtherLogsPage(QString path, IPathMatcher::Ptr fileFilter, QWidget* parent = 0); + explicit OtherLogsPage(InstancePtr instance, IPathMatcher::Ptr fileFilter, QWidget* parent = 0); ~OtherLogsPage(); QString id() const override { return "logs"; } @@ -82,8 +83,12 @@ class OtherLogsPage : public QWidget, public BasePage { private: Ui::OtherLogsPage* ui; + InstancePtr m_instance; QString m_path; QString m_currentFile; IPathMatcher::Ptr m_fileFilter; RecursiveFileSystemWatcher* m_watcher; + + LogFormatProxyModel* m_proxy; + shared_qobject_ptr m_model; }; diff --git a/launcher/ui/pages/instance/OtherLogsPage.ui b/launcher/ui/pages/instance/OtherLogsPage.ui index 3fdb023fe..de3091917 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.ui +++ b/launcher/ui/pages/instance/OtherLogsPage.ui @@ -44,16 +44,25 @@ - + false + + false + true + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + false + @@ -130,6 +139,13 @@ + + + LogView + QPlainTextEdit +
ui/widgets/LogView.h
+
+
tabWidget selectLogBox From 1ee1bab067d94035b01c76c8728b3d4e09b702ef Mon Sep 17 00:00:00 2001 From: Yihe Li Date: Tue, 15 Apr 2025 05:07:56 +0800 Subject: [PATCH 092/695] Add color lines button Signed-off-by: Yihe Li --- launcher/launch/LogModel.cpp | 12 ++++++++++++ launcher/launch/LogModel.h | 3 +++ launcher/ui/pages/instance/LogPage.cpp | 16 ++++++++++++++++ launcher/ui/pages/instance/LogPage.h | 1 + launcher/ui/pages/instance/LogPage.ui | 11 +++++++++++ launcher/ui/widgets/LogView.cpp | 12 ++++++++++-- launcher/ui/widgets/LogView.h | 2 ++ 7 files changed, 55 insertions(+), 2 deletions(-) diff --git a/launcher/launch/LogModel.cpp b/launcher/launch/LogModel.cpp index 23a33ae18..dd32d46a2 100644 --- a/launcher/launch/LogModel.cpp +++ b/launcher/launch/LogModel.cpp @@ -149,3 +149,15 @@ bool LogModel::wrapLines() const { return m_lineWrap; } + +void LogModel::setColorLines(bool state) +{ + if (m_colorLines != state) { + m_colorLines = state; + } +} + +bool LogModel::colorLines() const +{ + return m_colorLines; +} diff --git a/launcher/launch/LogModel.h b/launcher/launch/LogModel.h index 167f74190..6c2a8cff3 100644 --- a/launcher/launch/LogModel.h +++ b/launcher/launch/LogModel.h @@ -27,6 +27,8 @@ class LogModel : public QAbstractListModel { void setLineWrap(bool state); bool wrapLines() const; + void setColorLines(bool state); + bool colorLines() const; enum Roles { LevelRole = Qt::UserRole }; @@ -47,6 +49,7 @@ class LogModel : public QAbstractListModel { QString m_overflowMessage = "OVERFLOW"; bool m_suspended = false; bool m_lineWrap = true; + bool m_colorLines = true; private: Q_DISABLE_COPY(LogModel) diff --git a/launcher/ui/pages/instance/LogPage.cpp b/launcher/ui/pages/instance/LogPage.cpp index f050212b0..7897a2932 100644 --- a/launcher/ui/pages/instance/LogPage.cpp +++ b/launcher/ui/pages/instance/LogPage.cpp @@ -180,6 +180,13 @@ void LogPage::modelStateToUI() ui->text->setWordWrap(false); ui->wrapCheckbox->setCheckState(Qt::Unchecked); } + if (m_model->colorLines()) { + ui->text->setColorLines(true); + ui->colorCheckbox->setCheckState(Qt::Checked); + } else { + ui->text->setColorLines(false); + ui->colorCheckbox->setCheckState(Qt::Unchecked); + } if (m_model->suspended()) { ui->trackLogCheckbox->setCheckState(Qt::Unchecked); } else { @@ -193,6 +200,7 @@ void LogPage::UIToModelState() return; } m_model->setLineWrap(ui->wrapCheckbox->checkState() == Qt::Checked); + m_model->setColorLines(ui->colorCheckbox->checkState() == Qt::Checked); m_model->suspend(ui->trackLogCheckbox->checkState() != Qt::Checked); } @@ -282,6 +290,14 @@ void LogPage::on_wrapCheckbox_clicked(bool checked) m_model->setLineWrap(checked); } +void LogPage::on_colorCheckbox_clicked(bool checked) +{ + ui->text->setColorLines(checked); + if (!m_model) + return; + m_model->setColorLines(checked); +} + void LogPage::on_findButton_clicked() { auto modifiers = QApplication::keyboardModifiers(); diff --git a/launcher/ui/pages/instance/LogPage.h b/launcher/ui/pages/instance/LogPage.h index 1295410ea..b4d74fb9c 100644 --- a/launcher/ui/pages/instance/LogPage.h +++ b/launcher/ui/pages/instance/LogPage.h @@ -82,6 +82,7 @@ class LogPage : public QWidget, public BasePage { void on_trackLogCheckbox_clicked(bool checked); void on_wrapCheckbox_clicked(bool checked); + void on_colorCheckbox_clicked(bool checked); void on_findButton_clicked(); void findActivated(); diff --git a/launcher/ui/pages/instance/LogPage.ui b/launcher/ui/pages/instance/LogPage.ui index 31bb368c8..fb8690581 100644 --- a/launcher/ui/pages/instance/LogPage.ui +++ b/launcher/ui/pages/instance/LogPage.ui @@ -74,6 +74,16 @@
+ + + + Color lines + + + true + + + @@ -170,6 +180,7 @@ tabWidget trackLogCheckbox wrapCheckbox + colorCheckbox btnCopy btnPaste btnClear diff --git a/launcher/ui/widgets/LogView.cpp b/launcher/ui/widgets/LogView.cpp index 6578b1f12..181893af4 100644 --- a/launcher/ui/widgets/LogView.cpp +++ b/launcher/ui/widgets/LogView.cpp @@ -60,6 +60,14 @@ void LogView::setWordWrap(bool wrapping) } } +void LogView::setColorLines(bool colorLines) +{ + if (m_colorLines == colorLines) + return; + m_colorLines = colorLines; + repopulate(); +} + void LogView::setModel(QAbstractItemModel* model) { if (m_model) { @@ -130,11 +138,11 @@ void LogView::rowsInserted(const QModelIndex& parent, int first, int last) format.setFont(font.value()); } auto fg = m_model->data(idx, Qt::ForegroundRole); - if (fg.isValid()) { + if (fg.isValid() && m_colorLines) { format.setForeground(fg.value()); } auto bg = m_model->data(idx, Qt::BackgroundRole); - if (bg.isValid()) { + if (bg.isValid() && m_colorLines) { format.setBackground(bg.value()); } cursor.movePosition(QTextCursor::End); diff --git a/launcher/ui/widgets/LogView.h b/launcher/ui/widgets/LogView.h index dde5f8f76..69ca332bb 100644 --- a/launcher/ui/widgets/LogView.h +++ b/launcher/ui/widgets/LogView.h @@ -15,6 +15,7 @@ class LogView : public QPlainTextEdit { public slots: void setWordWrap(bool wrapping); + void setColorLines(bool colorLines); void findNext(const QString& what, bool reverse); void scrollToBottom(); @@ -32,4 +33,5 @@ class LogView : public QPlainTextEdit { QTextCharFormat* m_defaultFormat = nullptr; bool m_scroll = false; bool m_scrolling = false; + bool m_colorLines = true; }; From 5634723ecd7e966b4722b7f62692de18cbde631b Mon Sep 17 00:00:00 2001 From: Yihe Li Date: Tue, 15 Apr 2025 06:00:49 +0800 Subject: [PATCH 093/695] Harmonizing other log controls with minecraft log Signed-off-by: Yihe Li --- launcher/ui/pages/instance/OtherLogsPage.cpp | 25 +++ launcher/ui/pages/instance/OtherLogsPage.h | 4 + launcher/ui/pages/instance/OtherLogsPage.ui | 173 +++++++++++++------ 3 files changed, 147 insertions(+), 55 deletions(-) diff --git a/launcher/ui/pages/instance/OtherLogsPage.cpp b/launcher/ui/pages/instance/OtherLogsPage.cpp index 6fa7cb4c1..40a5d9d94 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.cpp +++ b/launcher/ui/pages/instance/OtherLogsPage.cpp @@ -203,6 +203,8 @@ void OtherLogsPage::on_btnReload_clicked() // Try to determine a level for each line if (content.back() == '\n') content = content.removeLast(); + ui->text->clear(); + m_model->clear(); for (auto& line : content.split('\n')) { MessageLevel::Enum level = MessageLevel::Unknown; @@ -232,6 +234,11 @@ void OtherLogsPage::on_btnCopy_clicked() GuiUtil::setClipboardText(ui->text->toPlainText()); } +void OtherLogsPage::on_btnBottom_clicked() +{ + ui->text->scrollToBottom(); +} + void OtherLogsPage::on_btnDelete_clicked() { if (m_currentFile.isEmpty()) { @@ -308,6 +315,24 @@ void OtherLogsPage::on_btnClean_clicked() } } +void OtherLogsPage::on_wrapCheckbox_clicked(bool checked) +{ + ui->text->setWordWrap(checked); + if (!m_model) + return; + m_model->setLineWrap(checked); + ui->text->scrollToBottom(); +} + +void OtherLogsPage::on_colorCheckbox_clicked(bool checked) +{ + ui->text->setColorLines(checked); + if (!m_model) + return; + m_model->setColorLines(checked); + ui->text->scrollToBottom(); +} + void OtherLogsPage::setControlsEnabled(const bool enabled) { ui->btnReload->setEnabled(enabled); diff --git a/launcher/ui/pages/instance/OtherLogsPage.h b/launcher/ui/pages/instance/OtherLogsPage.h index d65ed6456..9394ab9b8 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.h +++ b/launcher/ui/pages/instance/OtherLogsPage.h @@ -72,6 +72,10 @@ class OtherLogsPage : public QWidget, public BasePage { void on_btnCopy_clicked(); void on_btnDelete_clicked(); void on_btnClean_clicked(); + void on_btnBottom_clicked(); + + void on_wrapCheckbox_clicked(bool checked); + void on_colorCheckbox_clicked(bool checked); void on_findButton_clicked(); void findActivated(); diff --git a/launcher/ui/pages/instance/OtherLogsPage.ui b/launcher/ui/pages/instance/OtherLogsPage.ui index de3091917..ca700e103 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.ui +++ b/launcher/ui/pages/instance/OtherLogsPage.ui @@ -33,17 +33,41 @@ Tab 1 + + + + Search: + + + - Find + &Find + + + + + + + Qt::Vertical + + + + + + + Scroll all the way to bottom + + + &Bottom - + false @@ -65,54 +89,98 @@ - + - - - - Copy the whole log into the clipboard - - - &Copy - - + + + + + + Delete the selected log + + + &Delete This + + + + + + + Delete all the logs + + + Delete &All + + + + - - - - Clear the log - - - Delete - - - - - - - Upload the log to the paste service configured in preferences. - - - Upload - - - - - - - Clear the log - - - Clean - - - - - - - Reload - - + + + + + + Wrap lines + + + true + + + + + + + Color lines + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Copy the whole log into the clipboard + + + &Copy + + + + + + + Upload the log to the paste service configured in preferences + + + Upload + + + + + + + Reload the contents of the log from the disk + + + &Reload + + + + @@ -126,13 +194,6 @@ - - - - Search: - - - @@ -154,6 +215,8 @@ btnPaste btnDelete btnClean + wrapCheckbox + colorCheckbox text searchBar findButton From de66fe4eda954165a9d00f4fb94aad562c70a21a Mon Sep 17 00:00:00 2001 From: Yihe Li Date: Tue, 15 Apr 2025 06:16:47 +0800 Subject: [PATCH 094/695] Apparently removeLast() only comes in Qt 6.5+ Signed-off-by: Yihe Li --- launcher/ui/pages/instance/OtherLogsPage.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/pages/instance/OtherLogsPage.cpp b/launcher/ui/pages/instance/OtherLogsPage.cpp index 40a5d9d94..2b4bcb59b 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.cpp +++ b/launcher/ui/pages/instance/OtherLogsPage.cpp @@ -202,7 +202,7 @@ void OtherLogsPage::on_btnReload_clicked() // Try to determine a level for each line if (content.back() == '\n') - content = content.removeLast(); + content = content.remove(content.size() - 1, 1); ui->text->clear(); m_model->clear(); for (auto& line : content.split('\n')) { From ec2552e501713119a2c6b8820ec40c35f93cc326 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Tue, 15 Apr 2025 10:40:55 +0100 Subject: [PATCH 095/695] Make padding consistent Signed-off-by: TheKodeToad --- launcher/ui/pages/global/APIPage.ui | 28 +++++++++++++------ launcher/ui/pages/global/AppearancePage.h | 11 ++++---- launcher/ui/pages/global/ExternalToolsPage.ui | 16 +++++++++-- launcher/ui/pages/global/JavaPage.ui | 16 +++++++++-- launcher/ui/pages/global/LauncherPage.ui | 18 ++++++++++-- launcher/ui/pages/global/ProxyPage.ui | 11 +++++++- 6 files changed, 78 insertions(+), 22 deletions(-) diff --git a/launcher/ui/pages/global/APIPage.ui b/launcher/ui/pages/global/APIPage.ui index 0b9dde764..9352162f0 100644 --- a/launcher/ui/pages/global/APIPage.ui +++ b/launcher/ui/pages/global/APIPage.ui @@ -11,6 +11,18 @@ + + 0 + + + 0 + + + 0 + + + 0 + @@ -21,7 +33,7 @@ 0 0 - 804 + 816 832 @@ -84,13 +96,6 @@ Meta&data Server - - - - Use Default - - - @@ -107,6 +112,13 @@ + + + + Use Default + + + diff --git a/launcher/ui/pages/global/AppearancePage.h b/launcher/ui/pages/global/AppearancePage.h index bf58ebb53..29b2d34bf 100644 --- a/launcher/ui/pages/global/AppearancePage.h +++ b/launcher/ui/pages/global/AppearancePage.h @@ -36,13 +36,12 @@ #pragma once #include -#include - -#include -#include -#include +#include +#include "Application.h" #include "java/JavaChecker.h" +#include "translations/TranslationsModel.h" #include "ui/pages/BasePage.h" +#include "ui/widgets/AppearanceWidget.h" class QTextCharFormat; class SettingsObject; @@ -51,7 +50,7 @@ class AppearancePage : public AppearanceWidget, public BasePage { Q_OBJECT public: - explicit AppearancePage(QWidget *parent = nullptr) : AppearanceWidget(false, parent) {} + explicit AppearancePage(QWidget* parent = nullptr) : AppearanceWidget(false, parent) { layout()->setContentsMargins(0, 0, 6, 0); } QString displayName() const override { return tr("Appearance"); } QIcon icon() const override { return APPLICATION->getThemedIcon("appearance"); } diff --git a/launcher/ui/pages/global/ExternalToolsPage.ui b/launcher/ui/pages/global/ExternalToolsPage.ui index 37eb7de3e..6667f34d9 100644 --- a/launcher/ui/pages/global/ExternalToolsPage.ui +++ b/launcher/ui/pages/global/ExternalToolsPage.ui @@ -11,6 +11,18 @@ + + 0 + + + 0 + + + 0 + + + 0 + @@ -21,8 +33,8 @@ 0 0 - 653 - 803 + 669 + 819 diff --git a/launcher/ui/pages/global/JavaPage.ui b/launcher/ui/pages/global/JavaPage.ui index 6b350fe07..a40e38868 100644 --- a/launcher/ui/pages/global/JavaPage.ui +++ b/launcher/ui/pages/global/JavaPage.ui @@ -17,6 +17,18 @@ + + 0 + + + 0 + + + 0 + + + 0 + @@ -37,8 +49,8 @@ 0 0 - 523 - 594 + 535 + 606 diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index b70cc6c1b..3821c09c4 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -17,6 +17,18 @@ + + 0 + + + 0 + + + 0 + + + 0 + @@ -29,9 +41,9 @@ 0 - -616 - 566 - 1296 + 0 + 575 + 1294 diff --git a/launcher/ui/pages/global/ProxyPage.ui b/launcher/ui/pages/global/ProxyPage.ui index 830ae1ac8..c8882a9dd 100644 --- a/launcher/ui/pages/global/ProxyPage.ui +++ b/launcher/ui/pages/global/ProxyPage.ui @@ -17,13 +17,22 @@ + + 0 + + + 0 + + + 0 + This only applies to the launcher. Minecraft does not accept proxy settings. - Qt::AlignCenter + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter true From a0c0262a197495e1ae509209da2636685a0a7a39 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Tue, 15 Apr 2025 11:39:26 +0100 Subject: [PATCH 096/695] Delete libraries/filesystem Signed-off-by: TheKodeToad --- libraries/filesystem | 1 - 1 file changed, 1 deletion(-) delete mode 160000 libraries/filesystem diff --git a/libraries/filesystem b/libraries/filesystem deleted file mode 160000 index 076592ce6..000000000 --- a/libraries/filesystem +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 076592ce6e64568521b88a11881aa36b3d3f7048 From e04acdb8fbb120fb13bf5d5dcfaf9a69a219ffae Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Tue, 15 Apr 2025 11:44:19 +0100 Subject: [PATCH 097/695] Cleanup modpack update prompt phrasing Signed-off-by: TheKodeToad --- launcher/ui/pages/global/LauncherPage.ui | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index 2d04093d2..769721dd5 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -41,7 +41,7 @@ 0 - 0 + -312 575 1368 @@ -415,7 +415,7 @@ When creating a new modpack instance, suggest updating an existing instance instead. - Suggest to update an existing instance + Suggest to update an existing instance during modpack installation From 1a76e0492567e3d8b3e171de3ca1ac7f4b62849a Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Tue, 15 Apr 2025 11:58:41 +0100 Subject: [PATCH 098/695] Stop OK or Browse from getting implicit focus; add accelerator Signed-off-by: TheKodeToad --- launcher/ui/pagedialog/PageDialog.cpp | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/launcher/ui/pagedialog/PageDialog.cpp b/launcher/ui/pagedialog/PageDialog.cpp index bfa9ebdb0..3200a7c39 100644 --- a/launcher/ui/pagedialog/PageDialog.cpp +++ b/launcher/ui/pagedialog/PageDialog.cpp @@ -21,9 +21,7 @@ #include #include "Application.h" -#include "settings/SettingsObject.h" -#include "ui/widgets/IconLabel.h" #include "ui/widgets/PageContainer.h" PageDialog::PageDialog(BasePageProvider* pageProvider, QString defaultId, QWidget* parent) : QDialog(parent) @@ -31,14 +29,22 @@ PageDialog::PageDialog(BasePageProvider* pageProvider, QString defaultId, QWidge setWindowTitle(pageProvider->dialogTitle()); m_container = new PageContainer(pageProvider, std::move(defaultId), this); - QVBoxLayout* mainLayout = new QVBoxLayout(this); + auto* mainLayout = new QVBoxLayout(this); + + auto* focusStealer = new QPushButton(this); + mainLayout->addWidget(focusStealer); + focusStealer->setDefault(true); + focusStealer->hide(); + mainLayout->addWidget(m_container); mainLayout->setSpacing(0); mainLayout->setContentsMargins(0, 0, 0, 0); + setLayout(mainLayout); - QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Help | QDialogButtonBox::Ok | QDialogButtonBox::Cancel); - buttons->button(QDialogButtonBox::Ok)->setDefault(true); + auto* buttons = new QDialogButtonBox(QDialogButtonBox::Help | QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + buttons->button(QDialogButtonBox::Ok)->setText(tr("&OK")); + buttons->button(QDialogButtonBox::Cancel)->setText(tr("&Cancel")); buttons->button(QDialogButtonBox::Help)->setText(tr("Help")); buttons->setContentsMargins(6, 0, 6, 0); m_container->addButtons(buttons); From 2c869c7a02391ebb6f48702f367f41b4acefc6ff Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Tue, 15 Apr 2025 12:09:13 +0100 Subject: [PATCH 099/695] Revert to old close behaviour Signed-off-by: TheKodeToad --- launcher/ui/pagedialog/PageDialog.cpp | 19 +++++++------------ launcher/ui/pagedialog/PageDialog.h | 10 ++++------ 2 files changed, 11 insertions(+), 18 deletions(-) diff --git a/launcher/ui/pagedialog/PageDialog.cpp b/launcher/ui/pagedialog/PageDialog.cpp index 3200a7c39..d01c2217b 100644 --- a/launcher/ui/pagedialog/PageDialog.cpp +++ b/launcher/ui/pagedialog/PageDialog.cpp @@ -14,6 +14,7 @@ */ #include "PageDialog.h" +#include #include #include @@ -53,22 +54,16 @@ PageDialog::PageDialog(BasePageProvider* pageProvider, QString defaultId, QWidge connect(buttons->button(QDialogButtonBox::Cancel), &QPushButton::clicked, this, &PageDialog::reject); connect(buttons->button(QDialogButtonBox::Help), &QPushButton::clicked, m_container, &PageContainer::help); - connect(this, &QDialog::accepted, this, &PageDialog::onAccepted); - connect(this, &QDialog::rejected, this, &PageDialog::storeGeometry); - restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("PagedGeometry").toByteArray())); } -void PageDialog::onAccepted() + +void PageDialog::closeEvent([[maybe_unused]] QCloseEvent* event) { - qDebug() << "Paged dialog accepted"; + qDebug() << "Paged dialog close requested"; if (m_container->prepareToClose()) { qDebug() << "Paged dialog close approved"; - emit applied(); + APPLICATION->settings()->set("PagedGeometry", saveGeometry().toBase64()); + qDebug() << "Paged dialog geometry saved"; + QDialog::closeEvent(event); } } - -void PageDialog::storeGeometry() -{ - APPLICATION->settings()->set("PagedGeometry", saveGeometry().toBase64()); - qDebug() << "Paged dialog geometry saved"; -} diff --git a/launcher/ui/pagedialog/PageDialog.h b/launcher/ui/pagedialog/PageDialog.h index d4af862f3..cc250af75 100644 --- a/launcher/ui/pagedialog/PageDialog.h +++ b/launcher/ui/pagedialog/PageDialog.h @@ -25,13 +25,11 @@ class PageDialog : public QDialog { explicit PageDialog(BasePageProvider* pageProvider, QString defaultId = QString(), QWidget* parent = 0); virtual ~PageDialog() {} - signals: - void applied(); + signals: + void applied(); - - private slots: - void onAccepted(); - void storeGeometry(); + private: + void closeEvent(QCloseEvent* event) override; private: PageContainer* m_container; From 4a2e4e9dc21dcb3458ccb8c7cdeacecd147147a2 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Tue, 15 Apr 2025 12:32:14 +0100 Subject: [PATCH 100/695] Use title case for 'When Renaming Instances...' Signed-off-by: TheKodeToad --- launcher/ui/pages/global/LauncherPage.ui | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index 769721dd5..4421215a7 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -41,7 +41,7 @@ 0 - -312 + 0 575 1368 @@ -102,7 +102,7 @@ - When renaming instances... + When Renaming Instances... From 33ac9e3f475056d005406baa65bed9a0b1696b3d Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Tue, 15 Apr 2025 12:39:25 +0100 Subject: [PATCH 101/695] Apparently this is correct Signed-off-by: TheKodeToad --- launcher/ui/pages/global/LauncherPage.ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index 4421215a7..b5374e7cc 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -126,7 +126,7 @@ - Never rename the folder - only the displayed name + Never rename the folder—only the displayed name From 917abd60e1e1872acc4feee280537c1c0a161a04 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Tue, 15 Apr 2025 13:08:58 +0100 Subject: [PATCH 102/695] Remove accidental qevent import Signed-off-by: TheKodeToad --- launcher/ui/pagedialog/PageDialog.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/launcher/ui/pagedialog/PageDialog.cpp b/launcher/ui/pagedialog/PageDialog.cpp index d01c2217b..aad204979 100644 --- a/launcher/ui/pagedialog/PageDialog.cpp +++ b/launcher/ui/pagedialog/PageDialog.cpp @@ -14,7 +14,6 @@ */ #include "PageDialog.h" -#include #include #include From 54e63fee6e19e59ffe702ce074587eb9c5c9822a Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Tue, 15 Apr 2025 13:17:16 +0100 Subject: [PATCH 103/695] Fix spacer Signed-off-by: TheKodeToad --- launcher/ui/pages/global/LauncherPage.ui | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index b5374e7cc..03b93e425 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -136,9 +136,12 @@ Qt::Vertical + + QSizePolicy::Fixed + - 20 + 0 6 From 521302a96251b2f55de2f15736c5ff3093fa4e22 Mon Sep 17 00:00:00 2001 From: Yihe Li Date: Tue, 15 Apr 2025 20:22:21 +0800 Subject: [PATCH 104/695] Move delete buttons to the same line & set model to nullptr before adding lines Signed-off-by: Yihe Li --- launcher/ui/pages/instance/OtherLogsPage.cpp | 3 +++ launcher/ui/pages/instance/OtherLogsPage.ui | 28 ++++++++++---------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/launcher/ui/pages/instance/OtherLogsPage.cpp b/launcher/ui/pages/instance/OtherLogsPage.cpp index 2b4bcb59b..974118626 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.cpp +++ b/launcher/ui/pages/instance/OtherLogsPage.cpp @@ -204,6 +204,7 @@ void OtherLogsPage::on_btnReload_clicked() if (content.back() == '\n') content = content.remove(content.size() - 1, 1); ui->text->clear(); + ui->text->setModel(nullptr); m_model->clear(); for (auto& line : content.split('\n')) { MessageLevel::Enum level = MessageLevel::Unknown; @@ -221,6 +222,8 @@ void OtherLogsPage::on_btnReload_clicked() m_model->append(level, line); } + ui->text->setModel(m_proxy); + ui->text->scrollToBottom(); } } diff --git a/launcher/ui/pages/instance/OtherLogsPage.ui b/launcher/ui/pages/instance/OtherLogsPage.ui index ca700e103..b4bb25b08 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.ui +++ b/launcher/ui/pages/instance/OtherLogsPage.ui @@ -91,15 +91,25 @@ - + + + + + + 0 + 0 + + + + Delete the selected log - &Delete This + &Delete Selected @@ -115,7 +125,7 @@ - + @@ -166,7 +176,7 @@ Upload the log to the paste service configured in preferences - Upload + &Upload @@ -182,16 +192,6 @@ - - - - - 0 - 0 - - - - From 53305bf5a7a0fa3bb4cfb508c96d3f70d2feb9b4 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Tue, 15 Apr 2025 14:17:03 +0100 Subject: [PATCH 105/695] Align task spinboxes Signed-off-by: TheKodeToad --- launcher/ui/pages/global/LauncherPage.ui | 26 +++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index 03b93e425..8f5eb69d5 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -41,7 +41,7 @@ 0 - 0 + -672 575 1368 @@ -513,6 +513,12 @@ 0 + + + 60 + 0 + + 1 @@ -549,6 +555,12 @@ 0 + + + 60 + 0 + + 1 @@ -585,6 +597,12 @@ 0 + + + 60 + 0 + + 0 @@ -624,6 +642,12 @@ 0 + + + 60 + 0 + + s From be5a1b47acc19aa7e186bca318028233b6504665 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Tue, 15 Apr 2025 14:20:59 +0100 Subject: [PATCH 106/695] Make PermGen max consistent Signed-off-by: TheKodeToad --- launcher/ui/widgets/JavaSettingsWidget.ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/widgets/JavaSettingsWidget.ui b/launcher/ui/widgets/JavaSettingsWidget.ui index c34b07cfa..8a8b20935 100644 --- a/launcher/ui/widgets/JavaSettingsWidget.ui +++ b/launcher/ui/widgets/JavaSettingsWidget.ui @@ -288,7 +288,7 @@ 4 - 999999999 + 1048576 8 From c7401ad649187b5431014d7f54647664b16fd25e Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Tue, 15 Apr 2025 14:27:47 +0100 Subject: [PATCH 107/695] Try to avoid cramming too much next to file path boxes Signed-off-by: TheKodeToad --- launcher/ui/pages/global/ExternalToolsPage.ui | 60 ++++--- launcher/ui/widgets/JavaSettingsWidget.ui | 152 +++++++++++------- 2 files changed, 133 insertions(+), 79 deletions(-) diff --git a/launcher/ui/pages/global/ExternalToolsPage.ui b/launcher/ui/pages/global/ExternalToolsPage.ui index 6667f34d9..b094e3693 100644 --- a/launcher/ui/pages/global/ExternalToolsPage.ui +++ b/launcher/ui/pages/global/ExternalToolsPage.ui @@ -106,13 +106,6 @@ - - - - Check - - - @@ -122,6 +115,19 @@ + + + + + 0 + 0 + + + + Check + + + @@ -179,13 +185,6 @@ - - - - Check - - - @@ -195,6 +194,19 @@ + + + + + 0 + 0 + + + + Check + + + @@ -233,13 +245,6 @@ - - - - Check - - - @@ -249,6 +254,19 @@ + + + + + 0 + 0 + + + + Check + + + diff --git a/launcher/ui/widgets/JavaSettingsWidget.ui b/launcher/ui/widgets/JavaSettingsWidget.ui index 8a8b20935..4028ca544 100644 --- a/launcher/ui/widgets/JavaSettingsWidget.ui +++ b/launcher/ui/widgets/JavaSettingsWidget.ui @@ -29,21 +29,15 @@ false - - + + - Java &Executable - - - javaPathTextBox + Auto-&detect Java version - - - - - + + @@ -53,57 +47,27 @@ - - - 0 - 0 - - &Browse + + + + Qt::Horizontal + + + + 40 + 20 + + + + - - - - Automatically downloads and selects the Java build recommended by Mojang. - - - Auto-download &Mojang Java - - - - - - - If enabled, the launcher won't prompt you to choose a Java version if one is not found on startup. - - - Skip Java setup prompt on startup - - - - - - - Auto-&detect Java version - - - - - - - If enabled, the launcher will not check if an instance is compatible with the selected Java version. - - - Skip Java compatibility checks - - - - + @@ -134,6 +98,81 @@ + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 0 + 6 + + + + + + + + Automatically downloads and selects the Java build recommended by Mojang. + + + Auto-download &Mojang Java + + + + + + + If enabled, the launcher will not check if an instance is compatible with the selected Java version. + + + Skip Java compatibility checks + + + + + + + + + + Java &Executable + + + javaPathTextBox + + + + + + + If enabled, the launcher won't prompt you to choose a Java version if one is not found on startup. + + + Skip Java setup prompt on startup + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 0 + 6 + + + + @@ -350,9 +389,6 @@ javaTestBtn javaDownloadBtn - javaPathTextBox - javaDetectBtn - javaBrowseBtn skipCompatibilityCheckBox skipWizardCheckBox autodetectJavaCheckBox From be803b32791e4ed2b34867354df3f19293cff604 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Tue, 15 Apr 2025 15:27:47 +0100 Subject: [PATCH 108/695] Optimise guessLevel Signed-off-by: TheKodeToad --- launcher/minecraft/MinecraftInstance.cpp | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index d1780d497..b155e535a 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -36,6 +36,7 @@ */ #include "MinecraftInstance.h" +#include #include "Application.h" #include "BuildConfig.h" #include "QObjectPtr.h" @@ -1014,8 +1015,18 @@ QMap MinecraftInstance::createCensorFilterFromSession(AuthSess MessageLevel::Enum MinecraftInstance::guessLevel(const QString& line, MessageLevel::Enum level) { - QRegularExpression re("\\[(?[0-9:]+)\\] \\[[^/]+/(?[^\\]]+)\\]"); - auto match = re.match(line); + if (line.contains("overwriting existing")) + return MessageLevel::Fatal; + + // NOTE: this diverges from the real regexp. no unicode, the first section is + instead of * + static const QRegularExpression JAVA_EXCEPTION( + R"(Exception in thread|...\d more$|(\s+at |Caused by: )([a-zA-Z_$][a-zA-Z\\d_$]*\.)+[a-zA-Z_$][a-zA-Z\\d_$]*)"); + + if (line.contains(JAVA_EXCEPTION)) + return MessageLevel::Error; + + static const QRegularExpression LINE_WITH_LEVEL("\\[(?[0-9:]+)\\] \\[[^/]+/(?[^\\]]+)\\]"); + auto match = LINE_WITH_LEVEL.match(line); if (match.hasMatch()) { // New style logs from log4j QString timestamp = match.captured("timestamp"); @@ -1042,15 +1053,6 @@ MessageLevel::Enum MinecraftInstance::guessLevel(const QString& line, MessageLev if (line.contains("[DEBUG]")) level = MessageLevel::Debug; } - if (line.contains("overwriting existing")) - return MessageLevel::Fatal; - // NOTE: this diverges from the real regexp. no unicode, the first section is + instead of * - static const QString javaSymbol = "([a-zA-Z_$][a-zA-Z\\d_$]*\\.)+[a-zA-Z_$][a-zA-Z\\d_$]*"; - if (line.contains("Exception in thread") || line.contains(QRegularExpression("\\s+at " + javaSymbol)) || - line.contains(QRegularExpression("Caused by: " + javaSymbol)) || - line.contains(QRegularExpression("([a-zA-Z_$][a-zA-Z\\d_$]*\\.)+[a-zA-Z_$]?[a-zA-Z\\d_$]*(Exception|Error|Throwable)")) || - line.contains(QRegularExpression("... \\d+ more$"))) - return MessageLevel::Error; return level; } From ce76320b23282c678f64d92312af450d8635faa7 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Tue, 15 Apr 2025 15:58:11 +0100 Subject: [PATCH 109/695] Remove unnecessary import Signed-off-by: TheKodeToad --- launcher/minecraft/MinecraftInstance.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index b155e535a..7b02e93ba 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -36,7 +36,6 @@ */ #include "MinecraftInstance.h" -#include #include "Application.h" #include "BuildConfig.h" #include "QObjectPtr.h" From 29b81e7163019f45c77d668afe17794286011e24 Mon Sep 17 00:00:00 2001 From: Yihe Li Date: Tue, 15 Apr 2025 23:11:54 +0800 Subject: [PATCH 110/695] Set parent for LogModel Signed-off-by: Yihe Li --- launcher/ui/pages/instance/OtherLogsPage.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/pages/instance/OtherLogsPage.cpp b/launcher/ui/pages/instance/OtherLogsPage.cpp index 974118626..0d94843c0 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.cpp +++ b/launcher/ui/pages/instance/OtherLogsPage.cpp @@ -53,7 +53,7 @@ OtherLogsPage::OtherLogsPage(InstancePtr instance, IPathMatcher::Ptr fileFilter, , m_path(instance->getLogFileRoot()) , m_fileFilter(fileFilter) , m_watcher(new RecursiveFileSystemWatcher(this)) - , m_model(new LogModel()) + , m_model(new LogModel(this)) { ui->setupUi(this); ui->tabWidget->tabBar()->hide(); From 76db89460b3faf53e8557cd9e16ad98dcfaad386 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Tue, 15 Apr 2025 19:21:38 +0100 Subject: [PATCH 111/695] Redo layouts Signed-off-by: TheKodeToad --- launcher/ui/pages/global/APIPage.ui | 11 +- launcher/ui/pages/global/LauncherPage.ui | 388 ++++++++---------- launcher/ui/pages/global/ProxyPage.ui | 63 ++- launcher/ui/widgets/AppearanceWidget.ui | 173 ++++---- launcher/ui/widgets/JavaSettingsWidget.ui | 155 ++++--- .../ui/widgets/MinecraftSettingsWidget.ui | 348 ++++++++-------- .../ui/widgets/ThemeCustomizationWidget.ui | 171 -------- 7 files changed, 551 insertions(+), 758 deletions(-) delete mode 100644 launcher/ui/widgets/ThemeCustomizationWidget.ui diff --git a/launcher/ui/pages/global/APIPage.ui b/launcher/ui/pages/global/APIPage.ui index 9352162f0..c6a4593fc 100644 --- a/launcher/ui/pages/global/APIPage.ui +++ b/launcher/ui/pages/global/APIPage.ui @@ -32,7 +32,7 @@ 0 - 0 + -216 816 832 @@ -55,7 +55,14 @@ - + + + + 0 + 0 + + + diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index 8f5eb69d5..22ac6a018 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -6,8 +6,8 @@ 0 0 - 600 - 700 + 767 + 796 @@ -41,9 +41,9 @@ 0 - -672 - 575 - 1368 + -356 + 742 + 1148 @@ -55,7 +55,7 @@ User Interface - + @@ -102,7 +102,7 @@ - When Renaming Instances... + Instance Renaming @@ -165,65 +165,60 @@ Updater - - + + + + + + + How Often? + + + + + + + + 0 + 0 + + + + Set to 0 to only check on launch + + + On Launch + + + hours + + + Every + + + 0 + + + 168 + + + + + + + Qt::Horizontal + + + + + + Check for updates automatically - - - - Qt::Vertical - - - QSizePolicy::Fixed - - - - 0 - 6 - - - - - - - - How Often? - - - - - - - - 0 - 0 - - - - Set to 0 to only check on launch - - - On Launch - - - hours - - - Every - - - 0 - - - 168 - - - @@ -232,24 +227,14 @@ Folders - - - - - &Icons - - - iconsDirTextBox - - + + + - - + + - &Downloads - - - downloadsDirTextBox + Browse @@ -263,6 +248,13 @@ + + + + Browse + + + @@ -273,86 +265,89 @@ - - + + - I&nstances + &Mods - instDirTextBox + modsDirTextBox - - - - - + + Browse - - - - - + + - Browse + &Downloads + + + downloadsDirTextBox - - - - - + + - - + + - - + + - Browse + I&nstances + + + instDirTextBox - - + + Browse - + Browse - - + + + + + Browse - - + + + + + + + + - &Mods + &Icons - modsDirTextBox + iconsDirTextBox - - - @@ -430,9 +425,18 @@ Console - - + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + 0 + 0 + + Log History &Limit @@ -441,7 +445,7 @@ - + @@ -466,23 +470,7 @@ - - - - Qt::Vertical - - - QSizePolicy::Fixed - - - - 0 - 6 - - - - - + &Stop logging when log overflows @@ -497,16 +485,9 @@ Tasks - - - - - Number of concurrent tasks - - - - - + + + 0 @@ -520,34 +501,11 @@ - 1 - - - - - - - Qt::Vertical - - - QSizePolicy::Fixed - - - - 0 - 6 - - - - - - - - Number of concurrent downloads + 0 - + @@ -566,31 +524,8 @@ - - - - Qt::Vertical - - - QSizePolicy::Fixed - - - - 0 - 6 - - - - - - - - Number of manual retries - - - - - + + 0 @@ -603,39 +538,37 @@ 0 - - 0 + + s - - - - Qt::Vertical - - - QSizePolicy::Fixed + + + + Retry Limit - - - 0 - 6 - + + + + + + Concurrent Download Limit - + - + Seconds to wait until the requests are terminated - Timeout for HTTP requests + HTTP Timeout - - + + 0 @@ -648,11 +581,25 @@ 0 - - s + + 1 + + + + Concurrent Task Limit + + + + + + + Qt::Horizontal + + + @@ -678,21 +625,8 @@ scrollArea autoUpdateCheckBox - instDirTextBox - instDirBrowseBtn - modsDirTextBox - modsDirBrowseBtn - iconsDirTextBox - iconsDirBrowseBtn - javaDirTextBox - javaDirBrowseBtn - skinsDirTextBox - skinsDirBrowseBtn - downloadsDirTextBox - downloadsDirBrowseBtn metadataEnableBtn dependenciesEnableBtn - sortByNameBtn diff --git a/launcher/ui/pages/global/ProxyPage.ui b/launcher/ui/pages/global/ProxyPage.ui index c8882a9dd..3042a4bba 100644 --- a/launcher/ui/pages/global/ProxyPage.ui +++ b/launcher/ui/pages/global/ProxyPage.ui @@ -51,7 +51,7 @@ Uses your system's default proxy settings. - S&ystem Settings + Use S&ystem Settings proxyGroup @@ -99,6 +99,18 @@ + + + 0 + 0 + + + + + 300 + 0 + + 127.0.0.1 @@ -120,6 +132,19 @@ + + + + Qt::Horizontal + + + + 40 + 20 + + + + @@ -128,27 +153,27 @@ Authentication - - - - - - + + + - Note: Proxy username and password are stored in plain text inside the launcher's configuration file! + &Username - - true + + proxyUserEdit - - + + + + + - &Username: + &Password - proxyUserEdit + proxyPassEdit @@ -159,13 +184,13 @@ - - + + - &Password: + Note: Proxy username and password are stored in plain text inside the launcher's configuration file! - - proxyPassEdit + + true diff --git a/launcher/ui/widgets/AppearanceWidget.ui b/launcher/ui/widgets/AppearanceWidget.ui index deb5a94d4..d56e6f031 100644 --- a/launcher/ui/widgets/AppearanceWidget.ui +++ b/launcher/ui/widgets/AppearanceWidget.ui @@ -142,9 +142,65 @@ - + + + + + Console Font + + + + + + + + + + + 0 + 0 + + + + 5 + + + 16 + + + 11 + + + + + + + Qt::Horizontal + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 0 + 6 + + + + + + - Console Font + Cat Opacity @@ -169,48 +225,9 @@ 0 - - - - - - - 5 - - - 16 - - - 11 - - - - - - - Qt::Vertical - - - QSizePolicy::Fixed - - - - 0 - 6 - - - - - - - - Cat Opacity - - - @@ -232,20 +249,34 @@ 0 - + + + + false + + + Opaque + + + + Qt::Horizontal - - - 40 - 20 - - - + + + + false + + + Transparent + + + + @@ -261,29 +292,25 @@ - - - - false - - - Transparent - - - - - - - false - - - Opaque - - - + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 0 + 6 + + + + @@ -499,12 +526,6 @@ Qt::Horizontal - - - 0 - 0 - - @@ -545,8 +566,6 @@ catPackComboBox catPackFolder reloadThemesButton - consoleFont - fontSizeBox catOpacitySlider diff --git a/launcher/ui/widgets/JavaSettingsWidget.ui b/launcher/ui/widgets/JavaSettingsWidget.ui index 4028ca544..027b20d2f 100644 --- a/launcher/ui/widgets/JavaSettingsWidget.ui +++ b/launcher/ui/widgets/JavaSettingsWidget.ui @@ -7,13 +7,13 @@ 0 0 500 - 1123 + 1000 Form - + @@ -190,19 +190,23 @@ false - - - + + + - M&inimum Memory Usage (-Xms) + Memory Notice - - minMemSpinBox + + + + + + (-XX:PermSize) - - + + 0 @@ -210,52 +214,26 @@ - The amount of memory Minecraft is started with. + The amount of memory available to store loaded Java classes. MiB - 8 + 4 1048576 - 128 + 8 - 256 - - - - - - - Qt::Vertical - - - QSizePolicy::Fixed - - - - 0 - 6 - - - - - - - - Ma&ximum Memory Usage (-Xmx) - - - maxMemSpinBox + 64 - + @@ -283,34 +261,15 @@ - - - - Qt::Vertical - - - QSizePolicy::Fixed - - - - 0 - 6 - - - - - - + + - &PermGen Size (-XX:PermSize) - - - permGenSpinBox + (-Xmx) - - + + 0 @@ -318,48 +277,75 @@ - The amount of memory available to store loaded Java classes. + The amount of memory Minecraft is started with. MiB - 4 + 8 1048576 - 8 + 128 - 64 + 256 - - - - Qt::Vertical + + + + &PermGen Size - - QSizePolicy::Fixed + + permGenSpinBox + + + + + + + (-Xms) + + + + + + + Ma&ximum Memory + + + maxMemSpinBox + + + + + + + M&inimum Memory + + + minMemSpinBox + + + + + + + Qt::Horizontal 0 - 6 + 0 - - - - Memory Notice - - - @@ -393,9 +379,6 @@ skipWizardCheckBox autodetectJavaCheckBox autodownloadJavaCheckBox - minMemSpinBox - maxMemSpinBox - permGenSpinBox jvmArgsTextBox diff --git a/launcher/ui/widgets/MinecraftSettingsWidget.ui b/launcher/ui/widgets/MinecraftSettingsWidget.ui index 4c317c15d..9a9adb8b7 100644 --- a/launcher/ui/widgets/MinecraftSettingsWidget.ui +++ b/launcher/ui/widgets/MinecraftSettingsWidget.ui @@ -7,7 +7,7 @@ 0 0 648 - 400 + 600 @@ -58,9 +58,9 @@ 0 - -130 + 0 603 - 812 + 694 @@ -78,26 +78,65 @@ false - - + + + + + The base game only supports resolution. In order to simulate the maximized behaviour the current implementation approximates the maximum display size. + + + <html><head/><body><p><span style=" font-weight:600; color:#f5c211;">Warning</span><span style=" color:#f5c211;">: The maximized option may not be fully supported on all Minecraft versions.</span></p></body></html> + + + + + + + When the game window closes, quit the launcher + + + + Start Minecraft maximized - - - - The base game only supports resolution. In order to simulate the maximized behaviour the current implementation approximates the maximum display size. - + + - <html><head/><body><p><span style=" font-weight:600; color:#f5c211;">Warning</span><span style=" color:#f5c211;">: The maximized option may not be fully supported on all Minecraft versions.</span></p></body></html> + When the game window opens, hide the launcher - - + + + + + 0 + 0 + + + + + + + 1 + + + 65536 + + + 1 + + + 854 + + + + + Qt::Vertical @@ -112,7 +151,29 @@ - + + + + + 0 + 0 + + + + + + + 1 + + + 65536 + + + 480 + + + + &Window Size @@ -122,114 +183,33 @@ - - - - - - - 0 - 0 - - - - - - - 1 - - - 65536 - - - 480 - - - - - - - × - - - - - - - - 0 - 0 - - - - - - - 1 - - - 65536 - - - 1 - - - 854 - - - - - - - pixels - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - + + + + × + + - - - - Qt::Vertical + + + + pixels - - QSizePolicy::Fixed + + + + + + Qt::Horizontal 0 - 6 + 0 - - - - When the game window opens, hide the launcher - - - - - - - When the game window closes, quit the launcher - - - @@ -329,15 +309,9 @@ false - + - - - 0 - 0 - - Account @@ -353,6 +327,19 @@ + + + + Qt::Horizontal + + + + 0 + 0 + + + + @@ -367,15 +354,32 @@ false - - + + + + + + 0 + 0 + + + + + + + + Singleplayer world + + + + Server address - + @@ -385,22 +389,12 @@ - - - - Singleplayer world - - - - - - - - 0 - 0 - + + + + Qt::Horizontal - + @@ -440,7 +434,7 @@ 0 0 624 - 287 + 487 @@ -463,8 +457,8 @@ 0 0 - 603 - 470 + 624 + 487 @@ -507,15 +501,11 @@ false - - - - - Use system installation of OpenAL - - - - + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + &GLFW library path @@ -525,31 +515,7 @@ - - - - false - - - - - - - Use system installation of GLFW - - - - - - - &OpenAL library path - - - lineEditOpenALPath - - - - + Qt::Vertical @@ -565,13 +531,44 @@ - + + + + &OpenAL library path + + + lineEditOpenALPath + + + + false + + + + Use system installation of GLFW + + + + + + + Use system installation of OpenAL + + + + + + + false + + + @@ -715,7 +712,6 @@ javaScrollArea scrollArea_2 onlineFixes - useNativeGLFWCheck useNativeOpenALCheck perfomanceGroupBox enableFeralGamemodeCheck diff --git a/launcher/ui/widgets/ThemeCustomizationWidget.ui b/launcher/ui/widgets/ThemeCustomizationWidget.ui deleted file mode 100644 index 3e2808a48..000000000 --- a/launcher/ui/widgets/ThemeCustomizationWidget.ui +++ /dev/null @@ -1,171 +0,0 @@ - - - ThemeCustomizationWidget - - - - 0 - 0 - 400 - 168 - - - - Form - - - - - - - 0 - 0 - - - - Qt::StrongFocus - - - - - - - &Widgets - - - widgetStyleComboBox - - - - - - - View widget themes folder. - - - Open Folder - - - - - - - - 0 - 0 - - - - Qt::StrongFocus - - - - - - - View icon themes folder. - - - Open Folder - - - - - - - - - Qt::Horizontal - - - - 0 - 0 - - - - - - - - Qt::Horizontal - - - - 0 - 0 - - - - - - - - - - The cat appears in the background and is not shown by default. It is only made visible when pressing the Cat button in the Toolbar. - - - C&at - - - backgroundCatComboBox - - - - - - - &Icons - - - iconsComboBox - - - - - - - View cat packs folder. - - - Open Folder - - - - - - - - 0 - 0 - - - - Qt::StrongFocus - - - The cat appears in the background and is not shown by default. It is only made visible when pressing the Cat button in the Toolbar. - - - - - - - Refresh All - - - - - - - iconsComboBox - iconsFolder - widgetStyleComboBox - widgetStyleFolder - backgroundCatComboBox - catPackFolder - - - - From 0b3b9debd8d86797b721fa597b409e35380c9140 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Tue, 15 Apr 2025 19:42:46 +0100 Subject: [PATCH 112/695] Fix PageDialog OK button Signed-off-by: TheKodeToad --- launcher/ui/pagedialog/PageDialog.cpp | 27 ++++++++++++++++++++------- launcher/ui/pagedialog/PageDialog.h | 2 ++ 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/launcher/ui/pagedialog/PageDialog.cpp b/launcher/ui/pagedialog/PageDialog.cpp index aad204979..6292beec3 100644 --- a/launcher/ui/pagedialog/PageDialog.cpp +++ b/launcher/ui/pagedialog/PageDialog.cpp @@ -56,13 +56,26 @@ PageDialog::PageDialog(BasePageProvider* pageProvider, QString defaultId, QWidge restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("PagedGeometry").toByteArray())); } -void PageDialog::closeEvent([[maybe_unused]] QCloseEvent* event) +void PageDialog::accept() { - qDebug() << "Paged dialog close requested"; - if (m_container->prepareToClose()) { - qDebug() << "Paged dialog close approved"; - APPLICATION->settings()->set("PagedGeometry", saveGeometry().toBase64()); - qDebug() << "Paged dialog geometry saved"; + if (handleClose()) + QDialog::accept(); +} + +void PageDialog::closeEvent(QCloseEvent* event) +{ + if (handleClose()) QDialog::closeEvent(event); - } +} + +bool PageDialog::handleClose() const +{ + qDebug() << "Paged dialog close requested"; + if (!m_container->prepareToClose()) + return false; + + qDebug() << "Paged dialog close approved"; + APPLICATION->settings()->set("PagedGeometry", saveGeometry().toBase64()); + qDebug() << "Paged dialog geometry saved"; + return true; } diff --git a/launcher/ui/pagedialog/PageDialog.h b/launcher/ui/pagedialog/PageDialog.h index cc250af75..f3b914923 100644 --- a/launcher/ui/pagedialog/PageDialog.h +++ b/launcher/ui/pagedialog/PageDialog.h @@ -29,7 +29,9 @@ class PageDialog : public QDialog { void applied(); private: + void accept() override; void closeEvent(QCloseEvent* event) override; + bool handleClose() const; private: PageContainer* m_container; From fc94bb6af3f8e79bb7c0d9a6fb58edb75e9143d8 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Tue, 15 Apr 2025 19:42:54 +0100 Subject: [PATCH 113/695] Add colons to single-line settings Signed-off-by: TheKodeToad --- launcher/ui/pages/global/LauncherPage.ui | 36 ++++++++++++------- launcher/ui/pages/global/ProxyPage.ui | 4 +-- launcher/ui/widgets/AppearanceWidget.ui | 26 +++++++++++--- launcher/ui/widgets/JavaSettingsWidget.ui | 6 ++-- .../ui/widgets/MinecraftSettingsWidget.ui | 12 +++---- 5 files changed, 57 insertions(+), 27 deletions(-) diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index 22ac6a018..b8530a609 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -41,7 +41,7 @@ 0 - -356 + -300 742 1148 @@ -208,6 +208,12 @@ Qt::Horizontal + + + 0 + 0 + + @@ -241,7 +247,7 @@ - &Java + &Java: javaDirTextBox @@ -258,7 +264,7 @@ - &Skins + &Skins: skinsDirTextBox @@ -268,7 +274,7 @@ - &Mods + &Mods: modsDirTextBox @@ -285,7 +291,7 @@ - &Downloads + &Downloads: downloadsDirTextBox @@ -301,7 +307,7 @@ - I&nstances + I&nstances: instDirTextBox @@ -341,7 +347,7 @@ - &Icons + &Icons: iconsDirTextBox @@ -438,7 +444,7 @@ - Log History &Limit + Log History &Limit: lineLimitSpinBox @@ -546,14 +552,14 @@ - Retry Limit + Retry Limit: - Concurrent Download Limit + Concurrent Download Limit: @@ -563,7 +569,7 @@ Seconds to wait until the requests are terminated - HTTP Timeout + HTTP Timeout: @@ -589,7 +595,7 @@ - Concurrent Task Limit + Concurrent Task Limit: @@ -598,6 +604,12 @@ Qt::Horizontal + + + 0 + 0 + + diff --git a/launcher/ui/pages/global/ProxyPage.ui b/launcher/ui/pages/global/ProxyPage.ui index 3042a4bba..0cbe894e8 100644 --- a/launcher/ui/pages/global/ProxyPage.ui +++ b/launcher/ui/pages/global/ProxyPage.ui @@ -157,7 +157,7 @@ - &Username + &Username: proxyUserEdit @@ -170,7 +170,7 @@ - &Password + &Password: proxyPassEdit diff --git a/launcher/ui/widgets/AppearanceWidget.ui b/launcher/ui/widgets/AppearanceWidget.ui index d56e6f031..886ac984a 100644 --- a/launcher/ui/widgets/AppearanceWidget.ui +++ b/launcher/ui/widgets/AppearanceWidget.ui @@ -61,7 +61,7 @@ - &Cat Pack + &Cat Pack: catPackComboBox @@ -113,7 +113,7 @@ - Theme + Theme: widgetStyleComboBox @@ -123,7 +123,7 @@ - &Icons + &Icons: iconsComboBox @@ -146,7 +146,7 @@ - Console Font + Console Font: @@ -177,6 +177,12 @@ Qt::Horizontal + + + 0 + 0 + + @@ -264,6 +270,12 @@ Qt::Horizontal + + + 0 + 0 + + @@ -526,6 +538,12 @@ Qt::Horizontal + + + 0 + 0 + + diff --git a/launcher/ui/widgets/JavaSettingsWidget.ui b/launcher/ui/widgets/JavaSettingsWidget.ui index 027b20d2f..743c71a13 100644 --- a/launcher/ui/widgets/JavaSettingsWidget.ui +++ b/launcher/ui/widgets/JavaSettingsWidget.ui @@ -299,7 +299,7 @@ - &PermGen Size + &PermGen Size: permGenSpinBox @@ -316,7 +316,7 @@ - Ma&ximum Memory + Ma&ximum Memory Usage: maxMemSpinBox @@ -326,7 +326,7 @@ - M&inimum Memory + M&inimum Memory Usage: minMemSpinBox diff --git a/launcher/ui/widgets/MinecraftSettingsWidget.ui b/launcher/ui/widgets/MinecraftSettingsWidget.ui index 9a9adb8b7..73dbfcad9 100644 --- a/launcher/ui/widgets/MinecraftSettingsWidget.ui +++ b/launcher/ui/widgets/MinecraftSettingsWidget.ui @@ -176,7 +176,7 @@ - &Window Size + &Window Size: windowWidthSpinBox @@ -313,7 +313,7 @@ - Account + Account: @@ -368,14 +368,14 @@ - Singleplayer world + Singleplayer world: - Server address + Server address: @@ -508,7 +508,7 @@ - &GLFW library path + &GLFW library path: lineEditGLFWPath @@ -534,7 +534,7 @@ - &OpenAL library path + &OpenAL library path: lineEditOpenALPath From 6d3d4d0083d7c92e0faccde58836ae9812f9da9e Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Tue, 15 Apr 2025 19:52:21 +0100 Subject: [PATCH 114/695] Redo tab orderings Signed-off-by: TheKodeToad --- launcher/ui/pages/global/LauncherPage.ui | 28 ++++++++++++++++++- launcher/ui/pages/global/ProxyPage.ui | 10 +++++++ launcher/ui/widgets/AppearanceWidget.ui | 3 ++ launcher/ui/widgets/JavaSettingsWidget.ui | 10 +++++-- .../ui/widgets/MinecraftSettingsWidget.ui | 25 +++++++++++++---- 5 files changed, 67 insertions(+), 9 deletions(-) diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index b8530a609..55478e6a0 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -41,7 +41,7 @@ 0 - -300 + -356 742 1148 @@ -636,9 +636,35 @@ scrollArea + sortByNameBtn + sortLastLaunchedBtn + renamingBehaviorComboBox + preferMenuBarCheckBox autoUpdateCheckBox + updateIntervalSpinBox + instDirTextBox + instDirBrowseBtn + modsDirTextBox + modsDirBrowseBtn + iconsDirTextBox + iconsDirBrowseBtn + javaDirTextBox + javaDirBrowseBtn + skinsDirTextBox + skinsDirBrowseBtn + downloadsDirTextBox + downloadsDirBrowseBtn + downloadsDirWatchRecursiveCheckBox + downloadsDirMoveCheckBox metadataEnableBtn dependenciesEnableBtn + modpackUpdatePromptBtn + lineLimitSpinBox + checkStopLogging + numberOfConcurrentTasksSpinBox + numberOfConcurrentDownloadsSpinBox + numberOfManualRetriesSpinBox + timeoutSecondsSpinBox diff --git a/launcher/ui/pages/global/ProxyPage.ui b/launcher/ui/pages/global/ProxyPage.ui index 0cbe894e8..dec8d0a26 100644 --- a/launcher/ui/pages/global/ProxyPage.ui +++ b/launcher/ui/pages/global/ProxyPage.ui @@ -212,6 +212,16 @@ + + proxyDefaultBtn + proxyNoneBtn + proxySOCKS5Btn + proxyHTTPBtn + proxyAddrEdit + proxyPortEdit + proxyUserEdit + proxyPassEdit + diff --git a/launcher/ui/widgets/AppearanceWidget.ui b/launcher/ui/widgets/AppearanceWidget.ui index 886ac984a..c672279f0 100644 --- a/launcher/ui/widgets/AppearanceWidget.ui +++ b/launcher/ui/widgets/AppearanceWidget.ui @@ -584,7 +584,10 @@ catPackComboBox catPackFolder reloadThemesButton + consoleFont + fontSizeBox catOpacitySlider + consolePreview diff --git a/launcher/ui/widgets/JavaSettingsWidget.ui b/launcher/ui/widgets/JavaSettingsWidget.ui index 743c71a13..fb974570f 100644 --- a/launcher/ui/widgets/JavaSettingsWidget.ui +++ b/launcher/ui/widgets/JavaSettingsWidget.ui @@ -373,12 +373,18 @@ - javaTestBtn - javaDownloadBtn + javaPathTextBox + javaDetectBtn + javaBrowseBtn skipCompatibilityCheckBox skipWizardCheckBox autodetectJavaCheckBox autodownloadJavaCheckBox + javaTestBtn + javaDownloadBtn + minMemSpinBox + maxMemSpinBox + permGenSpinBox jvmArgsTextBox diff --git a/launcher/ui/widgets/MinecraftSettingsWidget.ui b/launcher/ui/widgets/MinecraftSettingsWidget.ui index 73dbfcad9..ed12604fd 100644 --- a/launcher/ui/widgets/MinecraftSettingsWidget.ui +++ b/launcher/ui/widgets/MinecraftSettingsWidget.ui @@ -58,7 +58,7 @@ 0 - 0 + -207 603 694 @@ -702,24 +702,37 @@ openGlobalSettingsButton settingsTabs + scrollArea maximizedCheckBox + windowHeightSpinBox + windowWidthSpinBox + closeAfterLaunchCheck + quitAfterGameStopCheck + showConsoleCheck + showConsoleErrorCheck + autoCloseConsoleCheck showGameTime recordGameTime showGlobalGameTime showGameTimeWithoutDays - showConsoleCheck - autoCloseConsoleCheck + instanceAccountGroupBox + instanceAccountSelector + serverJoinGroupBox + serverJoinAddressButton + serverJoinAddress + worldJoinButton + worldsCb javaScrollArea scrollArea_2 onlineFixes + useNativeGLFWCheck + lineEditGLFWPath useNativeOpenALCheck - perfomanceGroupBox + lineEditOpenALPath enableFeralGamemodeCheck enableMangoHud useDiscreteGpuCheck useZink - serverJoinAddressButton - worldJoinButton From 9b815e8bee967b554d26e9489c92b8bb57012cf8 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Wed, 16 Apr 2025 11:54:31 +0100 Subject: [PATCH 115/695] Emit applied (oops) Signed-off-by: TheKodeToad --- launcher/ui/pagedialog/PageDialog.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/launcher/ui/pagedialog/PageDialog.cpp b/launcher/ui/pagedialog/PageDialog.cpp index 6292beec3..79bcb5701 100644 --- a/launcher/ui/pagedialog/PageDialog.cpp +++ b/launcher/ui/pagedialog/PageDialog.cpp @@ -77,5 +77,7 @@ bool PageDialog::handleClose() const qDebug() << "Paged dialog close approved"; APPLICATION->settings()->set("PagedGeometry", saveGeometry().toBase64()); qDebug() << "Paged dialog geometry saved"; + + emit applied(); return true; } From 5ce6ad604b06c3274f24bb49984f904c1fa5bdb2 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Wed, 16 Apr 2025 16:52:45 +0300 Subject: [PATCH 116/695] chore: sync cmake version with the one used in the launcher Signed-off-by: Trial97 --- libraries/LocalPeer/CMakeLists.txt | 2 +- libraries/gamemode/CMakeLists.txt | 2 +- libraries/javacheck/CMakeLists.txt | 2 +- libraries/launcher/CMakeLists.txt | 2 +- libraries/murmur2/CMakeLists.txt | 2 +- libraries/qdcss/CMakeLists.txt | 2 +- libraries/rainbow/CMakeLists.txt | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/libraries/LocalPeer/CMakeLists.txt b/libraries/LocalPeer/CMakeLists.txt index b736cefcb..f6de6581c 100644 --- a/libraries/LocalPeer/CMakeLists.txt +++ b/libraries/LocalPeer/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.9.4) +cmake_minimum_required(VERSION 3.15) project(LocalPeer) if(QT_VERSION_MAJOR EQUAL 5) diff --git a/libraries/gamemode/CMakeLists.txt b/libraries/gamemode/CMakeLists.txt index 9e07f34ac..61195ac21 100644 --- a/libraries/gamemode/CMakeLists.txt +++ b/libraries/gamemode/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.9.4) +cmake_minimum_required(VERSION 3.15) project(gamemode VERSION 1.6.1) diff --git a/libraries/javacheck/CMakeLists.txt b/libraries/javacheck/CMakeLists.txt index fd545d2bc..b9bcb121a 100644 --- a/libraries/javacheck/CMakeLists.txt +++ b/libraries/javacheck/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.9.4) +cmake_minimum_required(VERSION 3.15) project(launcher Java) find_package(Java 1.7 REQUIRED COMPONENTS Development) diff --git a/libraries/launcher/CMakeLists.txt b/libraries/launcher/CMakeLists.txt index 4cd1ba58b..dfc4ebb32 100644 --- a/libraries/launcher/CMakeLists.txt +++ b/libraries/launcher/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.9.4) +cmake_minimum_required(VERSION 3.15) project(launcher Java) find_package(Java 1.7 REQUIRED COMPONENTS Development) diff --git a/libraries/murmur2/CMakeLists.txt b/libraries/murmur2/CMakeLists.txt index f3068201d..be989ee36 100644 --- a/libraries/murmur2/CMakeLists.txt +++ b/libraries/murmur2/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.9.4) +cmake_minimum_required(VERSION 3.15) project(murmur2) set(MURMUR_SOURCES diff --git a/libraries/qdcss/CMakeLists.txt b/libraries/qdcss/CMakeLists.txt index 0afdef321..ab8aaef94 100644 --- a/libraries/qdcss/CMakeLists.txt +++ b/libraries/qdcss/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.9.4) +cmake_minimum_required(VERSION 3.15) project(qdcss) if(QT_VERSION_MAJOR EQUAL 5) diff --git a/libraries/rainbow/CMakeLists.txt b/libraries/rainbow/CMakeLists.txt index b6bbe7101..0867b2d27 100644 --- a/libraries/rainbow/CMakeLists.txt +++ b/libraries/rainbow/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.9.4) +cmake_minimum_required(VERSION 3.15) project(rainbow) if(QT_VERSION_MAJOR EQUAL 5) From c3f4735808c28e1db66b1dbacdad731fdf9cb465 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Wed, 16 Apr 2025 17:02:22 +0300 Subject: [PATCH 117/695] fix: compile warning regarding duplicate object name Signed-off-by: Trial97 --- launcher/ui/pages/instance/OtherLogsPage.ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/pages/instance/OtherLogsPage.ui b/launcher/ui/pages/instance/OtherLogsPage.ui index b4bb25b08..6d1a46139 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.ui +++ b/launcher/ui/pages/instance/OtherLogsPage.ui @@ -126,7 +126,7 @@ - + From b70d9d6537bf530594a3c69644283b231bb2d5e2 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Wed, 16 Apr 2025 17:56:01 +0300 Subject: [PATCH 118/695] chore: update submodules Signed-off-by: Trial97 --- flatpak/shared-modules | 2 +- libraries/extra-cmake-modules | 2 +- libraries/libnbtplusplus | 2 +- libraries/quazip | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/flatpak/shared-modules b/flatpak/shared-modules index f5d368a31..1f8e591b2 160000 --- a/flatpak/shared-modules +++ b/flatpak/shared-modules @@ -1 +1 @@ -Subproject commit f5d368a31d6ef046eb2955c74ec6f54f32ed5c4e +Subproject commit 1f8e591b263eef8a0dc04929f2da135af59fac3c diff --git a/libraries/extra-cmake-modules b/libraries/extra-cmake-modules index a3d9394ab..1f820dc98 160000 --- a/libraries/extra-cmake-modules +++ b/libraries/extra-cmake-modules @@ -1 +1 @@ -Subproject commit a3d9394aba4b35789293378e04fb7473d65edf97 +Subproject commit 1f820dc98d0a520c175433bcbb0098327d82aac6 diff --git a/libraries/libnbtplusplus b/libraries/libnbtplusplus index 23b955121..531449ba1 160000 --- a/libraries/libnbtplusplus +++ b/libraries/libnbtplusplus @@ -1 +1 @@ -Subproject commit 23b955121b8217c1c348a9ed2483167a6f3ff4ad +Subproject commit 531449ba1c930c98e0bcf5d332b237a8566f9d78 diff --git a/libraries/quazip b/libraries/quazip index 8aeb3f7d8..3fd3b299b 160000 --- a/libraries/quazip +++ b/libraries/quazip @@ -1 +1 @@ -Subproject commit 8aeb3f7d8254f4bf1f7c6cf2a8f59c2ca141a552 +Subproject commit 3fd3b299b875fbd2beac4894b8a870d80022cad7 From 6e00f94a578838337f93d39c80d7f6edc0f031ea Mon Sep 17 00:00:00 2001 From: Trial97 Date: Thu, 17 Apr 2025 22:43:32 +0300 Subject: [PATCH 119/695] chore: rename varibales to match code standards Signed-off-by: Trial97 --- launcher/minecraft/World.cpp | 36 +++++++++++++-------------- launcher/minecraft/World.h | 6 ++--- launcher/minecraft/WorldList.cpp | 42 ++++++++++++++++---------------- launcher/minecraft/WorldList.h | 10 ++++---- 4 files changed, 47 insertions(+), 47 deletions(-) diff --git a/launcher/minecraft/World.cpp b/launcher/minecraft/World.cpp index bd28f9e9a..fc67fbb13 100644 --- a/launcher/minecraft/World.cpp +++ b/launcher/minecraft/World.cpp @@ -252,41 +252,41 @@ void World::readFromFS(const QFileInfo& file) { auto bytes = getLevelDatDataFromFS(file); if (bytes.isEmpty()) { - is_valid = false; + m_isValid = false; return; } loadFromLevelDat(bytes); - levelDatTime = file.lastModified(); + m_levelDatTime = file.lastModified(); } void World::readFromZip(const QFileInfo& file) { QuaZip zip(file.absoluteFilePath()); - is_valid = zip.open(QuaZip::mdUnzip); - if (!is_valid) { + m_isValid = zip.open(QuaZip::mdUnzip); + if (!m_isValid) { return; } auto location = MMCZip::findFolderOfFileInZip(&zip, "level.dat"); - is_valid = !location.isEmpty(); - if (!is_valid) { + m_isValid = !location.isEmpty(); + if (!m_isValid) { return; } m_containerOffsetPath = location; QuaZipFile zippedFile(&zip); // read the install profile - is_valid = zip.setCurrentFile(location + "level.dat"); - if (!is_valid) { + m_isValid = zip.setCurrentFile(location + "level.dat"); + if (!m_isValid) { return; } - is_valid = zippedFile.open(QIODevice::ReadOnly); + m_isValid = zippedFile.open(QIODevice::ReadOnly); QuaZipFileInfo64 levelDatInfo; zippedFile.getFileInfo(&levelDatInfo); auto modTime = levelDatInfo.getNTFSmTime(); if (!modTime.isValid()) { modTime = levelDatInfo.dateTime; } - levelDatTime = modTime; - if (!is_valid) { + m_levelDatTime = modTime; + if (!m_isValid) { return; } loadFromLevelDat(zippedFile.readAll()); @@ -430,7 +430,7 @@ void World::loadFromLevelDat(QByteArray data) { auto levelData = parseLevelDat(data); if (!levelData) { - is_valid = false; + m_isValid = false; return; } @@ -439,20 +439,20 @@ void World::loadFromLevelDat(QByteArray data) valPtr = &levelData->at("Data"); } catch (const std::out_of_range& e) { qWarning() << "Unable to read NBT tags from " << m_folderName << ":" << e.what(); - is_valid = false; + m_isValid = false; return; } nbt::value& val = *valPtr; - is_valid = val.get_type() == nbt::tag_type::Compound; - if (!is_valid) + m_isValid = val.get_type() == nbt::tag_type::Compound; + if (!m_isValid) return; auto name = read_string(val, "LevelName"); m_actualName = name ? *name : m_folderName; auto timestamp = read_long(val, "LastPlayed"); - m_lastPlayed = timestamp ? QDateTime::fromMSecsSinceEpoch(*timestamp) : levelDatTime; + m_lastPlayed = timestamp ? QDateTime::fromMSecsSinceEpoch(*timestamp) : m_levelDatTime; m_gameType = read_gametype(val, "GameType"); @@ -490,7 +490,7 @@ bool World::replace(World& with) bool World::destroy() { - if (!is_valid) + if (!m_isValid) return false; if (FS::trash(m_containerFile.filePath())) @@ -508,7 +508,7 @@ bool World::destroy() bool World::operator==(const World& other) const { - return is_valid == other.is_valid && folderName() == other.folderName(); + return m_isValid == other.m_isValid && folderName() == other.folderName(); } bool World::isSymLinkUnder(const QString& instPath) const diff --git a/launcher/minecraft/World.h b/launcher/minecraft/World.h index 4303dc553..6a4e1f092 100644 --- a/launcher/minecraft/World.h +++ b/launcher/minecraft/World.h @@ -39,7 +39,7 @@ class World { QDateTime lastPlayed() const { return m_lastPlayed; } GameType gameType() const { return m_gameType; } int64_t seed() const { return m_randomSeed; } - bool isValid() const { return is_valid; } + bool isValid() const { return m_isValid; } bool isOnFS() const { return m_containerFile.isDir(); } QFileInfo container() const { return m_containerFile; } // delete all the files of this world @@ -83,10 +83,10 @@ class World { QString m_folderName; QString m_actualName; QString m_iconFile; - QDateTime levelDatTime; + QDateTime m_levelDatTime; QDateTime m_lastPlayed; int64_t m_size; int64_t m_randomSeed = 0; GameType m_gameType; - bool is_valid = false; + bool m_isValid = false; }; diff --git a/launcher/minecraft/WorldList.cpp b/launcher/minecraft/WorldList.cpp index cf27be676..f06eddb66 100644 --- a/launcher/minecraft/WorldList.cpp +++ b/launcher/minecraft/WorldList.cpp @@ -51,18 +51,18 @@ WorldList::WorldList(const QString& dir, BaseInstance* instance) : QAbstractList m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs); m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); m_watcher = new QFileSystemWatcher(this); - is_watching = false; + m_isWatching = false; connect(m_watcher, &QFileSystemWatcher::directoryChanged, this, &WorldList::directoryChanged); } void WorldList::startWatching() { - if (is_watching) { + if (m_isWatching) { return; } update(); - is_watching = m_watcher->addPath(m_dir.absolutePath()); - if (is_watching) { + m_isWatching = m_watcher->addPath(m_dir.absolutePath()); + if (m_isWatching) { qDebug() << "Started watching " << m_dir.absolutePath(); } else { qDebug() << "Failed to start watching " << m_dir.absolutePath(); @@ -71,11 +71,11 @@ void WorldList::startWatching() void WorldList::stopWatching() { - if (!is_watching) { + if (!m_isWatching) { return; } - is_watching = !m_watcher->removePath(m_dir.absolutePath()); - if (!is_watching) { + m_isWatching = !m_watcher->removePath(m_dir.absolutePath()); + if (!m_isWatching) { qDebug() << "Stopped watching " << m_dir.absolutePath(); } else { qDebug() << "Failed to stop watching " << m_dir.absolutePath(); @@ -101,12 +101,12 @@ bool WorldList::update() } } beginResetModel(); - worlds.swap(newWorlds); + m_worlds.swap(newWorlds); endResetModel(); return true; } -void WorldList::directoryChanged(QString path) +void WorldList::directoryChanged(QString) { update(); } @@ -123,12 +123,12 @@ QString WorldList::instDirPath() const bool WorldList::deleteWorld(int index) { - if (index >= worlds.size() || index < 0) + if (index >= m_worlds.size() || index < 0) return false; - World& m = worlds[index]; + World& m = m_worlds[index]; if (m.destroy()) { beginRemoveRows(QModelIndex(), index, index); - worlds.removeAt(index); + m_worlds.removeAt(index); endRemoveRows(); emit changed(); return true; @@ -139,11 +139,11 @@ bool WorldList::deleteWorld(int index) bool WorldList::deleteWorlds(int first, int last) { for (int i = first; i <= last; i++) { - World& m = worlds[i]; + World& m = m_worlds[i]; m.destroy(); } beginRemoveRows(QModelIndex(), first, last); - worlds.erase(worlds.begin() + first, worlds.begin() + last + 1); + m_worlds.erase(m_worlds.begin() + first, m_worlds.begin() + last + 1); endRemoveRows(); emit changed(); return true; @@ -151,9 +151,9 @@ bool WorldList::deleteWorlds(int first, int last) bool WorldList::resetIcon(int row) { - if (row >= worlds.size() || row < 0) + if (row >= m_worlds.size() || row < 0) return false; - World& m = worlds[row]; + World& m = m_worlds[row]; if (m.resetIcon()) { emit dataChanged(index(row), index(row), { WorldList::IconFileRole }); return true; @@ -174,12 +174,12 @@ QVariant WorldList::data(const QModelIndex& index, int role) const int row = index.row(); int column = index.column(); - if (row < 0 || row >= worlds.size()) + if (row < 0 || row >= m_worlds.size()) return QVariant(); QLocale locale; - auto& world = worlds[row]; + auto& world = m_worlds[row]; switch (role) { case Qt::DisplayRole: switch (column) { @@ -339,9 +339,9 @@ QMimeData* WorldList::mimeData(const QModelIndexList& indexes) const if (idx.column() != 0) continue; int row = idx.row(); - if (row < 0 || row >= this->worlds.size()) + if (row < 0 || row >= this->m_worlds.size()) continue; - worlds_.append(this->worlds[row]); + worlds_.append(this->m_worlds[row]); } if (!worlds_.size()) { return new QMimeData(); @@ -393,7 +393,7 @@ bool WorldList::dropMimeData(const QMimeData* data, return false; // files dropped from outside? if (data->hasUrls()) { - bool was_watching = is_watching; + bool was_watching = m_isWatching; if (was_watching) stopWatching(); auto urls = data->urls(); diff --git a/launcher/minecraft/WorldList.h b/launcher/minecraft/WorldList.h index bea24bb9a..45e2eecac 100644 --- a/launcher/minecraft/WorldList.h +++ b/launcher/minecraft/WorldList.h @@ -40,9 +40,9 @@ class WorldList : public QAbstractListModel { virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; virtual int columnCount(const QModelIndex& parent) const; - size_t size() const { return worlds.size(); }; + size_t size() const { return m_worlds.size(); }; bool empty() const { return size() == 0; } - World& operator[](size_t index) { return worlds[index]; } + World& operator[](size_t index) { return m_worlds[index]; } /// Reloads the mod list and returns true if the list changed. virtual bool update(); @@ -82,7 +82,7 @@ class WorldList : public QAbstractListModel { QString instDirPath() const; - const QList& allWorlds() const { return worlds; } + const QList& allWorlds() const { return m_worlds; } private slots: void directoryChanged(QString path); @@ -93,7 +93,7 @@ class WorldList : public QAbstractListModel { protected: BaseInstance* m_instance; QFileSystemWatcher* m_watcher; - bool is_watching; + bool m_isWatching; QDir m_dir; - QList worlds; + QList m_worlds; }; From efeaefbf2e50b06d7c09a20e384d63b94b1f0699 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Thu, 17 Apr 2025 23:31:25 +0300 Subject: [PATCH 120/695] fix: load world size async Signed-off-by: Trial97 --- launcher/minecraft/World.cpp | 22 ++++------------ launcher/minecraft/World.h | 2 ++ launcher/minecraft/WorldList.cpp | 44 +++++++++++++++++++++++++++++++- launcher/minecraft/WorldList.h | 1 + 4 files changed, 51 insertions(+), 18 deletions(-) diff --git a/launcher/minecraft/World.cpp b/launcher/minecraft/World.cpp index fc67fbb13..8ae097bad 100644 --- a/launcher/minecraft/World.cpp +++ b/launcher/minecraft/World.cpp @@ -198,22 +198,6 @@ bool putLevelDatDataToFS(const QFileInfo& file, QByteArray& data) return f.commit(); } -int64_t calculateWorldSize(const QFileInfo& file) -{ - if (file.isFile() && file.suffix() == "zip") { - return file.size(); - } else if (file.isDir()) { - QDirIterator it(file.absoluteFilePath(), QDir::Files, QDirIterator::Subdirectories); - int64_t total = 0; - while (it.hasNext()) { - it.next(); - total += it.fileInfo().size(); - } - return total; - } - return -1; -} - World::World(const QFileInfo& file) { repath(file); @@ -223,7 +207,6 @@ void World::repath(const QFileInfo& file) { m_containerFile = file; m_folderName = file.fileName(); - m_size = calculateWorldSize(file); if (file.isFile() && file.suffix() == "zip") { m_iconFile = QString(); readFromZip(file); @@ -531,3 +514,8 @@ bool World::isMoreThanOneHardLink() const } return FS::hardLinkCount(m_containerFile.absoluteFilePath()) > 1; } + +void World::setSize(int64_t size) +{ + m_size = size; +} diff --git a/launcher/minecraft/World.h b/launcher/minecraft/World.h index 6a4e1f092..34d418e79 100644 --- a/launcher/minecraft/World.h +++ b/launcher/minecraft/World.h @@ -54,6 +54,8 @@ class World { bool rename(const QString& to); bool install(const QString& to, const QString& name = QString()); + void setSize(int64_t size); + // WEAK compare operator - used for replacing worlds bool operator==(const World& other) const; diff --git a/launcher/minecraft/WorldList.cpp b/launcher/minecraft/WorldList.cpp index f06eddb66..5f192740e 100644 --- a/launcher/minecraft/WorldList.cpp +++ b/launcher/minecraft/WorldList.cpp @@ -37,13 +37,14 @@ #include #include +#include #include #include #include +#include #include #include #include -#include "Application.h" WorldList::WorldList(const QString& dir, BaseInstance* instance) : QAbstractListModel(), m_instance(instance), m_dir(dir) { @@ -103,6 +104,7 @@ bool WorldList::update() beginResetModel(); m_worlds.swap(newWorlds); endResetModel(); + loadWorldsAsync(); return true; } @@ -416,4 +418,44 @@ bool WorldList::dropMimeData(const QMimeData* data, return false; } +int64_t calculateWorldSize(const QFileInfo& file) +{ + if (file.isFile() && file.suffix() == "zip") { + return file.size(); + } else if (file.isDir()) { + QDirIterator it(file.absoluteFilePath(), QDir::Files, QDirIterator::Subdirectories); + int64_t total = 0; + while (it.hasNext()) { + it.next(); + total += it.fileInfo().size(); + } + return total; + } + return -1; +} + +void WorldList::loadWorldsAsync() +{ + for (int i = 0; i < m_worlds.size(); ++i) { + auto file = m_worlds.at(i).container(); + int row = i; + QThreadPool::globalInstance()->start([this, file, row]() mutable { + auto size = calculateWorldSize(file); + + QMetaObject::invokeMethod( + this, + [this, size, row, file]() { + if (row < m_worlds.size() && m_worlds[row].container() == file) { + m_worlds[row].setSize(size); + + // Notify views + QModelIndex modelIndex = index(row); + emit dataChanged(modelIndex, modelIndex, { SizeRole }); + } + }, + Qt::QueuedConnection); + }); + } +} + #include "WorldList.moc" diff --git a/launcher/minecraft/WorldList.h b/launcher/minecraft/WorldList.h index 45e2eecac..93fecf1f5 100644 --- a/launcher/minecraft/WorldList.h +++ b/launcher/minecraft/WorldList.h @@ -86,6 +86,7 @@ class WorldList : public QAbstractListModel { private slots: void directoryChanged(QString path); + void loadWorldsAsync(); signals: void changed(); From 8a3aafc2743a2137480a6adf7fc02b9d14eb16eb Mon Sep 17 00:00:00 2001 From: Trial97 Date: Thu, 17 Apr 2025 22:17:17 +0300 Subject: [PATCH 121/695] chore: remove qt5 from github actions Signed-off-by: Trial97 --- .github/workflows/build.yml | 28 +++++++--------------------- .github/workflows/codeql.yml | 25 ++++++++++++++++++------- 2 files changed, 25 insertions(+), 28 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 11ef262ea..5d5cbc893 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -52,13 +52,6 @@ jobs: fail-fast: false matrix: include: - - os: ubuntu-22.04 - qt_ver: 5 - qt_host: linux - qt_arch: "" - qt_version: "5.15.2" - qt_modules: "qtnetworkauth" - - os: ubuntu-22.04 qt_ver: 6 qt_host: linux @@ -254,7 +247,7 @@ jobs: arch: ${{ matrix.vcvars_arch }} - name: Prepare AppImage (Linux) - if: runner.os == 'Linux' && matrix.qt_ver != 5 + if: runner.os == 'Linux' env: APPIMAGEUPDATE_HASH: ${{ matrix.appimageupdate_hash }} LINUXDEPLOY_HASH: ${{ matrix.linuxdeploy_hash }} @@ -287,7 +280,7 @@ jobs: ## - name: Configure CMake (macOS) - if: runner.os == 'macOS' && matrix.qt_ver == 6 + 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 -DLauncher_ENABLE_JAVA_DOWNLOADER=ON -DLauncher_BUILD_PLATFORM=official -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DCMAKE_OSX_ARCHITECTURES="x86_64;arm64" -G Ninja @@ -502,7 +495,7 @@ jobs: } - name: Package AppImage (Linux) - if: runner.os == 'Linux' && matrix.qt_ver != 5 + if: runner.os == 'Linux' shell: bash env: GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} @@ -598,29 +591,22 @@ jobs: name: PrismLauncher-${{ matrix.name }}-Setup-${{ env.VERSION }}-${{ inputs.build_type }} path: PrismLauncher-Setup.exe - - name: Upload binary tarball (Linux, portable, Qt 5) - if: runner.os == 'Linux' && matrix.qt_ver != 6 - uses: actions/upload-artifact@v4 - with: - name: PrismLauncher-${{ runner.os }}-Qt5-Portable-${{ env.VERSION }}-${{ inputs.build_type }} - path: PrismLauncher-portable.tar.gz - - - name: Upload binary tarball (Linux, portable, Qt 6) - if: runner.os == 'Linux' && matrix.qt_ver != 5 + - name: Upload binary tarball (Linux, portable) + if: runner.os == 'Linux' uses: actions/upload-artifact@v4 with: name: PrismLauncher-${{ runner.os }}-Qt6-Portable-${{ env.VERSION }}-${{ inputs.build_type }} path: PrismLauncher-portable.tar.gz - name: Upload AppImage (Linux) - if: runner.os == 'Linux' && matrix.qt_ver != 5 + if: runner.os == 'Linux' uses: actions/upload-artifact@v4 with: name: PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage path: PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage - name: Upload AppImage Zsync (Linux) - if: runner.os == 'Linux' && matrix.qt_ver != 5 + if: runner.os == 'Linux' uses: actions/upload-artifact@v4 with: name: PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage.zsync diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index d1d810374..e3243097d 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,16 +1,16 @@ name: "CodeQL Code Scanning" -on: [ push, pull_request, workflow_dispatch ] +on: [push, pull_request, workflow_dispatch] jobs: CodeQL: runs-on: ubuntu-latest - + steps: - name: Checkout repository uses: actions/checkout@v4 with: - submodules: 'true' + submodules: "true" - name: Initialize CodeQL uses: github/codeql-action/init@v3 @@ -20,14 +20,25 @@ jobs: languages: cpp, java - name: Install Dependencies - run: - sudo apt-get -y update + run: sudo apt-get -y update + + sudo apt-get -y install ninja-build extra-cmake-modules scdoc - sudo apt-get -y install ninja-build extra-cmake-modules scdoc qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools libqt5core5a libqt5network5 libqt5gui5 libqt5networkauth5 libqt5networkauth5-dev libqt5opengl5 libqt5opengl5-dev + - name: Install Qt + uses: jurplel/install-qt-action@v3 + with: + aqtversion: "==3.1.*" + py7zrversion: ">=0.20.2" + version: "6.8.1" + host: "linux" + target: "desktop" + arch: "" + modules: "qt5compat qtimageformats qtnetworkauth" + tools: "" - name: Configure and Build run: | - cmake -S . -B build -DCMAKE_INSTALL_PREFIX=/usr -DLauncher_QT_VERSION_MAJOR=5 -G Ninja + cmake -S . -B build -DCMAKE_INSTALL_PREFIX=/usr -G Ninja cmake --build build From 59bd6a915bba933ea2e9a61827b65a2c2f0d9c3e Mon Sep 17 00:00:00 2001 From: Trial97 Date: Thu, 10 Apr 2025 10:54:28 +0300 Subject: [PATCH 122/695] chore: remove qt5 from cmake files Signed-off-by: Trial97 --- CMakeLists.txt | 25 ++----------------------- libraries/LocalPeer/CMakeLists.txt | 4 +--- libraries/qdcss/CMakeLists.txt | 4 +--- libraries/rainbow/CMakeLists.txt | 4 +--- libraries/systeminfo/CMakeLists.txt | 4 +--- 5 files changed, 6 insertions(+), 35 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 10153c3ec..e2ea079bf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -310,23 +310,7 @@ endif() # Find the required Qt parts include(QtVersionlessBackport) -if(Launcher_QT_VERSION_MAJOR EQUAL 5) - set(QT_VERSION_MAJOR 5) - find_package(Qt5 REQUIRED COMPONENTS Core Widgets Concurrent Network Test Xml NetworkAuth OpenGL) - find_package(Qt5 COMPONENTS DBus) - list(APPEND Launcher_QT_DBUS Qt5::DBus) - - if(NOT Launcher_FORCE_BUNDLED_LIBS) - find_package(QuaZip-Qt5 1.3 QUIET) - endif() - if (NOT QuaZip-Qt5_FOUND) - set(QUAZIP_QT_MAJOR_VERSION ${QT_VERSION_MAJOR} CACHE STRING "Qt version to use (4, 5 or 6), defaults to ${QT_VERSION_MAJOR}" FORCE) - set(FORCE_BUNDLED_QUAZIP 1) - endif() - - # Qt 6 sets these by default. Notably causes Windows APIs to use UNICODE strings. - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DUNICODE -D_UNICODE") -elseif(Launcher_QT_VERSION_MAJOR EQUAL 6) +if(Launcher_QT_VERSION_MAJOR EQUAL 6) set(QT_VERSION_MAJOR 6) find_package(Qt6 REQUIRED COMPONENTS Core CoreTools Widgets Concurrent Network Test Xml Core5Compat NetworkAuth OpenGL) find_package(Qt6 COMPONENTS DBus) @@ -344,12 +328,7 @@ else() message(FATAL_ERROR "Qt version ${Launcher_QT_VERSION_MAJOR} is not supported") endif() -if(Launcher_QT_VERSION_MAJOR EQUAL 5) - include(ECMQueryQt) - ecm_query_qt(QT_PLUGINS_DIR QT_INSTALL_PLUGINS) - ecm_query_qt(QT_LIBS_DIR QT_INSTALL_LIBS) - ecm_query_qt(QT_LIBEXECS_DIR QT_INSTALL_LIBEXECS) -else() +if(Launcher_QT_VERSION_MAJOR EQUAL 6) set(QT_PLUGINS_DIR ${QT${QT_VERSION_MAJOR}_INSTALL_PREFIX}/${QT${QT_VERSION_MAJOR}_INSTALL_PLUGINS}) set(QT_LIBS_DIR ${QT${QT_VERSION_MAJOR}_INSTALL_PREFIX}/${QT${QT_VERSION_MAJOR}_INSTALL_LIBS}) set(QT_LIBEXECS_DIR ${QT${QT_VERSION_MAJOR}_INSTALL_PREFIX}/${QT${QT_VERSION_MAJOR}_INSTALL_LIBEXECS}) diff --git a/libraries/LocalPeer/CMakeLists.txt b/libraries/LocalPeer/CMakeLists.txt index f6de6581c..dd78647c0 100644 --- a/libraries/LocalPeer/CMakeLists.txt +++ b/libraries/LocalPeer/CMakeLists.txt @@ -1,9 +1,7 @@ cmake_minimum_required(VERSION 3.15) project(LocalPeer) -if(QT_VERSION_MAJOR EQUAL 5) - find_package(Qt5 COMPONENTS Core Network REQUIRED) -elseif(Launcher_QT_VERSION_MAJOR EQUAL 6) +if(Launcher_QT_VERSION_MAJOR EQUAL 6) find_package(Qt6 COMPONENTS Core Network Core5Compat REQUIRED) list(APPEND LocalPeer_LIBS Qt${QT_VERSION_MAJOR}::Core5Compat) endif() diff --git a/libraries/qdcss/CMakeLists.txt b/libraries/qdcss/CMakeLists.txt index ab8aaef94..7e497feca 100644 --- a/libraries/qdcss/CMakeLists.txt +++ b/libraries/qdcss/CMakeLists.txt @@ -1,9 +1,7 @@ cmake_minimum_required(VERSION 3.15) project(qdcss) -if(QT_VERSION_MAJOR EQUAL 5) - find_package(Qt5 COMPONENTS Core REQUIRED) -elseif(Launcher_QT_VERSION_MAJOR EQUAL 6) +if(Launcher_QT_VERSION_MAJOR EQUAL 6) find_package(Qt6 COMPONENTS Core Core5Compat REQUIRED) list(APPEND qdcss_LIBS Qt${QT_VERSION_MAJOR}::Core5Compat) endif() diff --git a/libraries/rainbow/CMakeLists.txt b/libraries/rainbow/CMakeLists.txt index 0867b2d27..c971889d8 100644 --- a/libraries/rainbow/CMakeLists.txt +++ b/libraries/rainbow/CMakeLists.txt @@ -1,9 +1,7 @@ cmake_minimum_required(VERSION 3.15) project(rainbow) -if(QT_VERSION_MAJOR EQUAL 5) - find_package(Qt5 COMPONENTS Core Gui REQUIRED) -elseif(Launcher_QT_VERSION_MAJOR EQUAL 6) +if(Launcher_QT_VERSION_MAJOR EQUAL 6) find_package(Qt6 COMPONENTS Core Gui REQUIRED) endif() diff --git a/libraries/systeminfo/CMakeLists.txt b/libraries/systeminfo/CMakeLists.txt index 33d246050..80b6b8094 100644 --- a/libraries/systeminfo/CMakeLists.txt +++ b/libraries/systeminfo/CMakeLists.txt @@ -1,8 +1,6 @@ project(systeminfo) -if(QT_VERSION_MAJOR EQUAL 5) - find_package(Qt5 COMPONENTS Core REQUIRED) -elseif(Launcher_QT_VERSION_MAJOR EQUAL 6) +if(Launcher_QT_VERSION_MAJOR EQUAL 6) find_package(Qt6 COMPONENTS Core Core5Compat REQUIRED) list(APPEND systeminfo_LIBS Qt${QT_VERSION_MAJOR}::Core5Compat) endif() From 442aae88ce793ac1e0d686f0839ad1f7ff46666b Mon Sep 17 00:00:00 2001 From: Trial97 Date: Thu, 10 Apr 2025 12:03:50 +0300 Subject: [PATCH 123/695] chore: remove qt version checks from code Signed-off-by: Trial97 --- launcher/DataMigrationTask.cpp | 8 -------- launcher/FileIgnoreProxy.cpp | 4 ---- launcher/FileSystem.cpp | 12 ----------- launcher/InstanceList.cpp | 4 ---- launcher/Version.h | 8 -------- launcher/icons/IconList.cpp | 6 +----- launcher/java/JavaChecker.cpp | 8 -------- launcher/launch/steps/PostLaunchCommand.cpp | 4 ---- launcher/launch/steps/PreLaunchCommand.cpp | 4 ---- launcher/minecraft/MinecraftInstance.cpp | 8 -------- launcher/minecraft/PackProfile.cpp | 4 ---- launcher/minecraft/WorldList.cpp | 4 ---- launcher/minecraft/auth/MinecraftAccount.cpp | 9 --------- launcher/minecraft/auth/Parsers.cpp | 4 ---- launcher/minecraft/auth/steps/MSAStep.cpp | 7 +------ .../minecraft/mod/ResourceFolderModel.cpp | 5 ----- .../atlauncher/ATLPackInstallTask.cpp | 10 ---------- .../legacy_ftb/PackInstallTask.cpp | 5 ----- .../legacy_ftb/PrivatePackManager.cpp | 4 ---- launcher/net/NetRequest.cpp | 10 ---------- launcher/net/PasteUpload.cpp | 4 ---- launcher/ui/MainWindow.cpp | 4 ---- launcher/ui/dialogs/NewInstanceDialog.cpp | 4 ---- launcher/ui/instanceview/InstanceView.cpp | 20 ------------------- launcher/ui/instanceview/VisualGroup.cpp | 4 ---- launcher/ui/pages/instance/ServersPage.cpp | 8 -------- libraries/systeminfo/src/distroutils.cpp | 12 ----------- 27 files changed, 2 insertions(+), 182 deletions(-) diff --git a/launcher/DataMigrationTask.cpp b/launcher/DataMigrationTask.cpp index 92e310a16..18decc7c3 100644 --- a/launcher/DataMigrationTask.cpp +++ b/launcher/DataMigrationTask.cpp @@ -37,11 +37,7 @@ void DataMigrationTask::dryRunFinished() disconnect(&m_copyFutureWatcher, &QFutureWatcher::finished, this, &DataMigrationTask::dryRunFinished); disconnect(&m_copyFutureWatcher, &QFutureWatcher::canceled, this, &DataMigrationTask::dryRunAborted); -#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) if (!m_copyFuture.isValid() || !m_copyFuture.result()) { -#else - if (!m_copyFuture.result()) { -#endif emitFailed(tr("Failed to scan source path.")); return; } @@ -75,11 +71,7 @@ void DataMigrationTask::copyFinished() disconnect(&m_copyFutureWatcher, &QFutureWatcher::finished, this, &DataMigrationTask::copyFinished); disconnect(&m_copyFutureWatcher, &QFutureWatcher::canceled, this, &DataMigrationTask::copyAborted); -#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) if (!m_copyFuture.isValid() || !m_copyFuture.result()) { -#else - if (!m_copyFuture.result()) { -#endif emitFailed(tr("Some paths could not be copied!")); return; } diff --git a/launcher/FileIgnoreProxy.cpp b/launcher/FileIgnoreProxy.cpp index 89c91ec1d..0314057d1 100644 --- a/launcher/FileIgnoreProxy.cpp +++ b/launcher/FileIgnoreProxy.cpp @@ -282,11 +282,7 @@ void FileIgnoreProxy::loadBlockedPathsFromFile(const QString& fileName) } auto ignoreData = ignoreFile.readAll(); auto string = QString::fromUtf8(ignoreData); -#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) setBlockedPaths(string.split('\n', Qt::SkipEmptyParts)); -#else - setBlockedPaths(string.split('\n', QString::SkipEmptyParts)); -#endif } void FileIgnoreProxy::saveBlockedPathsToFile(const QString& fileName) diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp index 7189ca841..08dc7d2cc 100644 --- a/launcher/FileSystem.cpp +++ b/launcher/FileSystem.cpp @@ -679,9 +679,6 @@ bool deletePath(QString path) bool trash(QString path, QString* pathInTrash) { -#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) - return false; -#else // FIXME: Figure out trash in Flatpak. Qt seemingly doesn't use the Trash portal if (DesktopServices::isFlatpak()) return false; @@ -690,7 +687,6 @@ bool trash(QString path, QString* pathInTrash) return false; #endif return QFile::moveToTrash(path, pathInTrash); -#endif } QString PathCombine(const QString& path1, const QString& path2) @@ -724,11 +720,7 @@ int pathDepth(const QString& path) QFileInfo info(path); -#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) - auto parts = QDir::toNativeSeparators(info.path()).split(QDir::separator(), QString::SkipEmptyParts); -#else auto parts = QDir::toNativeSeparators(info.path()).split(QDir::separator(), Qt::SkipEmptyParts); -#endif int numParts = parts.length(); numParts -= parts.count("."); @@ -748,11 +740,7 @@ QString pathTruncate(const QString& path, int depth) return pathTruncate(trunc, depth); } -#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) - auto parts = QDir::toNativeSeparators(trunc).split(QDir::separator(), QString::SkipEmptyParts); -#else auto parts = QDir::toNativeSeparators(trunc).split(QDir::separator(), Qt::SkipEmptyParts); -#endif if (parts.startsWith(".") && !path.startsWith(".")) { parts.removeFirst(); diff --git a/launcher/InstanceList.cpp b/launcher/InstanceList.cpp index 918fa1073..f76f8599a 100644 --- a/launcher/InstanceList.cpp +++ b/launcher/InstanceList.cpp @@ -449,11 +449,7 @@ QList InstanceList::discoverInstances() out.append(id); qDebug() << "Found instance ID" << id; } -#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) instanceSet = QSet(out.begin(), out.end()); -#else - instanceSet = out.toSet(); -#endif m_instancesProbed = true; return out; } diff --git a/launcher/Version.h b/launcher/Version.h index b06e256aa..12e7f0832 100644 --- a/launcher/Version.h +++ b/launcher/Version.h @@ -72,22 +72,14 @@ class Version { } } -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) auto numPart = QStringView{ m_fullString }.left(cutoff); -#else - auto numPart = m_fullString.leftRef(cutoff); -#endif if (!numPart.isEmpty()) { m_isNull = false; m_numPart = numPart.toInt(); } -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) auto stringPart = QStringView{ m_fullString }.mid(cutoff); -#else - auto stringPart = m_fullString.midRef(cutoff); -#endif if (!stringPart.isEmpty()) { m_isNull = false; diff --git a/launcher/icons/IconList.cpp b/launcher/icons/IconList.cpp index 7369d8b4b..8a2a482e1 100644 --- a/launcher/icons/IconList.cpp +++ b/launcher/icons/IconList.cpp @@ -137,11 +137,7 @@ QString formatName(const QDir& iconsDir, const QFileInfo& iconFile) /// Split into a separate function because the preprocessing impedes readability QSet toStringSet(const QList& list) { -#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) QSet set(list.begin(), list.end()); -#else - QSet set = list.toSet(); -#endif return set; } @@ -477,4 +473,4 @@ QString IconList::iconDirectory(const QString& key) const } } return getDirectory(); -} +} \ No newline at end of file diff --git a/launcher/java/JavaChecker.cpp b/launcher/java/JavaChecker.cpp index 07b5d7b40..0aa725705 100644 --- a/launcher/java/JavaChecker.cpp +++ b/launcher/java/JavaChecker.cpp @@ -137,11 +137,7 @@ void JavaChecker::finished(int exitcode, QProcess::ExitStatus status) QMap results; -#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) QStringList lines = m_stdout.split("\n", Qt::SkipEmptyParts); -#else - QStringList lines = m_stdout.split("\n", QString::SkipEmptyParts); -#endif for (QString line : lines) { line = line.trimmed(); // NOTE: workaround for GH-4125, where garbage is getting printed into stdout on bedrock linux @@ -149,11 +145,7 @@ void JavaChecker::finished(int exitcode, QProcess::ExitStatus status) continue; } -#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) auto parts = line.split('=', Qt::SkipEmptyParts); -#else - auto parts = line.split('=', QString::SkipEmptyParts); -#endif if (parts.size() != 2 || parts[0].isEmpty() || parts[1].isEmpty()) { continue; } else { diff --git a/launcher/launch/steps/PostLaunchCommand.cpp b/launcher/launch/steps/PostLaunchCommand.cpp index 5d893c71f..6b960974e 100644 --- a/launcher/launch/steps/PostLaunchCommand.cpp +++ b/launcher/launch/steps/PostLaunchCommand.cpp @@ -49,14 +49,10 @@ void PostLaunchCommand::executeTask() { auto cmd = m_parent->substituteVariables(m_command); emit logLine(tr("Running Post-Launch command: %1").arg(cmd), MessageLevel::Launcher); -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) auto args = QProcess::splitCommand(cmd); const QString program = args.takeFirst(); m_process.start(program, args); -#else - m_process.start(cmd); -#endif } void PostLaunchCommand::on_state(LoggedProcess::State state) diff --git a/launcher/launch/steps/PreLaunchCommand.cpp b/launcher/launch/steps/PreLaunchCommand.cpp index 318237e99..7e843ca3f 100644 --- a/launcher/launch/steps/PreLaunchCommand.cpp +++ b/launcher/launch/steps/PreLaunchCommand.cpp @@ -49,13 +49,9 @@ void PreLaunchCommand::executeTask() { auto cmd = m_parent->substituteVariables(m_command); emit logLine(tr("Running Pre-Launch command: %1").arg(cmd), MessageLevel::Launcher); -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) auto args = QProcess::splitCommand(cmd); const QString program = args.takeFirst(); m_process.start(program, args); -#else - m_process.start(cmd); -#endif } void PreLaunchCommand::on_state(LoggedProcess::State state) diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index d1780d497..463523a72 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -757,11 +757,7 @@ QStringList MinecraftInstance::processMinecraftArgs(AuthSessionPtr session, Mine token_mapping["assets_root"] = absAssetsDir; token_mapping["assets_index_name"] = assets->id; -#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) QStringList parts = args_pattern.split(' ', Qt::SkipEmptyParts); -#else - QStringList parts = args_pattern.split(' ', QString::SkipEmptyParts); -#endif for (int i = 0; i < parts.length(); i++) { parts[i] = replaceTokensIn(parts[i], token_mapping); } @@ -816,11 +812,7 @@ QString MinecraftInstance::createLaunchScript(AuthSessionPtr session, MinecraftT auto mainWindow = qobject_cast(w); if (mainWindow) { auto m = mainWindow->windowHandle()->frameMargins(); -#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) screenGeometry = screenGeometry.shrunkBy(m); -#else - screenGeometry = { screenGeometry.width() - m.left() - m.right(), screenGeometry.height() - m.top() - m.bottom() }; -#endif break; } } diff --git a/launcher/minecraft/PackProfile.cpp b/launcher/minecraft/PackProfile.cpp index 4deee9712..8475a1f32 100644 --- a/launcher/minecraft/PackProfile.cpp +++ b/launcher/minecraft/PackProfile.cpp @@ -645,11 +645,7 @@ void PackProfile::move(const int index, const MoveDirection direction) return; } beginMoveRows(QModelIndex(), index, index, QModelIndex(), togap); -#if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0) d->components.swapItemsAt(index, theirIndex); -#else - d->components.swap(index, theirIndex); -#endif endMoveRows(); invalidateLaunchProfile(); scheduleSave(); diff --git a/launcher/minecraft/WorldList.cpp b/launcher/minecraft/WorldList.cpp index cf27be676..ae6f918e5 100644 --- a/launcher/minecraft/WorldList.cpp +++ b/launcher/minecraft/WorldList.cpp @@ -307,11 +307,7 @@ class WorldMimeData : public QMimeData { QStringList formats() const { return QMimeData::formats() << "text/uri-list"; } protected: -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) QVariant retrieveData(const QString& mimetype, QMetaType type) const -#else - QVariant retrieveData(const QString& mimetype, QVariant::Type type) const -#endif { QList urls; for (auto& world : m_worlds) { diff --git a/launcher/minecraft/auth/MinecraftAccount.cpp b/launcher/minecraft/auth/MinecraftAccount.cpp index 1ed39b5ca..1613a42b1 100644 --- a/launcher/minecraft/auth/MinecraftAccount.cpp +++ b/launcher/minecraft/auth/MinecraftAccount.cpp @@ -106,11 +106,7 @@ QPixmap MinecraftAccount::getFace() const return QPixmap(); } QPixmap skin = QPixmap(8, 8); -#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) skin.fill(QColorConstants::Transparent); -#else - skin.fill(QColor(0, 0, 0, 0)); -#endif QPainter painter(&skin); painter.drawPixmap(0, 0, skinTexture.copy(8, 8, 8, 8)); painter.drawPixmap(0, 0, skinTexture.copy(40, 8, 8, 8)); @@ -290,13 +286,8 @@ QUuid MinecraftAccount::uuidFromUsername(QString username) // basically a reimplementation of Java's UUID#nameUUIDFromBytes QByteArray digest = QCryptographicHash::hash(input, QCryptographicHash::Md5); -#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) - auto bOr = [](QByteArray& array, int index, char value) { array[index] = array.at(index) | value; }; - auto bAnd = [](QByteArray& array, int index, char value) { array[index] = array.at(index) & value; }; -#else auto bOr = [](QByteArray& array, qsizetype index, char value) { array[index] |= value; }; auto bAnd = [](QByteArray& array, qsizetype index, char value) { array[index] &= value; }; -#endif bAnd(digest, 6, (char)0x0f); // clear version bOr(digest, 6, (char)0x30); // set to version 3 bAnd(digest, 8, (char)0x3f); // clear variant diff --git a/launcher/minecraft/auth/Parsers.cpp b/launcher/minecraft/auth/Parsers.cpp index f9d89baa2..de1ffda86 100644 --- a/launcher/minecraft/auth/Parsers.cpp +++ b/launcher/minecraft/auth/Parsers.cpp @@ -315,11 +315,7 @@ 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()) { diff --git a/launcher/minecraft/auth/steps/MSAStep.cpp b/launcher/minecraft/auth/steps/MSAStep.cpp index 87a0f8f08..aa972be71 100644 --- a/launcher/minecraft/auth/steps/MSAStep.cpp +++ b/launcher/minecraft/auth/steps/MSAStep.cpp @@ -168,13 +168,8 @@ void MSAStep::perform() m_oauth2.setRefreshToken(m_data->msaToken.refresh_token); m_oauth2.refreshAccessToken(); } else { -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) // QMultiMap param changed in 6.0 m_oauth2.setModifyParametersFunction( [](QAbstractOAuth::Stage stage, QMultiMap* map) { map->insert("prompt", "select_account"); }); -#else - m_oauth2.setModifyParametersFunction( - [](QAbstractOAuth::Stage stage, QMap* map) { map->insert("prompt", "select_account"); }); -#endif *m_data = AccountData(); m_data->msaClientID = m_clientId; @@ -182,4 +177,4 @@ void MSAStep::perform() } } -#include "MSAStep.moc" \ No newline at end of file +#include "MSAStep.moc" diff --git a/launcher/minecraft/mod/ResourceFolderModel.cpp b/launcher/minecraft/mod/ResourceFolderModel.cpp index d4900616b..601df84bd 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.cpp +++ b/launcher/minecraft/mod/ResourceFolderModel.cpp @@ -363,16 +363,11 @@ void ResourceFolderModel::onUpdateSucceeded() auto& new_resources = update_results->resources; -#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) auto current_list = m_resources_index.keys(); QSet current_set(current_list.begin(), current_list.end()); auto new_list = new_resources.keys(); QSet new_set(new_list.begin(), new_list.end()); -#else - QSet current_set(m_resources_index.keys().toSet()); - QSet new_set(new_resources.keys().toSet()); -#endif applyUpdates(current_set, new_set, new_resources); } diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp index a0898edbd..a9706a768 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp @@ -678,13 +678,8 @@ void PackInstallTask::extractConfigs() return; } -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), QOverload::of(MMCZip::extractDir), archivePath, extractDir.absolutePath() + "/minecraft"); -#else - m_extractFuture = - QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractDir, archivePath, extractDir.absolutePath() + "/minecraft"); -#endif connect(&m_extractFutureWatcher, &QFutureWatcher::finished, this, [this]() { downloadMods(); }); connect(&m_extractFutureWatcher, &QFutureWatcher::canceled, this, [this]() { emitAborted(); }); m_extractFutureWatcher.setFuture(m_extractFuture); @@ -897,13 +892,8 @@ void PackInstallTask::onModsDownloaded() jobPtr.reset(); if (!modsToExtract.empty() || !modsToDecomp.empty() || !modsToCopy.empty()) { -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) m_modExtractFuture = QtConcurrent::run(QThreadPool::globalInstance(), &PackInstallTask::extractMods, this, modsToExtract, modsToDecomp, modsToCopy); -#else - m_modExtractFuture = - QtConcurrent::run(QThreadPool::globalInstance(), this, &PackInstallTask::extractMods, modsToExtract, modsToDecomp, modsToCopy); -#endif connect(&m_modExtractFutureWatcher, &QFutureWatcher::finished, this, &PackInstallTask::onModsExtracted); connect(&m_modExtractFutureWatcher, &QFutureWatcher::canceled, this, &PackInstallTask::emitAborted); m_modExtractFutureWatcher.setFuture(m_modExtractFuture); diff --git a/launcher/modplatform/legacy_ftb/PackInstallTask.cpp b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp index c04c0b2f3..c8d04828c 100644 --- a/launcher/modplatform/legacy_ftb/PackInstallTask.cpp +++ b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp @@ -108,13 +108,8 @@ void PackInstallTask::unzip() return; } -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), QOverload::of(MMCZip::extractDir), archivePath, extractDir.absolutePath() + "/unzip"); -#else - m_extractFuture = - QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractDir, archivePath, extractDir.absolutePath() + "/unzip"); -#endif connect(&m_extractFutureWatcher, &QFutureWatcher::finished, this, &PackInstallTask::onUnzipFinished); connect(&m_extractFutureWatcher, &QFutureWatcher::canceled, this, &PackInstallTask::onUnzipCanceled); m_extractFutureWatcher.setFuture(m_extractFuture); diff --git a/launcher/modplatform/legacy_ftb/PrivatePackManager.cpp b/launcher/modplatform/legacy_ftb/PrivatePackManager.cpp index 2ae351329..17e9f7d76 100644 --- a/launcher/modplatform/legacy_ftb/PrivatePackManager.cpp +++ b/launcher/modplatform/legacy_ftb/PrivatePackManager.cpp @@ -44,12 +44,8 @@ namespace LegacyFTB { void PrivatePackManager::load() { try { -#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) auto foo = QString::fromUtf8(FS::read(m_filename)).split('\n', Qt::SkipEmptyParts); currentPacks = QSet(foo.begin(), foo.end()); -#else - currentPacks = QString::fromUtf8(FS::read(m_filename)).split('\n', QString::SkipEmptyParts).toSet(); -#endif dirty = false; } catch (...) { diff --git a/launcher/net/NetRequest.cpp b/launcher/net/NetRequest.cpp index 310653508..ef533f599 100644 --- a/launcher/net/NetRequest.cpp +++ b/launcher/net/NetRequest.cpp @@ -104,12 +104,10 @@ void NetRequest::executeTask() header_proxy->writeHeaders(request); } -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) #if defined(LAUNCHER_APPLICATION) request.setTransferTimeout(APPLICATION->settings()->get("RequestTimeout").toInt() * 1000); #else request.setTransferTimeout(); -#endif #endif m_last_progress_time = m_clock.now(); @@ -122,11 +120,7 @@ void NetRequest::executeTask() connect(rep, &QNetworkReply::uploadProgress, this, &NetRequest::onProgress); connect(rep, &QNetworkReply::downloadProgress, this, &NetRequest::onProgress); connect(rep, &QNetworkReply::finished, this, &NetRequest::downloadFinished); -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15 connect(rep, &QNetworkReply::errorOccurred, this, &NetRequest::downloadError); -#else - connect(rep, QOverload::of(&QNetworkReply::error), this, &NetRequest::downloadError); -#endif connect(rep, &QNetworkReply::sslErrors, this, &NetRequest::sslErrors); connect(rep, &QNetworkReply::readyRead, this, &NetRequest::downloadReadyRead); } @@ -323,11 +317,7 @@ auto NetRequest::abort() -> bool { m_state = State::AbortedByUser; if (m_reply) { -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15 disconnect(m_reply.get(), &QNetworkReply::errorOccurred, nullptr, nullptr); -#else - disconnect(m_reply.get(), QOverload::of(&QNetworkReply::error), nullptr, nullptr); -#endif m_reply->abort(); } return true; diff --git a/launcher/net/PasteUpload.cpp b/launcher/net/PasteUpload.cpp index c67d3b23c..86a44669e 100644 --- a/launcher/net/PasteUpload.cpp +++ b/launcher/net/PasteUpload.cpp @@ -130,11 +130,7 @@ void PasteUpload::executeTask() connect(rep, &QNetworkReply::uploadProgress, this, &Task::setProgress); connect(rep, &QNetworkReply::finished, this, &PasteUpload::downloadFinished); -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) connect(rep, &QNetworkReply::errorOccurred, this, &PasteUpload::downloadError); -#else - connect(rep, QOverload::of(&QNetworkReply::error), this, &PasteUpload::downloadError); -#endif m_reply = std::shared_ptr(rep); diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index ddf726373..53e87b57e 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -814,11 +814,7 @@ void MainWindow::updateNewsLabel() QList stringToIntList(const QString& string) { -#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) QStringList split = string.split(',', Qt::SkipEmptyParts); -#else - QStringList split = string.split(',', QString::SkipEmptyParts); -#endif QList out; for (int i = 0; i < split.size(); ++i) { out.append(split.at(i).toInt()); diff --git a/launcher/ui/dialogs/NewInstanceDialog.cpp b/launcher/ui/dialogs/NewInstanceDialog.cpp index d9ea0aafb..6036663ba 100644 --- a/launcher/ui/dialogs/NewInstanceDialog.cpp +++ b/launcher/ui/dialogs/NewInstanceDialog.cpp @@ -136,11 +136,7 @@ NewInstanceDialog::NewInstanceDialog(const QString& initialGroup, if (APPLICATION->settings()->get("NewInstanceGeometry").isValid()) { restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("NewInstanceGeometry").toByteArray())); } else { -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) auto screen = parent->screen(); -#else - auto screen = QGuiApplication::primaryScreen(); -#endif auto geometry = screen->availableSize(); resize(width(), qMin(geometry.height() - 50, 710)); } diff --git a/launcher/ui/instanceview/InstanceView.cpp b/launcher/ui/instanceview/InstanceView.cpp index c677f3951..fa1af4266 100644 --- a/launcher/ui/instanceview/InstanceView.cpp +++ b/launcher/ui/instanceview/InstanceView.cpp @@ -400,12 +400,8 @@ void InstanceView::mouseReleaseEvent(QMouseEvent* event) if (event->button() == Qt::LeftButton) { emit clicked(index); } -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) QStyleOptionViewItem option; initViewItemOption(&option); -#else - QStyleOptionViewItem option = viewOptions(); -#endif if (m_pressedAlreadySelected) { option.state |= QStyle::State_Selected; } @@ -431,12 +427,8 @@ void InstanceView::mouseDoubleClickEvent(QMouseEvent* event) QPersistentModelIndex persistent = index; emit doubleClicked(persistent); -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) QStyleOptionViewItem option; initViewItemOption(&option); -#else - QStyleOptionViewItem option = viewOptions(); -#endif if ((model()->flags(index) & Qt::ItemIsEnabled) && !style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick, &option, this)) { emit activated(index); } @@ -472,12 +464,8 @@ void InstanceView::paintEvent([[maybe_unused]] QPaintEvent* event) painter.setOpacity(1.0); } -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) QStyleOptionViewItem option; initViewItemOption(&option); -#else - QStyleOptionViewItem option = viewOptions(); -#endif option.widget = this; if (model()->rowCount() == 0) { @@ -732,12 +720,8 @@ QRect InstanceView::geometryRect(const QModelIndex& index) const int x = pos.first; // int y = pos.second; -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) QStyleOptionViewItem option; initViewItemOption(&option); -#else - QStyleOptionViewItem option = viewOptions(); -#endif QRect out; out.setTop(cat->verticalPosition() + cat->headerHeight() + 5 + cat->rowTopOf(index)); @@ -784,12 +768,8 @@ QPixmap InstanceView::renderToPixmap(const QModelIndexList& indices, QRect* r) c QPixmap pixmap(r->size()); pixmap.fill(Qt::transparent); QPainter painter(&pixmap); -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) QStyleOptionViewItem option; initViewItemOption(&option); -#else - QStyleOptionViewItem option = viewOptions(); -#endif option.state |= QStyle::State_Selected; for (int j = 0; j < paintPairs.count(); ++j) { option.rect = paintPairs.at(j).first.translated(-r->topLeft()); diff --git a/launcher/ui/instanceview/VisualGroup.cpp b/launcher/ui/instanceview/VisualGroup.cpp index 83103c502..089db8ad7 100644 --- a/launcher/ui/instanceview/VisualGroup.cpp +++ b/launcher/ui/instanceview/VisualGroup.cpp @@ -73,12 +73,8 @@ void VisualGroup::update() positionInRow = 0; maxRowHeight = 0; } -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) QStyleOptionViewItem viewItemOption; view->initViewItemOption(&viewItemOption); -#else - QStyleOptionViewItem viewItemOption = view->viewOptions(); -#endif auto itemHeight = view->itemDelegate()->sizeHint(viewItemOption, item).height(); if (itemHeight > maxRowHeight) { diff --git a/launcher/ui/pages/instance/ServersPage.cpp b/launcher/ui/pages/instance/ServersPage.cpp index 136fb47c7..a93966ae9 100644 --- a/launcher/ui/pages/instance/ServersPage.cpp +++ b/launcher/ui/pages/instance/ServersPage.cpp @@ -255,11 +255,7 @@ class ServersModel : public QAbstractListModel { return false; } beginMoveRows(QModelIndex(), row, row, QModelIndex(), row - 1); -#if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0) m_servers.swapItemsAt(row - 1, row); -#else - m_servers.swap(row - 1, row); -#endif endMoveRows(); scheduleSave(); return true; @@ -275,11 +271,7 @@ class ServersModel : public QAbstractListModel { return false; } beginMoveRows(QModelIndex(), row, row, QModelIndex(), row + 2); -#if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0) m_servers.swapItemsAt(row + 1, row); -#else - m_servers.swap(row + 1, row); -#endif endMoveRows(); scheduleSave(); return true; diff --git a/libraries/systeminfo/src/distroutils.cpp b/libraries/systeminfo/src/distroutils.cpp index 57e6c8320..5891282fe 100644 --- a/libraries/systeminfo/src/distroutils.cpp +++ b/libraries/systeminfo/src/distroutils.cpp @@ -145,11 +145,7 @@ void Sys::lsb_postprocess(Sys::LsbInfo& lsb, Sys::DistributionInfo& out) vers = lsb.codename; } else { // ubuntu, debian, gentoo, scientific, slackware, ... ? -#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) auto parts = dist.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts); -#else - auto parts = dist.split(QRegularExpression("\\s+"), QString::SkipEmptyParts); -#endif if (parts.size()) { dist = parts[0]; } @@ -182,11 +178,7 @@ QString Sys::_extract_distribution(const QString& x) if (release.startsWith("suse linux enterprise")) { return "sles"; } -#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) QStringList list = release.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts); -#else - QStringList list = release.split(QRegularExpression("\\s+"), QString::SkipEmptyParts); -#endif if (list.size()) { return list[0]; } @@ -196,11 +188,7 @@ QString Sys::_extract_distribution(const QString& x) QString Sys::_extract_version(const QString& x) { QRegularExpression versionish_string(QRegularExpression::anchoredPattern("\\d+(?:\\.\\d+)*$")); -#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) QStringList list = x.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts); -#else - QStringList list = x.split(QRegularExpression("\\s+"), QString::SkipEmptyParts); -#endif for (int i = list.size() - 1; i >= 0; --i) { QString chunk = list[i]; if (versionish_string.match(chunk).hasMatch()) { From 5fee4e3f8bbab3063e15fcc4f1563433d7001c1b Mon Sep 17 00:00:00 2001 From: Trial97 Date: Fri, 18 Apr 2025 00:17:29 +0300 Subject: [PATCH 124/695] chore: remove qt5 from release and copyright Signed-off-by: Trial97 --- .github/workflows/trigger_release.yml | 2 -- CMakeLists.txt | 10 ---------- COPYING.md | 2 +- nix/unwrapped.nix | 3 --- 4 files changed, 1 insertion(+), 16 deletions(-) diff --git a/.github/workflows/trigger_release.yml b/.github/workflows/trigger_release.yml index 411a5bbeb..96f616a43 100644 --- a/.github/workflows/trigger_release.yml +++ b/.github/workflows/trigger_release.yml @@ -46,7 +46,6 @@ jobs: run: | mv ${{ github.workspace }}/PrismLauncher-source PrismLauncher-${{ env.VERSION }} mv PrismLauncher-Linux-Qt6-Portable*/PrismLauncher-portable.tar.gz PrismLauncher-Linux-Qt6-Portable-${{ env.VERSION }}.tar.gz - mv PrismLauncher-Linux-Qt5-Portable*/PrismLauncher-portable.tar.gz PrismLauncher-Linux-Qt5-Portable-${{ env.VERSION }}.tar.gz mv PrismLauncher-*.AppImage/PrismLauncher-*.AppImage PrismLauncher-Linux-x86_64.AppImage mv PrismLauncher-*.AppImage.zsync/PrismLauncher-*.AppImage.zsync PrismLauncher-Linux-x86_64.AppImage.zsync mv PrismLauncher-macOS*/PrismLauncher.zip PrismLauncher-macOS-${{ env.VERSION }}.zip @@ -89,7 +88,6 @@ jobs: draft: true prerelease: false files: | - PrismLauncher-Linux-Qt5-Portable-${{ env.VERSION }}.tar.gz PrismLauncher-Linux-x86_64.AppImage PrismLauncher-Linux-x86_64.AppImage.zsync PrismLauncher-Linux-Qt6-Portable-${{ env.VERSION }}.tar.gz diff --git a/CMakeLists.txt b/CMakeLists.txt index e2ea079bf..321232378 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -88,11 +88,6 @@ else() endif() endif() -# 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") - # Fix aarch64 build for toml++ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DTOML_ENABLE_FLOAT16=0") @@ -334,11 +329,6 @@ if(Launcher_QT_VERSION_MAJOR EQUAL 6) set(QT_LIBEXECS_DIR ${QT${QT_VERSION_MAJOR}_INSTALL_PREFIX}/${QT${QT_VERSION_MAJOR}_INSTALL_LIBEXECS}) endif() -# NOTE: Qt 6 already sets this by default -if (Qt5_POSITION_INDEPENDENT_CODE) - SET(CMAKE_POSITION_INDEPENDENT_CODE ON) -endif() - if(NOT Launcher_FORCE_BUNDLED_LIBS) # Find toml++ find_package(tomlplusplus 3.2.0 QUIET) diff --git a/COPYING.md b/COPYING.md index f1f0b3a70..818c13c78 100644 --- a/COPYING.md +++ b/COPYING.md @@ -108,7 +108,7 @@ Information on third party licenses used in MinGW-w64 can be found in its COPYING.MinGW-w64-runtime.txt. -## Qt 5/6 +## Qt 6 Copyright (C) 2022 The Qt Company Ltd and other contributors. Contact: https://www.qt.io/licensing diff --git a/nix/unwrapped.nix b/nix/unwrapped.nix index 93cda8e1a..14882e893 100644 --- a/nix/unwrapped.nix +++ b/nix/unwrapped.nix @@ -96,9 +96,6 @@ stdenv.mkDerivation { ++ lib.optionals (msaClientID != null) [ (lib.cmakeFeature "Launcher_MSA_CLIENT_ID" (toString msaClientID)) ] - ++ lib.optionals (lib.versionOlder kdePackages.qtbase.version "6") [ - (lib.cmakeFeature "Launcher_QT_VERSION_MAJOR" "5") - ] ++ lib.optionals stdenv.hostPlatform.isDarwin [ # we wrap our binary manually (lib.cmakeFeature "INSTALL_BUNDLE" "nodeps") From 4a2b5c72dc75e03695566af399c504c19170d042 Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Thu, 17 Apr 2025 14:54:44 -0700 Subject: [PATCH 125/695] feat(color-console): support ansi colors in console Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- launcher/Application.cpp | 2 +- launcher/CMakeLists.txt | 8 ++++---- launcher/{ => console}/WindowsConsole.cpp | 3 +++ launcher/{ => console}/WindowsConsole.h | 3 +++ 4 files changed, 11 insertions(+), 5 deletions(-) rename launcher/{ => console}/WindowsConsole.cpp (99%) rename launcher/{ => console}/WindowsConsole.h (93%) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index d773d9a1c..f98112641 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -155,7 +155,7 @@ #if defined Q_OS_WIN32 #include #include -#include "WindowsConsole.h" +#include "console/WindowsConsole.h" #endif #define STRINGIFY(x) #x diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 30d657f9e..5d757a130 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -589,8 +589,8 @@ set(ATLAUNCHER_SOURCES ) set(LINKEXE_SOURCES - WindowsConsole.cpp - WindowsConsole.h + console/WindowsConsole.cpp + console/WindowsConsole.h filelink/FileLink.h filelink/FileLink.cpp @@ -1164,8 +1164,8 @@ endif() if(WIN32) set(LAUNCHER_SOURCES - WindowsConsole.cpp - WindowsConsole.h + console/WindowsConsole.cpp + console/WindowsConsole.h ${LAUNCHER_SOURCES} ) endif() diff --git a/launcher/WindowsConsole.cpp b/launcher/console/WindowsConsole.cpp similarity index 99% rename from launcher/WindowsConsole.cpp rename to launcher/console/WindowsConsole.cpp index 83cad5afa..f388bd3b1 100644 --- a/launcher/WindowsConsole.cpp +++ b/launcher/console/WindowsConsole.cpp @@ -19,6 +19,7 @@ #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif +#include "WindowsConsole.h" #include #include #include @@ -126,3 +127,5 @@ bool AttachWindowsConsole() return false; } + +std::error_code diff --git a/launcher/WindowsConsole.h b/launcher/console/WindowsConsole.h similarity index 93% rename from launcher/WindowsConsole.h rename to launcher/console/WindowsConsole.h index ab53864b4..4c1f3ee28 100644 --- a/launcher/WindowsConsole.h +++ b/launcher/console/WindowsConsole.h @@ -21,5 +21,8 @@ #pragma once +#include + void BindCrtHandlesToStdHandles(bool bindStdIn, bool bindStdOut, bool bindStdErr); bool AttachWindowsConsole(); +std::error_code EnableAnsiSupport(); From 45b6454222a83b932e0505cbd815d697226080cd Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Thu, 17 Apr 2025 23:18:57 -0700 Subject: [PATCH 126/695] feat(ansi-console): Format console with ansi excapes Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- launcher/Application.cpp | 181 +++++++++++++----- launcher/CMakeLists.txt | 31 ++- launcher/InstanceList.cpp | 6 +- launcher/console/Console.h | 33 ++++ launcher/console/WindowsConsole.cpp | 34 +++- launcher/filelink/FileLink.cpp | 5 +- .../updater/prismupdater/PrismUpdater.cpp | 109 +---------- 7 files changed, 225 insertions(+), 174 deletions(-) create mode 100644 launcher/console/Console.h diff --git a/launcher/Application.cpp b/launcher/Application.cpp index f98112641..70ffce12f 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -125,6 +125,7 @@ #include #include +#include #include #include #include "SysInfo.h" @@ -153,11 +154,16 @@ #endif #if defined Q_OS_WIN32 +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif #include #include #include "console/WindowsConsole.h" #endif +#include "console/Console.h" + #define STRINGIFY(x) #x #define TOSTRING(x) STRINGIFY(x) @@ -165,6 +171,69 @@ static const QLatin1String liveCheckFile("live.check"); PixmapCache* PixmapCache::s_instance = nullptr; +static bool isANSIColorConsole; + +static QString defaultLogFormat = QStringLiteral( + "%{time process}" + " " + "%{if-debug}D%{endif}" + "%{if-info}I%{endif}" + "%{if-warning}W%{endif}" + "%{if-critical}C%{endif}" + "%{if-fatal}F%{endif}" + " " + "|" + " " + "%{function}:%{line}" + " " + "|" + " " + "%{if-category}[%{category}]: %{endif}" + "%{message}"); + +#define ansi_reset "\x1b[0m" +#define ansi_bold "\x1b[1m" +#define ansi_faint "\x1b[2m" +#define ansi_italic "\x1b[3m" +#define ansi_red_fg "\x1b[31m" +#define ansi_green_fg "\x1b[32m" +#define ansi_yellow_fg "\x1b[33m" +#define ansi_blue_fg "\x1b[34m" +#define ansi_purple_fg "\x1b[35m" + +static QString ansiLogFormat = QStringLiteral( + "%{time process}" + " " + "%{if-debug}" ansi_bold ansi_blue_fg "D" ansi_reset + "%{endif}" + "%{if-info}" ansi_bold ansi_green_fg "I" ansi_reset + "%{endif}" + "%{if-warning}" ansi_bold ansi_yellow_fg "W" ansi_reset + "%{endif}" + "%{if-critical}" ansi_bold ansi_red_fg "C" ansi_reset + "%{endif}" + "%{if-fatal}" ansi_bold ansi_red_fg "F" ansi_reset + "%{endif}" + " " + "|" + " " ansi_faint ansi_italic "%{function}:%{line}" ansi_reset + " " + "|" + " " + "%{if-category}[" ansi_bold ansi_purple_fg "%{category}" ansi_reset + "]: %{endif}" + "%{message}"); + +#undef ansi_purple_fg +#undef ansi_blue_fg +#undef ansi_yellow_fg +#undef ansi_green_fg +#undef ansi_red_fg +#undef ansi_italic +#undef ansi_faint +#undef ansi_bold +#undef ansi_reset + namespace { /** This is used so that we can output to the log file in addition to the CLI. */ @@ -173,11 +242,24 @@ void appDebugOutput(QtMsgType type, const QMessageLogContext& context, const QSt static std::mutex loggerMutex; const std::lock_guard lock(loggerMutex); // synchronized, QFile logFile is not thread-safe + if (isANSIColorConsole) { + // ensure default is set for log file + qSetMessagePattern(defaultLogFormat); + } + QString out = qFormatLogMessage(type, context, msg); out += QChar::LineFeed; APPLICATION->logFile->write(out.toUtf8()); APPLICATION->logFile->flush(); + + if (isANSIColorConsole) { + // format ansi for console; + qSetMessagePattern(ansiLogFormat); + out = qFormatLogMessage(type, context, msg); + out += QChar::LineFeed; + } + QTextStream(stderr) << out.toLocal8Bit(); fflush(stderr); } @@ -218,8 +300,18 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) // attach the parent console if stdout not already captured if (AttachWindowsConsole()) { consoleAttached = true; + if (auto err = EnableAnsiSupport(); !err) { + isANSIColorConsole = true; + } else { + std::cout << "Error setting up ansi console" << err.message() << std::endl; + } + } +#else + if (console::isConsole()) { + isANSIColorConsole = true; } #endif + setOrganizationName(BuildConfig.LAUNCHER_NAME); setOrganizationDomain(BuildConfig.LAUNCHER_DOMAIN); setApplicationName(BuildConfig.LAUNCHER_NAME); @@ -448,27 +540,14 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) return; } qInstallMessageHandler(appDebugOutput); - - qSetMessagePattern( - "%{time process}" - " " - "%{if-debug}D%{endif}" - "%{if-info}I%{endif}" - "%{if-warning}W%{endif}" - "%{if-critical}C%{endif}" - "%{if-fatal}F%{endif}" - " " - "|" - " " - "%{if-category}[%{category}]: %{endif}" - "%{message}"); + qSetMessagePattern(defaultLogFormat); bool foundLoggingRules = false; auto logRulesFile = QStringLiteral("qtlogging.ini"); auto logRulesPath = FS::PathCombine(dataPath, logRulesFile); - qDebug() << "Testing" << logRulesPath << "..."; + qInfo() << "Testing" << logRulesPath << "..."; foundLoggingRules = QFile::exists(logRulesPath); // search the dataPath() @@ -476,7 +555,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) if (!foundLoggingRules && !isPortable() && dirParam.isEmpty() && dataDirEnv.isEmpty()) { logRulesPath = QStandardPaths::locate(QStandardPaths::AppDataLocation, FS::PathCombine("..", logRulesFile)); if (!logRulesPath.isEmpty()) { - qDebug() << "Found" << logRulesPath << "..."; + qInfo() << "Found" << logRulesPath << "..."; foundLoggingRules = true; } } @@ -487,28 +566,28 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) #else logRulesPath = FS::PathCombine(m_rootPath, logRulesFile); #endif - qDebug() << "Testing" << logRulesPath << "..."; + qInfo() << "Testing" << logRulesPath << "..."; foundLoggingRules = QFile::exists(logRulesPath); } if (foundLoggingRules) { // load and set logging rules - qDebug() << "Loading logging rules from:" << logRulesPath; + qInfo() << "Loading logging rules from:" << logRulesPath; QSettings loggingRules(logRulesPath, QSettings::IniFormat); loggingRules.beginGroup("Rules"); QStringList rule_names = loggingRules.childKeys(); QStringList rules; - qDebug() << "Setting log rules:"; + qInfo() << "Setting log rules:"; for (auto rule_name : rule_names) { auto rule = QString("%1=%2").arg(rule_name).arg(loggingRules.value(rule_name).toString()); rules.append(rule); - qDebug() << " " << rule; + qInfo() << " " << rule; } auto rules_str = rules.join("\n"); QLoggingCategory::setFilterRules(rules_str); } - qDebug() << "<> Log initialized."; + qInfo() << "<> Log initialized."; } { @@ -525,33 +604,33 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) } { - qDebug() << qPrintable(BuildConfig.LAUNCHER_DISPLAYNAME + ", " + QString(BuildConfig.LAUNCHER_COPYRIGHT).replace("\n", ", ")); - qDebug() << "Version : " << BuildConfig.printableVersionString(); - qDebug() << "Platform : " << BuildConfig.BUILD_PLATFORM; - qDebug() << "Git commit : " << BuildConfig.GIT_COMMIT; - qDebug() << "Git refspec : " << BuildConfig.GIT_REFSPEC; - qDebug() << "Compiled for : " << BuildConfig.systemID(); - qDebug() << "Compiled by : " << BuildConfig.compilerID(); - qDebug() << "Build Artifact : " << BuildConfig.BUILD_ARTIFACT; - qDebug() << "Updates Enabled : " << (updaterEnabled() ? "Yes" : "No"); + qInfo() << qPrintable(BuildConfig.LAUNCHER_DISPLAYNAME + ", " + QString(BuildConfig.LAUNCHER_COPYRIGHT).replace("\n", ", ")); + qInfo() << "Version : " << BuildConfig.printableVersionString(); + qInfo() << "Platform : " << BuildConfig.BUILD_PLATFORM; + qInfo() << "Git commit : " << BuildConfig.GIT_COMMIT; + qInfo() << "Git refspec : " << BuildConfig.GIT_REFSPEC; + qInfo() << "Compiled for : " << BuildConfig.systemID(); + qInfo() << "Compiled by : " << BuildConfig.compilerID(); + qInfo() << "Build Artifact : " << BuildConfig.BUILD_ARTIFACT; + qInfo() << "Updates Enabled : " << (updaterEnabled() ? "Yes" : "No"); if (adjustedBy.size()) { - qDebug() << "Work dir before adjustment : " << origcwdPath; - qDebug() << "Work dir after adjustment : " << QDir::currentPath(); - qDebug() << "Adjusted by : " << adjustedBy; + qInfo() << "Work dir before adjustment : " << origcwdPath; + qInfo() << "Work dir after adjustment : " << QDir::currentPath(); + qInfo() << "Adjusted by : " << adjustedBy; } else { - qDebug() << "Work dir : " << QDir::currentPath(); + qInfo() << "Work dir : " << QDir::currentPath(); } - qDebug() << "Binary path : " << binPath; - qDebug() << "Application root path : " << m_rootPath; + qInfo() << "Binary path : " << binPath; + qInfo() << "Application root path : " << m_rootPath; if (!m_instanceIdToLaunch.isEmpty()) { - qDebug() << "ID of instance to launch : " << m_instanceIdToLaunch; + qInfo() << "ID of instance to launch : " << m_instanceIdToLaunch; } if (!m_serverToJoin.isEmpty()) { - qDebug() << "Address of server to join :" << m_serverToJoin; + qInfo() << "Address of server to join :" << m_serverToJoin; } else if (!m_worldToJoin.isEmpty()) { - qDebug() << "Name of the world to join :" << m_worldToJoin; + qInfo() << "Name of the world to join :" << m_worldToJoin; } - qDebug() << "<> Paths set."; + qInfo() << "<> Paths set."; } if (m_liveCheck) { @@ -818,7 +897,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) PixmapCache::setInstance(new PixmapCache(this)); - qDebug() << "<> Settings loaded."; + qInfo() << "<> Settings loaded."; } #ifndef QT_NO_ACCESSIBILITY @@ -834,7 +913,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) QString user = settings()->get("ProxyUser").toString(); QString pass = settings()->get("ProxyPass").toString(); updateProxySettings(proxyTypeStr, addr, port, user, pass); - qDebug() << "<> Network done."; + qInfo() << "<> Network done."; } // load translations @@ -842,8 +921,8 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_translations.reset(new TranslationsModel("translations")); auto bcp47Name = m_settings->get("Language").toString(); m_translations->selectLanguage(bcp47Name); - qDebug() << "Your language is" << bcp47Name; - qDebug() << "<> Translations loaded."; + qInfo() << "Your language is" << bcp47Name; + qInfo() << "<> Translations loaded."; } // Instance icons @@ -854,7 +933,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_icons.reset(new IconList(instFolders, setting->get().toString())); connect(setting.get(), &Setting::SettingChanged, [this](const Setting&, QVariant value) { m_icons->directoryChanged(value.toString()); }); - qDebug() << "<> Instance icons initialized."; + qInfo() << "<> Instance icons initialized."; } // Themes @@ -866,25 +945,25 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) // instance path: check for problems with '!' in instance path and warn the user in the log // and remember that we have to show him a dialog when the gui starts (if it does so) QString instDir = InstDirSetting->get().toString(); - qDebug() << "Instance path : " << instDir; + qInfo() << "Instance path : " << instDir; if (FS::checkProblemticPathJava(QDir(instDir))) { qWarning() << "Your instance path contains \'!\' and this is known to cause java problems!"; } m_instances.reset(new InstanceList(m_settings, instDir, this)); connect(InstDirSetting.get(), &Setting::SettingChanged, m_instances.get(), &InstanceList::on_InstFolderChanged); - qDebug() << "Loading Instances..."; + qInfo() << "Loading Instances..."; m_instances->loadList(); - qDebug() << "<> Instances loaded."; + qInfo() << "<> Instances loaded."; } // and accounts { m_accounts.reset(new AccountList(this)); - qDebug() << "Loading accounts..."; + qInfo() << "Loading accounts..."; m_accounts->setListFilePath("accounts.json", true); m_accounts->loadList(); m_accounts->fillQueue(); - qDebug() << "<> Accounts loaded."; + qInfo() << "<> Accounts loaded."; } // init the http meta cache @@ -905,7 +984,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_metacache->addBase("meta", QDir("meta").absolutePath()); m_metacache->addBase("java", QDir("cache/java").absolutePath()); m_metacache->Load(); - qDebug() << "<> Cache initialized."; + qInfo() << "<> Cache initialized."; } // now we have network, download translation updates diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 5d757a130..f60d7960e 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -99,7 +99,7 @@ set(CORE_SOURCES MTPixmapCache.h ) if (UNIX AND NOT CYGWIN AND NOT APPLE) -set(CORE_SOURCES + set(CORE_SOURCES ${CORE_SOURCES} # MangoHud @@ -589,8 +589,8 @@ set(ATLAUNCHER_SOURCES ) set(LINKEXE_SOURCES - console/WindowsConsole.cpp console/WindowsConsole.h + console/WindowsConsole.cpp filelink/FileLink.h filelink/FileLink.cpp @@ -659,6 +659,14 @@ set(PRISMUPDATER_SOURCES ) +if(WIN32) + set(PRISMUPDATER_SOURCES + console/WindowsConsole.h + console/WindowsConsole.cpp + ${PRISMUPDATER_SOURCES} + ) +endif() + ######## Logging categories ######## ecm_qt_declare_logging_category(CORE_SOURCES @@ -786,6 +794,9 @@ SET(LAUNCHER_SOURCES SysInfo.h SysInfo.cpp + # console utils + console/Console.h + # GUI - general utilities DesktopServices.h DesktopServices.cpp @@ -926,7 +937,7 @@ SET(LAUNCHER_SOURCES ui/pages/instance/McResolver.h ui/pages/instance/ServerPingTask.cpp ui/pages/instance/ServerPingTask.h - + # GUI - global settings pages ui/pages/global/AccountListPage.cpp ui/pages/global/AccountListPage.h @@ -1154,7 +1165,7 @@ SET(LAUNCHER_SOURCES ) if (NOT Apple) -set(LAUNCHER_SOURCES + set(LAUNCHER_SOURCES ${LAUNCHER_SOURCES} ui/dialogs/UpdateAvailableDialog.h @@ -1164,8 +1175,8 @@ endif() if(WIN32) set(LAUNCHER_SOURCES - console/WindowsConsole.cpp console/WindowsConsole.h + console/WindowsConsole.cpp ${LAUNCHER_SOURCES} ) endif() @@ -1324,11 +1335,11 @@ if(APPLE) set(CMAKE_INSTALL_RPATH "@loader_path/../Frameworks/") if(Launcher_ENABLE_UPDATER) - file(DOWNLOAD ${MACOSX_SPARKLE_DOWNLOAD_URL} ${CMAKE_BINARY_DIR}/Sparkle.tar.xz EXPECTED_HASH SHA256=${MACOSX_SPARKLE_SHA256}) - file(ARCHIVE_EXTRACT INPUT ${CMAKE_BINARY_DIR}/Sparkle.tar.xz DESTINATION ${CMAKE_BINARY_DIR}/frameworks/Sparkle) + file(DOWNLOAD ${MACOSX_SPARKLE_DOWNLOAD_URL} ${CMAKE_BINARY_DIR}/Sparkle.tar.xz EXPECTED_HASH SHA256=${MACOSX_SPARKLE_SHA256}) + file(ARCHIVE_EXTRACT INPUT ${CMAKE_BINARY_DIR}/Sparkle.tar.xz DESTINATION ${CMAKE_BINARY_DIR}/frameworks/Sparkle) - find_library(SPARKLE_FRAMEWORK Sparkle "${CMAKE_BINARY_DIR}/frameworks/Sparkle") - add_compile_definitions(SPARKLE_ENABLED) + find_library(SPARKLE_FRAMEWORK Sparkle "${CMAKE_BINARY_DIR}/frameworks/Sparkle") + add_compile_definitions(SPARKLE_ENABLED) endif() target_link_libraries(Launcher_logic @@ -1338,7 +1349,7 @@ if(APPLE) "-framework ApplicationServices" ) if(Launcher_ENABLE_UPDATER) - target_link_libraries(Launcher_logic ${SPARKLE_FRAMEWORK}) + target_link_libraries(Launcher_logic ${SPARKLE_FRAMEWORK}) endif() endif() diff --git a/launcher/InstanceList.cpp b/launcher/InstanceList.cpp index 918fa1073..3df44f408 100644 --- a/launcher/InstanceList.cpp +++ b/launcher/InstanceList.cpp @@ -428,7 +428,7 @@ static QMap getIdMapping(const QList& QList InstanceList::discoverInstances() { - qDebug() << "Discovering instances in" << m_instDir; + qInfo() << "Discovering instances in" << m_instDir; QList out; QDirIterator iter(m_instDir, QDir::Dirs | QDir::NoDot | QDir::NoDotDot | QDir::Readable | QDir::Hidden, QDirIterator::FollowSymlinks); while (iter.hasNext()) { @@ -447,7 +447,7 @@ QList InstanceList::discoverInstances() } auto id = dirInfo.fileName(); out.append(id); - qDebug() << "Found instance ID" << id; + qInfo() << "Found instance ID" << id; } #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) instanceSet = QSet(out.begin(), out.end()); @@ -468,7 +468,7 @@ InstanceList::InstListError InstanceList::loadList() if (existingIds.contains(id)) { auto instPair = existingIds[id]; existingIds.remove(id); - qDebug() << "Should keep and soft-reload" << id; + qInfo() << "Should keep and soft-reload" << id; } else { InstancePtr instPtr = loadInstance(id); if (instPtr) { diff --git a/launcher/console/Console.h b/launcher/console/Console.h new file mode 100644 index 000000000..7aaf83dcc --- /dev/null +++ b/launcher/console/Console.h @@ -0,0 +1,33 @@ +#pragma once + +#include + +#include +#if defined Q_OS_WIN32 +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#else +#include +#include +#endif + +namespace console { + +inline bool isConsole() +{ +#if defined Q_OS_WIN32 + DWORD procIDs[2]; + DWORD maxCount = 2; + DWORD result = GetConsoleProcessList((LPDWORD)procIDs, maxCount); + return result > 1; +#else + if (isatty(fileno(stdout))) { + return true; + } + return false; +#endif +} + +} // namespace console diff --git a/launcher/console/WindowsConsole.cpp b/launcher/console/WindowsConsole.cpp index f388bd3b1..4a0eb3d3d 100644 --- a/launcher/console/WindowsConsole.cpp +++ b/launcher/console/WindowsConsole.cpp @@ -16,14 +16,18 @@ * */ +#include "WindowsConsole.h" +#include + #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif -#include "WindowsConsole.h" +#include + #include #include #include -#include +#include #include void RedirectHandle(DWORD handle, FILE* stream, const char* mode) @@ -128,4 +132,28 @@ bool AttachWindowsConsole() return false; } -std::error_code +std::error_code EnableAnsiSupport() +{ + // ref: https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew + // Using `CreateFileW("CONOUT$", ...)` to retrieve the console handle works correctly even if STDOUT and/or STDERR are redirected + HANDLE console_handle = CreateFileW(L"CONOUT$", FILE_GENERIC_READ | FILE_GENERIC_WRITE, FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, 0); + if (console_handle == INVALID_HANDLE_VALUE) { + return std::error_code(GetLastError(), std::system_category()); + } + + // ref: https://docs.microsoft.com/en-us/windows/console/getconsolemode + DWORD console_mode; + if (0 == GetConsoleMode(console_handle, &console_mode)) { + return std::error_code(GetLastError(), std::system_category()); + } + + // VT processing not already enabled? + if ((console_mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) == 0) { + // https://docs.microsoft.com/en-us/windows/console/setconsolemode + if (0 == SetConsoleMode(console_handle, console_mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING)) { + return std::error_code(GetLastError(), std::system_category()); + } + } + + return {}; +} diff --git a/launcher/filelink/FileLink.cpp b/launcher/filelink/FileLink.cpp index a082b4b5b..1494fa8cc 100644 --- a/launcher/filelink/FileLink.cpp +++ b/launcher/filelink/FileLink.cpp @@ -37,7 +37,10 @@ #include #if defined Q_OS_WIN32 -#include "WindowsConsole.h" +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include "console/WindowsConsole.h" #endif #include diff --git a/launcher/updater/prismupdater/PrismUpdater.cpp b/launcher/updater/prismupdater/PrismUpdater.cpp index f9ffeb658..365647db9 100644 --- a/launcher/updater/prismupdater/PrismUpdater.cpp +++ b/launcher/updater/prismupdater/PrismUpdater.cpp @@ -46,11 +46,8 @@ #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif -#include -#include -#include #include -#include +#include "console/WindowsConsole.h" #endif #include @@ -87,112 +84,12 @@ void appDebugOutput(QtMsgType type, const QMessageLogContext& context, const QSt } } -#if defined Q_OS_WIN32 - -// taken from https://stackoverflow.com/a/25927081 -// getting a proper output to console with redirection support on windows is apparently hell -void BindCrtHandlesToStdHandles(bool bindStdIn, bool bindStdOut, bool bindStdErr) -{ - // Re-initialize the C runtime "FILE" handles with clean handles bound to "nul". We do this because it has been - // observed that the file number of our standard handle file objects can be assigned internally to a value of -2 - // when not bound to a valid target, which represents some kind of unknown internal invalid state. In this state our - // call to "_dup2" fails, as it specifically tests to ensure that the target file number isn't equal to this value - // before allowing the operation to continue. We can resolve this issue by first "re-opening" the target files to - // use the "nul" device, which will place them into a valid state, after which we can redirect them to our target - // using the "_dup2" function. - if (bindStdIn) { - FILE* dummyFile; - freopen_s(&dummyFile, "nul", "r", stdin); - } - if (bindStdOut) { - FILE* dummyFile; - freopen_s(&dummyFile, "nul", "w", stdout); - } - if (bindStdErr) { - FILE* dummyFile; - freopen_s(&dummyFile, "nul", "w", stderr); - } - - // Redirect unbuffered stdin from the current standard input handle - if (bindStdIn) { - HANDLE stdHandle = GetStdHandle(STD_INPUT_HANDLE); - if (stdHandle != INVALID_HANDLE_VALUE) { - int fileDescriptor = _open_osfhandle((intptr_t)stdHandle, _O_TEXT); - if (fileDescriptor != -1) { - FILE* file = _fdopen(fileDescriptor, "r"); - if (file != NULL) { - int dup2Result = _dup2(_fileno(file), _fileno(stdin)); - if (dup2Result == 0) { - setvbuf(stdin, NULL, _IONBF, 0); - } - } - } - } - } - - // Redirect unbuffered stdout to the current standard output handle - if (bindStdOut) { - HANDLE stdHandle = GetStdHandle(STD_OUTPUT_HANDLE); - if (stdHandle != INVALID_HANDLE_VALUE) { - int fileDescriptor = _open_osfhandle((intptr_t)stdHandle, _O_TEXT); - if (fileDescriptor != -1) { - FILE* file = _fdopen(fileDescriptor, "w"); - if (file != NULL) { - int dup2Result = _dup2(_fileno(file), _fileno(stdout)); - if (dup2Result == 0) { - setvbuf(stdout, NULL, _IONBF, 0); - } - } - } - } - } - - // Redirect unbuffered stderr to the current standard error handle - if (bindStdErr) { - HANDLE stdHandle = GetStdHandle(STD_ERROR_HANDLE); - if (stdHandle != INVALID_HANDLE_VALUE) { - int fileDescriptor = _open_osfhandle((intptr_t)stdHandle, _O_TEXT); - if (fileDescriptor != -1) { - FILE* file = _fdopen(fileDescriptor, "w"); - if (file != NULL) { - int dup2Result = _dup2(_fileno(file), _fileno(stderr)); - if (dup2Result == 0) { - setvbuf(stderr, NULL, _IONBF, 0); - } - } - } - } - } - - // Clear the error state for each of the C++ standard stream objects. We need to do this, as attempts to access the - // standard streams before they refer to a valid target will cause the iostream objects to enter an error state. In - // versions of Visual Studio after 2005, this seems to always occur during startup regardless of whether anything - // has been read from or written to the targets or not. - if (bindStdIn) { - std::wcin.clear(); - std::cin.clear(); - } - if (bindStdOut) { - std::wcout.clear(); - std::cout.clear(); - } - if (bindStdErr) { - std::wcerr.clear(); - std::cerr.clear(); - } -} -#endif - PrismUpdaterApp::PrismUpdaterApp(int& argc, char** argv) : QApplication(argc, argv) { #if defined Q_OS_WIN32 // attach the parent console if stdout not already captured - auto stdout_type = GetFileType(GetStdHandle(STD_OUTPUT_HANDLE)); - if (stdout_type == FILE_TYPE_CHAR || stdout_type == FILE_TYPE_UNKNOWN) { - if (AttachConsole(ATTACH_PARENT_PROCESS)) { - BindCrtHandlesToStdHandles(true, true, true); - consoleAttached = true; - } + if (AttachWindowsConsole()) { + consoleAttached = true; } #endif setOrganizationName(BuildConfig.LAUNCHER_NAME); From 2271a05b19e259f9e1e727c09e1c68542dcc02e2 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Fri, 18 Apr 2025 12:37:54 +0300 Subject: [PATCH 127/695] chore: add back deprecation warnings and disable all API deprecated before 6.0 Signed-off-by: Trial97 --- CMakeLists.txt | 3 +++ launcher/StringUtils.cpp | 2 +- launcher/modplatform/legacy_ftb/PackFetchTask.cpp | 2 +- launcher/modplatform/legacy_ftb/PackInstallTask.cpp | 2 +- launcher/translations/TranslationsModel.cpp | 2 +- launcher/ui/MainWindow.cpp | 2 +- launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.cpp | 4 ++-- launcher/ui/instanceview/InstanceView.cpp | 8 ++++---- libraries/LocalPeer/src/LocalPeer.cpp | 2 +- 9 files changed, 15 insertions(+), 12 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 321232378..22e3978d1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -88,6 +88,9 @@ else() endif() endif() +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_WARN_DEPRECATED_UP_TO=0x060200") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_DISABLE_DEPRECATED_UP_TO=0x060000") + # Fix aarch64 build for toml++ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DTOML_ENABLE_FLOAT16=0") diff --git a/launcher/StringUtils.cpp b/launcher/StringUtils.cpp index edda9f247..2ea67762e 100644 --- a/launcher/StringUtils.cpp +++ b/launcher/StringUtils.cpp @@ -53,7 +53,7 @@ static inline QChar getNextChar(const QString& s, int location) int StringUtils::naturalCompare(const QString& s1, const QString& s2, Qt::CaseSensitivity cs) { int l1 = 0, l2 = 0; - while (l1 <= s1.count() && l2 <= s2.count()) { + while (l1 <= s1.size() && l2 <= s2.size()) { // skip spaces, tabs and 0's QChar c1 = getNextChar(s1, l1); while (c1.isSpace()) diff --git a/launcher/modplatform/legacy_ftb/PackFetchTask.cpp b/launcher/modplatform/legacy_ftb/PackFetchTask.cpp index a0beeddcc..aea9cefad 100644 --- a/launcher/modplatform/legacy_ftb/PackFetchTask.cpp +++ b/launcher/modplatform/legacy_ftb/PackFetchTask.cpp @@ -79,7 +79,7 @@ void PackFetchTask::fetchPrivate(const QStringList& toFetch) QObject::connect(job, &NetJob::succeeded, this, [this, job, data, packCode] { ModpackList packs; parseAndAddPacks(*data, PackType::Private, packs); - foreach (Modpack currentPack, packs) { + for (auto& currentPack : packs) { currentPack.packCode = packCode; emit privateFileDownloadFinished(currentPack); } diff --git a/launcher/modplatform/legacy_ftb/PackInstallTask.cpp b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp index c8d04828c..2b9bd127a 100644 --- a/launcher/modplatform/legacy_ftb/PackInstallTask.cpp +++ b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp @@ -160,7 +160,7 @@ void PackInstallTask::install() // we only care about the libs QJsonArray libs = doc.object().value("libraries").toArray(); - foreach (const QJsonValue& value, libs) { + for (const auto& value : libs) { QString nameValue = value.toObject().value("name").toString(); if (!nameValue.startsWith("net.minecraftforge")) { continue; diff --git a/launcher/translations/TranslationsModel.cpp b/launcher/translations/TranslationsModel.cpp index 429ead47d..e863dfef4 100644 --- a/launcher/translations/TranslationsModel.cpp +++ b/launcher/translations/TranslationsModel.cpp @@ -480,7 +480,7 @@ bool TranslationsModel::selectLanguage(QString key) bool successful = false; // FIXME: this is likely never present. FIX IT. d->m_qt_translator.reset(new QTranslator()); - if (d->m_qt_translator->load("qt_" + langCode, QLibraryInfo::location(QLibraryInfo::TranslationsPath))) { + if (d->m_qt_translator->load("qt_" + langCode, QLibraryInfo::path(QLibraryInfo::TranslationsPath))) { qDebug() << "Loading Qt Language File for" << langCode.toLocal8Bit().constData() << "..."; if (!QCoreApplication::installTranslator(d->m_qt_translator.get())) { qCritical() << "Loading Qt Language File failed."; diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 53e87b57e..2d8e4ebf5 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -724,7 +724,7 @@ void MainWindow::changeActiveAccount() QAction* sAction = (QAction*)sender(); // Profile's associated Mojang username - if (sAction->data().type() != QVariant::Type::Int) + if (sAction->data().typeId() != QMetaType::Int) return; QVariant action_data = sAction->data(); diff --git a/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.cpp b/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.cpp index 97fe44175..e1e539050 100644 --- a/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.cpp +++ b/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.cpp @@ -76,8 +76,8 @@ void SkinOpenGLWindow::mousePressEvent(QMouseEvent* e) void SkinOpenGLWindow::mouseMoveEvent(QMouseEvent* event) { if (m_isMousePressed) { - int dx = event->x() - m_mousePosition.x(); - int dy = event->y() - m_mousePosition.y(); + int dx = event->position().x() - m_mousePosition.x(); + int dy = event->position().y() - m_mousePosition.y(); m_yaw += dx * 0.5f; m_pitch += dy * 0.5f; diff --git a/launcher/ui/instanceview/InstanceView.cpp b/launcher/ui/instanceview/InstanceView.cpp index fa1af4266..f52c994d3 100644 --- a/launcher/ui/instanceview/InstanceView.cpp +++ b/launcher/ui/instanceview/InstanceView.cpp @@ -418,7 +418,7 @@ void InstanceView::mouseDoubleClickEvent(QMouseEvent* event) QModelIndex index = indexAt(event->pos()); if (!index.isValid() || !(index.flags() & Qt::ItemIsEnabled) || (m_pressedIndex != index)) { - QMouseEvent me(QEvent::MouseButtonPress, event->localPos(), event->windowPos(), event->screenPos(), event->button(), + QMouseEvent me(QEvent::MouseButtonPress, event->position(), event->scenePosition(), event->globalPosition(), event->button(), event->buttons(), event->modifiers()); mousePressEvent(&me); return; @@ -598,7 +598,7 @@ void InstanceView::dragEnterEvent(QDragEnterEvent* event) if (!isDragEventAccepted(event)) { return; } - m_lastDragPosition = event->pos() + offset(); + m_lastDragPosition = event->position().toPoint() + offset(); viewport()->update(); event->accept(); } @@ -610,7 +610,7 @@ void InstanceView::dragMoveEvent(QDragMoveEvent* event) if (!isDragEventAccepted(event)) { return; } - m_lastDragPosition = event->pos() + offset(); + m_lastDragPosition = event->position().toPoint() + offset(); viewport()->update(); event->accept(); } @@ -636,7 +636,7 @@ void InstanceView::dropEvent(QDropEvent* event) if (event->source() == this) { if (event->possibleActions() & Qt::MoveAction) { - std::pair dropPos = rowDropPos(event->pos()); + std::pair dropPos = rowDropPos(event->position().toPoint()); const VisualGroup* group = dropPos.first; auto hitResult = dropPos.second; diff --git a/libraries/LocalPeer/src/LocalPeer.cpp b/libraries/LocalPeer/src/LocalPeer.cpp index c1875bf98..3761c109e 100644 --- a/libraries/LocalPeer/src/LocalPeer.cpp +++ b/libraries/LocalPeer/src/LocalPeer.cpp @@ -75,7 +75,7 @@ ApplicationId ApplicationId::fromTraditionalApp() prefix.remove(QRegularExpression("[^a-zA-Z]")); prefix.truncate(6); QByteArray idc = protoId.toUtf8(); - quint16 idNum = qChecksum(idc.constData(), idc.size()); + quint16 idNum = qChecksum(idc); auto socketName = QLatin1String("pl") + prefix + QLatin1Char('-') + QString::number(idNum, 16).left(12); #if defined(Q_OS_WIN) if (!pProcessIdToSessionId) { From cddf00c61bae89885ac802f048fdcf36aa77baf5 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Fri, 18 Apr 2025 14:10:00 +0300 Subject: [PATCH 128/695] fix: beginResetModel called before endResetModel Signed-off-by: Trial97 --- launcher/VersionProxyModel.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/launcher/VersionProxyModel.cpp b/launcher/VersionProxyModel.cpp index 3d9d95eb6..165dd4cb7 100644 --- a/launcher/VersionProxyModel.cpp +++ b/launcher/VersionProxyModel.cpp @@ -295,13 +295,11 @@ void VersionProxyModel::sourceDataChanged(const QModelIndex& source_top_left, co void VersionProxyModel::setSourceModel(QAbstractItemModel* replacingRaw) { auto replacing = dynamic_cast(replacingRaw); - beginResetModel(); m_columns.clear(); if (!replacing) { roles.clear(); filterModel->setSourceModel(replacing); - endResetModel(); return; } @@ -343,8 +341,6 @@ void VersionProxyModel::setSourceModel(QAbstractItemModel* replacingRaw) hasLatest = true; } filterModel->setSourceModel(replacing); - - endResetModel(); } QModelIndex VersionProxyModel::getRecommended() const From cb591ba52ece20b79db70ee7e6d5919aab5c9a41 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Tue, 31 Dec 2024 17:25:44 +0200 Subject: [PATCH 129/695] chore:add qr code support for login Signed-off-by: Trial97 --- .gitmodules | 3 ++ CMakeLists.txt | 1 + COPYING.md | 9 ++++++ flake.lock | 19 ++++++++++- flake.nix | 7 ++++ launcher/CMakeLists.txt | 1 + launcher/resources/documents/documents.qrc | 1 - launcher/resources/documents/login-qr.svg | 8 ----- launcher/ui/dialogs/MSALoginDialog.cpp | 26 +++++++++------ libraries/README.md | 8 +++++ libraries/qt-qrcodegenerator/CMakeLists.txt | 32 +++++++++++++++++++ .../qt-qrcodegenerator/QR-Code-generator | 1 + libraries/qt-qrcodegenerator/qr.cpp | 29 +++++++++++++++++ libraries/qt-qrcodegenerator/qr.h | 8 +++++ nix/unwrapped.nix | 6 ++-- 15 files changed, 138 insertions(+), 21 deletions(-) delete mode 100644 launcher/resources/documents/login-qr.svg create mode 100644 libraries/qt-qrcodegenerator/CMakeLists.txt create mode 160000 libraries/qt-qrcodegenerator/QR-Code-generator create mode 100644 libraries/qt-qrcodegenerator/qr.cpp create mode 100644 libraries/qt-qrcodegenerator/qr.h diff --git a/.gitmodules b/.gitmodules index 7ad40becb..0c56d8768 100644 --- a/.gitmodules +++ b/.gitmodules @@ -19,3 +19,6 @@ [submodule "flatpak/shared-modules"] path = flatpak/shared-modules url = https://github.com/flathub/shared-modules.git +[submodule "libraries/qt-qrcodegenerator/QR-Code-generator"] + path = libraries/qt-qrcodegenerator/QR-Code-generator + url = https://github.com/nayuki/QR-Code-generator diff --git a/CMakeLists.txt b/CMakeLists.txt index 10153c3ec..91e10daf7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -503,6 +503,7 @@ add_subdirectory(libraries/libnbtplusplus) add_subdirectory(libraries/systeminfo) # system information library add_subdirectory(libraries/launcher) # java based launcher part for Minecraft add_subdirectory(libraries/javacheck) # java compatibility checker +add_subdirectory(libraries/qt-qrcodegenerator) # qr code generator if(FORCE_BUNDLED_ZLIB) message(STATUS "Using bundled zlib") diff --git a/COPYING.md b/COPYING.md index f1f0b3a70..f742c0132 100644 --- a/COPYING.md +++ b/COPYING.md @@ -403,3 +403,12 @@ You should have received a copy of the GNU Lesser General Public License along with this library. If not, see . + +## qt-qrcodegenerator (`libraries/qt-qrcodegenerator`) + + Copyright © 2024 Project Nayuki. (MIT License) + https://www.nayuki.io/page/qr-code-generator-library + + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + - The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose and noninfringement. In no event shall the authors or copyright holders be liable for any claim, damages or other liability, whether in an action of contract, tort or otherwise, arising from, out of or in connection with the Software or the use or other dealings in the Software. diff --git a/flake.lock b/flake.lock index 2d79b8335..070d069e5 100644 --- a/flake.lock +++ b/flake.lock @@ -32,10 +32,27 @@ "type": "github" } }, + "qt-qrcodegenerator": { + "flake": false, + "locked": { + "lastModified": 1731907326, + "narHash": "sha256-5+iYwsbX8wjKZPCy7ENj5HCYgOqzeSNLs/YrX2Vc7CQ=", + "owner": "nayuki", + "repo": "QR-Code-generator", + "rev": "f40366c40d8d1956081f7ec643d240c02a81df52", + "type": "github" + }, + "original": { + "owner": "nayuki", + "repo": "QR-Code-generator", + "type": "github" + } + }, "root": { "inputs": { "libnbtplusplus": "libnbtplusplus", - "nixpkgs": "nixpkgs" + "nixpkgs": "nixpkgs", + "qt-qrcodegenerator": "qt-qrcodegenerator" } } }, diff --git a/flake.nix b/flake.nix index fd3003bc4..2c7c69b3d 100644 --- a/flake.nix +++ b/flake.nix @@ -15,6 +15,11 @@ url = "github:PrismLauncher/libnbtplusplus"; flake = false; }; + + qt-qrcodegenerator = { + url = "github:nayuki/QR-Code-generator"; + flake = false; + }; }; outputs = @@ -22,6 +27,7 @@ self, nixpkgs, libnbtplusplus, + qt-qrcodegenerator, }: let @@ -167,6 +173,7 @@ prismlauncher-unwrapped = prev.callPackage ./nix/unwrapped.nix { inherit libnbtplusplus + qt-qrcodegenerator self ; }; diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 30d657f9e..e10921d3e 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -1289,6 +1289,7 @@ target_link_libraries(Launcher_logic qdcss BuildConfig Qt${QT_VERSION_MAJOR}::Widgets + qrcode ) if (UNIX AND NOT CYGWIN AND NOT APPLE) diff --git a/launcher/resources/documents/documents.qrc b/launcher/resources/documents/documents.qrc index 489d1d5a2..007efcde3 100644 --- a/launcher/resources/documents/documents.qrc +++ b/launcher/resources/documents/documents.qrc @@ -2,7 +2,6 @@ ../../../COPYING.md - login-qr.svg diff --git a/launcher/resources/documents/login-qr.svg b/launcher/resources/documents/login-qr.svg deleted file mode 100644 index 1b88e3c83..000000000 --- a/launcher/resources/documents/login-qr.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/launcher/ui/dialogs/MSALoginDialog.cpp b/launcher/ui/dialogs/MSALoginDialog.cpp index 40d1eff1e..83f46294d 100644 --- a/launcher/ui/dialogs/MSALoginDialog.cpp +++ b/launcher/ui/dialogs/MSALoginDialog.cpp @@ -36,6 +36,7 @@ #include "MSALoginDialog.h" #include "Application.h" +#include "qr.h" #include "ui_MSALoginDialog.h" #include "DesktopServices.h" @@ -60,7 +61,6 @@ MSALoginDialog::MSALoginDialog(QWidget* parent) : QDialog(parent), ui(new Ui::MS ui->code->setFont(font); connect(ui->copyCode, &QPushButton::clicked, this, [this] { QApplication::clipboard()->setText(ui->code->text()); }); - ui->qr->setPixmap(QIcon((":/documents/login-qr.svg")).pixmap(QSize(150, 150))); connect(ui->loginButton, &QPushButton::clicked, this, [this] { if (m_url.isValid()) { if (!DesktopServices::openUrl(m_url)) { @@ -139,19 +139,27 @@ void MSALoginDialog::authorizeWithBrowser(const QUrl& url) m_url = url; } -void MSALoginDialog::authorizeWithBrowserWithExtra(QString url, QString code, int expiresIn) +void MSALoginDialog::authorizeWithBrowserWithExtra(QString url, QString code, [[maybe_unused]] int expiresIn) { ui->stackedWidget->setCurrentIndex(1); const auto linkString = QString("%2").arg(url, url); - ui->code->setText(code); - auto isDefaultUrl = url == "https://www.microsoft.com/link"; - ui->qr->setVisible(isDefaultUrl); - if (isDefaultUrl) { - ui->qrMessage->setText(tr("Open %1 or scan the QR and enter the above code.").arg(linkString)); - } else { - ui->qrMessage->setText(tr("Open %1 and enter the above code.").arg(linkString)); + if (url == "https://www.microsoft.com/link" && !code.isEmpty()) { + url += QString("?otc=%1").arg(code); } + ui->code->setText(code); + + auto size = QSize(150, 150); + QPixmap pixmap(size); + pixmap.fill(Qt::white); + + QPainter painter(&pixmap); + paintQR(painter, size, url, Qt::black); + + // Set the generated pixmap to the label + ui->qr->setPixmap(pixmap); + + ui->qrMessage->setText(tr("Open %1 or scan the QR and enter the above code if needed.").arg(linkString)); } void MSALoginDialog::onDeviceFlowStatus(QString status) diff --git a/libraries/README.md b/libraries/README.md index 3c5f5a4ab..5f7b685e5 100644 --- a/libraries/README.md +++ b/libraries/README.md @@ -99,6 +99,14 @@ Canonical implementation of the murmur2 hash, taken from [SMHasher](https://gith Public domain (the author disclaimed the copyright). +## qt-qrcodegenerator + +A simple library for generating QR codes + +See [github repo](https://github.com/nayuki/QR-Code-generator). + +MIT + ## quazip A zip manipulation library. diff --git a/libraries/qt-qrcodegenerator/CMakeLists.txt b/libraries/qt-qrcodegenerator/CMakeLists.txt new file mode 100644 index 000000000..e18da0e71 --- /dev/null +++ b/libraries/qt-qrcodegenerator/CMakeLists.txt @@ -0,0 +1,32 @@ +cmake_minimum_required(VERSION 3.6) + +project(qrcode) + +set(CMAKE_AUTOMOC ON) +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +set(CMAKE_CXX_STANDARD_REQUIRED true) +set(CMAKE_C_STANDARD_REQUIRED true) +set(CMAKE_CXX_STANDARD 11) +set(CMAKE_C_STANDARD 11) + + +if(QT_VERSION_MAJOR EQUAL 5) + find_package(Qt5 COMPONENTS Core Gui REQUIRED) +elseif(Launcher_QT_VERSION_MAJOR EQUAL 6) + find_package(Qt6 COMPONENTS Core Gui Core5Compat REQUIRED) + list(APPEND systeminfo_LIBS Qt${QT_VERSION_MAJOR}::Core5Compat) +endif() + +add_library(qrcode STATIC qr.h qr.cpp QR-Code-generator/cpp/qrcodegen.cpp QR-Code-generator/cpp/qrcodegen.hpp ) + +target_link_libraries(qrcode Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Gui ${systeminfo_LIBS}) + + +# needed for statically linked qrcode in shared libs on x86_64 +set_target_properties(qrcode + PROPERTIES POSITION_INDEPENDENT_CODE TRUE +) + +target_include_directories(qrcode PUBLIC ./ PRIVATE QR-Code-generator/cpp/) + diff --git a/libraries/qt-qrcodegenerator/QR-Code-generator b/libraries/qt-qrcodegenerator/QR-Code-generator new file mode 160000 index 000000000..f40366c40 --- /dev/null +++ b/libraries/qt-qrcodegenerator/QR-Code-generator @@ -0,0 +1 @@ +Subproject commit f40366c40d8d1956081f7ec643d240c02a81df52 diff --git a/libraries/qt-qrcodegenerator/qr.cpp b/libraries/qt-qrcodegenerator/qr.cpp new file mode 100644 index 000000000..69bfb6da5 --- /dev/null +++ b/libraries/qt-qrcodegenerator/qr.cpp @@ -0,0 +1,29 @@ + +#include "qr.h" +#include "qrcodegen.hpp" + +void paintQR(QPainter& painter, const QSize sz, const QString& data, QColor fg) +{ + // NOTE: At this point you will use the API to get the encoding and format you want, instead of my hardcoded stuff: + qrcodegen::QrCode qr = qrcodegen::QrCode::encodeText(data.toUtf8().constData(), qrcodegen::QrCode::Ecc::LOW); + const int s = qr.getSize() > 0 ? qr.getSize() : 1; + const double w = sz.width(); + const double h = sz.height(); + const double aspect = w / h; + const double size = ((aspect > 1.0) ? h : w); + const double scale = size / (s + 2); + // NOTE: For performance reasons my implementation only draws the foreground parts in supplied color. + // It expects background to be prepared already (in white or whatever is preferred). + painter.setPen(Qt::NoPen); + painter.setBrush(fg); + for (int y = 0; y < s; y++) { + for (int x = 0; x < s; x++) { + const int color = qr.getModule(x, y); // 0 for white, 1 for black + if (0 != color) { + const double rx1 = (x + 1) * scale, ry1 = (y + 1) * scale; + QRectF r(rx1, ry1, scale, scale); + painter.drawRects(&r, 1); + } + } + } +} \ No newline at end of file diff --git a/libraries/qt-qrcodegenerator/qr.h b/libraries/qt-qrcodegenerator/qr.h new file mode 100644 index 000000000..290d49001 --- /dev/null +++ b/libraries/qt-qrcodegenerator/qr.h @@ -0,0 +1,8 @@ +#pragma once + +#include +#include +#include + +// https://stackoverflow.com/questions/21400254/how-to-draw-a-qr-code-with-qt-in-native-c-c +void paintQR(QPainter& painter, const QSize sz, const QString& data, QColor fg); diff --git a/nix/unwrapped.nix b/nix/unwrapped.nix index 93cda8e1a..060518242 100644 --- a/nix/unwrapped.nix +++ b/nix/unwrapped.nix @@ -9,16 +9,15 @@ jdk17, kdePackages, libnbtplusplus, + qt-qrcodegenerator, ninja, self, stripJavaArchivesHook, tomlplusplus, zlib, - msaClientID ? null, gamemodeSupport ? stdenv.hostPlatform.isLinux, }: - assert lib.assertMsg ( gamemodeSupport -> stdenv.hostPlatform.isLinux ) "gamemodeSupport is only available on Linux."; @@ -64,6 +63,9 @@ stdenv.mkDerivation { postUnpack = '' rm -rf source/libraries/libnbtplusplus ln -s ${libnbtplusplus} source/libraries/libnbtplusplus + + rm -rf source/libraries/qt-qrcodegenerator/QR-Code-generator + ln -s ${qt-qrcodegenerator} source/libraries/qt-qrcodegenerator/QR-Code-generator ''; nativeBuildInputs = [ From 77f53c02a9fdb79bbccfc7159607b49ad7abf0dd Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Fri, 18 Apr 2025 16:58:34 +0100 Subject: [PATCH 130/695] Fix compilation Signed-off-by: TheKodeToad --- launcher/ui/pagedialog/PageDialog.cpp | 2 +- launcher/ui/pagedialog/PageDialog.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/launcher/ui/pagedialog/PageDialog.cpp b/launcher/ui/pagedialog/PageDialog.cpp index 79bcb5701..8ce53448a 100644 --- a/launcher/ui/pagedialog/PageDialog.cpp +++ b/launcher/ui/pagedialog/PageDialog.cpp @@ -68,7 +68,7 @@ void PageDialog::closeEvent(QCloseEvent* event) QDialog::closeEvent(event); } -bool PageDialog::handleClose() const +bool PageDialog::handleClose() { qDebug() << "Paged dialog close requested"; if (!m_container->prepareToClose()) diff --git a/launcher/ui/pagedialog/PageDialog.h b/launcher/ui/pagedialog/PageDialog.h index f3b914923..9a8a3ccaa 100644 --- a/launcher/ui/pagedialog/PageDialog.h +++ b/launcher/ui/pagedialog/PageDialog.h @@ -31,7 +31,7 @@ class PageDialog : public QDialog { private: void accept() override; void closeEvent(QCloseEvent* event) override; - bool handleClose() const; + bool handleClose(); private: PageContainer* m_container; From 5af06dec8522ea12719600deaa014137509ee8e5 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Thu, 10 Apr 2025 22:49:29 +0300 Subject: [PATCH 131/695] chore: add getOrRegisterSetting function Signed-off-by: Trial97 --- launcher/minecraft/mod/ResourceFolderModel.cpp | 6 ++---- launcher/settings/SettingsObject.cpp | 5 +++++ launcher/settings/SettingsObject.h | 10 ++++++++++ launcher/ui/MainWindow.cpp | 5 +---- launcher/ui/pages/instance/ExternalResourcesPage.cpp | 5 +---- launcher/ui/pages/instance/ScreenshotsPage.cpp | 5 +---- launcher/ui/pages/instance/ServersPage.cpp | 5 +---- launcher/ui/pages/instance/VersionPage.cpp | 5 +---- launcher/ui/pages/instance/WorldListPage.cpp | 5 +---- 9 files changed, 23 insertions(+), 28 deletions(-) diff --git a/launcher/minecraft/mod/ResourceFolderModel.cpp b/launcher/minecraft/mod/ResourceFolderModel.cpp index d4900616b..5f4748b92 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.cpp +++ b/launcher/minecraft/mod/ResourceFolderModel.cpp @@ -593,8 +593,7 @@ void ResourceFolderModel::setupHeaderAction(QAction* act, int column) void ResourceFolderModel::saveColumns(QTreeView* tree) { auto const setting_name = QString("UI/%1_Page/Columns").arg(id()); - auto setting = (m_instance->settings()->contains(setting_name)) ? m_instance->settings()->getSetting(setting_name) - : m_instance->settings()->registerSetting(setting_name); + auto setting = m_instance->settings()->getOrRegisterSetting(setting_name); setting->set(tree->header()->saveState()); } @@ -606,8 +605,7 @@ void ResourceFolderModel::loadColumns(QTreeView* tree) } auto const setting_name = QString("UI/%1_Page/Columns").arg(id()); - auto setting = (m_instance->settings()->contains(setting_name)) ? m_instance->settings()->getSetting(setting_name) - : m_instance->settings()->registerSetting(setting_name); + auto setting = m_instance->settings()->getOrRegisterSetting(setting_name); tree->header()->restoreState(setting->get().toByteArray()); } diff --git a/launcher/settings/SettingsObject.cpp b/launcher/settings/SettingsObject.cpp index 1e5dce251..0df22b42d 100644 --- a/launcher/settings/SettingsObject.cpp +++ b/launcher/settings/SettingsObject.cpp @@ -124,3 +124,8 @@ void SettingsObject::connectSignals(const Setting& setting) connect(&setting, &Setting::settingReset, this, &SettingsObject::resetSetting); connect(&setting, SIGNAL(settingReset(Setting)), this, SIGNAL(settingReset(const Setting&))); } + +std::shared_ptr SettingsObject::getOrRegisterSetting(const QString& id, QVariant defVal) +{ + return contains(id) ? getSetting(id) : registerSetting(id, defVal); +} diff --git a/launcher/settings/SettingsObject.h b/launcher/settings/SettingsObject.h index f133f2f7f..bd3f71b36 100644 --- a/launcher/settings/SettingsObject.h +++ b/launcher/settings/SettingsObject.h @@ -103,6 +103,16 @@ class SettingsObject : public QObject { */ std::shared_ptr getSetting(const QString& id) const; + /*! + * \brief Gets the setting with the given ID. + * \brief if is not registered yet it does that + * \param id The ID of the setting to get. + * \return A pointer to the setting with the given ID. + * Returns null if there is no setting with the given ID. + * \sa operator []() + */ + std::shared_ptr getOrRegisterSetting(const QString& id, QVariant defVal = QVariant()); + /*! * \brief Gets the value of the setting with the given ID. * \param id The ID of the setting to get. diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index ddf726373..0111d1253 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -176,10 +176,7 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi // restore the instance toolbar settings auto const setting_name = QString("WideBarVisibility_%1").arg(ui->instanceToolBar->objectName()); - if (!APPLICATION->settings()->contains(setting_name)) - instanceToolbarSetting = APPLICATION->settings()->registerSetting(setting_name); - else - instanceToolbarSetting = APPLICATION->settings()->getSetting(setting_name); + instanceToolbarSetting = APPLICATION->settings()->getOrRegisterSetting(setting_name); ui->instanceToolBar->setVisibilityState(instanceToolbarSetting->get().toByteArray()); diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.cpp b/launcher/ui/pages/instance/ExternalResourcesPage.cpp index 50217f982..be65e6948 100644 --- a/launcher/ui/pages/instance/ExternalResourcesPage.cpp +++ b/launcher/ui/pages/instance/ExternalResourcesPage.cpp @@ -146,10 +146,7 @@ void ExternalResourcesPage::openedImpl() m_model->startWatching(); auto const setting_name = QString("WideBarVisibility_%1").arg(id()); - if (!APPLICATION->settings()->contains(setting_name)) - m_wide_bar_setting = APPLICATION->settings()->registerSetting(setting_name); - else - m_wide_bar_setting = APPLICATION->settings()->getSetting(setting_name); + m_wide_bar_setting = APPLICATION->settings()->getOrRegisterSetting(setting_name); ui->actionsToolbar->setVisibilityState(m_wide_bar_setting->get().toByteArray()); } diff --git a/launcher/ui/pages/instance/ScreenshotsPage.cpp b/launcher/ui/pages/instance/ScreenshotsPage.cpp index b619a07b8..fa568c794 100644 --- a/launcher/ui/pages/instance/ScreenshotsPage.cpp +++ b/launcher/ui/pages/instance/ScreenshotsPage.cpp @@ -559,10 +559,7 @@ void ScreenshotsPage::openedImpl() } auto const setting_name = QString("WideBarVisibility_%1").arg(id()); - if (!APPLICATION->settings()->contains(setting_name)) - m_wide_bar_setting = APPLICATION->settings()->registerSetting(setting_name); - else - m_wide_bar_setting = APPLICATION->settings()->getSetting(setting_name); + m_wide_bar_setting = APPLICATION->settings()->getOrRegisterSetting(setting_name); ui->toolBar->setVisibilityState(m_wide_bar_setting->get().toByteArray()); } diff --git a/launcher/ui/pages/instance/ServersPage.cpp b/launcher/ui/pages/instance/ServersPage.cpp index 136fb47c7..f8cd8304f 100644 --- a/launcher/ui/pages/instance/ServersPage.cpp +++ b/launcher/ui/pages/instance/ServersPage.cpp @@ -711,10 +711,7 @@ void ServersPage::openedImpl() m_model->observe(); auto const setting_name = QString("WideBarVisibility_%1").arg(id()); - if (!APPLICATION->settings()->contains(setting_name)) - m_wide_bar_setting = APPLICATION->settings()->registerSetting(setting_name); - else - m_wide_bar_setting = APPLICATION->settings()->getSetting(setting_name); + m_wide_bar_setting = APPLICATION->settings()->getOrRegisterSetting(setting_name); ui->toolBar->setVisibilityState(m_wide_bar_setting->get().toByteArray()); diff --git a/launcher/ui/pages/instance/VersionPage.cpp b/launcher/ui/pages/instance/VersionPage.cpp index 975c44de2..a1eeb3d25 100644 --- a/launcher/ui/pages/instance/VersionPage.cpp +++ b/launcher/ui/pages/instance/VersionPage.cpp @@ -124,10 +124,7 @@ void VersionPage::retranslate() void VersionPage::openedImpl() { auto const setting_name = QString("WideBarVisibility_%1").arg(id()); - if (!APPLICATION->settings()->contains(setting_name)) - m_wide_bar_setting = APPLICATION->settings()->registerSetting(setting_name); - else - m_wide_bar_setting = APPLICATION->settings()->getSetting(setting_name); + m_wide_bar_setting = APPLICATION->settings()->getOrRegisterSetting(setting_name); ui->toolBar->setVisibilityState(m_wide_bar_setting->get().toByteArray()); } diff --git a/launcher/ui/pages/instance/WorldListPage.cpp b/launcher/ui/pages/instance/WorldListPage.cpp index dd7486a6c..9e1a0fb55 100644 --- a/launcher/ui/pages/instance/WorldListPage.cpp +++ b/launcher/ui/pages/instance/WorldListPage.cpp @@ -119,10 +119,7 @@ void WorldListPage::openedImpl() } auto const setting_name = QString("WideBarVisibility_%1").arg(id()); - if (!APPLICATION->settings()->contains(setting_name)) - m_wide_bar_setting = APPLICATION->settings()->registerSetting(setting_name); - else - m_wide_bar_setting = APPLICATION->settings()->getSetting(setting_name); + m_wide_bar_setting = APPLICATION->settings()->getOrRegisterSetting(setting_name); ui->toolBar->setVisibilityState(m_wide_bar_setting->get().toByteArray()); } From 0948d3598b9dff7b7ad0979f42f65c3302d037ad Mon Sep 17 00:00:00 2001 From: Trial97 Date: Fri, 18 Apr 2025 16:39:36 +0300 Subject: [PATCH 132/695] chore: improve log display Signed-off-by: Trial97 --- launcher/GZip.cpp | 64 +++++++++++++++++ launcher/GZip.h | 20 ++++++ launcher/launch/LogModel.cpp | 5 ++ launcher/launch/LogModel.h | 1 + launcher/ui/pages/instance/OtherLogsPage.cpp | 74 ++++++++++++++------ launcher/ui/widgets/LogView.cpp | 5 +- 6 files changed, 145 insertions(+), 24 deletions(-) diff --git a/launcher/GZip.cpp b/launcher/GZip.cpp index 1c2539e08..eaf1c9035 100644 --- a/launcher/GZip.cpp +++ b/launcher/GZip.cpp @@ -36,6 +36,8 @@ #include "GZip.h" #include #include +#include +#include bool GZip::unzip(const QByteArray& compressedBytes, QByteArray& uncompressedBytes) { @@ -136,3 +138,65 @@ bool GZip::zip(const QByteArray& uncompressedBytes, QByteArray& compressedBytes) } return true; } + +GZipStream::GZipStream(const QString& filePath) : GZipStream(new QFile(filePath)) {} + +GZipStream::GZipStream(QFile* file) : m_file(file) {} + +bool GZipStream::initStream() +{ + memset(&m_strm, 0, sizeof(m_strm)); + return (inflateInit2(&m_strm, 16 + MAX_WBITS) == Z_OK); +} + +bool GZipStream::unzipBlockByBlock(QByteArray& uncompressedBytes) +{ + uncompressedBytes.clear(); + if (!m_file->isOpen()) { + if (!m_file->open(QIODevice::ReadOnly)) { + qWarning() << "Failed to open file:" << (m_file->fileName()); + return false; + } + } + + if (!m_strm.state && !initStream()) { + return false; + } + + QByteArray compressedBlock; + unsigned int blockSize = 4096; + + compressedBlock = m_file->read(blockSize); + if (compressedBlock.isEmpty()) { + return true; // End of file reached + } + + bool done = processBlock(compressedBlock, uncompressedBytes); + if (inflateEnd(&m_strm) != Z_OK || !done) { + return false; + } + return done; +} + +bool GZipStream::processBlock(const QByteArray& compressedBlock, QByteArray& uncompressedBytes) +{ + m_strm.next_in = (Bytef*)compressedBlock.data(); + m_strm.avail_in = compressedBlock.size(); + + unsigned int uncompLength = uncompressedBytes.size(); + if (m_strm.total_out >= uncompLength) { + uncompressedBytes.resize(uncompLength * 2); + uncompLength *= 2; + } + + m_strm.next_out = reinterpret_cast(uncompressedBytes.data() + m_strm.total_out); + m_strm.avail_out = uncompLength - m_strm.total_out; + + int err = inflate(&m_strm, Z_NO_FLUSH); + if (err != Z_OK && err != Z_STREAM_END) { + qWarning() << "Decompression failed with error code" << err; + return false; + } + + return true; +} diff --git a/launcher/GZip.h b/launcher/GZip.h index 0bdb70407..dd4162839 100644 --- a/launcher/GZip.h +++ b/launcher/GZip.h @@ -1,8 +1,28 @@ #pragma once +#include #include +#include class GZip { public: static bool unzip(const QByteArray& compressedBytes, QByteArray& uncompressedBytes); static bool zip(const QByteArray& uncompressedBytes, QByteArray& compressedBytes); }; + +class GZipStream { + public: + explicit GZipStream(const QString& filePath); + explicit GZipStream(QFile* file); + + // Decompress the next block and return the decompressed data + bool unzipBlockByBlock(QByteArray& uncompressedBytes); + + private: + bool initStream(); + + bool processBlock(const QByteArray& compressedBlock, QByteArray& uncompressedBytes); + + private: + QFile* m_file; + z_stream m_strm; +}; diff --git a/launcher/launch/LogModel.cpp b/launcher/launch/LogModel.cpp index dd32d46a2..45aac6099 100644 --- a/launcher/launch/LogModel.cpp +++ b/launcher/launch/LogModel.cpp @@ -161,3 +161,8 @@ bool LogModel::colorLines() const { return m_colorLines; } + +bool LogModel::isOverFlow() +{ + return m_numLines >= m_maxLines && m_stopOnOverflow; +} diff --git a/launcher/launch/LogModel.h b/launcher/launch/LogModel.h index 6c2a8cff3..ba7b14487 100644 --- a/launcher/launch/LogModel.h +++ b/launcher/launch/LogModel.h @@ -24,6 +24,7 @@ class LogModel : public QAbstractListModel { void setMaxLines(int maxLines); void setStopOnOverflow(bool stop); void setOverflowMessage(const QString& overflowMessage); + bool isOverFlow(); void setLineWrap(bool state); bool wrapLines() const; diff --git a/launcher/ui/pages/instance/OtherLogsPage.cpp b/launcher/ui/pages/instance/OtherLogsPage.cpp index 0d94843c0..b7d10a41a 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.cpp +++ b/launcher/ui/pages/instance/OtherLogsPage.cpp @@ -151,6 +151,48 @@ void OtherLogsPage::on_selectLogBox_currentIndexChanged(const int index) } } +class ReadLineAbstract { + public: + ReadLineAbstract(QFile* file) : m_file(file) + { + if (file->fileName().endsWith(".gz")) + m_gz = new GZipStream(file); + } + ~ReadLineAbstract() { delete m_gz; } + + QString readLine() + { + if (!m_gz) + return QString::fromUtf8(m_file->readLine()); + QString line; + for (;;) { + if (!m_decodedData.isEmpty()) { + int newlineIndex = m_decodedData.indexOf('\n'); + if (newlineIndex != -1) { + line += QString::fromUtf8(m_decodedData).left(newlineIndex); + m_decodedData.remove(0, newlineIndex + 1); + return line; + } + + line += QString::fromUtf8(m_decodedData); + m_decodedData.clear(); + } + + if (!m_gz->unzipBlockByBlock(m_decodedData)) { // If error occurs during unzipping + m_decodedData.clear(); + return QObject::tr("The content of the file(%1) could not be decoded.").arg(m_file->fileName()); + } + } + } + + bool done() { return m_gz ? m_decodedData.isEmpty() : m_file->atEnd(); } + + private: + QFile* m_file; + GZipStream* m_gz = nullptr; + QByteArray m_decodedData; +}; + void OtherLogsPage::on_btnReload_clicked() { if (m_currentFile.isEmpty()) { @@ -178,35 +220,17 @@ void OtherLogsPage::on_btnReload_clicked() showTooBig(); return; } - QString content; - if (file.fileName().endsWith(".gz")) { - QByteArray temp; - if (!GZip::unzip(file.readAll(), temp)) { - setPlainText(tr("The file (%1) is not readable.").arg(file.fileName())); - return; - } - content = QString::fromUtf8(temp); - } else { - content = QString::fromUtf8(file.readAll()); - } - if (content.size() >= 50000000ll) { - showTooBig(); - return; - } - // If the file is not too big for display, but too slow for syntax highlighting, just show content as plain text - if (content.size() >= 10000000ll || content.isEmpty()) { - setPlainText(content); - return; - } + ReadLineAbstract stream(&file); // Try to determine a level for each line - if (content.back() == '\n') - content = content.remove(content.size() - 1, 1); ui->text->clear(); ui->text->setModel(nullptr); m_model->clear(); - for (auto& line : content.split('\n')) { + auto line = stream.readLine(); + while (!stream.done()) { // just read until the model is full or the file ended + if (line.back() == '\n') + line = line.remove(line.size() - 1, 1); MessageLevel::Enum level = MessageLevel::Unknown; // if the launcher part set a log level, use it @@ -221,6 +245,10 @@ void OtherLogsPage::on_btnReload_clicked() } m_model->append(level, line); + if (m_model->isOverFlow()) + break; + + line = stream.readLine(); } ui->text->setModel(m_proxy); ui->text->scrollToBottom(); diff --git a/launcher/ui/widgets/LogView.cpp b/launcher/ui/widgets/LogView.cpp index 181893af4..df25a2434 100644 --- a/launcher/ui/widgets/LogView.cpp +++ b/launcher/ui/widgets/LogView.cpp @@ -42,6 +42,7 @@ LogView::LogView(QWidget* parent) : QPlainTextEdit(parent) { setWordWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere); m_defaultFormat = new QTextCharFormat(currentCharFormat()); + setUndoRedoEnabled(false); } LogView::~LogView() @@ -129,6 +130,8 @@ void LogView::rowsInserted(const QModelIndex& parent, int first, int last) QTextDocument document; QTextCursor cursor(&document); + cursor.movePosition(QTextCursor::End); + cursor.beginEditBlock(); for (int i = first; i <= last; i++) { auto idx = m_model->index(i, 0, parent); auto text = m_model->data(idx, Qt::DisplayRole).toString(); @@ -145,10 +148,10 @@ void LogView::rowsInserted(const QModelIndex& parent, int first, int last) if (bg.isValid() && m_colorLines) { format.setBackground(bg.value()); } - cursor.movePosition(QTextCursor::End); cursor.insertText(text, format); cursor.insertBlock(); } + cursor.endEditBlock(); QTextDocumentFragment fragment(&document); QTextCursor workCursor = textCursor(); From 166b2cb286e1af10ca38df6e7b398216fc39ad6a Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Fri, 18 Apr 2025 12:26:47 +0100 Subject: [PATCH 133/695] Tweak log formatting Signed-off-by: TheKodeToad --- launcher/Application.cpp | 56 ++++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 31 deletions(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 70ffce12f..018309e63 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -125,9 +125,9 @@ #include #include -#include #include #include +#include #include "SysInfo.h" #ifdef Q_OS_LINUX @@ -176,23 +176,20 @@ static bool isANSIColorConsole; static QString defaultLogFormat = QStringLiteral( "%{time process}" " " - "%{if-debug}D%{endif}" - "%{if-info}I%{endif}" - "%{if-warning}W%{endif}" - "%{if-critical}C%{endif}" - "%{if-fatal}F%{endif}" - " " - "|" + "%{if-debug}Debug:%{endif}" + "%{if-info}Info:%{endif}" + "%{if-warning}Warning:%{endif}" + "%{if-critical}Critical:%{endif}" + "%{if-fatal}Fatal:%{endif}" " " - "%{function}:%{line}" + "%{if-category}[%{category}] %{endif}" + "%{message}" " " - "|" - " " - "%{if-category}[%{category}]: %{endif}" - "%{message}"); + "(%{function}:%{line})"); #define ansi_reset "\x1b[0m" #define ansi_bold "\x1b[1m" +#define ansi_reset_bold "\x1b[22m" #define ansi_faint "\x1b[2m" #define ansi_italic "\x1b[3m" #define ansi_red_fg "\x1b[31m" @@ -200,30 +197,26 @@ static QString defaultLogFormat = QStringLiteral( #define ansi_yellow_fg "\x1b[33m" #define ansi_blue_fg "\x1b[34m" #define ansi_purple_fg "\x1b[35m" +#define ansi_inverse "\x1b[7m" +// clang-format off static QString ansiLogFormat = QStringLiteral( - "%{time process}" - " " - "%{if-debug}" ansi_bold ansi_blue_fg "D" ansi_reset - "%{endif}" - "%{if-info}" ansi_bold ansi_green_fg "I" ansi_reset - "%{endif}" - "%{if-warning}" ansi_bold ansi_yellow_fg "W" ansi_reset - "%{endif}" - "%{if-critical}" ansi_bold ansi_red_fg "C" ansi_reset - "%{endif}" - "%{if-fatal}" ansi_bold ansi_red_fg "F" ansi_reset - "%{endif}" + ansi_faint "%{time process}" ansi_reset " " - "|" - " " ansi_faint ansi_italic "%{function}:%{line}" ansi_reset + "%{if-debug}" ansi_bold ansi_green_fg "D:" ansi_reset "%{endif}" + "%{if-info}" ansi_bold ansi_blue_fg "I:" ansi_reset "%{endif}" + "%{if-warning}" ansi_bold ansi_yellow_fg "W:" ansi_reset_bold "%{endif}" + "%{if-critical}" ansi_bold ansi_red_fg "C:" ansi_reset_bold "%{endif}" + "%{if-fatal}" ansi_bold ansi_inverse ansi_red_fg "F:" ansi_reset_bold "%{endif}" " " - "|" + "%{if-category}" ansi_bold "[%{category}]" ansi_reset_bold " %{endif}" + "%{message}" " " - "%{if-category}[" ansi_bold ansi_purple_fg "%{category}" ansi_reset - "]: %{endif}" - "%{message}"); + ansi_reset ansi_faint "(%{function}:%{line})" ansi_reset +); +// clang-format on +#undef ansi_inverse #undef ansi_purple_fg #undef ansi_blue_fg #undef ansi_yellow_fg @@ -232,6 +225,7 @@ static QString ansiLogFormat = QStringLiteral( #undef ansi_italic #undef ansi_faint #undef ansi_bold +#undef ansi_reset_bold #undef ansi_reset namespace { From f3073406908cd318751fd393c78b265c8b3a8085 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Fri, 18 Apr 2025 15:56:20 +0100 Subject: [PATCH 134/695] Fix formatting Signed-off-by: TheKodeToad --- launcher/Application.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 018309e63..cfe028279 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -125,9 +125,9 @@ #include #include +#include #include #include -#include #include "SysInfo.h" #ifdef Q_OS_LINUX From 0f847d66820c73701e336c59f7740d90e8b4cae8 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Fri, 18 Apr 2025 19:40:19 +0100 Subject: [PATCH 135/695] Fix mistakes Signed-off-by: TheKodeToad --- launcher/minecraft/MinecraftInstance.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index 7b02e93ba..6f367a1bd 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -1019,7 +1019,7 @@ MessageLevel::Enum MinecraftInstance::guessLevel(const QString& line, MessageLev // NOTE: this diverges from the real regexp. no unicode, the first section is + instead of * static const QRegularExpression JAVA_EXCEPTION( - R"(Exception in thread|...\d more$|(\s+at |Caused by: )([a-zA-Z_$][a-zA-Z\\d_$]*\.)+[a-zA-Z_$][a-zA-Z\\d_$]*)"); + R"(Exception in thread|...\d more$|(\s+at |Caused by: )([a-zA-Z_$][a-zA-Z\d_$]*\.)+[a-zA-Z_$][a-zA-Z\d_$]*)|([a-zA-Z_$][a-zA-Z\d_$]*\.)+[a-zA-Z_$][a-zA-Z\d_$]*(Exception|Error|Throwable)"); if (line.contains(JAVA_EXCEPTION)) return MessageLevel::Error; From d5db974008f6991c1d7aaec4fb47cfddbdebf04f Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Fri, 18 Apr 2025 19:52:30 +0100 Subject: [PATCH 136/695] Shallow search and lazy loading for Other Logs page Signed-off-by: TheKodeToad --- launcher/BaseInstance.h | 7 +- launcher/InstancePageProvider.h | 5 +- launcher/NullInstance.h | 4 +- launcher/minecraft/MinecraftInstance.cpp | 14 +--- launcher/minecraft/MinecraftInstance.h | 4 +- launcher/ui/pages/instance/OtherLogsPage.cpp | 85 ++++++++++++++------ launcher/ui/pages/instance/OtherLogsPage.h | 13 ++- 7 files changed, 78 insertions(+), 54 deletions(-) diff --git a/launcher/BaseInstance.h b/launcher/BaseInstance.h index 2a2b4dc4a..f93321922 100644 --- a/launcher/BaseInstance.h +++ b/launcher/BaseInstance.h @@ -198,15 +198,10 @@ class BaseInstance : public QObject, public std::enable_shared_from_thisgameRoot(), "screenshots"))); values.append(new InstanceSettingsPage(onesix)); - auto logMatcher = inst->getLogFileMatcher(); - if (logMatcher) { - values.append(new OtherLogsPage(inst, logMatcher)); - } + values.append(new OtherLogsPage(inst)); return values; } diff --git a/launcher/NullInstance.h b/launcher/NullInstance.h index 3d01c9d33..93bab6c8b 100644 --- a/launcher/NullInstance.h +++ b/launcher/NullInstance.h @@ -35,6 +35,7 @@ */ #pragma once +#include #include "BaseInstance.h" #include "launch/LaunchTask.h" @@ -57,8 +58,7 @@ class NullInstance : public BaseInstance { QProcessEnvironment createEnvironment() override { return QProcessEnvironment(); } QProcessEnvironment createLaunchEnvironment() override { return QProcessEnvironment(); } QMap getVariables() override { return QMap(); } - IPathMatcher::Ptr getLogFileMatcher() override { return nullptr; } - QString getLogFileRoot() override { return instanceRoot(); } + QStringList getLogFileSearchPaths() override { return {}; } QString typeName() const override { return "Null"; } bool canExport() const override { return false; } bool canEdit() const override { return false; } diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index 6f367a1bd..121b8035c 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -1055,19 +1055,9 @@ MessageLevel::Enum MinecraftInstance::guessLevel(const QString& line, MessageLev return level; } -IPathMatcher::Ptr MinecraftInstance::getLogFileMatcher() +QStringList MinecraftInstance::getLogFileSearchPaths() { - auto combined = std::make_shared(); - combined->add(std::make_shared(".*\\.log(\\.[0-9]*)?(\\.gz)?$")); - combined->add(std::make_shared("crash-.*\\.txt")); - combined->add(std::make_shared("IDMap dump.*\\.txt$")); - combined->add(std::make_shared("ModLoader\\.txt(\\..*)?$")); - return combined; -} - -QString MinecraftInstance::getLogFileRoot() -{ - return gameRoot(); + return { FS::PathCombine(gameRoot(), "logs"), FS::PathCombine(gameRoot(), "crash-reports"), gameRoot() }; } QString MinecraftInstance::getStatusbarDescription() diff --git a/launcher/minecraft/MinecraftInstance.h b/launcher/minecraft/MinecraftInstance.h index 5d9bb45ef..677ea5b84 100644 --- a/launcher/minecraft/MinecraftInstance.h +++ b/launcher/minecraft/MinecraftInstance.h @@ -142,9 +142,7 @@ class MinecraftInstance : public BaseInstance { /// guess log level from a line of minecraft log MessageLevel::Enum guessLevel(const QString& line, MessageLevel::Enum level) override; - IPathMatcher::Ptr getLogFileMatcher() override; - - QString getLogFileRoot() override; + QStringList getLogFileSearchPaths() override; QString getStatusbarDescription() override; diff --git a/launcher/ui/pages/instance/OtherLogsPage.cpp b/launcher/ui/pages/instance/OtherLogsPage.cpp index b7d10a41a..671e0ab7f 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.cpp +++ b/launcher/ui/pages/instance/OtherLogsPage.cpp @@ -43,16 +43,18 @@ #include #include +#include +#include +#include +#include #include -#include "RecursiveFileSystemWatcher.h" -OtherLogsPage::OtherLogsPage(InstancePtr instance, IPathMatcher::Ptr fileFilter, QWidget* parent) +OtherLogsPage::OtherLogsPage(InstancePtr instance, QWidget* parent) : QWidget(parent) , ui(new Ui::OtherLogsPage) , m_instance(instance) - , m_path(instance->getLogFileRoot()) - , m_fileFilter(fileFilter) - , m_watcher(new RecursiveFileSystemWatcher(this)) + , m_basePath(instance->gameRoot()) + , m_logSearchPaths(instance->getLogFileSearchPaths()) , m_model(new LogModel(this)) { ui->setupUi(this); @@ -78,11 +80,7 @@ OtherLogsPage::OtherLogsPage(InstancePtr instance, IPathMatcher::Ptr fileFilter, m_model->setOverflowMessage(tr("Cannot display this log since the log length surpassed %1 lines.").arg(m_model->getMaxLines())); m_proxy->setSourceModel(m_model.get()); - m_watcher->setMatcher(fileFilter); - m_watcher->setRootDir(QDir::current().absoluteFilePath(m_path)); - - connect(m_watcher, &RecursiveFileSystemWatcher::filesChanged, this, &OtherLogsPage::populateSelectLogBox); - populateSelectLogBox(); + connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &OtherLogsPage::populateSelectLogBox); auto findShortcut = new QShortcut(QKeySequence(QKeySequence::Find), this); connect(findShortcut, &QShortcut::activated, this, &OtherLogsPage::findActivated); @@ -108,22 +106,39 @@ void OtherLogsPage::retranslate() void OtherLogsPage::openedImpl() { - m_watcher->enable(); + const QStringList failedPaths = m_watcher.addPaths(m_logSearchPaths); + + for (const QString& path : m_logSearchPaths) { + if (failedPaths.contains(path)) + qDebug() << "Failed to start watching" << path; + else + qDebug() << "Started watching" << path; + } + + populateSelectLogBox(); } + void OtherLogsPage::closedImpl() { - m_watcher->disable(); + const QStringList failedPaths = m_watcher.removePaths(m_logSearchPaths); + + for (const QString& path : m_logSearchPaths) { + if (failedPaths.contains(path)) + qDebug() << "Failed to stop watching" << path; + else + qDebug() << "Stopped watching" << path; + } } void OtherLogsPage::populateSelectLogBox() { + QString prevCurrentFile = m_currentFile; + ui->selectLogBox->clear(); - ui->selectLogBox->addItems(m_watcher->files()); - if (m_currentFile.isEmpty()) { - setControlsEnabled(false); - ui->selectLogBox->setCurrentIndex(-1); - } else { - const int index = ui->selectLogBox->findText(m_currentFile); + ui->selectLogBox->addItems(getPaths()); + + if (!prevCurrentFile.isEmpty()) { + const int index = ui->selectLogBox->findText(prevCurrentFile); if (index != -1) { ui->selectLogBox->setCurrentIndex(index); setControlsEnabled(true); @@ -140,7 +155,7 @@ void OtherLogsPage::on_selectLogBox_currentIndexChanged(const int index) file = ui->selectLogBox->itemText(index); } - if (file.isEmpty() || !QFile::exists(FS::PathCombine(m_path, file))) { + if (file.isEmpty() || !QFile::exists(FS::PathCombine(m_basePath, file))) { m_currentFile = QString(); ui->text->clear(); setControlsEnabled(false); @@ -199,7 +214,7 @@ void OtherLogsPage::on_btnReload_clicked() setControlsEnabled(false); return; } - QFile file(FS::PathCombine(m_path, m_currentFile)); + QFile file(FS::PathCombine(m_basePath, m_currentFile)); if (!file.open(QFile::ReadOnly)) { setControlsEnabled(false); ui->btnReload->setEnabled(true); // allow reload @@ -284,7 +299,7 @@ void OtherLogsPage::on_btnDelete_clicked() QMessageBox::Yes, QMessageBox::No) == QMessageBox::No) { return; } - QFile file(FS::PathCombine(m_path, m_currentFile)); + QFile file(FS::PathCombine(m_basePath, m_currentFile)); if (FS::trash(file.fileName())) { return; @@ -297,7 +312,7 @@ void OtherLogsPage::on_btnDelete_clicked() void OtherLogsPage::on_btnClean_clicked() { - auto toDelete = m_watcher->files(); + auto toDelete = getPaths(); if (toDelete.isEmpty()) { return; } @@ -320,7 +335,9 @@ void OtherLogsPage::on_btnClean_clicked() } QStringList failed; for (auto item : toDelete) { - QFile file(FS::PathCombine(m_path, item)); + QString absolutePath = FS::PathCombine(m_basePath, item); + QFile file(absolutePath); + qDebug() << "Deleting log" << absolutePath; if (FS::trash(file.fileName())) { continue; } @@ -374,6 +391,28 @@ void OtherLogsPage::setControlsEnabled(const bool enabled) ui->btnClean->setEnabled(enabled); } +QStringList OtherLogsPage::getPaths() +{ + QDir baseDir(m_basePath); + + QStringList result; + + for (QString searchPath : m_logSearchPaths) { + QDirIterator iterator(searchPath, QDir::Files | QDir::Readable); + + while (iterator.hasNext()) { + const QString name = iterator.next(); + + if (!name.endsWith(".log") && !name.endsWith(".log.gz")) + continue; + + result.append(baseDir.relativeFilePath(name)); + } + } + + return result; +} + void OtherLogsPage::on_findButton_clicked() { auto modifiers = QApplication::keyboardModifiers(); diff --git a/launcher/ui/pages/instance/OtherLogsPage.h b/launcher/ui/pages/instance/OtherLogsPage.h index 9394ab9b8..fedb2506c 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.h +++ b/launcher/ui/pages/instance/OtherLogsPage.h @@ -39,6 +39,8 @@ #include #include +#include +#include #include "LogPage.h" #include "ui/pages/BasePage.h" @@ -52,7 +54,7 @@ class OtherLogsPage : public QWidget, public BasePage { Q_OBJECT public: - explicit OtherLogsPage(InstancePtr instance, IPathMatcher::Ptr fileFilter, QWidget* parent = 0); + explicit OtherLogsPage(InstancePtr instance, QWidget* parent = 0); ~OtherLogsPage(); QString id() const override { return "logs"; } @@ -85,13 +87,16 @@ class OtherLogsPage : public QWidget, public BasePage { private: void setControlsEnabled(bool enabled); + QStringList getPaths(); + private: Ui::OtherLogsPage* ui; InstancePtr m_instance; - QString m_path; + /** Path to display log paths relative to. */ + QString m_basePath; + QStringList m_logSearchPaths; QString m_currentFile; - IPathMatcher::Ptr m_fileFilter; - RecursiveFileSystemWatcher* m_watcher; + QFileSystemWatcher m_watcher; LogFormatProxyModel* m_proxy; shared_qobject_ptr m_model; From 01efd5b5d8b7f37b2e227cc8c448a78cccd84a24 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Fri, 18 Apr 2025 21:24:25 +0100 Subject: [PATCH 137/695] Fix double load (again lol) Signed-off-by: TheKodeToad --- launcher/ui/pages/instance/OtherLogsPage.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/launcher/ui/pages/instance/OtherLogsPage.cpp b/launcher/ui/pages/instance/OtherLogsPage.cpp index 671e0ab7f..ec597497d 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.cpp +++ b/launcher/ui/pages/instance/OtherLogsPage.cpp @@ -132,20 +132,25 @@ void OtherLogsPage::closedImpl() void OtherLogsPage::populateSelectLogBox() { - QString prevCurrentFile = m_currentFile; + const QString prevCurrentFile = m_currentFile; + ui->selectLogBox->blockSignals(true); ui->selectLogBox->clear(); ui->selectLogBox->addItems(getPaths()); + ui->selectLogBox->blockSignals(false); if (!prevCurrentFile.isEmpty()) { const int index = ui->selectLogBox->findText(prevCurrentFile); if (index != -1) { ui->selectLogBox->setCurrentIndex(index); setControlsEnabled(true); + return; } else { setControlsEnabled(false); } } + + on_selectLogBox_currentIndexChanged(ui->selectLogBox->currentIndex()); } void OtherLogsPage::on_selectLogBox_currentIndexChanged(const int index) From 7f2f62afa876d5fe40bdf5a4d08bf52a7d56624c Mon Sep 17 00:00:00 2001 From: Seth Flynn Date: Fri, 18 Apr 2025 16:50:25 -0400 Subject: [PATCH 138/695] ci(blocked-prs): use pull_request_target This needs to run in the context of our repo to have access to it's secrets Signed-off-by: Seth Flynn --- .github/workflows/merge-blocking-pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/merge-blocking-pr.yml b/.github/workflows/merge-blocking-pr.yml index d6410f997..b77313b09 100644 --- a/.github/workflows/merge-blocking-pr.yml +++ b/.github/workflows/merge-blocking-pr.yml @@ -1,7 +1,7 @@ name: Merged Blocking Pull Request Automation on: - pull_request: + pull_request_target: types: - closed From e9f7ba188bfb40785de59dee269dbe7b8f9e0dd0 Mon Sep 17 00:00:00 2001 From: Seth Flynn Date: Fri, 18 Apr 2025 16:53:16 -0400 Subject: [PATCH 139/695] ci(blocked-prs): use object filter to check pr label https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/evaluate-expressions-in-workflows-and-actions#example-using-an-object-filter Signed-off-by: Seth Flynn --- .github/workflows/merge-blocking-pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/merge-blocking-pr.yml b/.github/workflows/merge-blocking-pr.yml index b77313b09..fe99e1c14 100644 --- a/.github/workflows/merge-blocking-pr.yml +++ b/.github/workflows/merge-blocking-pr.yml @@ -12,7 +12,7 @@ jobs: # a pr that was a `blocking:` label was merged. # find the open pr's it was blocked by and trigger a refresh of their state - if: github.event.pull_request.merged == true && contains( join( github.event.pull_request.labels.*.name, ',' ), 'blocking' ) + if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'blocking') steps: - name: Generate token From b6e48ac641b684c9f977454c8c4c9d90c5aea408 Mon Sep 17 00:00:00 2001 From: Seth Flynn Date: Fri, 18 Apr 2025 16:56:19 -0400 Subject: [PATCH 140/695] ci(blocked-prs): allow workflow_dispatch on blocking prs Signed-off-by: Seth Flynn --- .github/workflows/merge-blocking-pr.yml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/merge-blocking-pr.yml b/.github/workflows/merge-blocking-pr.yml index fe99e1c14..d37c33761 100644 --- a/.github/workflows/merge-blocking-pr.yml +++ b/.github/workflows/merge-blocking-pr.yml @@ -4,6 +4,12 @@ on: pull_request_target: types: - closed + workflow_dispatch: + inputs: + pr_id: + description: Local Pull Request number to work on + required: true + type: number jobs: update-blocked-status: @@ -12,7 +18,7 @@ jobs: # a pr that was a `blocking:` label was merged. # find the open pr's it was blocked by and trigger a refresh of their state - if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'blocking') + if: ${{ github.event_name == 'workflow_dispatch' || github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'blocking') }} steps: - name: Generate token @@ -26,11 +32,11 @@ jobs: id: gather_deps env: GH_TOKEN: ${{ steps.generate-token.outputs.token }} - PR_NUMBER: ${{ github.event.pull_request.number }} + PR_NUMBER: ${{ inputs.pr_id || github.event.pull_request.number }} run: | blocked_prs=$( gh -R ${{ github.repository }} pr list --label 'blocked' --json 'number,body' \ - | jq -c --argjson pr "${{ github.event.pull_request.number }}" ' + | jq -c --argjson pr "$PR_NUMBER" ' reduce ( .[] | select( .body | scan("(?:blocked (?:by|on)|stacked on):? #([0-9]+)") | From 95492cdeef86ec92da141a951c29037132715191 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 18 Apr 2025 20:57:31 +0000 Subject: [PATCH 141/695] chore(deps): update cachix/install-nix-action digest to 754537a --- .github/workflows/update-flake.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/update-flake.yml b/.github/workflows/update-flake.yml index cdd360589..f4b1c4f5d 100644 --- a/.github/workflows/update-flake.yml +++ b/.github/workflows/update-flake.yml @@ -17,7 +17,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: cachix/install-nix-action@d1ca217b388ee87b2507a9a93bf01368bde7cec2 # v31 + - uses: cachix/install-nix-action@754537aaedb35f72ab11a60cc162c49ef3016495 # v31 - uses: DeterminateSystems/update-flake-lock@v24 with: From d1c71075756845efe3c1730874f20f71ca52408d Mon Sep 17 00:00:00 2001 From: Trial97 Date: Sat, 19 Apr 2025 00:41:24 +0300 Subject: [PATCH 142/695] fix: gzip file parsing as a stream Signed-off-by: Trial97 --- launcher/GZip.cpp | 120 +++++++++++-------- launcher/GZip.h | 27 +---- launcher/ui/pages/instance/OtherLogsPage.cpp | 91 ++++++-------- 3 files changed, 110 insertions(+), 128 deletions(-) diff --git a/launcher/GZip.cpp b/launcher/GZip.cpp index eaf1c9035..29c71c012 100644 --- a/launcher/GZip.cpp +++ b/launcher/GZip.cpp @@ -139,64 +139,80 @@ bool GZip::zip(const QByteArray& uncompressedBytes, QByteArray& compressedBytes) return true; } -GZipStream::GZipStream(const QString& filePath) : GZipStream(new QFile(filePath)) {} +int inf(QFile* source, std::function handleBlock) +{ + constexpr auto CHUNK = 16384; + int ret; + unsigned have; + z_stream strm; + memset(&strm, 0, sizeof(strm)); + char in[CHUNK]; + unsigned char out[CHUNK]; -GZipStream::GZipStream(QFile* file) : m_file(file) {} + ret = inflateInit2(&strm, (16 + MAX_WBITS)); + if (ret != Z_OK) + return ret; -bool GZipStream::initStream() -{ - memset(&m_strm, 0, sizeof(m_strm)); - return (inflateInit2(&m_strm, 16 + MAX_WBITS) == Z_OK); + /* decompress until deflate stream ends or end of file */ + do { + strm.avail_in = source->read(in, CHUNK); + if (source->error()) { + (void)inflateEnd(&strm); + return Z_ERRNO; + } + if (strm.avail_in == 0) + break; + strm.next_in = reinterpret_cast(in); + + /* run inflate() on input until output buffer not full */ + do { + strm.avail_out = CHUNK; + strm.next_out = out; + ret = inflate(&strm, Z_NO_FLUSH); + assert(ret != Z_STREAM_ERROR); /* state not clobbered */ + switch (ret) { + case Z_NEED_DICT: + ret = Z_DATA_ERROR; /* and fall through */ + case Z_DATA_ERROR: + case Z_MEM_ERROR: + (void)inflateEnd(&strm); + return ret; + } + have = CHUNK - strm.avail_out; + if (!handleBlock(QByteArray(reinterpret_cast(out), have))) { + (void)inflateEnd(&strm); + return Z_OK; + } + + } while (strm.avail_out == 0); + + /* done when inflate() says it's done */ + } while (ret != Z_STREAM_END); + + /* clean up and return */ + (void)inflateEnd(&strm); + return ret == Z_STREAM_END ? Z_OK : Z_DATA_ERROR; } -bool GZipStream::unzipBlockByBlock(QByteArray& uncompressedBytes) +QString zerr(int ret) { - uncompressedBytes.clear(); - if (!m_file->isOpen()) { - if (!m_file->open(QIODevice::ReadOnly)) { - qWarning() << "Failed to open file:" << (m_file->fileName()); - return false; - } - } - - if (!m_strm.state && !initStream()) { - return false; - } - - QByteArray compressedBlock; - unsigned int blockSize = 4096; - - compressedBlock = m_file->read(blockSize); - if (compressedBlock.isEmpty()) { - return true; // End of file reached + switch (ret) { + case Z_ERRNO: + return QObject::tr("error handling file"); + case Z_STREAM_ERROR: + return QObject::tr("invalid compression level"); + case Z_DATA_ERROR: + return QObject::tr("invalid or incomplete deflate data"); + case Z_MEM_ERROR: + return QObject::tr("out of memory"); + case Z_VERSION_ERROR: + return QObject::tr("zlib version mismatch!"); } - - bool done = processBlock(compressedBlock, uncompressedBytes); - if (inflateEnd(&m_strm) != Z_OK || !done) { - return false; - } - return done; + return {}; } -bool GZipStream::processBlock(const QByteArray& compressedBlock, QByteArray& uncompressedBytes) +QString GZip::readGzFileByBlocks(QFile* source, std::function handleBlock) { - m_strm.next_in = (Bytef*)compressedBlock.data(); - m_strm.avail_in = compressedBlock.size(); - - unsigned int uncompLength = uncompressedBytes.size(); - if (m_strm.total_out >= uncompLength) { - uncompressedBytes.resize(uncompLength * 2); - uncompLength *= 2; - } - - m_strm.next_out = reinterpret_cast(uncompressedBytes.data() + m_strm.total_out); - m_strm.avail_out = uncompLength - m_strm.total_out; - - int err = inflate(&m_strm, Z_NO_FLUSH); - if (err != Z_OK && err != Z_STREAM_END) { - qWarning() << "Decompression failed with error code" << err; - return false; - } - - return true; -} + auto ret = inf(source, handleBlock); + return zerr(ret); +} \ No newline at end of file diff --git a/launcher/GZip.h b/launcher/GZip.h index dd4162839..b736ca93f 100644 --- a/launcher/GZip.h +++ b/launcher/GZip.h @@ -1,28 +1,11 @@ #pragma once -#include #include #include -class GZip { - public: - static bool unzip(const QByteArray& compressedBytes, QByteArray& uncompressedBytes); - static bool zip(const QByteArray& uncompressedBytes, QByteArray& compressedBytes); -}; +namespace GZip { -class GZipStream { - public: - explicit GZipStream(const QString& filePath); - explicit GZipStream(QFile* file); +bool unzip(const QByteArray& compressedBytes, QByteArray& uncompressedBytes); +bool zip(const QByteArray& uncompressedBytes, QByteArray& compressedBytes); +QString readGzFileByBlocks(QFile* source, std::function handleBlock); - // Decompress the next block and return the decompressed data - bool unzipBlockByBlock(QByteArray& uncompressedBytes); - - private: - bool initStream(); - - bool processBlock(const QByteArray& compressedBlock, QByteArray& uncompressedBytes); - - private: - QFile* m_file; - z_stream m_strm; -}; +} // namespace GZip diff --git a/launcher/ui/pages/instance/OtherLogsPage.cpp b/launcher/ui/pages/instance/OtherLogsPage.cpp index b7d10a41a..0aeb942a8 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.cpp +++ b/launcher/ui/pages/instance/OtherLogsPage.cpp @@ -151,54 +151,13 @@ void OtherLogsPage::on_selectLogBox_currentIndexChanged(const int index) } } -class ReadLineAbstract { - public: - ReadLineAbstract(QFile* file) : m_file(file) - { - if (file->fileName().endsWith(".gz")) - m_gz = new GZipStream(file); - } - ~ReadLineAbstract() { delete m_gz; } - - QString readLine() - { - if (!m_gz) - return QString::fromUtf8(m_file->readLine()); - QString line; - for (;;) { - if (!m_decodedData.isEmpty()) { - int newlineIndex = m_decodedData.indexOf('\n'); - if (newlineIndex != -1) { - line += QString::fromUtf8(m_decodedData).left(newlineIndex); - m_decodedData.remove(0, newlineIndex + 1); - return line; - } - - line += QString::fromUtf8(m_decodedData); - m_decodedData.clear(); - } - - if (!m_gz->unzipBlockByBlock(m_decodedData)) { // If error occurs during unzipping - m_decodedData.clear(); - return QObject::tr("The content of the file(%1) could not be decoded.").arg(m_file->fileName()); - } - } - } - - bool done() { return m_gz ? m_decodedData.isEmpty() : m_file->atEnd(); } - - private: - QFile* m_file; - GZipStream* m_gz = nullptr; - QByteArray m_decodedData; -}; - void OtherLogsPage::on_btnReload_clicked() { if (m_currentFile.isEmpty()) { setControlsEnabled(false); return; } + QFile file(FS::PathCombine(m_path, m_currentFile)); if (!file.open(QFile::ReadOnly)) { setControlsEnabled(false); @@ -220,15 +179,9 @@ void OtherLogsPage::on_btnReload_clicked() showTooBig(); return; } - - ReadLineAbstract stream(&file); - - // Try to determine a level for each line - ui->text->clear(); - ui->text->setModel(nullptr); - m_model->clear(); - auto line = stream.readLine(); - while (!stream.done()) { // just read until the model is full or the file ended + auto handleLine = [this](QString line) { + if (line.isEmpty()) + return false; if (line.back() == '\n') line = line.remove(line.size() - 1, 1); MessageLevel::Enum level = MessageLevel::Unknown; @@ -245,10 +198,40 @@ void OtherLogsPage::on_btnReload_clicked() } m_model->append(level, line); - if (m_model->isOverFlow()) - break; + return m_model->isOverFlow(); + }; - line = stream.readLine(); + // Try to determine a level for each line + ui->text->clear(); + ui->text->setModel(nullptr); + m_model->clear(); + if (file.fileName().endsWith(".gz")) { + QString line; + auto error = GZip::readGzFileByBlocks(&file, [&line, handleLine](const QByteArray& d) { + auto block = d; + int newlineIndex = block.indexOf('\n'); + while (newlineIndex != -1) { + line += QString::fromUtf8(block).left(newlineIndex); + block.remove(0, newlineIndex + 1); + if (handleLine(line)) { + line.clear(); + return false; + } + line.clear(); + newlineIndex = block.indexOf('\n'); + } + line += QString::fromUtf8(block); + return true; + }); + if (!error.isEmpty()) { + setPlainText(tr("The file (%1) encountered an error when reading: %2.").arg(file.fileName(), error)); + return; + } else if (!line.isEmpty()) { + handleLine(line); + } + } else { + while (!file.atEnd() && !handleLine(QString::fromUtf8(file.readLine()))) { + } } ui->text->setModel(m_proxy); ui->text->scrollToBottom(); From e3ff9630e984ee3284639cef6d1dcb2356dcd93d Mon Sep 17 00:00:00 2001 From: Trial97 Date: Fri, 18 Apr 2025 13:15:00 +0300 Subject: [PATCH 143/695] fix: 6.2 deprecation warning regard the QScopedPointer::swap function Signed-off-by: Trial97 --- launcher/ui/pages/modplatform/ModPage.cpp | 16 +++++++++------- launcher/ui/pages/modplatform/ModPage.h | 6 +++--- .../ui/pages/modplatform/flame/FlamePage.cpp | 13 ++++++++----- launcher/ui/pages/modplatform/flame/FlamePage.h | 2 +- .../modplatform/flame/FlameResourcePages.cpp | 4 ++-- .../pages/modplatform/flame/FlameResourcePages.h | 2 +- .../pages/modplatform/modrinth/ModrinthPage.cpp | 12 +++++++----- .../ui/pages/modplatform/modrinth/ModrinthPage.h | 2 +- .../modrinth/ModrinthResourcePages.cpp | 4 ++-- .../modplatform/modrinth/ModrinthResourcePages.h | 2 +- launcher/ui/widgets/ModFilterWidget.cpp | 5 ----- launcher/ui/widgets/ModFilterWidget.h | 4 +--- 12 files changed, 36 insertions(+), 36 deletions(-) diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index f0cc2df54..bfd160235 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -62,22 +62,24 @@ ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance& instance) : ResourcePa connect(m_ui->packView, &QListView::doubleClicked, this, &ModPage::onResourceSelected); } -void ModPage::setFilterWidget(unique_qobject_ptr& widget) +void ModPage::setFilterWidget(ModFilterWidget* widget) { if (m_filter_widget) - disconnect(m_filter_widget.get(), nullptr, nullptr, nullptr); + disconnect(m_filter_widget, nullptr, nullptr, nullptr); - auto old = m_ui->splitter->replaceWidget(0, widget.get()); + auto old = m_ui->splitter->replaceWidget(0, widget); // because we replaced the widget we also need to delete it if (old) { - delete old; + old->deleteLater(); } - m_filter_widget.swap(widget); - + m_filter_widget = widget; + if (m_filter_widget) { + m_filter_widget->deleteLater(); + } m_filter = m_filter_widget->getFilter(); - connect(m_filter_widget.get(), &ModFilterWidget::filterChanged, this, &ModPage::triggerSearch); + connect(m_filter_widget, &ModFilterWidget::filterChanged, this, &ModPage::triggerSearch); prepareProviderCategories(); } diff --git a/launcher/ui/pages/modplatform/ModPage.h b/launcher/ui/pages/modplatform/ModPage.h index 5c9a82303..25d9a4e90 100644 --- a/launcher/ui/pages/modplatform/ModPage.h +++ b/launcher/ui/pages/modplatform/ModPage.h @@ -50,11 +50,11 @@ class ModPage : public ResourcePage { void addResourceToPage(ModPlatform::IndexedPack::Ptr, ModPlatform::IndexedVersion&, std::shared_ptr) override; - virtual unique_qobject_ptr createFilterWidget() = 0; + virtual ModFilterWidget* createFilterWidget() = 0; [[nodiscard]] bool supportsFiltering() const override { return true; }; auto getFilter() const -> const std::shared_ptr { return m_filter; } - void setFilterWidget(unique_qobject_ptr&); + void setFilterWidget(ModFilterWidget*); protected: ModPage(ModDownloadDialog* dialog, BaseInstance& instance); @@ -66,7 +66,7 @@ class ModPage : public ResourcePage { void triggerSearch() override; protected: - unique_qobject_ptr m_filter_widget; + ModFilterWidget* m_filter_widget = nullptr; std::shared_ptr m_filter; }; diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.cpp b/launcher/ui/pages/modplatform/flame/FlamePage.cpp index de6b3d633..bcbae0d76 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlamePage.cpp @@ -341,17 +341,20 @@ void FlamePage::setSearchTerm(QString term) void FlamePage::createFilterWidget() { - auto widget = ModFilterWidget::create(nullptr, false, this); - m_filterWidget.swap(widget); - auto old = ui->splitter->replaceWidget(0, m_filterWidget.get()); + auto widget = new ModFilterWidget(nullptr, false, this); + if (m_filterWidget) { + m_filterWidget->deleteLater(); + } + m_filterWidget = (widget); + auto old = ui->splitter->replaceWidget(0, m_filterWidget); // because we replaced the widget we also need to delete it if (old) { - delete old; + old->deleteLater(); } connect(ui->filterButton, &QPushButton::clicked, this, [this] { m_filterWidget->setHidden(!m_filterWidget->isHidden()); }); - connect(m_filterWidget.get(), &ModFilterWidget::filterChanged, this, &FlamePage::triggerSearch); + connect(m_filterWidget, &ModFilterWidget::filterChanged, this, &FlamePage::triggerSearch); auto response = std::make_shared(); m_categoriesTask = FlameAPI::getCategories(response, ModPlatform::ResourceType::MODPACK); QObject::connect(m_categoriesTask.get(), &Task::succeeded, [this, response]() { diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.h b/launcher/ui/pages/modplatform/flame/FlamePage.h index 27c96d2f1..a828a2a29 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.h +++ b/launcher/ui/pages/modplatform/flame/FlamePage.h @@ -100,6 +100,6 @@ class FlamePage : public QWidget, public ModpackProviderBasePage { // Used to do instant searching with a delay to cache quick changes QTimer m_search_timer; - unique_qobject_ptr m_filterWidget; + ModFilterWidget* m_filterWidget; Task::Ptr m_categoriesTask; }; diff --git a/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp index 4e01f3a65..bfe873ac8 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp @@ -207,9 +207,9 @@ auto FlameShaderPackPage::shouldDisplay() const -> bool return true; } -unique_qobject_ptr FlameModPage::createFilterWidget() +ModFilterWidget* FlameModPage::createFilterWidget() { - return ModFilterWidget::create(&static_cast(m_baseInstance), false, this); + return new ModFilterWidget(&static_cast(m_baseInstance), false, this); } void FlameModPage::prepareProviderCategories() diff --git a/launcher/ui/pages/modplatform/flame/FlameResourcePages.h b/launcher/ui/pages/modplatform/flame/FlameResourcePages.h index 052706549..3117851a5 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourcePages.h +++ b/launcher/ui/pages/modplatform/flame/FlameResourcePages.h @@ -96,7 +96,7 @@ class FlameModPage : public ModPage { [[nodiscard]] inline auto helpPage() const -> QString override { return "Mod-platform"; } void openUrl(const QUrl& url) override; - unique_qobject_ptr createFilterWidget() override; + ModFilterWidget* createFilterWidget() override; protected: virtual void prepareProviderCategories() override; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index 7d70abec4..784b656a1 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -391,17 +391,19 @@ QString ModrinthPage::getSerachTerm() const void ModrinthPage::createFilterWidget() { - auto widget = ModFilterWidget::create(nullptr, true, this); - m_filterWidget.swap(widget); - auto old = ui->splitter->replaceWidget(0, m_filterWidget.get()); + auto widget = new ModFilterWidget(nullptr, true, this); + if (m_filterWidget) + m_filterWidget->deleteLater(); + m_filterWidget = widget; + auto old = ui->splitter->replaceWidget(0, m_filterWidget); // because we replaced the widget we also need to delete it if (old) { - delete old; + old->deleteLater(); } connect(ui->filterButton, &QPushButton::clicked, this, [this] { m_filterWidget->setHidden(!m_filterWidget->isHidden()); }); - connect(m_filterWidget.get(), &ModFilterWidget::filterChanged, this, &ModrinthPage::triggerSearch); + connect(m_filterWidget, &ModFilterWidget::filterChanged, this, &ModrinthPage::triggerSearch); auto response = std::make_shared(); m_categoriesTask = ModrinthAPI::getModCategories(response); QObject::connect(m_categoriesTask.get(), &Task::succeeded, [this, response]() { diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h index 7f504cdbd..c90402f52 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h @@ -103,6 +103,6 @@ class ModrinthPage : public QWidget, public ModpackProviderBasePage { // Used to do instant searching with a delay to cache quick changes QTimer m_search_timer; - unique_qobject_ptr m_filterWidget; + ModFilterWidget* m_filterWidget; Task::Ptr m_categoriesTask; }; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp index 4ee620677..38a750622 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp @@ -142,9 +142,9 @@ auto ModrinthShaderPackPage::shouldDisplay() const -> bool return true; } -unique_qobject_ptr ModrinthModPage::createFilterWidget() +ModFilterWidget* ModrinthModPage::createFilterWidget() { - return ModFilterWidget::create(&static_cast(m_baseInstance), true, this); + return new ModFilterWidget(&static_cast(m_baseInstance), true, this); } void ModrinthModPage::prepareProviderCategories() diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h index eaf6129a5..e2ad60b51 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h @@ -94,7 +94,7 @@ class ModrinthModPage : public ModPage { [[nodiscard]] inline auto helpPage() const -> QString override { return "Mod-platform"; } - unique_qobject_ptr createFilterWidget() override; + ModFilterWidget* createFilterWidget() override; protected: virtual void prepareProviderCategories() override; diff --git a/launcher/ui/widgets/ModFilterWidget.cpp b/launcher/ui/widgets/ModFilterWidget.cpp index 03522bc19..8be3ce8d3 100644 --- a/launcher/ui/widgets/ModFilterWidget.cpp +++ b/launcher/ui/widgets/ModFilterWidget.cpp @@ -49,11 +49,6 @@ #include "Application.h" #include "minecraft/PackProfile.h" -unique_qobject_ptr ModFilterWidget::create(MinecraftInstance* instance, bool extended, QWidget* parent) -{ - return unique_qobject_ptr(new ModFilterWidget(instance, extended, parent)); -} - class VersionBasicModel : public QIdentityProxyModel { Q_OBJECT diff --git a/launcher/ui/widgets/ModFilterWidget.h b/launcher/ui/widgets/ModFilterWidget.h index 41a2f1bbd..c7192a0d6 100644 --- a/launcher/ui/widgets/ModFilterWidget.h +++ b/launcher/ui/widgets/ModFilterWidget.h @@ -83,7 +83,7 @@ class ModFilterWidget : public QTabWidget { } }; - static unique_qobject_ptr create(MinecraftInstance* instance, bool extended, QWidget* parent = nullptr); + ModFilterWidget(MinecraftInstance* instance, bool extendedSupport, QWidget* parent = nullptr); virtual ~ModFilterWidget(); auto getFilter() -> std::shared_ptr; @@ -96,8 +96,6 @@ class ModFilterWidget : public QTabWidget { void setCategories(const QList&); private: - ModFilterWidget(MinecraftInstance* instance, bool extendedSupport, QWidget* parent = nullptr); - void loadVersionList(); void prepareBasicFilter(); From 49f0e8ef6b8fe1f18380b5ffe1d7c565c0d20b85 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Wed, 26 Mar 2025 08:29:40 +0200 Subject: [PATCH 144/695] replace qvector with qlist Signed-off-by: Trial97 --- launcher/Json.h | 20 ++++++++--------- launcher/icons/IconList.h | 2 +- launcher/launch/LogModel.cpp | 2 +- launcher/launch/LogModel.h | 2 +- launcher/meta/Index.cpp | 4 ++-- launcher/meta/Index.h | 6 ++--- launcher/meta/JsonFormat.cpp | 8 +++---- launcher/meta/Version.h | 2 +- launcher/meta/VersionList.cpp | 4 ++-- launcher/meta/VersionList.h | 6 ++--- launcher/minecraft/auth/AccountData.h | 2 +- launcher/minecraft/auth/AuthFlow.h | 1 - launcher/minecraft/skins/SkinList.cpp | 2 +- launcher/minecraft/skins/SkinList.h | 2 +- launcher/modplatform/ModIndex.h | 3 +-- .../modplatform/atlauncher/ATLPackIndex.h | 4 ++-- .../atlauncher/ATLPackInstallTask.cpp | 4 ++-- .../atlauncher/ATLPackInstallTask.h | 2 +- .../modplatform/atlauncher/ATLPackManifest.h | 16 +++++++------- .../modplatform/atlauncher/ATLShareCode.h | 4 ++-- launcher/modplatform/flame/FlameAPI.cpp | 2 +- launcher/modplatform/flame/FlameAPI.h | 2 +- launcher/modplatform/flame/FlameModIndex.cpp | 4 ++-- launcher/modplatform/flame/FlamePackIndex.cpp | 2 +- launcher/modplatform/flame/FlamePackIndex.h | 3 +-- launcher/modplatform/flame/PackManifest.h | 4 ++-- .../modrinth/ModrinthPackIndex.cpp | 4 ++-- .../modrinth/ModrinthPackManifest.cpp | 2 +- .../modrinth/ModrinthPackManifest.h | 4 ++-- .../modplatform/technic/SolderPackManifest.h | 6 ++--- launcher/translations/TranslationsModel.cpp | 4 ++-- launcher/translations/TranslationsModel.h | 2 +- .../ui/dialogs/skins/draw/BoxGeometry.cpp | 22 +++++++++---------- launcher/ui/dialogs/skins/draw/Scene.h | 6 ++--- launcher/ui/instanceview/InstanceView.cpp | 2 +- launcher/ui/instanceview/InstanceView.h | 2 +- launcher/ui/instanceview/VisualGroup.cpp | 2 +- launcher/ui/instanceview/VisualGroup.h | 4 ++-- .../atlauncher/AtlOptionalModDialog.cpp | 8 +++---- .../atlauncher/AtlOptionalModDialog.h | 12 +++++----- .../AtlUserInteractionSupportImpl.cpp | 4 ++-- .../AtlUserInteractionSupportImpl.h | 3 +-- .../modplatform/flame/FlameResourceModels.cpp | 2 +- .../pages/modplatform/technic/TechnicData.h | 3 +-- libraries/LocalPeer/src/LockedFile.h | 4 ++-- 45 files changed, 102 insertions(+), 107 deletions(-) diff --git a/launcher/Json.h b/launcher/Json.h index 28891f398..c13be6470 100644 --- a/launcher/Json.h +++ b/launcher/Json.h @@ -188,10 +188,10 @@ T ensureIsType(const QJsonObject& parent, const QString& key, const T default_ = } template -QVector requireIsArrayOf(const QJsonDocument& doc) +QList requireIsArrayOf(const QJsonDocument& doc) { const QJsonArray array = requireArray(doc); - QVector out; + QList out; for (const QJsonValue val : array) { out.append(requireIsType(val, "Document")); } @@ -199,10 +199,10 @@ QVector requireIsArrayOf(const QJsonDocument& doc) } template -QVector ensureIsArrayOf(const QJsonValue& value, const QString& what = "Value") +QList ensureIsArrayOf(const QJsonValue& value, const QString& what = "Value") { const QJsonArray array = ensureIsType(value, QJsonArray(), what); - QVector out; + QList out; for (const QJsonValue val : array) { out.append(requireIsType(val, what)); } @@ -210,7 +210,7 @@ QVector ensureIsArrayOf(const QJsonValue& value, const QString& what = "Value } template -QVector ensureIsArrayOf(const QJsonValue& value, const QVector default_, const QString& what = "Value") +QList ensureIsArrayOf(const QJsonValue& value, const QList default_, const QString& what = "Value") { if (value.isUndefined()) { return default_; @@ -220,7 +220,7 @@ QVector ensureIsArrayOf(const QJsonValue& value, const QVector default_, c /// @throw JsonException template -QVector requireIsArrayOf(const QJsonObject& parent, const QString& key, const QString& what = "__placeholder__") +QList requireIsArrayOf(const QJsonObject& parent, const QString& key, const QString& what = "__placeholder__") { const QString localWhat = QString(what).replace("__placeholder__", '\'' + key + '\''); if (!parent.contains(key)) { @@ -230,10 +230,10 @@ QVector requireIsArrayOf(const QJsonObject& parent, const QString& key, const } template -QVector ensureIsArrayOf(const QJsonObject& parent, - const QString& key, - const QVector& default_ = QVector(), - const QString& what = "__placeholder__") +QList ensureIsArrayOf(const QJsonObject& parent, + const QString& key, + const QList& default_ = QList(), + const QString& what = "__placeholder__") { const QString localWhat = QString(what).replace("__placeholder__", '\'' + key + '\''); if (!parent.contains(key)) { diff --git a/launcher/icons/IconList.h b/launcher/icons/IconList.h index 8936195c3..d2f904448 100644 --- a/launcher/icons/IconList.h +++ b/launcher/icons/IconList.h @@ -106,6 +106,6 @@ class IconList : public QAbstractListModel { shared_qobject_ptr m_watcher; bool m_isWatching; QMap m_nameIndex; - QVector m_icons; + QList m_icons; QDir m_dir; }; diff --git a/launcher/launch/LogModel.cpp b/launcher/launch/LogModel.cpp index 45aac6099..32a266428 100644 --- a/launcher/launch/LogModel.cpp +++ b/launcher/launch/LogModel.cpp @@ -100,7 +100,7 @@ void LogModel::setMaxLines(int maxLines) return; } // otherwise, we need to reorganize the data because it crosses the wrap boundary - QVector newContent; + QList newContent; newContent.resize(maxLines); if (m_numLines <= maxLines) { // if it all fits in the new buffer, just copy it over diff --git a/launcher/launch/LogModel.h b/launcher/launch/LogModel.h index ba7b14487..bf178e35f 100644 --- a/launcher/launch/LogModel.h +++ b/launcher/launch/LogModel.h @@ -40,7 +40,7 @@ class LogModel : public QAbstractListModel { }; private: /* data */ - QVector m_content; + QList m_content; int m_maxLines = 1000; // first line in the circular buffer int m_firstLine = 0; diff --git a/launcher/meta/Index.cpp b/launcher/meta/Index.cpp index 1707854be..25a4cd146 100644 --- a/launcher/meta/Index.cpp +++ b/launcher/meta/Index.cpp @@ -23,7 +23,7 @@ namespace Meta { Index::Index(QObject* parent) : QAbstractListModel(parent) {} -Index::Index(const QVector& lists, QObject* parent) : QAbstractListModel(parent), m_lists(lists) +Index::Index(const QList& lists, QObject* parent) : QAbstractListModel(parent), m_lists(lists) { for (int i = 0; i < m_lists.size(); ++i) { m_uids.insert(m_lists.at(i)->uid(), m_lists.at(i)); @@ -103,7 +103,7 @@ void Index::parse(const QJsonObject& obj) void Index::merge(const std::shared_ptr& other) { - const QVector lists = other->m_lists; + const QList lists = other->m_lists; // initial load, no need to merge if (m_lists.isEmpty()) { beginResetModel(); diff --git a/launcher/meta/Index.h b/launcher/meta/Index.h index 026a00c07..fe5bf2170 100644 --- a/launcher/meta/Index.h +++ b/launcher/meta/Index.h @@ -29,7 +29,7 @@ class Index : public QAbstractListModel, public BaseEntity { Q_OBJECT public: explicit Index(QObject* parent = nullptr); - explicit Index(const QVector& lists, QObject* parent = nullptr); + explicit Index(const QList& lists, QObject* parent = nullptr); virtual ~Index() = default; enum { UidRole = Qt::UserRole, NameRole, ListPtrRole }; @@ -46,7 +46,7 @@ class Index : public QAbstractListModel, public BaseEntity { Version::Ptr get(const QString& uid, const QString& version); bool hasUid(const QString& uid) const; - QVector lists() const { return m_lists; } + QList lists() const { return m_lists; } Task::Ptr loadVersion(const QString& uid, const QString& version = {}, Net::Mode mode = Net::Mode::Online, bool force = false); @@ -60,7 +60,7 @@ class Index : public QAbstractListModel, public BaseEntity { void parse(const QJsonObject& obj) override; private: - QVector m_lists; + QList m_lists; QHash m_uids; void connectVersionList(int row, const VersionList::Ptr& list); diff --git a/launcher/meta/JsonFormat.cpp b/launcher/meta/JsonFormat.cpp index 86af7277e..8d8466c87 100644 --- a/launcher/meta/JsonFormat.cpp +++ b/launcher/meta/JsonFormat.cpp @@ -35,8 +35,8 @@ MetadataVersion currentFormatVersion() // Index static std::shared_ptr parseIndexInternal(const QJsonObject& obj) { - const QVector objects = requireIsArrayOf(obj, "packages"); - QVector lists; + const QList objects = requireIsArrayOf(obj, "packages"); + QList lists; lists.reserve(objects.size()); std::transform(objects.begin(), objects.end(), std::back_inserter(lists), [](const QJsonObject& obj) { VersionList::Ptr list = std::make_shared(requireString(obj, "uid")); @@ -79,8 +79,8 @@ static VersionList::Ptr parseVersionListInternal(const QJsonObject& obj) { const QString uid = requireString(obj, "uid"); - const QVector versionsRaw = requireIsArrayOf(obj, "versions"); - QVector versions; + const QList versionsRaw = requireIsArrayOf(obj, "versions"); + QList versions; versions.reserve(versionsRaw.size()); std::transform(versionsRaw.begin(), versionsRaw.end(), std::back_inserter(versions), [uid](const QJsonObject& vObj) { auto version = parseCommonVersion(uid, vObj); diff --git a/launcher/meta/Version.h b/launcher/meta/Version.h index 46dc740da..2327879a1 100644 --- a/launcher/meta/Version.h +++ b/launcher/meta/Version.h @@ -19,8 +19,8 @@ #include "BaseVersion.h" #include +#include #include -#include #include #include "minecraft/VersionFile.h" diff --git a/launcher/meta/VersionList.cpp b/launcher/meta/VersionList.cpp index 1de4e7f36..1f4a969fa 100644 --- a/launcher/meta/VersionList.cpp +++ b/launcher/meta/VersionList.cpp @@ -169,7 +169,7 @@ void VersionList::setName(const QString& name) emit nameChanged(name); } -void VersionList::setVersions(const QVector& versions) +void VersionList::setVersions(const QList& versions) { beginResetModel(); m_versions = versions; @@ -265,7 +265,7 @@ void VersionList::setupAddedVersion(const int row, const Version::Ptr& version) disconnect(version.get(), &Version::typeChanged, this, nullptr); connect(version.get(), &Version::requiresChanged, this, - [this, row]() { emit dataChanged(index(row), index(row), QVector() << RequiresRole); }); + [this, row]() { emit dataChanged(index(row), index(row), QList() << RequiresRole); }); connect(version.get(), &Version::timeChanged, this, [this, row]() { emit dataChanged(index(row), index(row), { TimeRole, SortRole }); }); connect(version.get(), &Version::typeChanged, this, [this, row]() { emit dataChanged(index(row), index(row), { TypeRole }); }); diff --git a/launcher/meta/VersionList.h b/launcher/meta/VersionList.h index 4215439db..21c86b751 100644 --- a/launcher/meta/VersionList.h +++ b/launcher/meta/VersionList.h @@ -61,14 +61,14 @@ class VersionList : public BaseVersionList, public BaseEntity { Version::Ptr getVersion(const QString& version); bool hasVersion(QString version) const; - QVector versions() const { return m_versions; } + QList versions() const { return m_versions; } // this blocks until the version list is loaded void waitToLoad(); public: // for usage only by parsers void setName(const QString& name); - void setVersions(const QVector& versions); + void setVersions(const QList& versions); void merge(const VersionList::Ptr& other); void mergeFromIndex(const VersionList::Ptr& other); void parse(const QJsonObject& obj) override; @@ -82,7 +82,7 @@ class VersionList : public BaseVersionList, public BaseEntity { void updateListData(QList) override {} private: - QVector m_versions; + QList m_versions; QStringList m_externalRecommendsVersions; QHash m_lookup; QString m_uid; diff --git a/launcher/minecraft/auth/AccountData.h b/launcher/minecraft/auth/AccountData.h index 1ada4e38a..df7d569da 100644 --- a/launcher/minecraft/auth/AccountData.h +++ b/launcher/minecraft/auth/AccountData.h @@ -36,8 +36,8 @@ #pragma once #include #include +#include #include -#include #include #include diff --git a/launcher/minecraft/auth/AuthFlow.h b/launcher/minecraft/auth/AuthFlow.h index bff4c04e4..710509d8e 100644 --- a/launcher/minecraft/auth/AuthFlow.h +++ b/launcher/minecraft/auth/AuthFlow.h @@ -5,7 +5,6 @@ #include #include #include -#include #include "minecraft/auth/AccountData.h" #include "minecraft/auth/AuthStep.h" diff --git a/launcher/minecraft/skins/SkinList.cpp b/launcher/minecraft/skins/SkinList.cpp index 124b69c85..56379aaab 100644 --- a/launcher/minecraft/skins/SkinList.cpp +++ b/launcher/minecraft/skins/SkinList.cpp @@ -67,7 +67,7 @@ void SkinList::stopWatching() bool SkinList::update() { - QVector newSkins; + QList newSkins; m_dir.refresh(); auto manifestInfo = QFileInfo(m_dir.absoluteFilePath("index.json")); diff --git a/launcher/minecraft/skins/SkinList.h b/launcher/minecraft/skins/SkinList.h index e77269d57..5a160909a 100644 --- a/launcher/minecraft/skins/SkinList.h +++ b/launcher/minecraft/skins/SkinList.h @@ -74,7 +74,7 @@ class SkinList : public QAbstractListModel { private: shared_qobject_ptr m_watcher; bool m_isWatching; - QVector m_skinList; + QList m_skinList; QDir m_dir; MinecraftAccountPtr m_acct; }; \ No newline at end of file diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h index 8fae1bf6c..523358e4e 100644 --- a/launcher/modplatform/ModIndex.h +++ b/launcher/modplatform/ModIndex.h @@ -23,7 +23,6 @@ #include #include #include -#include #include class QIODevice; @@ -141,7 +140,7 @@ struct IndexedPack { QString side; bool versionsLoaded = false; - QVector versions; + QList versions; // Don't load by default, since some modplatform don't have that info bool extraDataLoaded = true; diff --git a/launcher/modplatform/atlauncher/ATLPackIndex.h b/launcher/modplatform/atlauncher/ATLPackIndex.h index 8d18c671d..187bc05ec 100644 --- a/launcher/modplatform/atlauncher/ATLPackIndex.h +++ b/launcher/modplatform/atlauncher/ATLPackIndex.h @@ -18,9 +18,9 @@ #include "ATLPackManifest.h" +#include #include #include -#include namespace ATLauncher { @@ -34,7 +34,7 @@ struct IndexedPack { int position; QString name; PackType type; - QVector versions; + QList versions; bool system; QString description; diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp index a9706a768..fa3b1be3a 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp @@ -689,7 +689,7 @@ void PackInstallTask::downloadMods() { qDebug() << "PackInstallTask::installMods: " << QThread::currentThreadId(); - QVector optionalMods; + QList optionalMods; for (const auto& mod : m_version.mods) { if (mod.optional) { optionalMods.push_back(mod); @@ -697,7 +697,7 @@ void PackInstallTask::downloadMods() } // Select optional mods, if pack contains any - QVector selectedMods; + QList selectedMods; if (!optionalMods.isEmpty()) { setStatus(tr("Selecting optional mods...")); auto mods = m_support->chooseOptionalMods(m_version, optionalMods); diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.h b/launcher/modplatform/atlauncher/ATLPackInstallTask.h index ee5960e30..ce8bb636d 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.h +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.h @@ -62,7 +62,7 @@ class UserInteractionSupport { /** * Requests a user interaction to select which optional mods should be installed. */ - virtual std::optional> chooseOptionalMods(const PackVersion& version, QVector mods) = 0; + virtual std::optional> chooseOptionalMods(const PackVersion& version, QList mods) = 0; /** * Requests a user interaction to select a component version from a given version list diff --git a/launcher/modplatform/atlauncher/ATLPackManifest.h b/launcher/modplatform/atlauncher/ATLPackManifest.h index 8db91087d..b6c3b7a84 100644 --- a/launcher/modplatform/atlauncher/ATLPackManifest.h +++ b/launcher/modplatform/atlauncher/ATLPackManifest.h @@ -36,9 +36,9 @@ #pragma once #include +#include #include #include -#include namespace ATLauncher { @@ -113,7 +113,7 @@ struct VersionMod { bool hidden; bool library; QString group; - QVector depends; + QStringList depends; QString colour; QString warning; @@ -139,8 +139,8 @@ struct VersionKeep { }; struct VersionKeeps { - QVector files; - QVector folders; + QList files; + QList folders; }; struct VersionDelete { @@ -149,8 +149,8 @@ struct VersionDelete { }; struct VersionDeletes { - QVector files; - QVector folders; + QList files; + QList folders; }; struct PackVersionMainClass { @@ -171,8 +171,8 @@ struct PackVersion { PackVersionExtraArguments extraArguments; VersionLoader loader; - QVector libraries; - QVector mods; + QList libraries; + QList mods; VersionConfigs configs; QMap colours; diff --git a/launcher/modplatform/atlauncher/ATLShareCode.h b/launcher/modplatform/atlauncher/ATLShareCode.h index 531945bce..9b56c6d7c 100644 --- a/launcher/modplatform/atlauncher/ATLShareCode.h +++ b/launcher/modplatform/atlauncher/ATLShareCode.h @@ -19,8 +19,8 @@ #pragma once #include +#include #include -#include namespace ATLauncher { @@ -32,7 +32,7 @@ struct ShareCodeMod { struct ShareCode { QString pack; QString version; - QVector mods; + QList mods; }; struct ShareCodeResponse { diff --git a/launcher/modplatform/flame/FlameAPI.cpp b/launcher/modplatform/flame/FlameAPI.cpp index a06793de0..15eb7a696 100644 --- a/launcher/modplatform/flame/FlameAPI.cpp +++ b/launcher/modplatform/flame/FlameAPI.cpp @@ -215,7 +215,7 @@ QList FlameAPI::loadModCategories(std::shared_ptr FlameAPI::getLatestVersion(QVector versions, +std::optional FlameAPI::getLatestVersion(QList versions, QList instanceLoaders, ModPlatform::ModLoaderTypes modLoaders) { diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index 509e1abcd..f85a08eb1 100644 --- a/launcher/modplatform/flame/FlameAPI.h +++ b/launcher/modplatform/flame/FlameAPI.h @@ -15,7 +15,7 @@ class FlameAPI : public NetworkResourceAPI { QString getModFileChangelog(int modId, int fileId); QString getModDescription(int modId); - std::optional getLatestVersion(QVector versions, + std::optional getLatestVersion(QList versions, QList instanceLoaders, ModPlatform::ModLoaderTypes fallback); diff --git a/launcher/modplatform/flame/FlameModIndex.cpp b/launcher/modplatform/flame/FlameModIndex.cpp index ff9d2d9ce..c1b9e67af 100644 --- a/launcher/modplatform/flame/FlameModIndex.cpp +++ b/launcher/modplatform/flame/FlameModIndex.cpp @@ -78,7 +78,7 @@ static QString enumToString(int hash_algorithm) void FlameMod::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr) { - QVector unsortedVersions; + QList unsortedVersions; for (auto versionIter : arr) { auto obj = versionIter.toObject(); @@ -208,7 +208,7 @@ ModPlatform::IndexedVersion FlameMod::loadDependencyVersions(const ModPlatform:: auto profile = (dynamic_cast(inst))->getPackProfile(); QString mcVersion = profile->getComponentVersion("net.minecraft"); auto loaders = profile->getSupportedModLoaders(); - QVector versions; + QList versions; for (auto versionIter : arr) { auto obj = versionIter.toObject(); diff --git a/launcher/modplatform/flame/FlamePackIndex.cpp b/launcher/modplatform/flame/FlamePackIndex.cpp index 8c25b0482..8a7734be5 100644 --- a/launcher/modplatform/flame/FlamePackIndex.cpp +++ b/launcher/modplatform/flame/FlamePackIndex.cpp @@ -77,7 +77,7 @@ void Flame::loadIndexedInfo(IndexedPack& pack, QJsonObject& obj) void Flame::loadIndexedPackVersions(Flame::IndexedPack& pack, QJsonArray& arr) { - QVector unsortedVersions; + QList unsortedVersions; for (auto versionIter : arr) { auto version = Json::requireObject(versionIter); Flame::IndexedVersion file; diff --git a/launcher/modplatform/flame/FlamePackIndex.h b/launcher/modplatform/flame/FlamePackIndex.h index 11633deee..30391288b 100644 --- a/launcher/modplatform/flame/FlamePackIndex.h +++ b/launcher/modplatform/flame/FlamePackIndex.h @@ -3,7 +3,6 @@ #include #include #include -#include #include "modplatform/ModIndex.h" namespace Flame { @@ -39,7 +38,7 @@ struct IndexedPack { QString logoUrl; bool versionsLoaded = false; - QVector versions; + QList versions; bool extraInfoLoaded = false; ModpackExtra extra; diff --git a/launcher/modplatform/flame/PackManifest.h b/launcher/modplatform/flame/PackManifest.h index 7af3b9d6b..ebb3ed5cc 100644 --- a/launcher/modplatform/flame/PackManifest.h +++ b/launcher/modplatform/flame/PackManifest.h @@ -36,10 +36,10 @@ #pragma once #include +#include #include #include #include -#include #include "minecraft/mod/tasks/LocalResourceParse.h" #include "modplatform/ModIndex.h" @@ -66,7 +66,7 @@ struct Modloader { struct Minecraft { QString version; QString libraries; - QVector modLoaders; + QList modLoaders; }; struct Manifest { diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp index 16b300b02..744b058c0 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp @@ -114,7 +114,7 @@ void Modrinth::loadExtraPackData(ModPlatform::IndexedPack& pack, QJsonObject& ob void Modrinth::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr) { - QVector unsortedVersions; + QList unsortedVersions; for (auto versionIter : arr) { auto obj = versionIter.toObject(); auto file = loadIndexedPackVersion(obj); @@ -253,7 +253,7 @@ ModPlatform::IndexedVersion Modrinth::loadDependencyVersions([[maybe_unused]] co QString mcVersion = profile->getComponentVersion("net.minecraft"); auto loaders = profile->getSupportedModLoaders(); - QVector versions; + QList versions; for (auto versionIter : arr) { auto obj = versionIter.toObject(); auto file = loadIndexedPackVersion(obj); diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp index 89ef6e4c4..be565bf11 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp @@ -99,7 +99,7 @@ void loadIndexedInfo(Modpack& pack, QJsonObject& obj) void loadIndexedVersions(Modpack& pack, QJsonDocument& doc) { - QVector unsortedVersions; + QList unsortedVersions; auto arr = Json::requireArray(doc); diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.h b/launcher/modplatform/modrinth/ModrinthPackManifest.h index 2e5e2da84..97b8ab712 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.h +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.h @@ -40,10 +40,10 @@ #include #include +#include #include #include #include -#include #include "modplatform/ModIndex.h" @@ -110,7 +110,7 @@ struct Modpack { bool extraInfoLoaded = false; ModpackExtra extra; - QVector versions; + QList versions; }; void loadIndexedPack(Modpack&, QJsonObject&); diff --git a/launcher/modplatform/technic/SolderPackManifest.h b/launcher/modplatform/technic/SolderPackManifest.h index 1a06d7037..3a5947515 100644 --- a/launcher/modplatform/technic/SolderPackManifest.h +++ b/launcher/modplatform/technic/SolderPackManifest.h @@ -19,15 +19,15 @@ #pragma once #include +#include #include -#include namespace TechnicSolder { struct Pack { QString recommended; QString latest; - QVector builds; + QList builds; }; void loadPack(Pack& v, QJsonObject& obj); @@ -41,7 +41,7 @@ struct PackBuildMod { struct PackBuild { QString minecraft; - QVector mods; + QList mods; }; void loadPackBuild(PackBuild& v, QJsonObject& obj); diff --git a/launcher/translations/TranslationsModel.cpp b/launcher/translations/TranslationsModel.cpp index e863dfef4..75fc93b3b 100644 --- a/launcher/translations/TranslationsModel.cpp +++ b/launcher/translations/TranslationsModel.cpp @@ -153,7 +153,7 @@ struct TranslationsModel::Private { QDir m_dir; // initial state is just english - QVector m_languages = { Language(defaultLangCode) }; + QList m_languages = { Language(defaultLangCode) }; QString m_selectedLanguage = defaultLangCode; std::unique_ptr m_qt_translator; @@ -417,7 +417,7 @@ int TranslationsModel::columnCount([[maybe_unused]] const QModelIndex& parent) c return 2; } -QVector::Iterator TranslationsModel::findLanguage(const QString& key) +QList::Iterator TranslationsModel::findLanguage(const QString& key) { return std::find_if(d->m_languages.begin(), d->m_languages.end(), [key](Language& lang) { return lang.key == key; }); } diff --git a/launcher/translations/TranslationsModel.h b/launcher/translations/TranslationsModel.h index 96a0e9f8b..945e689fc 100644 --- a/launcher/translations/TranslationsModel.h +++ b/launcher/translations/TranslationsModel.h @@ -41,7 +41,7 @@ class TranslationsModel : public QAbstractListModel { void setUseSystemLocale(bool useSystemLocale); private: - QVector::Iterator findLanguage(const QString& key); + QList::Iterator findLanguage(const QString& key); std::optional findLanguageAsOptional(const QString& key); void reloadLocalFiles(); void downloadTranslation(QString key); diff --git a/launcher/ui/dialogs/skins/draw/BoxGeometry.cpp b/launcher/ui/dialogs/skins/draw/BoxGeometry.cpp index 9a5ad1ce2..b4ab8d4cc 100644 --- a/launcher/ui/dialogs/skins/draw/BoxGeometry.cpp +++ b/launcher/ui/dialogs/skins/draw/BoxGeometry.cpp @@ -18,10 +18,10 @@ #include "BoxGeometry.h" +#include #include #include #include -#include struct VertexData { QVector4D position; @@ -32,7 +32,7 @@ struct VertexData { // For cube we would need only 8 vertices but we have to // duplicate vertex for each face because texture coordinate // is different. -static const QVector vertices = { +static const QList vertices = { // Vertex data for face 0 QVector4D(-0.5f, -0.5f, 0.5f, 1.0f), // v0 QVector4D(0.5f, -0.5f, 0.5f, 1.0f), // v1 @@ -76,7 +76,7 @@ static const QVector vertices = { // index of the second strip needs to be duplicated. If // connecting strips have same vertex order then only last // index of the first strip needs to be duplicated. -static const QVector indices = { +static const QList indices = { 0, 1, 2, 3, 3, // Face 0 - triangle strip ( v0, v1, v2, v3) 4, 4, 5, 6, 7, 7, // Face 1 - triangle strip ( v4, v5, v6, v7) 8, 8, 9, 10, 11, 11, // Face 2 - triangle strip ( v8, v9, v10, v11) @@ -85,19 +85,19 @@ static const QVector indices = { 20, 20, 21, 22, 23 // Face 5 - triangle strip (v20, v21, v22, v23) }; -static const QVector planeVertices = { +static const QList planeVertices = { { QVector4D(-1.0f, -1.0f, -0.5f, 1.0f), QVector2D(0.0f, 0.0f) }, // Bottom-left { QVector4D(1.0f, -1.0f, -0.5f, 1.0f), QVector2D(1.0f, 0.0f) }, // Bottom-right { QVector4D(-1.0f, 1.0f, -0.5f, 1.0f), QVector2D(0.0f, 1.0f) }, // Top-left { QVector4D(1.0f, 1.0f, -0.5f, 1.0f), QVector2D(1.0f, 1.0f) }, // Top-right }; -static const QVector planeIndices = { +static const QList planeIndices = { 0, 1, 2, 3, 3 // Face 0 - triangle strip ( v0, v1, v2, v3) }; -QVector transformVectors(const QMatrix4x4& matrix, const QVector& vectors) +QList transformVectors(const QMatrix4x4& matrix, const QList& vectors) { - QVector transformedVectors; + QList transformedVectors; transformedVectors.reserve(vectors.size()); for (const QVector4D& vec : vectors) { @@ -113,9 +113,9 @@ QVector transformVectors(const QMatrix4x4& matrix, const QVector getCubeUVs(float u, float v, float width, float height, float depth, float textureWidth, float textureHeight) +QList getCubeUVs(float u, float v, float width, float height, float depth, float textureWidth, float textureHeight) { - auto toFaceVertices = [textureHeight, textureWidth](float x1, float y1, float x2, float y2) -> QVector { + auto toFaceVertices = [textureHeight, textureWidth](float x1, float y1, float x2, float y2) -> QList { return { QVector2D(x1 / textureWidth, 1.0 - y2 / textureHeight), QVector2D(x2 / textureWidth, 1.0 - y2 / textureHeight), @@ -168,7 +168,7 @@ QVector getCubeUVs(float u, float v, float width, float height, float back[2], }; // Create a new array to hold the modified UV data - QVector uvData; + QList uvData; uvData.reserve(24); // Iterate over the arrays and copy the data to newUVData @@ -237,7 +237,7 @@ void BoxGeometry::initGeometry(float u, float v, float width, float height, floa transformation.scale(m_size); auto positions = transformVectors(transformation, vertices); - QVector verticesData; + QList verticesData; verticesData.reserve(positions.size()); // Reserve space for efficiency for (int i = 0; i < positions.size(); ++i) { diff --git a/launcher/ui/dialogs/skins/draw/Scene.h b/launcher/ui/dialogs/skins/draw/Scene.h index de683a659..3560d1d74 100644 --- a/launcher/ui/dialogs/skins/draw/Scene.h +++ b/launcher/ui/dialogs/skins/draw/Scene.h @@ -34,9 +34,9 @@ class Scene { void setCapeVisible(bool visible); private: - QVector m_staticComponents; - QVector m_normalArms; - QVector m_slimArms; + QList m_staticComponents; + QList m_normalArms; + QList m_slimArms; BoxGeometry* m_cape = nullptr; QOpenGLTexture* m_skinTexture = nullptr; QOpenGLTexture* m_capeTexture = nullptr; diff --git a/launcher/ui/instanceview/InstanceView.cpp b/launcher/ui/instanceview/InstanceView.cpp index f52c994d3..2349c684d 100644 --- a/launcher/ui/instanceview/InstanceView.cpp +++ b/launcher/ui/instanceview/InstanceView.cpp @@ -89,7 +89,7 @@ void InstanceView::setModel(QAbstractItemModel* model) void InstanceView::dataChanged([[maybe_unused]] const QModelIndex& topLeft, [[maybe_unused]] const QModelIndex& bottomRight, - [[maybe_unused]] const QVector& roles) + [[maybe_unused]] const QList& roles) { scheduleDelayedItemsLayout(); } diff --git a/launcher/ui/instanceview/InstanceView.h b/launcher/ui/instanceview/InstanceView.h index 30be411a8..dea8b1212 100644 --- a/launcher/ui/instanceview/InstanceView.h +++ b/launcher/ui/instanceview/InstanceView.h @@ -83,7 +83,7 @@ class InstanceView : public QAbstractItemView { virtual void updateGeometries() override; protected slots: - virtual void dataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight, const QVector& roles) override; + virtual void dataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight, const QList& roles) override; virtual void rowsInserted(const QModelIndex& parent, int start, int end) override; virtual void rowsAboutToBeRemoved(const QModelIndex& parent, int start, int end) override; void modelReset(); diff --git a/launcher/ui/instanceview/VisualGroup.cpp b/launcher/ui/instanceview/VisualGroup.cpp index 089db8ad7..4f7a61eb5 100644 --- a/launcher/ui/instanceview/VisualGroup.cpp +++ b/launcher/ui/instanceview/VisualGroup.cpp @@ -55,7 +55,7 @@ void VisualGroup::update() auto itemsPerRow = view->itemsPerRow(); int numRows = qMax(1, qCeil((qreal)temp_items.size() / (qreal)itemsPerRow)); - rows = QVector(numRows); + rows = QList(numRows); int maxRowHeight = 0; int positionInRow = 0; diff --git a/launcher/ui/instanceview/VisualGroup.h b/launcher/ui/instanceview/VisualGroup.h index 8c6f06bcc..7210e0dfc 100644 --- a/launcher/ui/instanceview/VisualGroup.h +++ b/launcher/ui/instanceview/VisualGroup.h @@ -35,10 +35,10 @@ #pragma once +#include #include #include #include -#include class InstanceView; class QPainter; @@ -61,7 +61,7 @@ struct VisualGroup { InstanceView* view = nullptr; QString text; bool collapsed = false; - QVector rows; + QList rows; int firstItemIndex = 0; int m_verticalPosition = 0; diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp index d84737bf5..5ee8d2c04 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp @@ -47,7 +47,7 @@ AtlOptionalModListModel::AtlOptionalModListModel(QWidget* parent, const ATLauncher::PackVersion& version, - QVector mods) + QList mods) : QAbstractListModel(parent), m_version(version), m_mods(mods) { // fill mod index @@ -64,9 +64,9 @@ AtlOptionalModListModel::AtlOptionalModListModel(QWidget* parent, } } -QVector AtlOptionalModListModel::getResult() +QList AtlOptionalModListModel::getResult() { - QVector result; + QList result; for (const auto& mod : m_mods) { if (m_selection[mod.name]) { @@ -315,7 +315,7 @@ void AtlOptionalModListModel::setMod(const ATLauncher::VersionMod& mod, int inde } } -AtlOptionalModDialog::AtlOptionalModDialog(QWidget* parent, const ATLauncher::PackVersion& version, QVector mods) +AtlOptionalModDialog::AtlOptionalModDialog(QWidget* parent, const ATLauncher::PackVersion& version, QList mods) : QDialog(parent), ui(new Ui::AtlOptionalModDialog) { ui->setupUi(this); diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h index 0636715cc..fa39e997c 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h +++ b/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h @@ -55,9 +55,9 @@ class AtlOptionalModListModel : public QAbstractListModel { DescriptionColumn, }; - AtlOptionalModListModel(QWidget* parent, const ATLauncher::PackVersion& version, QVector mods); + AtlOptionalModListModel(QWidget* parent, const ATLauncher::PackVersion& version, QList mods); - QVector getResult(); + QList getResult(); int rowCount(const QModelIndex& parent) const override; int columnCount(const QModelIndex& parent) const override; @@ -86,21 +86,21 @@ class AtlOptionalModListModel : public QAbstractListModel { std::shared_ptr m_response = std::make_shared(); ATLauncher::PackVersion m_version; - QVector m_mods; + QList m_mods; QMap m_selection; QMap m_index; - QMap> m_dependents; + QMap> m_dependents; }; class AtlOptionalModDialog : public QDialog { Q_OBJECT public: - AtlOptionalModDialog(QWidget* parent, const ATLauncher::PackVersion& version, QVector mods); + AtlOptionalModDialog(QWidget* parent, const ATLauncher::PackVersion& version, QList mods); ~AtlOptionalModDialog() override; - QVector getResult() { return listModel->getResult(); } + QList getResult() { return listModel->getResult(); } void useShareCode(); diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.cpp index 7550ff758..dc9a4758f 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.cpp @@ -41,8 +41,8 @@ AtlUserInteractionSupportImpl::AtlUserInteractionSupportImpl(QWidget* parent) : m_parent(parent) {} -std::optional> AtlUserInteractionSupportImpl::chooseOptionalMods(const ATLauncher::PackVersion& version, - QVector mods) +std::optional> AtlUserInteractionSupportImpl::chooseOptionalMods(const ATLauncher::PackVersion& version, + QList mods) { AtlOptionalModDialog optionalModDialog(m_parent, version, mods); auto result = optionalModDialog.exec(); diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.h b/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.h index 7ff021105..99f907a19 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.h +++ b/launcher/ui/pages/modplatform/atlauncher/AtlUserInteractionSupportImpl.h @@ -48,8 +48,7 @@ class AtlUserInteractionSupportImpl : public QObject, public ATLauncher::UserInt private: QString chooseVersion(Meta::VersionList::Ptr vlist, QString minecraftVersion) override; - std::optional> chooseOptionalMods(const ATLauncher::PackVersion& version, - QVector mods) override; + std::optional> chooseOptionalMods(const ATLauncher::PackVersion& version, QList mods) override; void displayMessage(QString message) override; private: diff --git a/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp b/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp index fea1fc27a..a2a36141b 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp @@ -95,7 +95,7 @@ void FlameTexturePackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, { FlameMod::loadIndexedPackVersions(m, arr); - QVector filtered_versions(m.versions.size()); + QList filtered_versions(m.versions.size()); // FIXME: Client-side version filtering. This won't take into account any user-selected filtering. for (auto const& version : m.versions) { diff --git a/launcher/ui/pages/modplatform/technic/TechnicData.h b/launcher/ui/pages/modplatform/technic/TechnicData.h index fc7fa4d39..11d57f071 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicData.h +++ b/launcher/ui/pages/modplatform/technic/TechnicData.h @@ -37,7 +37,6 @@ #include #include -#include namespace Technic { struct Modpack { @@ -61,7 +60,7 @@ struct Modpack { bool versionsLoaded = false; QString recommended; - QVector versions; + QList versions; }; } // namespace Technic diff --git a/libraries/LocalPeer/src/LockedFile.h b/libraries/LocalPeer/src/LockedFile.h index e8023251c..0d3539708 100644 --- a/libraries/LocalPeer/src/LockedFile.h +++ b/libraries/LocalPeer/src/LockedFile.h @@ -42,7 +42,7 @@ #include #ifdef Q_OS_WIN -#include +#include #endif class LockedFile : public QFile { @@ -64,7 +64,7 @@ class LockedFile : public QFile { #ifdef Q_OS_WIN Qt::HANDLE wmutex; Qt::HANDLE rmutex; - QVector rmutexes; + QList rmutexes; QString mutexname; Qt::HANDLE getMutexHandle(int idx, bool doCreate); From 96a4b78e2edcc5225d4f831e6ef5c534e2c14d46 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Fri, 18 Apr 2025 23:45:46 +0100 Subject: [PATCH 145/695] Remove accidental return Signed-off-by: TheKodeToad --- launcher/ui/pages/instance/OtherLogsPage.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/launcher/ui/pages/instance/OtherLogsPage.cpp b/launcher/ui/pages/instance/OtherLogsPage.cpp index ec597497d..63dcf5cff 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.cpp +++ b/launcher/ui/pages/instance/OtherLogsPage.cpp @@ -43,11 +43,11 @@ #include #include -#include #include -#include -#include +#include +#include #include +#include OtherLogsPage::OtherLogsPage(InstancePtr instance, QWidget* parent) : QWidget(parent) @@ -144,7 +144,6 @@ void OtherLogsPage::populateSelectLogBox() if (index != -1) { ui->selectLogBox->setCurrentIndex(index); setControlsEnabled(true); - return; } else { setControlsEnabled(false); } From 8ea5eac29cd9340d9c2d1da6dd8e35355fdbce59 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Fri, 18 Apr 2025 23:48:56 +0100 Subject: [PATCH 146/695] Make requested changes Signed-off-by: TheKodeToad --- launcher/NullInstance.h | 1 - launcher/ui/pages/instance/OtherLogsPage.h | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/launcher/NullInstance.h b/launcher/NullInstance.h index 93bab6c8b..e603b1634 100644 --- a/launcher/NullInstance.h +++ b/launcher/NullInstance.h @@ -35,7 +35,6 @@ */ #pragma once -#include #include "BaseInstance.h" #include "launch/LaunchTask.h" diff --git a/launcher/ui/pages/instance/OtherLogsPage.h b/launcher/ui/pages/instance/OtherLogsPage.h index fedb2506c..70eb145fb 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.h +++ b/launcher/ui/pages/instance/OtherLogsPage.h @@ -39,8 +39,7 @@ #include #include -#include -#include +#include #include "LogPage.h" #include "ui/pages/BasePage.h" From 19b241fd31d89cd159ab075475ab1270eb8b2799 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Fri, 18 Apr 2025 23:59:35 +0100 Subject: [PATCH 147/695] Include txt too Signed-off-by: TheKodeToad --- launcher/ui/pages/instance/OtherLogsPage.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/launcher/ui/pages/instance/OtherLogsPage.cpp b/launcher/ui/pages/instance/OtherLogsPage.cpp index 63dcf5cff..4e401aa9c 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.cpp +++ b/launcher/ui/pages/instance/OtherLogsPage.cpp @@ -404,13 +404,17 @@ QStringList OtherLogsPage::getPaths() for (QString searchPath : m_logSearchPaths) { QDirIterator iterator(searchPath, QDir::Files | QDir::Readable); + const bool isRoot = searchPath == m_basePath; + while (iterator.hasNext()) { const QString name = iterator.next(); - if (!name.endsWith(".log") && !name.endsWith(".log.gz")) + QString relativePath = baseDir.relativeFilePath(name); + + if (!(name.endsWith(".log") || name.endsWith(".log.gz") || (!isRoot && name.endsWith(".txt")))) continue; - result.append(baseDir.relativeFilePath(name)); + result.append(relativePath); } } From 0aa3341d5827c4a1529323cb682592891641488d Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Sat, 19 Apr 2025 00:04:30 +0100 Subject: [PATCH 148/695] Fix other weird import Signed-off-by: TheKodeToad --- launcher/ui/pages/instance/OtherLogsPage.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/pages/instance/OtherLogsPage.cpp b/launcher/ui/pages/instance/OtherLogsPage.cpp index 4e401aa9c..caacea83e 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.cpp +++ b/launcher/ui/pages/instance/OtherLogsPage.cpp @@ -43,8 +43,8 @@ #include #include -#include #include +#include #include #include #include From 92ba13cfdb839ece93cea2429193a8c6c3982cd8 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Sat, 19 Apr 2025 00:12:38 +0100 Subject: [PATCH 149/695] Fix catastrophic regex mistake Signed-off-by: TheKodeToad --- launcher/minecraft/MinecraftInstance.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index ec136ede0..638979578 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -1011,7 +1011,7 @@ MessageLevel::Enum MinecraftInstance::guessLevel(const QString& line, MessageLev // NOTE: this diverges from the real regexp. no unicode, the first section is + instead of * static const QRegularExpression JAVA_EXCEPTION( - R"(Exception in thread|...\d more$|(\s+at |Caused by: )([a-zA-Z_$][a-zA-Z\d_$]*\.)+[a-zA-Z_$][a-zA-Z\d_$]*)|([a-zA-Z_$][a-zA-Z\d_$]*\.)+[a-zA-Z_$][a-zA-Z\d_$]*(Exception|Error|Throwable)"); + R"((Exception in thread|...\d more$|(\s+at |Caused by: )([a-zA-Z_$][a-zA-Z\d_$]*\.)+[a-zA-Z_$][a-zA-Z\d_$]*)|([a-zA-Z_$][a-zA-Z\d_$]*\.)+[a-zA-Z_$][a-zA-Z\d_$]*(Exception|Error|Throwable))"); if (line.contains(JAVA_EXCEPTION)) return MessageLevel::Error; From 111cdc240ef0232c155a9dbdd0bd87a077fcdf86 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Sat, 19 Apr 2025 09:34:12 +0100 Subject: [PATCH 150/695] Disable auto-reload of files Signed-off-by: TheKodeToad --- launcher/ui/pages/instance/OtherLogsPage.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/launcher/ui/pages/instance/OtherLogsPage.cpp b/launcher/ui/pages/instance/OtherLogsPage.cpp index caacea83e..f457195d8 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.cpp +++ b/launcher/ui/pages/instance/OtherLogsPage.cpp @@ -142,8 +142,11 @@ void OtherLogsPage::populateSelectLogBox() if (!prevCurrentFile.isEmpty()) { const int index = ui->selectLogBox->findText(prevCurrentFile); if (index != -1) { + ui->selectLogBox->blockSignals(true); ui->selectLogBox->setCurrentIndex(index); + ui->selectLogBox->blockSignals(false); setControlsEnabled(true); + return; } else { setControlsEnabled(false); } From 47295da3907a5be82f93ffc49165d0ce6ba452e0 Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Wed, 16 Apr 2025 00:37:35 -0700 Subject: [PATCH 151/695] feat(logs): parse log4j xml events in logs Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- launcher/CMakeLists.txt | 2 + launcher/MessageLevel.cpp | 2 + launcher/MessageLevel.h | 1 + launcher/launch/LaunchTask.cpp | 55 +++++- launcher/launch/LaunchTask.h | 6 + launcher/logs/LogParser.cpp | 322 +++++++++++++++++++++++++++++++++ launcher/logs/LogParser.h | 74 ++++++++ 7 files changed, 461 insertions(+), 1 deletion(-) create mode 100644 launcher/logs/LogParser.cpp create mode 100644 launcher/logs/LogParser.h diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index ca5a5bea9..4f8b9018a 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -175,6 +175,8 @@ set(LAUNCH_SOURCES launch/LogModel.h launch/TaskStepWrapper.cpp launch/TaskStepWrapper.h + logs/LogParser.cpp + logs/LogParser.h ) # Old update system diff --git a/launcher/MessageLevel.cpp b/launcher/MessageLevel.cpp index 116e70c4b..d0e8809ec 100644 --- a/launcher/MessageLevel.cpp +++ b/launcher/MessageLevel.cpp @@ -4,6 +4,8 @@ MessageLevel::Enum MessageLevel::getLevel(const QString& levelName) { if (levelName == "Launcher") return MessageLevel::Launcher; + else if (levelName == "Trace") + return MessageLevel::Trace; else if (levelName == "Debug") return MessageLevel::Debug; else if (levelName == "Info") diff --git a/launcher/MessageLevel.h b/launcher/MessageLevel.h index fd12583f2..321af9d92 100644 --- a/launcher/MessageLevel.h +++ b/launcher/MessageLevel.h @@ -12,6 +12,7 @@ enum Enum { StdOut, /**< Undetermined stderr messages */ StdErr, /**< Undetermined stdout messages */ Launcher, /**< Launcher Messages */ + Trace, /**< Trace Messages */ Debug, /**< Debug Messages */ Info, /**< Info Messages */ Message, /**< Standard Messages */ diff --git a/launcher/launch/LaunchTask.cpp b/launcher/launch/LaunchTask.cpp index 9ec746641..e1eaf8b1f 100644 --- a/launcher/launch/LaunchTask.cpp +++ b/launcher/launch/LaunchTask.cpp @@ -37,11 +37,14 @@ #include "launch/LaunchTask.h" #include +#include +#include #include #include #include #include #include +#include #include "MessageLevel.h" #include "tasks/Task.h" @@ -213,6 +216,52 @@ shared_qobject_ptr LaunchTask::getLogModel() return m_logModel; } +bool LaunchTask::parseXmlLogs(QString const& line, MessageLevel::Enum level) +{ + LogParser* parser; + switch (level) { + case MessageLevel::StdErr: + parser = &m_stderrParser; + break; + case MessageLevel::StdOut: + parser = &m_stdoutParser; + break; + default: + return false; + } + + parser->appendLine(line); + auto items = parser->parseAvailable(); + if (auto err = parser->getError(); err.has_value()) { + auto& model = *getLogModel(); + model.append(MessageLevel::Error, tr("[Log4j Parse Error] Failed to parse log4j log event: %1").arg(err.value().errMessage)); + return false; + } else { + if (items.has_value()) { + auto& model = *getLogModel(); + for (auto const& item : items.value()) { + if (std::holds_alternative(item)) { + auto entry = std::get(item); + auto msg = QString("[%1] [%2/%3] [%4]: %5") + .arg(entry.timestamp.toString("HH:mm:ss")) + .arg(entry.thread) + .arg(entry.levelText) + .arg(entry.logger) + .arg(entry.message); + msg = censorPrivateInfo(msg); + model.append(entry.level, msg); + } else if (std::holds_alternative(item)) { + auto msg = std::get(item).message; + level = m_instance->guessLevel(msg, level); + msg = censorPrivateInfo(msg); + model.append(level, msg); + } + } + } + } + return true; +} + void LaunchTask::onLogLines(const QStringList& lines, MessageLevel::Enum defaultLevel) { for (auto& line : lines) { @@ -222,6 +271,10 @@ void LaunchTask::onLogLines(const QStringList& lines, MessageLevel::Enum default void LaunchTask::onLogLine(QString line, MessageLevel::Enum level) { + if (parseXmlLogs(line, level)) { + return; + } + // if the launcher part set a log level, use it auto innerLevel = MessageLevel::fromLine(line); if (innerLevel != MessageLevel::Unknown) { @@ -229,7 +282,7 @@ void LaunchTask::onLogLine(QString line, MessageLevel::Enum level) } // If the level is still undetermined, guess level - if (level == MessageLevel::StdErr || level == MessageLevel::StdOut || level == MessageLevel::Unknown) { + if (level == MessageLevel::Unknown) { level = m_instance->guessLevel(line, level); } diff --git a/launcher/launch/LaunchTask.h b/launcher/launch/LaunchTask.h index 2e87ece95..5effab980 100644 --- a/launcher/launch/LaunchTask.h +++ b/launcher/launch/LaunchTask.h @@ -43,6 +43,7 @@ #include "LaunchStep.h" #include "LogModel.h" #include "MessageLevel.h" +#include "logs/LogParser.h" class LaunchTask : public Task { Q_OBJECT @@ -114,6 +115,9 @@ class LaunchTask : public Task { private: /*methods */ void finalizeSteps(bool successful, const QString& error); + protected: + bool parseXmlLogs(QString const& line, MessageLevel::Enum level); + protected: /* data */ MinecraftInstancePtr m_instance; shared_qobject_ptr m_logModel; @@ -122,4 +126,6 @@ class LaunchTask : public Task { int currentStep = -1; State state = NotStarted; qint64 m_pid = -1; + LogParser m_stdoutParser; + LogParser m_stderrParser; }; diff --git a/launcher/logs/LogParser.cpp b/launcher/logs/LogParser.cpp new file mode 100644 index 000000000..294036134 --- /dev/null +++ b/launcher/logs/LogParser.cpp @@ -0,0 +1,322 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2025 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * 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 "LogParser.h" + +void LogParser::appendLine(QAnyStringView data) +{ + if (!m_partialData.isEmpty()) { + m_buffer = QString(m_partialData); + m_buffer.append("\n"); + m_buffer.append(data.toString()); + m_partialData.clear(); + } else { + m_buffer.append(data.toString()); + } +} + +std::optional LogParser::getError() +{ + return m_error; +} + +MessageLevel::Enum LogParser::parseLogLevel(const QString& level) +{ + auto test = level.trimmed().toUpper(); + if (test == "TRACE") { + return MessageLevel::Trace; + } else if (test == "DEBUG") { + return MessageLevel::Debug; + } else if (test == "INFO") { + return MessageLevel::Info; + } else if (test == "WARN") { + return MessageLevel::Warning; + } else if (test == "ERROR") { + return MessageLevel::Error; + } else if (test == "FATAL") { + return MessageLevel::Fatal; + } else { + return MessageLevel::Unknown; + } +} + +std::optional LogParser::parseAttributes() +{ + LogParser::LogEntry entry{ + "", + MessageLevel::Info, + }; + auto attributes = m_parser.attributes(); + + for (const auto& attr : attributes) { + auto name = attr.name(); + auto value = attr.value(); + if (name == "logger") { + entry.logger = value.trimmed().toString(); + } else if (name == "timestamp") { + if (value.trimmed().isEmpty()) { + m_parser.raiseError("log4j:Event Missing required attribute: timestamp"); + return {}; + } + entry.timestamp = QDateTime::fromSecsSinceEpoch(value.trimmed().toLongLong()); + } else if (name == "level") { + entry.levelText = value.trimmed().toString(); + entry.level = parseLogLevel(entry.levelText); + } else if (name == "thread") { + entry.thread = value.trimmed().toString(); + } + } + if (entry.logger.isEmpty()) { + m_parser.raiseError("log4j:Event Missing required attribute: logger"); + return {}; + } + + return entry; +} + +void LogParser::setError() +{ + m_error = { + m_parser.errorString(), + m_parser.error(), + }; +} + +void LogParser::clearError() +{ + m_error = {}; // clear previous error +} + +bool isPotentialLog4JStart(QStringView buffer) +{ + static QString target = QStringLiteral(" LogParser::parseNext() +{ + clearError(); + + if (m_buffer.isEmpty()) { + return {}; + } + + if (m_buffer.trimmed().isEmpty()) { + m_buffer.clear(); + return {}; + } + + // check if we have a full xml log4j event + bool isCompleteLog4j = false; + m_parser.clear(); + m_parser.setNamespaceProcessing(false); + m_parser.addData(m_buffer); + if (m_parser.readNextStartElement()) { + if (m_parser.qualifiedName() == "log4j:Event") { + int depth = 1; + bool eod = false; + while (depth > 0 && !eod) { + auto tok = m_parser.readNext(); + switch (tok) { + case QXmlStreamReader::TokenType::StartElement: { + depth += 1; + } break; + case QXmlStreamReader::TokenType::EndElement: { + depth -= 1; + } break; + case QXmlStreamReader::TokenType::EndDocument: { + eod = true; // break outer while loop + } break; + default: { + // no op + } + } + if (m_parser.hasError()) { + break; + } + } + + isCompleteLog4j = depth == 0; + } + } + + if (isCompleteLog4j) { + return parseLog4J(); + } else { + if (isPotentialLog4JStart(m_buffer)) { + m_partialData = QString(m_buffer); + return LogParser::Partial{ QString(m_buffer) }; + } + + int start = 0; + auto bufView = QStringView(m_buffer); + while (start < bufView.length()) { + if (qsizetype pos = bufView.right(bufView.length() - start).indexOf('<'); pos != -1) { + auto slicestart = start + pos; + auto slice = bufView.right(bufView.length() - slicestart); + if (isPotentialLog4JStart(slice)) { + if (slicestart > 0) { + auto text = m_buffer.left(slicestart); + m_buffer = m_buffer.right(m_buffer.length() - slicestart); + if (!text.trimmed().isEmpty()) { + return LogParser::PlainText{ text }; + } + } + m_partialData = QString(m_buffer); + return LogParser::Partial{ QString(m_buffer) }; + } + start = slicestart + 1; + } else { + break; + } + } + + // no log4j found, all plain text + auto text = QString(m_buffer); + m_buffer.clear(); + if (text.trimmed().isEmpty()) { + return {}; + } else { + return LogParser::PlainText{ text }; + } + } +} + +std::optional> LogParser::parseAvailable() +{ + QList items; + bool doNext = true; + while (doNext) { + auto item_ = parseNext(); + if (m_error.has_value()) { + return {}; + } + if (item_.has_value()) { + auto item = item_.value(); + if (std::holds_alternative(item)) { + break; + } else { + items.push_back(item); + } + } else { + doNext = false; + } + } + return items; +} + +std::optional LogParser::parseLog4J() +{ + m_parser.clear(); + m_parser.setNamespaceProcessing(false); + m_parser.addData(m_buffer); + + m_parser.readNextStartElement(); + if (m_parser.qualifiedName() == "log4j:Event") { + auto entry_ = parseAttributes(); + if (!entry_.has_value()) { + setError(); + return {}; + } + auto entry = entry_.value(); + + bool foundMessage = false; + int depth = 1; + + while (!m_parser.atEnd()) { + auto tok = m_parser.readNext(); + switch (tok) { + case QXmlStreamReader::TokenType::StartElement: { + depth += 1; + if (m_parser.qualifiedName() == "log4j:Message") { + QString message; + bool messageComplete = false; + + while (!messageComplete) { + auto tok = m_parser.readNext(); + + switch (tok) { + case QXmlStreamReader::TokenType::Characters: { + message.append(m_parser.text()); + } break; + case QXmlStreamReader::TokenType::EndElement: { + if (m_parser.qualifiedName() == "log4j:Message") { + messageComplete = true; + } + } break; + case QXmlStreamReader::TokenType::EndDocument: { + return {}; // parse fail + } break; + default: { + // no op + } + } + + if (m_parser.hasError()) { + return {}; + } + } + + entry.message = message; + foundMessage = true; + depth -= 1; + } + break; + case QXmlStreamReader::TokenType::EndElement: { + depth -= 1; + if (depth == 0 && m_parser.qualifiedName() == "log4j:Event") { + if (foundMessage) { + auto consumed = m_parser.characterOffset(); + if (consumed > 0 && consumed <= m_buffer.length()) { + m_buffer = m_buffer.right(m_buffer.length() - consumed); + + if (!m_buffer.isEmpty() && m_buffer.trimmed().isEmpty()) { + // only whitespace, dump it + m_buffer.clear(); + } + } + clearError(); + return entry; + } + m_parser.raiseError("log4j:Event Missing required attribute: message"); + setError(); + return {}; + } + } break; + case QXmlStreamReader::TokenType::EndDocument: { + return {}; + } break; + default: { + // no op + } + } + } + + if (m_parser.hasError()) { + return {}; + } + } + } + + throw std::runtime_error("unreachable: already verified this was a complete log4j:Event"); +} diff --git a/launcher/logs/LogParser.h b/launcher/logs/LogParser.h new file mode 100644 index 000000000..462ea43cf --- /dev/null +++ b/launcher/logs/LogParser.h @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2025 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * 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 +#include +#include +#include +#include +#include +#include +#include +#include "MessageLevel.h" + +class LogParser { + public: + struct LogEntry { + QString logger; + MessageLevel::Enum level; + QString levelText; + QDateTime timestamp; + QString thread; + QString message; + }; + struct Partial { + QString data; + }; + struct PlainText { + QString message; + }; + struct Error { + QString errMessage; + QXmlStreamReader::Error error; + }; + + using ParsedItem = std::variant; + + public: + LogParser() = default; + + void appendLine(QAnyStringView data); + std::optional parseNext(); + std::optional> parseAvailable(); + std::optional getError(); + + protected: + MessageLevel::Enum parseLogLevel(const QString& level); + std::optional parseAttributes(); + void setError(); + void clearError(); + + std::optional parseLog4J(); + + private: + QString m_buffer; + QString m_partialData; + QXmlStreamReader m_parser; + std::optional m_error; +}; From bfdc77665d8b38db22dc1bf4f49b4772993a8766 Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Fri, 18 Apr 2025 15:21:14 -0700 Subject: [PATCH 152/695] feat(xml-logs): add tests Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- tests/XmlLogs_test.cpp | 85 + .../TerraFirmaGreg-Modern-forge.text.log | 947 ++++++ .../TerraFirmaGreg-Modern-forge.xml.log | 2854 +++++++++++++++++ .../testdata/TestLogs/vanilla-1.21.5.text.log | 25 + .../testdata/TestLogs/vanilla-1.21.5.xml.log | 75 + 5 files changed, 3986 insertions(+) create mode 100644 tests/XmlLogs_test.cpp create mode 100644 tests/testdata/TestLogs/TerraFirmaGreg-Modern-forge.text.log create mode 100644 tests/testdata/TestLogs/TerraFirmaGreg-Modern-forge.xml.log create mode 100644 tests/testdata/TestLogs/vanilla-1.21.5.text.log create mode 100644 tests/testdata/TestLogs/vanilla-1.21.5.xml.log diff --git a/tests/XmlLogs_test.cpp b/tests/XmlLogs_test.cpp new file mode 100644 index 000000000..072448c4e --- /dev/null +++ b/tests/XmlLogs_test.cpp @@ -0,0 +1,85 @@ +// SPDX-FileCopyrightText: 2025 Rachel Powers <508861+Ryex@users.noreply.github.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2025 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * 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 + +#include +#include +#include + +#include + +#include +#include +#include + +class XmlLogParseTest : public QObject { + Q_OBJECT + + private slots: + + void parseXml_data() + { + QString source = QFINDTESTDATA("testdata/TestLogs"); + + QString shortXml = QString::fromUtf8(FS::read(FS::PathCombine(source, "vanilla-1.21.5.xml.log"))); + QString shortText = QString::fromUtf8(FS::read(FS::PathCombine(source, "vanilla-1.21.5.text.log"))); + + QString longXml = QString::fromUtf8(FS::read(FS::PathCombine(source, "TerraFirmaGreg-Modern-forge.xml.log"))); + QString longText = QString::fromUtf8(FS::read(FS::PathCombine(source, "TerraFirmaGreg-Modern-forge.text.log"))); + QTest::addColumn("text"); + QTest::addColumn("xml"); + QTest::newRow("short-vanilla") << shortText << shortXml; + QTest::newRow("long-forge") << longText << longXml; + } + + void parseXml() { QFETCH(QString, ) } + + private: + LogParser m_parser; + + QList> parseLines(const QStringList& lines) + { + QList> out; + for (const auto& line : lines) + m_parser.appendLine(line); + auto items = m_parser.parseAvailable(); + for (const auto& item : items) { + if (std::holds_alternative(item)) { + auto entry = std::get(item); + auto msg = QString("[%1] [%2/%3] [%4]: %5") + .arg(entry.timestamp.toString("HH:mm:ss")) + .arg(entry.thread) + .arg(entry.levelText) + .arg(entry.logger) + .arg(entry.message); + msg = censorPrivateInfo(msg); + out.append(std::make_pair(entry.level, msg)); + } else if (std::holds_alternative(item)) { + auto msg = std::get(item).message; + level = m_instance->guessLevel(msg, level); + msg = censorPrivateInfo(msg); + out.append(std::make_pair(entry.level, msg)); + } + } + return out; + } +}; diff --git a/tests/testdata/TestLogs/TerraFirmaGreg-Modern-forge.text.log b/tests/testdata/TestLogs/TerraFirmaGreg-Modern-forge.text.log new file mode 100644 index 000000000..c0775ebb7 --- /dev/null +++ b/tests/testdata/TestLogs/TerraFirmaGreg-Modern-forge.text.log @@ -0,0 +1,947 @@ +Checking: MC_SLIM +Checking: MERGED_MAPPINGS +Checking: MAPPINGS +Checking: MC_EXTRA +Checking: MOJMAPS +Checking: PATCHED +Checking: MC_SRG +2025-04-18 12:47:23,932 main WARN Advanced terminal features are not available in this environment +[12:47:24] [main/INFO] [cp.mo.mo.Launcher/MODLAUNCHER]: ModLauncher running: args [--username, Ryexandrite, --version, 1.20.1, --gameDir, /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/instances/TerraFirmaGreg-Modern/minecraft, --assetsDir, /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/assets, --assetIndex, 5, --uuid, , --accessToken, ❄❄❄❄❄❄❄❄, --userType, msa, --versionType, release, --launchTarget, forgeclient, --fml.forgeVersion, 47.2.6, --fml.mcVersion, 1.20.1, --fml.forgeGroup, net.minecraftforge, --fml.mcpVersion, 20230612.114412, --width, 854, --height, 480] +[12:47:24] [main/INFO] [cp.mo.mo.Launcher/MODLAUNCHER]: ModLauncher 10.0.9+10.0.9+main.dcd20f30 starting: java version 17.0.8 by Microsoft; OS Linux arch amd64 version 6.6.85 +[12:47:24] [main/INFO] [ne.mi.fm.lo.ImmediateWindowHandler/]: Loading ImmediateWindowProvider fmlearlywindow +[12:47:24] [main/INFO] [EARLYDISPLAY/]: Trying GL version 4.6 +[12:47:24] [main/INFO] [EARLYDISPLAY/]: Requested GL version 4.6 got version 4.6 +[12:47:24] [main/INFO] [mixin/]: SpongePowered MIXIN Subsystem Version=0.8.5 Source=union:/home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/libraries/org/spongepowered/mixin/0.8.5/mixin-0.8.5.jar%23140!/ Service=ModLauncher Env=CLIENT +[12:47:24] [pool-2-thread-1/INFO] [EARLYDISPLAY/]: GL info: AMD Radeon RX 5700 XT (radeonsi, navi10, LLVM 19.1.7, DRM 3.54, 6.6.85) GL version 4.6 (Core Profile) Mesa 25.0.3 (git-c3afa2a74f), AMD +[12:47:25] [main/WARN] [ne.mi.fm.lo.mo.ModFileParser/LOADING]: Mod file /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/libraries/net/minecraftforge/fmlcore/1.20.1-47.2.6/fmlcore-1.20.1-47.2.6.jar is missing mods.toml file +[12:47:25] [main/WARN] [ne.mi.fm.lo.mo.ModFileParser/LOADING]: Mod file /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/libraries/net/minecraftforge/javafmllanguage/1.20.1-47.2.6/javafmllanguage-1.20.1-47.2.6.jar is missing mods.toml file +[12:47:25] [main/WARN] [ne.mi.fm.lo.mo.ModFileParser/LOADING]: Mod file /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/libraries/net/minecraftforge/lowcodelanguage/1.20.1-47.2.6/lowcodelanguage-1.20.1-47.2.6.jar is missing mods.toml file +[12:47:25] [main/WARN] [ne.mi.fm.lo.mo.ModFileParser/LOADING]: Mod file /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/libraries/net/minecraftforge/mclanguage/1.20.1-47.2.6/mclanguage-1.20.1-47.2.6.jar is missing mods.toml file +[12:47:25] [main/WARN] [ne.mi.ja.se.JarSelector/]: Attempted to select two dependency jars from JarJar which have the same identification: Mod File: and Mod File: . Using Mod File: +[12:47:25] [main/INFO] [ne.mi.fm.lo.mo.JarInJarDependencyLocator/]: Found 28 dependencies adding them to mods collection +[12:47:28] [main/ERROR] [mixin/]: Mixin config dynamiclightsreforged.mixins.json does not specify "minVersion" property +[12:47:28] [main/INFO] [mixin/]: Compatibility level set to JAVA_17 +[12:47:28] [main/ERROR] [mixin/]: Mixin config mixins.satin.client.json does not specify "minVersion" property +[12:47:28] [main/ERROR] [mixin/]: Mixin config firstperson.mixins.json does not specify "minVersion" property +[12:47:28] [main/ERROR] [mixin/]: Mixin config yacl.mixins.json does not specify "minVersion" property +[12:47:28] [main/INFO] [cp.mo.mo.LaunchServiceHandler/MODLAUNCHER]: Launching target 'forgeclient' with arguments [--version, 1.20.1, --gameDir, /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/instances/TerraFirmaGreg-Modern/minecraft, --assetsDir, /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/assets, --uuid, , --username, Ryexandrite, --assetIndex, 5, --accessToken, ❄❄❄❄❄❄❄❄, --userType, msa, --versionType, release, --width, 854, --height, 480] +[12:47:28] [main/INFO] [co.ab.sa.co.Saturn/]: Loaded Saturn config file with 4 configurable options +[12:47:28] [main/INFO] [ModernFix/]: Loaded configuration file for ModernFix 5.18.1+mc1.20.1: 83 options available, 1 override(s) found +[12:47:28] [main/WARN] [ModernFix/]: Option 'mixin.perf.thread_priorities' overriden (by mods [smoothboot]) to 'false' +[12:47:28] [main/INFO] [ModernFix/]: Applying Nashorn fix +[12:47:28] [main/INFO] [ModernFix/]: Applied Forge config corruption patch +[12:47:28] [main/INFO] [fpsreducer/]: OptiFine was NOT detected. +[12:47:28] [main/INFO] [fpsreducer/]: OptiFabric was NOT detected. +[12:47:28] [main/WARN] [EmbeddiumConfig/]: Mod 'tfc' attempted to override option 'mixin.features.fast_biome_colors', which doesn't exist, ignoring +[12:47:28] [main/INFO] [Embeddium/]: Loaded configuration file for Embeddium: 205 options available, 3 override(s) found +[12:47:28] [main/INFO] [Embeddium-GraphicsAdapterProbe/]: Searching for graphics cards... +[12:47:28] [main/INFO] [Embeddium-GraphicsAdapterProbe/]: Found graphics card: GraphicsAdapterInfo[vendor=AMD, name=Navi 10 [Radeon RX 5600 OEM/5600 XT / 5700/5700 XT], version=unknown] +[12:47:28] [main/WARN] [Embeddium-Workarounds/]: Sodium has applied one or more workarounds to prevent crashes or other issues on your system: [NO_ERROR_CONTEXT_UNSUPPORTED] +[12:47:28] [main/WARN] [Embeddium-Workarounds/]: This is not necessarily an issue, but it may result in certain features or optimizations being disabled. You can sometimes fix these issues by upgrading your graphics driver. +[12:47:28] [main/INFO] [Radium Config/]: Loaded configuration file for Radium: 125 options available, 7 override(s) found +[12:47:28] [main/WARN] [mixin/]: Reference map 'carpeted-common-refmap.json' for carpeted-common.mixins.json could not be read. If this is a development environment you can ignore this message +[12:47:28] [main/WARN] [mixin/]: Reference map 'carpeted-forge-refmap.json' for carpeted.mixins.json could not be read. If this is a development environment you can ignore this message +[12:47:28] [main/WARN] [mixin/]: Reference map 'emi-forge-refmap.json' for emi-forge.mixins.json could not be read. If this is a development environment you can ignore this message +[12:47:28] [main/WARN] [mixin/]: Reference map 'ftb-filter-system-common-refmap.json' for ftbfiltersystem-common.mixins.json could not be read. If this is a development environment you can ignore this message +[12:47:28] [main/WARN] [mixin/]: Reference map 'ftb-filter-system-forge-refmap.json' for ftbfiltersystem.mixins.json could not be read. If this is a development environment you can ignore this message +[12:47:28] [main/INFO] [Puzzles Lib/]: Loading 160 mods: + - additionalplacements 1.8.0 + - ae2 15.2.13 + - ae2insertexportcard 1.20.1-1.3.0 + - ae2netanalyser 1.20-1.0.6-forge + - ae2wtlib 15.2.3-forge + - aiimprovements 0.5.2 + - ambientsounds 6.1.1 + - architectury 9.2.14 + - astikorcarts 1.1.8 + - attributefix 21.0.4 + - balm 7.3.9 + \-- kuma_api 20.1.8 + - barrels_2012 2.1 + - betterf3 7.0.2 + - betterfoliage 5.0.2 + - betterpingdisplay 1.1 + - betterthirdperson 1.9.0 + - blur 3.1.1 + \-- satin 1.20.1+1.15.0-SNAPSHOT + - carpeted 1.20-1.4 + - carryon 2.1.2.7 + \-- mixinextras 0.2.0-beta.6 + - catalogue 1.8.0 + - chat_heads 0.13.9 + - cherishedworlds 6.1.7+1.20.1 + - clienttweaks 11.1.0 + - cloth_config 11.1.136 + - clumps 12.0.0.4 + - computercraft 1.113.1 + - controlling 12.0.2 + - coralstfc 1.0.0 + - corpse 1.20.1-1.0.19 + - cosmeticarmorreworked 1.20.1-v1a + - craftingtweaks 18.2.5 + - craftpresence 2.5.0 + - create 0.5.1.f + \-- flywheel 0.6.10-7 + - create_connected 0.8.2-mc1.20.1 + - createaddition 1.20.1-1.2.4c + - creativecore 2.12.15 + - cucumber 7.0.12 + - cupboard 1.20.1-2.7 + - curios 5.10.0+1.20.1 + - defaultoptions 18.0.1 + - do_a_barrel_roll 3.5.6+1.20.1 + - drippyloadingscreen 3.0.1 + - dynamiclightsreforged 1.20.1_v1.6.0 + - embeddium 0.3.19+mc1.20.1 + \-- rubidium 0.7.1 + - embeddiumplus 1.2.12 + - emi 1.1.7+1.20.1+forge + - enhancedvisuals 1.8.1 + - etched 3.0.2 + - everycomp 1.20-2.7.12 + - expatternprovider 1.20-1.1.14-forge + - exposure 1.7.7 + - fallingtrees 0.12.7 + - fancymenu 3.2.3 + - ferritecore 6.0.1 + - firmaciv 0.2.10-alpha-1.20.1 + - firmalife 2.1.15 + - firstperson 2.4.5 + - flickerfix 4.0.1 + - forge 47.2.6 + - fpsreducer 1.20-2.5 + - framedblocks 9.3.1 + - ftbbackups2 1.0.23 + - ftbessentials 2001.2.2 + - ftbfiltersystem 1.0.2 + - ftblibrary 2001.2.4 + - ftbquests 2001.4.8 + - ftbranks 2001.1.3 + - ftbteams 2001.3.0 + - ftbxmodcompat 2.1.1 + - gcyr 0.1.8 + - getittogetherdrops 1.3 + - glodium 1.20-1.5-forge + - gtceu 1.2.3.a + |-- configuration 2.2.0 + \-- ldlib 1.0.25.j + - hangglider 8.0.1 + - immediatelyfast 1.2.18+1.20.4 + - inventoryhud 3.4.26 + - invtweaks 1.1.0 + - itemphysiclite 1.6.5 + - jade 11.9.4+forge + - jadeaddons 5.2.2 + - jei 15.3.0.8 + - konkrete 1.8.0 + - ksyxis 1.3.2 + - kubejs 2001.6.5-build.14 + - kubejs_create 2001.2.5-build.2 + - kubejs_tfc 1.20.1-1.1.3 + - letmedespawn 1.3.2b + - lootjs 1.20.1-2.12.0 + - megacells 2.4.4-1.20.1 + - melody 1.0.2 + - memoryleakfix 1.1.5 + - merequester 1.20.1-1.1.5 + - minecraft 1.20.1 + - modelfix 1.15 + - modernfix 5.18.1+mc1.20.1 + - moonlight 1.20-2.13.51 + - morered 4.0.0.4 + |-- jumbofurnace 4.0.0.5 + \-- useitemonblockevent 1.0.0.2 + - mousetweaks 2.25.1 + - myserveriscompatible 1.0 + - nanhealthfixer 1.20.1-0.0.1 + - nerb 0.4.1 + - noisium 2.3.0+mc1.20-1.20.1 + - noreportbutton 1.5.0 + - notenoughanimations 1.7.6 + - octolib 0.4.2 + - oculus 1.7.0 + - openpartiesandclaims 0.23.2 + - packetfixer 1.4.2 + - pandalib 0.4.2 + - patchouli 1.20.1-84-FORGE + - pickupnotifier 8.0.0 + - placebo 8.6.2 + - playerrevive 2.0.27 + - polylib 2000.0.3-build.143 + - puzzleslib 8.1.23 + \-- puzzlesaccessapi 8.0.7 + - radium 0.12.3+git.50c5c33 + - railways 1.6.4+forge-mc1.20.1 + - recipeessentials 1.20.1-3.6 + - rhino 2001.2.2-build.18 + - saturn 0.1.3 + - searchables 1.0.3 + - shimmer 1.20.1-0.2.4 + - showcaseitem 1.20.1-1.2 + - simplylight 1.20.1-1.4.6-build.50 + - smoothboot 0.0.4 + - sophisticatedbackpacks 3.20.5.1044 + - sophisticatedcore 0.6.22.611 + - supermartijn642configlib 1.1.8 + - supermartijn642corelib 1.1.17 + - tfc 3.2.12 + - tfc_tumbleweed 1.2.2 + - tfcagedalcohol 2.1 + - tfcambiental 1.20.1-3.3.0 + - tfcastikorcarts 1.1.8.2 + - tfcchannelcasting 0.2.3-beta + - tfcea 0.0.2 + - tfcgroomer 1.20.1-0.1.2 + - tfchotornot 1.0.4 + - tfcvesseltooltip 1.1 + - tfg 0.5.9 + - toofast 0.4.3.5 + - toolbelt 1.20.01 + - treetap 1.20.1-0.4.0 + - tumbleweed 0.5.5 + - unilib 1.0.2 + - uteamcore 5.1.4.312 + - waterflasks 3.0.3 + - xaerominimap 24.4.0 + - xaeroworldmap 1.39.0 + - yeetusexperimentus 2.3.1-build.6+mc1.20.1 + - yet_another_config_lib_v3 3.5.0+1.20.1-forge +[12:47:28] [main/WARN] [mixin/]: Reference map 'packetfixer-forge-forge-refmap.json' for packetfixer-forge.mixins.json could not be read. If this is a development environment you can ignore this message +[12:47:28] [main/WARN] [mixin/]: Reference map 'tfchotornot.refmap.json' for tfchotornot.mixins.json could not be read. If this is a development environment you can ignore this message +[12:47:29] [main/INFO] [ne.mi.co.Co.placebo/COREMODLOG]: Patching IForgeItemStack#getEnchantmentLevel +[12:47:29] [main/INFO] [ne.mi.co.Co.placebo/COREMODLOG]: Patching IForgeItemStack#getEnchantmentLevel +[12:47:29] [main/WARN] [mixin/]: Error loading class: mezz/modnametooltip/TooltipEventHandler (java.lang.ClassNotFoundException: mezz.modnametooltip.TooltipEventHandler) +[12:47:29] [main/WARN] [mixin/]: Error loading class: me/shedaniel/rei/impl/client/ClientHelperImpl (java.lang.ClassNotFoundException: me.shedaniel.rei.impl.client.ClientHelperImpl) +[12:47:29] [main/WARN] [mixin/]: Error loading class: me/shedaniel/rei/impl/client/gui/ScreenOverlayImpl (java.lang.ClassNotFoundException: me.shedaniel.rei.impl.client.gui.ScreenOverlayImpl) +[12:47:29] [main/INFO] [co.cu.Cupboard/]: Loaded config for: recipeessentials.json +[12:47:30] [main/WARN] [mixin/]: Error loading class: loaderCommon/forge/com/seibel/distanthorizons/common/wrappers/worldGeneration/mimicObject/ChunkLoader (java.lang.ClassNotFoundException: loaderCommon.forge.com.seibel.distanthorizons.common.wrappers.worldGeneration.mimicObject.ChunkLoader) +[12:47:30] [main/INFO] [fpsreducer/]: bre2el.fpsreducer.mixin.RenderSystemMixin will be applied. +[12:47:30] [main/INFO] [fpsreducer/]: bre2el.fpsreducer.mixin.WindowMixin will NOT be applied because OptiFine was NOT detected. +[12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.ChatComponentMixin false false +[12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.ChatComponentMixin2 false false +[12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.ChatListenerMixin false false +[12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.ClientPacketListenerMixin false false +[12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.CommandSuggestionSuggestionsListMixin false false +[12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.ConnectionMixin false false +[12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.DownloadedPackSourceMixin false false +[12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.FontStringRenderOutputMixin false false +[12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.GuiMessageLineMixin false false +[12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.GuiMessageMixin false false +[12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.HttpTextureMixin false false +[12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.PlayerChatMessageMixin false false +[12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.SkinManagerMixin false false +[12:47:30] [main/WARN] [debug/]: dzwdz.chat_heads.mixin.compat.EmojifulMixin true false +[12:47:30] [main/WARN] [mixin/]: Error loading class: dan200/computercraft/shared/integration/jei/JEIComputerCraft (java.lang.ClassNotFoundException: dan200.computercraft.shared.integration.jei.JEIComputerCraft) +[12:47:30] [main/WARN] [mixin/]: @Mixin target dan200.computercraft.shared.integration.jei.JEIComputerCraft was not found tfg.mixins.json:common.cc.JEIComputerCraftMixin +[12:47:30] [main/WARN] [mixin/]: Error loading class: com/copycatsplus/copycats/content/copycat/slab/CopycatSlabBlock (java.lang.ClassNotFoundException: com.copycatsplus.copycats.content.copycat.slab.CopycatSlabBlock) +[12:47:30] [main/WARN] [mixin/]: @Mixin target com.copycatsplus.copycats.content.copycat.slab.CopycatSlabBlock was not found create_connected.mixins.json:compat.CopycatBlockMixin +[12:47:30] [main/WARN] [mixin/]: Error loading class: com/copycatsplus/copycats/content/copycat/board/CopycatBoardBlock (java.lang.ClassNotFoundException: com.copycatsplus.copycats.content.copycat.board.CopycatBoardBlock) +[12:47:30] [main/WARN] [mixin/]: @Mixin target com.copycatsplus.copycats.content.copycat.board.CopycatBoardBlock was not found create_connected.mixins.json:compat.CopycatBlockMixin +[12:47:30] [main/WARN] [mixin/]: Error loading class: me/jellysquid/mods/lithium/common/ai/pathing/PathNodeDefaults (java.lang.ClassNotFoundException: me.jellysquid.mods.lithium.common.ai.pathing.PathNodeDefaults) +[12:47:30] [main/WARN] [Radium Config/]: Force-disabling mixin 'alloc.blockstate.StateMixin' as option 'mixin.alloc.blockstate' (added by mods [ferritecore]) disables it and children +[12:47:30] [main/WARN] [Radium Config/]: Force-disabling mixin 'entity.collisions.fluid.EntityMixin' as option 'mixin.entity.collisions' (added by user configuration) disables it and children +[12:47:30] [main/WARN] [Radium Config/]: Force-disabling mixin 'entity.collisions.intersection.WorldMixin' as option 'mixin.entity.collisions' (added by user configuration) disables it and children +[12:47:30] [main/WARN] [Radium Config/]: Force-disabling mixin 'entity.collisions.movement.EntityMixin' as option 'mixin.entity.collisions' (added by user configuration) disables it and children +[12:47:30] [main/WARN] [Radium Config/]: Force-disabling mixin 'entity.collisions.unpushable_cramming.AbstractMinecartEntityMixin' as option 'mixin.entity.collisions' (added by user configuration) disables it and children +[12:47:30] [main/WARN] [Radium Config/]: Force-disabling mixin 'entity.collisions.unpushable_cramming.BoatEntityMixin' as option 'mixin.entity.collisions' (added by user configuration) disables it and children +[12:47:30] [main/WARN] [Radium Config/]: Force-disabling mixin 'entity.collisions.unpushable_cramming.EntityMixin' as option 'mixin.entity.collisions' (added by user configuration) disables it and children +[12:47:30] [main/WARN] [Radium Config/]: Force-disabling mixin 'entity.collisions.unpushable_cramming.EntityPredicatesMixin' as option 'mixin.entity.collisions' (added by user configuration) disables it and children +[12:47:30] [main/WARN] [Radium Config/]: Force-disabling mixin 'entity.collisions.unpushable_cramming.EntityTrackingSectionMixin' as option 'mixin.entity.collisions' (added by user configuration) disables it and children +[12:47:30] [main/WARN] [Radium Config/]: Force-disabling mixin 'entity.collisions.unpushable_cramming.LivingEntityMixin' as option 'mixin.entity.collisions' (added by user configuration) disables it and children +[12:47:30] [main/WARN] [Radium Config/]: Force-disabling mixin 'world.player_chunk_tick.ThreadedAnvilChunkStorageMixin' as option 'mixin.world.player_chunk_tick' (added by user configuration) disables it and children +[12:47:30] [main/WARN] [mixin/]: Error loading class: org/cyclops/integrateddynamics/block/BlockCable (java.lang.ClassNotFoundException: org.cyclops.integrateddynamics.block.BlockCable) +[12:47:30] [main/WARN] [mixin/]: @Mixin target org.cyclops.integrateddynamics.block.BlockCable was not found mixins.epp.json:MixinBlockCable +[12:47:30] [main/WARN] [mixin/]: Error loading class: blusunrize/immersiveengineering/api/wires/GlobalWireNetwork (java.lang.ClassNotFoundException: blusunrize.immersiveengineering.api.wires.GlobalWireNetwork) +[12:47:30] [main/WARN] [mixin/]: @Mixin target blusunrize.immersiveengineering.api.wires.GlobalWireNetwork was not found mixins.epp.json:MixinGlobalWireNetwork +[12:47:30] [main/WARN] [mixin/]: Error loading class: weather2/weathersystem/storm/TornadoHelper (java.lang.ClassNotFoundException: weather2.weathersystem.storm.TornadoHelper) +[12:47:30] [main/WARN] [mixin/]: @Mixin target weather2.weathersystem.storm.TornadoHelper was not found tfc_tumbleweed.mixins.json:TornadoHelperMixin +[12:47:30] [main/WARN] [mixin/]: Error loading class: weather2/weathersystem/storm/TornadoHelper (java.lang.ClassNotFoundException: weather2.weathersystem.storm.TornadoHelper) +[12:47:30] [main/WARN] [mixin/]: @Mixin target weather2.weathersystem.storm.TornadoHelper was not found tfc_tumbleweed.mixins.json:client.TornadoHelperMixin +[12:47:30] [main/INFO] [memoryleakfix/]: [MemoryLeakFix] Will be applying 3 memory leak fixes! +[12:47:30] [main/INFO] [memoryleakfix/]: [MemoryLeakFix] Currently enabled memory leak fixes: [targetEntityLeak, biomeTemperatureLeak, hugeScreenshotLeak] +[12:47:31] [main/WARN] [Embeddium/]: Force-disabling mixin 'features.render.world.sky.WorldRendererMixin' as rule 'mixin.features.render.world.sky' (added by mods [oculus, tfc]) disables it and children +[12:47:31] [main/WARN] [Embeddium/]: Force-disabling mixin 'features.render.world.sky.ClientWorldMixin' as rule 'mixin.features.render.world.sky' (added by mods [oculus, tfc]) disables it and children +[12:47:31] [main/WARN] [Embeddium/]: Force-disabling mixin 'features.render.world.sky.BackgroundRendererMixin' as rule 'mixin.features.render.world.sky' (added by mods [oculus, tfc]) disables it and children +[12:47:31] [main/WARN] [Embeddium/]: Force-disabling mixin 'features.render.gui.font.GlyphRendererMixin' as rule 'mixin.features.render.gui.font' (added by mods [oculus]) disables it and children +[12:47:31] [main/WARN] [Embeddium/]: Force-disabling mixin 'features.render.gui.font.FontSetMixin' as rule 'mixin.features.render.gui.font' (added by mods [oculus]) disables it and children +[12:47:31] [main/WARN] [Embeddium/]: Force-disabling mixin 'features.render.entity.shadows.EntityRenderDispatcherMixin' as rule 'mixin.features.render.entity' (added by mods [oculus]) disables it and children +[12:47:31] [main/WARN] [Embeddium/]: Force-disabling mixin 'features.render.entity.fast_render.ModelPartMixin' as rule 'mixin.features.render.entity' (added by mods [oculus]) disables it and children +[12:47:31] [main/WARN] [Embeddium/]: Force-disabling mixin 'features.render.entity.fast_render.CuboidMixin' as rule 'mixin.features.render.entity' (added by mods [oculus]) disables it and children +[12:47:31] [main/WARN] [Embeddium/]: Force-disabling mixin 'features.render.entity.cull.EntityRendererMixin' as rule 'mixin.features.render.entity' (added by mods [oculus]) disables it and children +[12:47:31] [main/WARN] [mixin/]: Error loading class: org/jetbrains/annotations/ApiStatus$Internal (java.lang.ClassNotFoundException: org.jetbrains.annotations.ApiStatus$Internal) +[12:47:31] [main/INFO] [MixinExtras|Service/]: Initializing MixinExtras via com.llamalad7.mixinextras.service.MixinExtrasServiceImpl(version=0.4.1). +[12:47:31] [main/INFO] [Smooth Boot (Reloaded)/]: Smooth Boot (Reloaded) config initialized +[12:47:31] [main/WARN] [mixin/]: Static binding violation: PRIVATE @Overwrite method m_216202_ in modernfix-forge.mixins.json:perf.tag_id_caching.TagOrElementLocationMixin cannot reduce visibiliy of PUBLIC target method, visibility will be upgraded. +[12:47:32] [pool-4-thread-1/INFO] [minecraft/Bootstrap]: ModernFix reached bootstrap stage (9.773 s after launch) +[12:47:32] [pool-4-thread-1/WARN] [mixin/]: @Final field delegatesByName:Ljava/util/Map; in modernfix-forge.mixins.json:perf.forge_registry_alloc.ForgeRegistryMixin should be final +[12:47:32] [pool-4-thread-1/WARN] [mixin/]: @Final field delegatesByValue:Ljava/util/Map; in modernfix-forge.mixins.json:perf.forge_registry_alloc.ForgeRegistryMixin should be final +[12:47:32] [pool-4-thread-1/INFO] [ne.mi.co.Co.placebo/COREMODLOG]: Patching IForgeItemStack#getEnchantmentLevel +[12:47:32] [pool-4-thread-1/INFO] [ne.mi.co.Co.placebo/COREMODLOG]: Patching IForgeItemStack#getEnchantmentLevel +[12:47:32] [pool-4-thread-1/INFO] [ModernFix/]: Injecting BlockStateBase cache population hook into getNeighborPathNodeType from me.jellysquid.mods.lithium.mixin.ai.pathing.AbstractBlockStateMixin +[12:47:32] [pool-4-thread-1/INFO] [ModernFix/]: Injecting BlockStateBase cache population hook into getPathNodeType from me.jellysquid.mods.lithium.mixin.ai.pathing.AbstractBlockStateMixin +[12:47:32] [pool-4-thread-1/INFO] [ModernFix/]: Injecting BlockStateBase cache population hook into getAllFlags from me.jellysquid.mods.lithium.mixin.util.block_tracking.AbstractBlockStateMixin +[12:47:32] [pool-4-thread-1/WARN] [mixin/]: Method overwrite conflict for m_6104_ in embeddium.mixins.json:features.options.render_layers.LeavesBlockMixin, previously written by me.srrapero720.embeddiumplus.mixins.impl.leaves_culling.LeavesBlockMixin. Skipping method. +[12:47:32] [pool-4-thread-1/INFO] [ne.mi.co.Co.placebo/COREMODLOG]: Patching IForgeItemStack#getEnchantmentLevel +[12:47:32] [pool-4-thread-1/INFO] [ne.mi.co.Co.placebo/COREMODLOG]: Patching IForgeItemStack#getEnchantmentLevel +[12:47:33] [pool-4-thread-1/INFO] [minecraft/Bootstrap]: Vanilla bootstrap took 779 milliseconds +[12:47:34] [pool-4-thread-1/WARN] [mixin/]: Method overwrite conflict for m_47505_ in lithium.mixins.json:world.temperature_cache.BiomeMixin, previously written by org.embeddedt.modernfix.common.mixin.perf.remove_biome_temperature_cache.BiomeMixin. Skipping method. +[12:47:34] [pool-4-thread-1/INFO] [co.al.me.MERequester/]: Registering content +[12:47:35] [Render thread/WARN] [minecraft/VanillaPackResourcesBuilder]: Assets URL 'union:/home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/libraries/net/minecraft/client/1.20.1-20230612.114412/client-1.20.1-20230612.114412-srg.jar%23444!/assets/.mcassetsroot' uses unexpected schema +[12:47:35] [Render thread/WARN] [minecraft/VanillaPackResourcesBuilder]: Assets URL 'union:/home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/libraries/net/minecraft/client/1.20.1-20230612.114412/client-1.20.1-20230612.114412-srg.jar%23444!/data/.mcassetsroot' uses unexpected schema +[12:47:35] [Render thread/INFO] [mojang/YggdrasilAuthenticationService]: Environment: authHost='https://authserver.mojang.com', accountsHost='https://api.mojang.com', sessionHost='https://sessionserver.mojang.com', servicesHost='https://api.minecraftservices.com', name='PROD' +[12:47:35] [Render thread/INFO] [minecraft/Minecraft]: Setting user: Ryexandrite +[12:47:35] [Render thread/INFO] [ModernFix/]: Bypassed Mojang DFU +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant is searching for constants in method with descriptor (Lnet/minecraft/network/chat/Component;Lnet/minecraft/client/GuiMessageTag;)V +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found STRING constant: value = , stringValue = null +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found a matching constant TYPE at ordinal 0 +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found LdcInsn +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found STRING constant: value = \\r, stringValue = null +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found a matching constant TYPE at ordinal 1 +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found LdcInsn \\r +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found STRING constant: value = +, stringValue = null +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found a matching constant TYPE at ordinal 2 +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found LdcInsn + +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found STRING constant: value = \\n, stringValue = null +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found a matching constant TYPE at ordinal 3 +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found LdcInsn \\n +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found CLASS constant: value = Ljava/lang/String;, typeValue = null +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found STRING constant: value = [{}] [CHAT] {}, stringValue = null +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found a matching constant TYPE at ordinal 4 +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found LdcInsn [{}] [CHAT] {} +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found STRING constant: value = [CHAT] {}, stringValue = null +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found a matching constant TYPE at ordinal 5 +[12:47:35] [Render thread/INFO] [mixin/]: BeforeConstant found LdcInsn [CHAT] {} +[12:47:35] [Render thread/INFO] [defaultoptions/]: Loaded default options for extra-folder +[12:47:35] [Render thread/INFO] [ModernFix/]: Instantiating Mojang DFU +[12:47:36] [Render thread/INFO] [minecraft/Minecraft]: Backend library: LWJGL version 3.3.1 build 7 +[12:47:36] [Render thread/INFO] [KubeJS/]: Loaded client.properties +[12:47:36] [Render thread/INFO] [Embeddium-PostlaunchChecks/]: OpenGL Vendor: AMD +[12:47:36] [Render thread/INFO] [Embeddium-PostlaunchChecks/]: OpenGL Renderer: AMD Radeon RX 5700 XT (radeonsi, navi10, LLVM 19.1.7, DRM 3.54, 6.6.85) +[12:47:36] [Render thread/INFO] [Embeddium-PostlaunchChecks/]: OpenGL Version: 4.6 (Core Profile) Mesa 25.0.3 (git-c3afa2a74f) +[12:47:36] [Render thread/WARN] [Embeddium++/Config]: Loading Embeddium++Config +[12:47:36] [Render thread/INFO] [Embeddium++/Config]: Updating config cache +[12:47:36] [Render thread/INFO] [Embeddium++/Config]: Cache updated successfully +[12:47:36] [Render thread/INFO] [ImmediatelyFast/]: Initializing ImmediatelyFast 1.2.18+1.20.4 on AMD Radeon RX 5700 XT (radeonsi, navi10, LLVM 19.1.7, DRM 3.54, 6.6.85) (AMD) with OpenGL 4.6 (Core Profile) Mesa 25.0.3 (git-c3afa2a74f) +[12:47:36] [Render thread/INFO] [ImmediatelyFast/]: AMD GPU detected. Enabling coherent buffer mapping +[12:47:36] [Datafixer Bootstrap/INFO] [mojang/DataFixerBuilder]: 188 Datafixer optimizations took 85 milliseconds +[12:47:36] [Render thread/INFO] [ImmediatelyFast/]: Found Iris/Oculus 1.7.0. Enabling compatibility. +[12:47:36] [Render thread/INFO] [Oculus/]: Debug functionality is disabled. +[12:47:36] [Render thread/INFO] [Oculus/]: OpenGL 4.5 detected, enabling DSA. +[12:47:36] [Render thread/INFO] [Oculus/]: Shaders are disabled because no valid shaderpack is selected +[12:47:36] [Render thread/INFO] [Oculus/]: Shaders are disabled +[12:47:36] [modloading-worker-0/INFO] [dynamiclightsreforged/]: [LambDynLights] Initializing Dynamic Lights Reforged... +[12:47:36] [modloading-worker-0/INFO] [LowDragLib/]: LowDragLib is initializing on platform: Forge +[12:47:36] [modloading-worker-0/INFO] [in.u_.u_.ut.ve.JarSignVerifier/]: Mod uteamcore is signed with a valid certificate. +[12:47:36] [modloading-worker-0/INFO] [de.ke.me.Melody/]: [MELODY] Loading Melody background audio library.. +[12:47:36] [modloading-worker-0/INFO] [Puzzles Lib/]: Constructing common components for pickupnotifier:main +[12:47:36] [modloading-worker-0/WARN] [mixin/]: Static binding violation: PRIVATE @Overwrite method m_109501_ in embeddium.mixins.json:core.render.world.WorldRendererMixin cannot reduce visibiliy of PUBLIC target method, visibility will be upgraded. +[12:47:36] [modloading-worker-0/INFO] [de.ke.ko.Konkrete/]: [KONKRETE] Successfully initialized! +[12:47:36] [modloading-worker-0/INFO] [de.ke.ko.Konkrete/]: [KONKRETE] Server-side libs ready to use! +[12:47:36] [modloading-worker-0/INFO] [Puzzles Lib/]: Constructing client components for pickupnotifier:main +[12:47:36] [modloading-worker-0/WARN] [mixin/]: Static binding violation: PRIVATE @Overwrite method m_215924_ in modernfix-forge.mixins.json:perf.tag_id_caching.TagEntryMixin cannot reduce visibiliy of PUBLIC target method, visibility will be upgraded. +[12:47:36] [modloading-worker-0/INFO] [Additional Placements/]: Attempting to manually load Additional Placements config early. +[12:47:36] [modloading-worker-0/INFO] [Additional Placements/]: manual config load successful. +[12:47:36] [modloading-worker-0/WARN] [Additional Placements/]: During block registration you may recieve several reports of "Potentially Dangerous alternative prefix `additionalplacements`". Ignore these, they are intended. +[12:47:36] [modloading-worker-0/INFO] [Puzzles Lib/]: Constructing common components for hangglider:main +[12:47:36] [modloading-worker-0/INFO] [noisium/]: Loading Noisium. +[12:47:36] [modloading-worker-0/INFO] [co.cu.Cupboard/]: Loaded config for: cupboard.json +[12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id architectury:sync_ids +[12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id architectury:sync_ids +[12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id pandalib:config_sync +[12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id pandalib:config_sync +[12:47:36] [modloading-worker-0/INFO] [fallingtrees | Config/]: Successfully loaded config 'fallingtrees_client' +[12:47:36] [modloading-worker-0/INFO] [fallingtrees | Config/]: Successfully saved config 'fallingtrees_client' +[12:47:36] [modloading-worker-0/INFO] [Puzzles Lib/]: Constructing client components for hangglider:main +[12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id polylib:container_to_client +[12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id polylib:tile_to_client +[12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id polylib:container_packet_server +[12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id polylib:tile_data_server +[12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id polylib:tile_packet_server +[12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftblibrary:edit_nbt +[12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftblibrary:edit_nbt_response +[12:47:36] [modloading-worker-0/INFO] [fallingtrees | Config/]: Successfully loaded config 'fallingtrees_common' +[12:47:36] [modloading-worker-0/INFO] [fallingtrees | Config/]: Successfully saved config 'fallingtrees_common' +[12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftblibrary:sync_known_server_registries +[12:47:36] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftblibrary:edit_config +[12:47:37] [UniLib/INFO] [unilib/]: Starting version check for "craftpresence" (MC 1.20.1) at "https://raw.githubusercontent.com/CDAGaming/VersionLibrary/master/CraftPresence/update.json" +[12:47:37] [UniLib/INFO] [unilib/]: Starting version check for "unilib" (MC 1.20.1) at "https://raw.githubusercontent.com/CDAGaming/VersionLibrary/master/UniLib/update.json" +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbessentials:update_tab_name +[12:47:37] [modloading-worker-0/INFO] [invtweaks/]: Registered 2 network packets +[12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Loaded common.properties +[12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Loaded dev.properties +[12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Looking for KubeJS plugins... +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbteams:sync_teams +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbteams:sync_message_history +[12:47:37] [modloading-worker-0/INFO] [GregTechCEu/]: GregTechCEu is initializing on platform: Forge +[12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Found plugin source kubejs +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbteams:open_gui +[12:47:37] [CraftPresence/INFO] [craftpresence/]: Configuration settings have been saved and reloaded successfully! +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbteams:open_my_team_gui +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbteams:update_settings +[12:47:37] [modloading-worker-0/WARN] [KubeJS/]: Plugin dev.latvian.mods.kubejs.integration.forge.gamestages.GameStagesIntegration does not have required mod gamestages loaded, skipping +[12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Found plugin source ldlib +[12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Found plugin source exposure +[12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Found plugin source tfg +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbteams:update_settings_response +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbteams:send_message +[12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Found plugin source ftbxmodcompat +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbteams:send_message_response +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbteams:update_presence +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbteams:create_party +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbteams:player_gui_operation +[12:47:37] [modloading-worker-0/WARN] [KubeJS/]: Plugin dev.ftb.mods.ftbxmodcompat.ftbchunks.kubejs.FTBChunksKubeJSPlugin does not have required mod ftbchunks loaded, skipping +[12:47:37] [modloading-worker-0/INFO] [de.ke.dr.DrippyLoadingScreen/]: [DRIPPY LOADING SCREEN] Loading v3.0.1 in client-side mode on FORGE! +[12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Found plugin source lootjs +[12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Found plugin source cucumber +[12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Found plugin source gtceu +[12:47:37] [modloading-worker-0/INFO] [ne.dr.tf.TerraFirmaCraft/]: Initializing TerraFirmaCraft +[12:47:37] [modloading-worker-0/INFO] [ne.dr.tf.TerraFirmaCraft/]: Options: Assertions Enabled = false, Boostrap = false, Test = false, Debug Logging = true +[12:47:37] [CraftPresence/INFO] [craftpresence/]: Checking Discord for available assets with Client Id: 1182610212121743470 +[12:47:37] [CraftPresence/INFO] [craftpresence/]: Originally coded by paulhobbel - https://github.com/paulhobbel +[12:47:37] [modloading-worker-0/INFO] [ne.mi.co.ForgeMod/FORGEMOD]: Forge mod loading, version 47.2.6, for MC 1.20.1 with MCP 20230612.114412 +[12:47:37] [modloading-worker-0/INFO] [ne.mi.co.MinecraftForge/FORGE]: MinecraftForge v47.2.6 Initialized +[12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Found plugin source gcyr +[12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Found plugin source kubejs_tfc +[12:47:37] [modloading-worker-0/WARN] [KubeJS/]: Plugin com.notenoughmail.kubejs_tfc.addons.precpros.PrecProsPlugin does not have required mod precisionprospecting loaded, skipping +[12:47:37] [modloading-worker-0/WARN] [KubeJS/]: Plugin com.notenoughmail.kubejs_tfc.addons.afc.AFCPlugin does not have required mod afc loaded, skipping +[12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Found plugin source kubejs_create +[Mouse Tweaks] Main.initialize() +[Mouse Tweaks] Initialized. +[12:47:37] [modloading-worker-0/INFO] [Every Compat/]: Loaded EveryCompat Create Module +[12:47:37] [modloading-worker-0/INFO] [Configuration/FileWatching]: Registered gtceu config for auto-sync function +[12:47:37] [modloading-worker-0/INFO] [Configuration/FileWatching]: Registered gtceu config for auto-sync function +[12:47:37] [modloading-worker-0/INFO] [Configuration/FileWatching]: Registered gcyr config for auto-sync function +[12:47:37] [modloading-worker-0/INFO] [GregTechCEu/]: High-Tier is Disabled. +[12:47:37] [modloading-worker-0/INFO] [ne.cr.ft.re.or.qu.im.StdSchedulerFactory/]: Using default implementation for ThreadExecutor +[12:47:37] [modloading-worker-0/INFO] [KubeJS/]: Done in 309.0 ms +[12:47:37] [CraftPresence/INFO] [craftpresence/]: 3 total assets detected! +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:sync_quests +[12:47:37] [modloading-worker-0/INFO] [Ksyxis/]: Ksyxis: Booting... (platform: Forge, manual: false) +[12:47:37] [modloading-worker-0/INFO] [Ksyxis/]: Ksyxis: Found Mixin library. (version: 0.8.5) +[12:47:37] [modloading-worker-0/INFO] [Ksyxis/]: Ksyxis: Ready. As always, this mod will speed up your world loading and might or might not break it. +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:sync_team_data +[12:47:37] [modloading-worker-0/INFO] [ne.cr.ft.re.or.qu.co.SchedulerSignalerImpl/]: Initialized Scheduler Signaller of type: class net.creeperhost.ftbbackups.repack.org.quartz.core.SchedulerSignalerImpl +[12:47:37] [modloading-worker-0/INFO] [ne.cr.ft.re.or.qu.co.QuartzScheduler/]: Quartz Scheduler v.2.0.2 created. +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:update_task_progress +[12:47:37] [modloading-worker-0/INFO] [ne.cr.ft.re.or.qu.si.RAMJobStore/]: RAMJobStore initialized. +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:submit_task +[12:47:37] [modloading-worker-0/INFO] [ne.cr.ft.re.or.qu.co.QuartzScheduler/]: Scheduler meta-data: Quartz Scheduler (v2.0.2) 'ftbbackups2' with instanceId 'NON_CLUSTERED' + Scheduler class: 'net.creeperhost.ftbbackups.repack.org.quartz.core.QuartzScheduler' - running locally. + NOT STARTED. + Currently in standby mode. + Number of jobs executed: 0 + Using thread pool 'net.creeperhost.ftbbackups.repack.org.quartz.simpl.SimpleThreadPool' - with 1 threads. + Using job-store 'net.creeperhost.ftbbackups.repack.org.quartz.simpl.RAMJobStore' - which does not support persistence. and is not clustered. + +[12:47:37] [modloading-worker-0/INFO] [ne.cr.ft.re.or.qu.im.StdSchedulerFactory/]: Quartz scheduler 'ftbbackups2' initialized from an externally provided properties instance. +[12:47:37] [modloading-worker-0/INFO] [ne.cr.ft.re.or.qu.im.StdSchedulerFactory/]: Quartz scheduler version: 2.0.2 +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:claim_reward +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:claim_reward_response +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:sync_editing_mode +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:get_emergency_items +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:create_other_team_data +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:claim_all_rewards +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:claim_choice_reward +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:display_completion_toast +[12:47:37] [modloading-worker-0/INFO] [ne.cr.ft.re.or.qu.co.QuartzScheduler/]: Scheduler ftbbackups2_$_NON_CLUSTERED started. +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:display_reward_toast +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:display_item_reward_toast +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:toggle_pinned +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:toggle_pinned_response +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:toggle_chapter_pinned +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:toggle_chapter_pinned_response +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:toggle_editing_mode +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:force_save +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:update_team_data +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:set_custom_image +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:object_started +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:object_completed +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:object_started_reset +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:object_completed_reset +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:sync_lock +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:reset_reward +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:team_data_changed +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:task_screen_config_req +[12:47:37] [modloading-worker-0/INFO] [co.jo.fl.ba.Backend/]: Oculus detected. +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:task_screen_config_resp +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:change_progress +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:create_object +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:create_object_response +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:create_task_at +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:delete_object +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:delete_object_response +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:edit_object +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:edit_object_response +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:move_chapter +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:move_chapter_response +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:move_quest +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:move_quest_response +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:change_chapter_group +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:change_chapter_group_response +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:move_chapter_group +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:move_chapter_group_response +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:sync_reward_blocking +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:copy_quest +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:copy_chapter_image +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:sync_structures_request +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:sync_structures_response +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbquests:request_team_data +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:sync_editor_permission +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:open_quest_book +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ftbquests:clear_display_cache +[12:47:37] [modloading-worker-0/INFO] [me.je.li.lo.PluginCaller/]: Sending ConfigManager... +[12:47:37] [modloading-worker-0/INFO] [me.je.li.lo.PluginCaller/]: Sending ConfigManager took 11.32 ms +[12:47:37] [modloading-worker-0/INFO] [MEGA Cells/]: Initialised items. +[12:47:37] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ftbfiltersystem:sync_filter +[12:47:37] [modloading-worker-0/INFO] [MEGA Cells/]: Initialised blocks. +[12:47:37] [modloading-worker-0/INFO] [MEGA Cells/]: Initialised block entities. +[12:47:37] [modloading-worker-0/INFO] [de.ke.fa.FancyMenu/]: [FANCYMENU] Loading v3.2.3 in client-side mode on FORGE! +[12:47:37] [modloading-worker-0/INFO] [showcaseitem/]: Loading config: /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/instances/TerraFirmaGreg-Modern/minecraft/config/showcaseitem-common.toml +[12:47:37] [modloading-worker-0/INFO] [showcaseitem/]: Built config: /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/instances/TerraFirmaGreg-Modern/minecraft/config/showcaseitem-common.toml +[12:47:37] [modloading-worker-0/INFO] [showcaseitem/]: Loaded config: /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/instances/TerraFirmaGreg-Modern/minecraft/config/showcaseitem-common.toml +[12:47:37] [modloading-worker-0/INFO] [FTB XMod Compat/]: [FTB Quests] Enabled KubeJS integration +[12:47:37] [modloading-worker-0/INFO] [me.tr.be.BetterF3Forge/]: [BetterF3] Starting... +[12:47:37] [modloading-worker-0/INFO] [me.tr.be.BetterF3Forge/]: [BetterF3] Loading... +[12:47:37] [modloading-worker-0/INFO] [de.to.pa.PacketFixer/]: Packet Fixer has been initialized successfully +[12:47:37] [modloading-worker-0/INFO] [YetAnotherConfigLib/]: Deserializing YACLConfig from '/home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/instances/TerraFirmaGreg-Modern/minecraft/config/yacl.json5' +[12:47:37] [modloading-worker-0/INFO] [Puzzles Lib/]: Constructing common components for puzzleslib:main +[12:47:37] [modloading-worker-0/INFO] [me.tr.be.BetterF3Forge/]: [BetterF3] All done! +[12:47:37] [modloading-worker-0/INFO] [Puzzles Lib/]: Constructing client components for puzzleslib:main +[12:47:37] [modloading-worker-0/INFO] [Rhino Script Remapper/]: Loading Rhino Minecraft remapper... +[12:47:37] [modloading-worker-0/INFO] [de.la.mo.rh.mo.ut.RhinoProperties/]: Rhino properties loaded. +[12:47:37] [modloading-worker-0/INFO] [Rhino Script Remapper/]: Loading mappings for 1.20.1 +[12:47:37] [modloading-worker-0/WARN] [mixin/]: @Inject(@At("INVOKE")) Shift.BY=2 on create_connected.mixins.json:sequencedgearshift.SequencedGearshiftScreenMixin::handler$cfa000$updateParamsOfRow exceeds the maximum allowed value: 0. Increase the value of maxShiftBy to suppress this warning. +[12:47:37] [modloading-worker-0/INFO] [Rhino Script Remapper/]: Done in 0.090 s +[12:47:38] [modloading-worker-0/INFO] [Railways/]: Registered bogey styles from railways +[12:47:38] [modloading-worker-0/INFO] [Railways/]: Registering data fixers +[12:47:38] [modloading-worker-0/WARN] [Railways/]: Skipping Datafixer Registration due to it being disabled in the config. +[12:47:38] [modloading-worker-0/INFO] [Railways/]: Registering tracks for Hex Casting +[12:47:38] [modloading-worker-0/INFO] [Railways/]: Registering tracks for Oh The Biomes You'll Go +[12:47:38] [modloading-worker-0/INFO] [Railways/]: Registering tracks for Blue Skies +[12:47:38] [modloading-worker-0/INFO] [Railways/]: Registering tracks for Twilight Forest +[12:47:38] [modloading-worker-0/INFO] [Railways/]: Registering tracks for Biomes O' Plenty +[12:47:38] [modloading-worker-0/INFO] [Railways/]: Registering tracks for Nature's Spirit +[12:47:38] [modloading-worker-0/INFO] [Railways/]: Registering tracks for Dreams and Desires +[12:47:38] [modloading-worker-0/INFO] [Railways/]: Registering tracks for Quark +[12:47:38] [modloading-worker-0/INFO] [Railways/]: Registering tracks for TerraFirmaCraft +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:main_startup_script.js in 0.058 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:tfc/constants.js in 0.024 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:horornot/constants.js in 0.0 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:minecraft/constants.js in 0.004 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:railways/constants.js in 0.001 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:gtceu/machines.js in 0.008 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:gtceu/constants.js in 0.0 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:gtceu/material_info.js in 0.002 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:gtceu/recipe_types.js in 0.0 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:gtceu/blocks.js in 0.001 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:gtceu/items.js in 0.0 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:framedblocks/constants.js in 0.0 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:firmalife/constants.js in 0.001 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:sophisticated_backpacks/constants.js in 0.0 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:more_red/constants.js in 0.0 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:mega_cells/constants.js in 0.0 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:create/constants.js in 0.002 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:ae2/constants.js in 0.001 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:create_additions/constants.js in 0.001 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:chisel_and_bits/constants.js in 0.0 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:extended_ae2/constants.js in 0.0 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:asticor_carts/constants.js in 0.0 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:firmaciv/constants.js in 0.0 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:firmaciv/blocks.js in 0.001 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:ftb_quests/constants.js in 0.0 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:tfg/fluids.js in 0.001 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:tfg/materials.js in 0.0 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:tfg/blocks.js in 0.001 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:tfg/items.js in 0.0 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded script startup_scripts:computer_craft/constants.js in 0.0 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: Loaded 30/30 KubeJS startup scripts in 0.721 s with 0 errors and 0 warnings +[12:47:38] [modloading-worker-0/INFO] [KubeJS Client/]: example.js#3: TerraFirmaGreg the best modpack in the world :) +[12:47:38] [modloading-worker-0/INFO] [KubeJS Client/]: Loaded script client_scripts:example.js in 0.0 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Client/]: Loaded script client_scripts:tooltips.js in 0.003 s +[12:47:38] [modloading-worker-0/INFO] [KubeJS Client/]: Loaded 2/2 KubeJS client scripts in 0.022 s with 0 errors and 0 warnings +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: tfg/blocks.js#48: Loaded Java class 'net.minecraft.world.level.block.AmethystClusterBlock' +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: tfg/blocks.js#49: Loaded Java class 'net.minecraft.world.level.block.Blocks' +[12:47:38] [modloading-worker-0/INFO] [KubeJS Startup/]: tfg/blocks.js#50: Loaded Java class 'net.minecraft.world.level.block.state.BlockBehaviour$Properties' +[12:47:38] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id kubejs:send_data_from_client +[12:47:38] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id kubejs:send_data_from_server +[12:47:38] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id kubejs:paint +[12:47:38] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id kubejs:add_stage +[12:47:38] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id kubejs:remove_stage +[12:47:38] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id kubejs:sync_stages +[12:47:38] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id kubejs:first_click +[12:47:38] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id kubejs:toast +[12:47:38] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id kubejs:reload_startup_scripts +[12:47:38] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id kubejs:display_server_errors +[12:47:38] [modloading-worker-0/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id kubejs:display_client_errors +[12:47:38] [Render thread/INFO] [GregTechCEu/]: GTCEu common proxy init! +[12:47:38] [Render thread/INFO] [GregTechCEu/]: Registering material registries +[12:47:38] [Render thread/INFO] [GregTechCEu/]: Registering GTCEu Materials +[12:47:38] [CraftPresence/INFO] [craftpresence/]: Attempting to connect to Discord (1/10)... +[12:47:39] [Render thread/INFO] [GregTechCEu/]: Registering addon Materials +[12:47:39] [Render thread/WARN] [GregTechCEu/]: FluidStorageKey{gtceu:liquid} already has an associated fluid for material gtceu:water +[12:47:39] [Render thread/WARN] [GregTechCEu/]: FluidStorageKey{gtceu:liquid} already has an associated fluid for material gtceu:lava +[12:47:39] [CraftPresence/INFO] [craftpresence/]: Loaded display data with Client Id: 1182610212121743470 (Logged in as RyRy) +[12:47:39] [Render thread/INFO] [GregTechCEu/]: Registering KeyBinds +[12:47:39] [Render thread/WARN] [ne.mi.fm.DeferredWorkQueue/LOADING]: Mod 'gtceu' took 1.043 s to run a deferred task. +[12:47:42] [Render thread/WARN] [ne.mi.re.ForgeRegistry/REGISTRIES]: Registry minecraft:menu: The object net.minecraft.world.inventory.MenuType@67141ef8 has been registered twice for the same name ae2:export_card. +[12:47:42] [Render thread/WARN] [ne.mi.re.ForgeRegistry/REGISTRIES]: Registry minecraft:menu: The object net.minecraft.world.inventory.MenuType@f4864e9 has been registered twice for the same name ae2:insert_card. +[12:47:42] [Render thread/INFO] [Moonlight/]: Initialized block sets in 21ms +[12:47:42] [Render thread/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering C2S receiver with id ae2wtlib:cycle_terminal +[12:47:43] [Render thread/INFO] [Every Compat/]: Registering Compat WoodType Blocks +[12:47:43] [Render thread/INFO] [Every Compat/]: EveryCompat Create Module: registered 42 WoodType blocks +[12:47:43] [Render thread/INFO] [tf.TFCTumbleweed/]: Injecting TFC Tumbleweed override pack +[12:47:43] [Render thread/INFO] [co.ee.fi.FirmaLife/]: Injecting firmalife override pack +[ALSOFT] (EE) Failed to set real-time priority for thread: Operation not permitted (1) +[12:47:43] [Render thread/INFO] [Oculus/]: Hardware information: +[12:47:43] [Render thread/INFO] [Oculus/]: CPU: 16x AMD Ryzen 7 3700X 8-Core Processor +[12:47:43] [Render thread/INFO] [Oculus/]: GPU: AMD Radeon RX 5700 XT (radeonsi, navi10, LLVM 19.1.7, DRM 3.54, 6.6.85) (Supports OpenGL 4.6 (Core Profile) Mesa 25.0.3 (git-c3afa2a74f)) +[12:47:43] [Render thread/INFO] [Oculus/]: OS: Linux (6.6.85) +[12:47:44] [Render thread/WARN] [mixin/]: Method overwrite conflict for isHidden in mixins.oculus.compat.sodium.json:copyEntity.ModelPartMixin, previously written by dev.tr7zw.firstperson.mixins.ModelPartMixin. Skipping method. +[12:47:44] [Render thread/INFO] [minecraft/Minecraft]: [FANCYMENU] Registering resource reload listener.. +[12:47:44] [Render thread/INFO] [de.ke.fa.cu.ScreenCustomization/]: [FANCYMENU] Initializing screen customization engine! Addons should NOT REGISTER TO REGISTRIES anymore now! +[12:47:44] [Render thread/INFO] [de.ke.fa.cu.la.ScreenCustomizationLayerHandler/]: [FANCYMENU] Minecraft resource reload: STARTING +[12:47:44] [Render thread/INFO] [ModernFix/]: Invalidating pack caches +[12:47:44] [Render thread/INFO] [minecraft/ReloadableResourceManager]: Reloading ResourceManager: Additional Placements blockstate redirection pack, vanilla, mod_resources, gtceu:dynamic_assets, Moonlight Mods Dynamic Assets, Firmalife-1.20.1-2.1.15.jar:overload, TFCTumbleweed-1.20.1-1.2.2.jar:overload, KubeJS Resource Pack [assets], ldlib +[12:47:44] [Finalizer/WARN] [ModernFix/]: One or more BufferBuilders have been leaked, ModernFix will attempt to correct this. +[12:47:45] [Render thread/INFO] [Every Compat/]: Generated runtime CLIENT_RESOURCES for pack Moonlight Mods Dynamic Assets (everycomp) in: 597 ms +[12:47:45] [Render thread/INFO] [Moonlight/]: Generated runtime CLIENT_RESOURCES for pack Moonlight Mods Dynamic Assets (moonlight) in: 0 ms +[12:47:45] [modloading-worker-0/INFO] [Puzzles Lib/]: Loading client config for pickupnotifier +[12:47:45] [modloading-worker-0/INFO] [Puzzles Lib/]: Loading client config for hangglider +[12:47:45] [Worker-ResourceReload-4/INFO] [minecraft/UnihexProvider]: Found unifont_all_no_pua-15.0.06.hex, loading +[12:47:45] [Worker-ResourceReload-3/INFO] [xa.pa.OpenPartiesAndClaims/]: Loading Open Parties and Claims! +[12:47:45] [Worker-ResourceReload-1/INFO] [co.re.RecipeEssentials/]: recipeessentials mod initialized +[12:47:45] [Worker-ResourceReload-10/INFO] [ne.dr.tf.TerraFirmaCraft/]: TFC Common Setup +[12:47:45] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [saturn] Starting version check at https://github.com/AbdElAziz333/Saturn/raw/mc1.20.1/dev/updates.json +[12:47:45] [Worker-ResourceReload-1/INFO] [FTB Library/]: Setting game stages provider implementation to: KubeJS Stages +[12:47:45] [Worker-ResourceReload-1/INFO] [FTB XMod Compat/]: Chose [KubeJS Stages] as the active game stages implementation +[12:47:45] [Worker-ResourceReload-1/INFO] [FTB Library/]: Setting permissions provider implementation to: FTB Ranks +[12:47:45] [Worker-ResourceReload-1/INFO] [FTB XMod Compat/]: Chose [FTB Ranks] as the active permissions implementation +[12:47:45] [Worker-ResourceReload-1/INFO] [FTB XMod Compat/]: [FTB Quests] recipe helper provider is [JEI] +[12:47:45] [Worker-ResourceReload-1/INFO] [FTB XMod Compat/]: [FTB Quests] Enabled FTB Filter System integration +[12:47:45] [Render thread/INFO] [GregTechCEu/]: GregTech Model loading took 520ms +[12:47:46] [Render thread/INFO] [minecraft/LoadingOverlay]: [DRIPPY LOADING SCREEN] Initializing fonts for text rendering.. +[12:47:46] [Render thread/INFO] [minecraft/LoadingOverlay]: [DRIPPY LOADING SCREEN] Calculating animation sizes for FancyMenu.. +[12:47:46] [Render thread/INFO] [de.ke.fa.cu.la.ScreenCustomizationLayerHandler/]: [FANCYMENU] ScreenCustomizationLayer registered: drippy_loading_overlay +[12:47:46] [Render thread/INFO] [de.ke.fa.cu.an.AnimationHandler/]: [FANCYMENU] Preloading animations! This could cause the loading screen to freeze for a while.. +[12:47:46] [Render thread/INFO] [de.ke.fa.cu.an.AnimationHandler/]: [FANCYMENU] Finished preloading animations! +[12:47:46] [Render thread/INFO] [de.ke.fa.FancyMenu/]: [FANCYMENU] Starting late client initialization phase.. +[12:47:46] [Forge Version Check/WARN] [ne.mi.fm.VersionChecker/]: Failed to process update information +com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 9 column 1 path $ + at com.google.gson.Gson.fromJson(Gson.java:1226) ~[gson-2.10.jar%2388!/:?] {} + at com.google.gson.Gson.fromJson(Gson.java:1124) ~[gson-2.10.jar%2388!/:?] {} + at com.google.gson.Gson.fromJson(Gson.java:1034) ~[gson-2.10.jar%2388!/:?] {} + at com.google.gson.Gson.fromJson(Gson.java:969) ~[gson-2.10.jar%2388!/:?] {} + at net.minecraftforge.fml.VersionChecker$1.process(VersionChecker.java:183) ~[fmlcore-1.20.1-47.2.6.jar%23445!/:?] {} + at java.lang.Iterable.forEach(Iterable.java:75) ~[?:?] {re:mixin} + at net.minecraftforge.fml.VersionChecker$1.run(VersionChecker.java:114) ~[fmlcore-1.20.1-47.2.6.jar%23445!/:?] {} +Caused by: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 9 column 1 path $ + at com.google.gson.stream.JsonReader.beginObject(JsonReader.java:393) ~[gson-2.10.jar%2388!/:?] {} + at com.google.gson.internal.bind.MapTypeAdapterFactory$Adapter.read(MapTypeAdapterFactory.java:182) ~[gson-2.10.jar%2388!/:?] {} + at com.google.gson.internal.bind.MapTypeAdapterFactory$Adapter.read(MapTypeAdapterFactory.java:144) ~[gson-2.10.jar%2388!/:?] {} + at com.google.gson.Gson.fromJson(Gson.java:1214) ~[gson-2.10.jar%2388!/:?] {} + ... 6 more +[12:47:46] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [controlling] Starting version check at https://updates.blamejared.com/get?n=controlling&gv=1.20.1 +[12:47:46] [Worker-ResourceReload-6/ERROR] [minecraft/SimpleJsonResourceReloadListener]: Couldn't parse data file tfc:field_guide/ru_ru/entries/tfg_ores/surface_copper from tfc:patchouli_books/field_guide/ru_ru/entries/tfg_ores/surface_copper.json +com.google.gson.JsonParseException: com.google.gson.stream.MalformedJsonException: Use JsonReader.setLenient(true) to accept malformed JSON at line 55 column 6 path $.pages[5] + at net.minecraft.util.GsonHelper.m_13780_(GsonHelper.java:526) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:classloading,re:mixin} + at net.minecraft.util.GsonHelper.m_263475_(GsonHelper.java:531) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:classloading,re:mixin} + at net.minecraft.util.GsonHelper.m_13776_(GsonHelper.java:581) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:classloading,re:mixin} + at net.minecraft.server.packs.resources.SimpleJsonResourceReloadListener.m_278771_(SimpleJsonResourceReloadListener.java:41) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,re:classloading} + at net.minecraft.server.packs.resources.SimpleJsonResourceReloadListener.m_5944_(SimpleJsonResourceReloadListener.java:29) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,re:classloading} + at net.minecraft.server.packs.resources.SimpleJsonResourceReloadListener.m_5944_(SimpleJsonResourceReloadListener.java:17) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,re:classloading} + at net.minecraft.server.packs.resources.SimplePreparableReloadListener.m_10786_(SimplePreparableReloadListener.java:11) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,re:classloading,pl:accesstransformer:B,pl:mixin:APP:moonlight.mixins.json:ConditionHackMixin,pl:mixin:A} + at java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1768) ~[?:?] {} + at java.util.concurrent.CompletableFuture$AsyncSupply.exec(CompletableFuture.java:1760) ~[?:?] {} + at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:373) ~[?:?] {} + at java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1182) ~[?:?] {} + at java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1655) ~[?:?] {re:mixin,re:computing_frames} + at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1622) ~[?:?] {re:mixin,re:computing_frames} + at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:165) ~[?:?] {re:mixin} +Caused by: com.google.gson.stream.MalformedJsonException: Use JsonReader.setLenient(true) to accept malformed JSON at line 55 column 6 path $.pages[5] + at com.google.gson.stream.JsonReader.syntaxError(JsonReader.java:1657) ~[gson-2.10.jar%2388!/:?] {} + at com.google.gson.stream.JsonReader.checkLenient(JsonReader.java:1463) ~[gson-2.10.jar%2388!/:?] {} + at com.google.gson.stream.JsonReader.doPeek(JsonReader.java:569) ~[gson-2.10.jar%2388!/:?] {} + at com.google.gson.stream.JsonReader.hasNext(JsonReader.java:422) ~[gson-2.10.jar%2388!/:?] {} + at com.google.gson.internal.bind.TypeAdapters$28.read(TypeAdapters.java:779) ~[gson-2.10.jar%2388!/:?] {} + at com.google.gson.internal.bind.TypeAdapters$28.read(TypeAdapters.java:725) ~[gson-2.10.jar%2388!/:?] {} + at com.google.gson.internal.bind.TypeAdapters$34$1.read(TypeAdapters.java:1007) ~[gson-2.10.jar%2388!/:?] {} + at net.minecraft.util.GsonHelper.m_13780_(GsonHelper.java:524) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:classloading,re:mixin} + ... 13 more +[12:47:46] [Render thread/INFO] [de.ke.fa.ut.wi.WindowHandler/]: [FANCYMENU] Custom window icon successfully updated! +[12:47:46] [Render thread/INFO] [KubeJS Client/]: Client resource reload complete! +[12:47:46] [Render thread/INFO] [defaultoptions/]: Loaded default options for keymappings +[12:47:46] [Render thread/INFO] [de.ke.fa.ut.wi.WindowHandler/]: [FANCYMENU] Custom window icon successfully updated! +[12:47:46] [Worker-Main-6/INFO] [minecraft/UnihexProvider]: Found unifont_all_no_pua-15.0.06.hex, loading +[12:47:47] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [controlling] Found status: BETA Current: 12.0.2 Target: 12.0.2 +[12:47:47] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [uteamcore] Starting version check at https://api.u-team.info/update/uteamcore.json +[12:47:47] [FTB Backups Config Watcher 0/INFO] [ne.cr.ft.FTBBackups/]: Config at /home/ryex/.var/app/org.prismlauncher.PrismLauncher/data/PrismLauncher/instances/TerraFirmaGreg-Modern/minecraft/config/ftbbackups2.json has changed, reloaded! +[12:47:47] [Worker-ResourceReload-14/WARN] [minecraft/SpriteLoader]: Texture create_connected:block/fluid_container_window_debug with size 40x32 limits mip level from 4 to 3 +[12:47:47] [UniLib/INFO] [unilib/]: Received update status for "unilib" -> Outdated (Target version: "v1.0.5") +[12:47:47] [UniLib/INFO] [unilib/]: Received update status for "craftpresence" -> Outdated (Target version: "v2.5.4") +[12:47:47] [Render thread/INFO] [Every Compat/]: Registered 42 compat blocks making up 0.31% of total blocks registered +[12:47:47] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [uteamcore] Found status: OUTDATED Current: 5.1.4.312 Target: 5.1.4.346 +[12:47:47] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [pickupnotifier] Starting version check at https://raw.githubusercontent.com/Fuzss/modresources/main/update/pickupnotifier.json +[12:47:47] [Render thread/INFO] [Moonlight/]: Initialized color sets in 104ms +[12:47:47] [Render thread/INFO] [co.no.ku.KubeJSTFC/]: KubeJS TFC configuration: +[12:47:47] [Render thread/INFO] [co.no.ku.KubeJSTFC/]: Debug mode enabled: false +[12:47:47] [Render thread/INFO] [MEGA Cells/]: Initialised AE2WT integration. +[12:47:47] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [pickupnotifier] Found status: UP_TO_DATE Current: 8.0.0 Target: null +[12:47:47] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [corpse] Starting version check at https://update.maxhenkel.de/forge/corpse +[12:47:47] [Worker-ResourceReload-0/INFO] [FirstPersonModel/]: Loading FirstPerson Mod +[12:47:47] [Worker-ResourceReload-4/INFO] [xa.ma.WorldMap/]: Loading Xaero's World Map - Stage 1/2 +[12:47:47] [Placebo Patreon Trail Loader/INFO] [placebo/]: Loading patreon trails data... +[12:47:47] [Placebo Patreon Wing Loader/INFO] [placebo/]: Loading patreon wing data... +[12:47:47] [Worker-ResourceReload-13/INFO] [de.ke.ko.Konkrete/]: [KONKRETE] Client-side libs ready to use! +[12:47:47] [Placebo Patreon Trail Loader/INFO] [placebo/]: Loaded 45 patreon trails. +[12:47:47] [Placebo Patreon Wing Loader/INFO] [placebo/]: Loaded 21 patreon wings. +[12:47:47] [Worker-ResourceReload-7/INFO] [EMI/]: [EMI] Discovered Sodium +[12:47:47] [Worker-ResourceReload-14/INFO] [xa.mi.XaeroMinimap/]: Loading Xaero's Minimap - Stage 1/2 +[12:47:47] [Worker-ResourceReload-13/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ae2wtlib:update_wut +[12:47:47] [Worker-ResourceReload-13/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ae2wtlib:update_restock +[12:47:47] [Worker-ResourceReload-13/INFO] [de.ar.ne.fo.NetworkManagerImpl/]: Registering S2C receiver with id ae2wtlib:restock_amounts +[12:47:48] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [corpse] Found status: OUTDATED Current: 1.20.1-1.0.19 Target: 1.20.1-1.0.20 +[12:47:48] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [create_connected] Starting version check at https://raw.githubusercontent.com/hlysine/create_connected/main/update.json +[12:47:48] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [create_connected] Found status: OUTDATED Current: 0.8.2-mc1.20.1 Target: 1.0.1-mc1.20.1 +[12:47:48] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [blur] Starting version check at https://api.modrinth.com/updates/rubidium-extra/forge_updates.json +[12:47:48] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [blur] Found status: AHEAD Current: 3.1.1 Target: null +[12:47:48] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [hangglider] Starting version check at https://raw.githubusercontent.com/Fuzss/modresources/main/update/hangglider.json +[12:47:48] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [hangglider] Found status: UP_TO_DATE Current: 8.0.1 Target: null +[12:47:48] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [searchables] Starting version check at https://updates.blamejared.com/get?n=searchables&gv=1.20.1 +[12:47:48] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [searchables] Found status: BETA Current: 1.0.3 Target: 1.0.3 +[12:47:48] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [computercraft] Starting version check at https://api.modrinth.com/updates/cc-tweaked/forge_updates.json +[12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [computercraft] Found status: OUTDATED Current: 1.113.1 Target: 1.115.1 +[12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [unilib] Starting version check at https://raw.githubusercontent.com/CDAGaming/VersionLibrary/master/UniLib/update.json +[12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [unilib] Found status: AHEAD Current: 1.0.2 Target: null +[12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [craftpresence] Starting version check at https://raw.githubusercontent.com/CDAGaming/VersionLibrary/master/CraftPresence/update.json +[12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [craftpresence] Found status: AHEAD Current: 2.5.0 Target: null +[12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [radium] Starting version check at https://api.modrinth.com/updates/radium/forge_updates.json +[12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [radium] Found status: OUTDATED Current: 0.12.3+git.50c5c33 Target: 0.12.4 +[12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [attributefix] Starting version check at https://updates.blamejared.com/get?n=attributefix&gv=1.20.1 +[12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [attributefix] Found status: BETA Current: 21.0.4 Target: 21.0.4 +[12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [clumps] Starting version check at https://updates.blamejared.com/get?n=clumps&gv=1.20.1 +[12:47:49] [Worker-ResourceReload-4/WARN] [xa.hu.mi.MinimapLogs/]: io exception while checking patreon: Online mod data expired! Date: Thu Apr 17 01:03:51 MST 2025 +[12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [clumps] Found status: BETA Current: 12.0.0.4 Target: 12.0.0.4 +[12:47:49] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [catalogue] Starting version check at https://mrcrayfish.com/modupdatejson?id=catalogue +[12:47:50] [Forge Version Check/WARN] [ne.mi.fm.VersionChecker/]: Failed to process update information +com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 1 column 1 path $ + at com.google.gson.Gson.fromJson(Gson.java:1226) ~[gson-2.10.jar%2388!/:?] {} + at com.google.gson.Gson.fromJson(Gson.java:1124) ~[gson-2.10.jar%2388!/:?] {} + at com.google.gson.Gson.fromJson(Gson.java:1034) ~[gson-2.10.jar%2388!/:?] {} + at com.google.gson.Gson.fromJson(Gson.java:969) ~[gson-2.10.jar%2388!/:?] {} + at net.minecraftforge.fml.VersionChecker$1.process(VersionChecker.java:183) ~[fmlcore-1.20.1-47.2.6.jar%23445!/:?] {} + at java.lang.Iterable.forEach(Iterable.java:75) ~[?:?] {re:mixin} + at net.minecraftforge.fml.VersionChecker$1.run(VersionChecker.java:114) ~[fmlcore-1.20.1-47.2.6.jar%23445!/:?] {} +Caused by: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 1 column 1 path $ + at com.google.gson.stream.JsonReader.beginObject(JsonReader.java:393) ~[gson-2.10.jar%2388!/:?] {} + at com.google.gson.internal.bind.MapTypeAdapterFactory$Adapter.read(MapTypeAdapterFactory.java:182) ~[gson-2.10.jar%2388!/:?] {} + at com.google.gson.internal.bind.MapTypeAdapterFactory$Adapter.read(MapTypeAdapterFactory.java:144) ~[gson-2.10.jar%2388!/:?] {} + at com.google.gson.Gson.fromJson(Gson.java:1214) ~[gson-2.10.jar%2388!/:?] {} + ... 6 more +[12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [puzzlesaccessapi] Starting version check at https://raw.githubusercontent.com/Fuzss/modresources/main/update/puzzlesaccessapi.json +[12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [puzzlesaccessapi] Found status: BETA Current: 8.0.7 Target: null +[12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [forge] Starting version check at https://files.minecraftforge.net/net/minecraftforge/forge/promotions_slim.json +[12:47:50] [Worker-ResourceReload-5/WARN] [minecraft/ModelBakery]: tfcambiental:snowshoes#inventory +java.io.FileNotFoundException: tfcambiental:models/item/snowshoes.json + at net.minecraft.client.resources.model.ModelBakery.m_119364_(ModelBakery.java:417) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,pl:mixin:A,pl:runtimedistcleaner:A} + at net.minecraft.client.resources.model.ModelBakery.m_119362_(ModelBakery.java:266) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,pl:mixin:A,pl:runtimedistcleaner:A} + at net.minecraft.client.resources.model.ModelBakery.m_119341_(ModelBakery.java:243) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,pl:mixin:A,pl:runtimedistcleaner:A} + at net.minecraft.client.resources.model.ModelBakery.m_119306_(ModelBakery.java:384) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,pl:mixin:A,pl:runtimedistcleaner:A} + at net.minecraft.client.resources.model.ModelBakery.(ModelBakery.java:150) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,pl:mixin:A,pl:runtimedistcleaner:A} + at net.minecraft.client.resources.model.ModelManager.m_246505_(ModelManager.java:83) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,pl:mixin:A,pl:runtimedistcleaner:A} + at java.util.concurrent.CompletableFuture.biApply(CompletableFuture.java:1311) ~[?:?] {re:mixin} + at java.util.concurrent.CompletableFuture$BiApply.tryFire(CompletableFuture.java:1280) ~[?:?] {} + at java.util.concurrent.CompletableFuture$Completion.exec(CompletableFuture.java:483) ~[?:?] {} + at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:373) ~[?:?] {} + at java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1182) ~[?:?] {} + at java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1655) ~[?:?] {re:mixin,re:computing_frames} + at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1622) ~[?:?] {re:mixin,re:computing_frames} + at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:165) ~[?:?] {re:mixin} +[12:47:50] [Worker-ResourceReload-5/WARN] [minecraft/ModelBakery]: carpeted:block/label +java.io.FileNotFoundException: carpeted:models/block/label.json + at net.minecraft.client.resources.model.ModelBakery.m_119364_(ModelBakery.java:417) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,pl:mixin:A,pl:runtimedistcleaner:A} + at net.minecraft.client.resources.model.ModelBakery.m_119362_(ModelBakery.java:262) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,pl:mixin:A,pl:runtimedistcleaner:A} + at net.minecraft.client.resources.model.ModelBakery.m_119341_(ModelBakery.java:243) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,pl:mixin:A,pl:runtimedistcleaner:A} + at net.minecraft.client.resources.model.ModelBakery.(ModelBakery.java:159) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,pl:mixin:A,pl:runtimedistcleaner:A} + at net.minecraft.client.resources.model.ModelManager.m_246505_(ModelManager.java:83) ~[client-1.20.1-20230612.114412-srg.jar%23444!/:?] {re:mixin,pl:accesstransformer:B,pl:runtimedistcleaner:A,re:classloading,pl:accesstransformer:B,pl:mixin:A,pl:runtimedistcleaner:A} + at java.util.concurrent.CompletableFuture.biApply(CompletableFuture.java:1311) ~[?:?] {re:mixin} + at java.util.concurrent.CompletableFuture$BiApply.tryFire(CompletableFuture.java:1280) ~[?:?] {} + at java.util.concurrent.CompletableFuture$Completion.exec(CompletableFuture.java:483) ~[?:?] {} + at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:373) ~[?:?] {} + at java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1182) ~[?:?] {} + at java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1655) ~[?:?] {re:mixin,re:computing_frames} + at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1622) ~[?:?] {re:mixin,re:computing_frames} + at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:165) ~[?:?] {re:mixin} +[12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [forge] Found status: OUTDATED Current: 47.2.6 Target: 47.4.0 +[12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [moonlight] Starting version check at https://raw.githubusercontent.com/MehVahdJukaar/Moonlight/multi-loader/forge/update.json +[12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [moonlight] Found status: BETA Current: 1.20-2.13.51 Target: null +[12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [configuration] Starting version check at https://raw.githubusercontent.com/Toma1O6/UpdateSchemas/master/configuration-forge.json +[12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [configuration] Found status: OUTDATED Current: 2.2.0 Target: 2.2.1 +[12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [smoothboot] Starting version check at https://github.com/AbdElAziz333/SmoothBoot-Reloaded/raw/mc1.20.1/dev/updates.json +[12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [smoothboot] Found status: UP_TO_DATE Current: 0.0.4 Target: null +[12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [ksyxis] Starting version check at https://raw.githubusercontent.com/VidTu/Ksyxis/main/updater_ksyxis_forge.json +[12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [ksyxis] Found status: OUTDATED Current: 1.3.2 Target: 1.3.3 +[12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [flywheel] Starting version check at https://api.modrinth.com/updates/flywheel/forge_updates.json +[12:47:50] [Worker-ResourceReload-4/ERROR] [xa.ma.WorldMap/]: io exception while checking versions: Online mod data expired! Date: Thu Apr 17 01:03:51 MST 2025 +[12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [flywheel] Found status: BETA Current: 0.6.10-7 Target: null +[12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [inventoryhud] Starting version check at https://raw.githubusercontent.com/DmitryLovin/pluginUpdate/master/invupdate.json +[12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [inventoryhud] Found status: UP_TO_DATE Current: 3.4.26 Target: null +[12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [puzzleslib] Starting version check at https://raw.githubusercontent.com/Fuzss/modresources/main/update/puzzleslib.json +[12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [puzzleslib] Found status: OUTDATED Current: 8.1.23 Target: 8.1.32 +[12:47:50] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [betterf3] Starting version check at https://api.modrinth.com/updates/betterf3/forge_updates.json +[12:47:50] [Render thread/INFO] [xa.mi.XaeroMinimap/]: Loading Xaero's Minimap - Stage 2/2 +[12:47:51] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [betterf3] Found status: UP_TO_DATE Current: 7.0.2 Target: null +[12:47:51] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [packetfixer] Starting version check at https://api.modrinth.com/updates/packet-fixer/forge_updates.json +[12:47:51] [Forge Version Check/INFO] [ne.mi.fm.VersionChecker/]: [packetfixer] Found status: OUTDATED Current: 1.4.2 Target: 2.0.0 +[12:47:51] [Render thread/WARN] [xa.hu.mi.MinimapLogs/]: io exception while checking versions: Online mod data expired! Date: Thu Apr 17 01:03:51 MST 2025 +[12:47:51] [Render thread/INFO] [xa.hu.mi.MinimapLogs/]: Registered player tracker system: minimap_synced +[12:47:51] [Render thread/INFO] [xa.hu.mi.MinimapLogs/]: Xaero's Minimap: World Map found! +[12:47:51] [Render thread/INFO] [xa.hu.mi.MinimapLogs/]: Registered player tracker system: openpartiesandclaims +[12:47:51] [Render thread/INFO] [xa.hu.mi.MinimapLogs/]: Xaero's Minimap: Open Parties And Claims found! +[12:47:51] [Render thread/INFO] [xa.hu.mi.MinimapLogs/]: No Optifine! +[12:47:51] [Render thread/INFO] [xa.hu.mi.MinimapLogs/]: Xaero's Minimap: No Vivecraft! +[12:47:51] [Render thread/INFO] [xa.hu.mi.MinimapLogs/]: Xaero's Minimap: Framed Blocks found! +[12:47:51] [Render thread/INFO] [xa.hu.mi.MinimapLogs/]: Xaero's Minimap: Iris found! +[12:47:51] [Render thread/INFO] [xa.ma.WorldMap/]: Loading Xaero's World Map - Stage 2/2 +[12:47:51] [Render thread/INFO] [xa.ma.WorldMap/]: New world map region cache hash code: -815523079 +[12:47:51] [Render thread/INFO] [xa.ma.WorldMap/]: Registered player tracker system: map_synced +[12:47:51] [Render thread/INFO] [xa.ma.WorldMap/]: Xaero's WorldMap Mod: Xaero's minimap found! +[12:47:51] [Render thread/INFO] [xa.ma.WorldMap/]: Registered player tracker system: minimap_synced +[12:47:51] [Render thread/INFO] [xa.ma.WorldMap/]: Registered player tracker system: openpartiesandclaims +[12:47:51] [Render thread/INFO] [xa.ma.WorldMap/]: Xaero's WorldMap Mod: Open Parties And Claims found! +[12:47:51] [Render thread/INFO] [xa.ma.WorldMap/]: No Optifine! +[12:47:51] [Render thread/INFO] [xa.ma.WorldMap/]: Xaero's World Map: No Vivecraft! +[12:47:51] [Render thread/INFO] [xa.ma.WorldMap/]: Xaero's World Map: Framed Blocks found! +[12:47:51] [Render thread/INFO] [xa.ma.WorldMap/]: Xaero's World Map: Iris found! +[12:47:56] [Worker-ResourceReload-5/WARN] [minecraft/ModelManager]: Missing textures in model firmaciv:firmaciv_compass#inventory: + minecraft:textures/atlas/blocks.png:minecraft:item/compass +[12:47:56] [Worker-ResourceReload-5/WARN] [minecraft/ModelManager]: Missing textures in model gtceu:tin_double_ingot#inventory: + minecraft:textures/atlas/blocks.png:gtceu:item/material_sets/dull/ingot_double_overlay +[12:47:56] [Worker-ResourceReload-5/WARN] [minecraft/ModelManager]: Missing textures in model createaddition:small_light_connector#facing=west,mode=push,powered=true,rotation=x_clockwise_180,variant=default: + minecraft:textures/atlas/blocks.png:create:block/chute_block +[12:47:56] [Worker-ResourceReload-5/WARN] [minecraft/ModelManager]: Missing textures in model gtceu:copper_double_ingot#inventory: + minecraft:textures/atlas/blocks.png:gtceu:item/material_sets/dull/ingot_double_overlay +Reloading Dynamic Lights +[12:47:57] [Render thread/INFO] [co.jo.fl.ba.Backend/]: Loaded all shader sources. +Create Crafts & Additions Initialized! +[12:47:57] [Worker-ResourceReload-2/INFO] [AttributeFix/]: Loaded values for 19 compatible attributes. +[12:47:57] [Worker-ResourceReload-2/ERROR] [AttributeFix/]: Attribute ID 'minecolonies:mc_mob_damage' does not belong to a known attribute. This entry will be ignored. +[12:47:57] [Worker-ResourceReload-2/INFO] [AttributeFix/]: Loaded 20 values from config. +[12:47:57] [Worker-ResourceReload-2/INFO] [AttributeFix/]: Saving config file. 20 entries. +[12:47:57] [Worker-ResourceReload-2/INFO] [AttributeFix/]: Applying changes for 20 attributes. +[12:47:57] [Worker-ResourceReload-11/INFO] [de.me.as.AstikorCarts/]: Automatic pull animal configuration: +pull_animals = [ + "minecraft:camel", + "minecraft:donkey", + "minecraft:horse", + "minecraft:mule", + "minecraft:skeleton_horse", + "minecraft:zombie_horse", + "minecraft:player", + "tfc:donkey", + "tfc:mule", + "tfc:horse" + ] +[12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at io.github.mortuusars.exposure.integration.jade.ExposureJadePlugin +[12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at de.maxhenkel.corpse.integration.waila.PluginCorpse +[12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at xfacthd.framedblocks.common.compat.jade.FramedJadePlugin +[12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at snownee.jade.addon.general.GeneralPlugin +[12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at snownee.jade.addon.create.CreatePlugin +[12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at me.pandamods.fallingtrees.compat.JadePlugin +[12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at com.gregtechceu.gtceu.integration.jade.GTJadePlugin +[12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at net.dries007.tfc.compat.jade.JadeIntegration +[12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at snownee.jade.addon.vanilla.VanillaPlugin +[12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at snownee.jade.addon.universal.UniversalPlugin +[12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at snownee.jade.addon.core.CorePlugin +[12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at appeng.integration.modules.jade.JadeModule +[12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at com.glodblock.github.extendedae.xmod.jade.JadePlugin +[12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at cy.jdkdigital.treetap.compat.jade.JadePlugin +[12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at com.eerussianguy.firmalife.compat.tooltip.JadeIntegration +[12:47:57] [Worker-ResourceReload-9/INFO] [Jade/]: Start loading plugin at com.ljuangbminecraft.tfcchannelcasting.compat.JadeIntegration +[12:47:59] [Render thread/WARN] [ne.mi.fm.DeferredWorkQueue/LOADING]: Mod 'create_connected' took 1.342 s to run a deferred task. +[12:47:59] [Render thread/INFO] [defaultoptions/]: Loaded default options for keymappings +[ALSOFT] (EE) Failed to set real-time priority for thread: Operation not permitted (1) +[12:47:59] [Render thread/INFO] [mojang/Library]: OpenAL initialized on device Starship/Matisse HD Audio Controller Analog Stereo +[12:47:59] [Render thread/INFO] [minecraft/SoundEngine]: Sound engine started +[12:47:59] [Render thread/INFO] [minecraft/SoundEngine]: [FANCYMENU] Reloading AudioResourceHandler after Minecraft SoundEngine reload.. +[12:47:59] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 4096x2048x4 minecraft:textures/atlas/blocks.png-atlas +[12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 1024x512x4 minecraft:textures/atlas/signs.png-atlas +[12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 512x512x4 minecraft:textures/atlas/banner_patterns.png-atlas +[12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 512x512x4 minecraft:textures/atlas/shield_patterns.png-atlas +[12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 2048x1024x4 minecraft:textures/atlas/armor_trims.png-atlas +[12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 1024x1024x4 minecraft:textures/atlas/chest.png-atlas +[12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 128x64x4 minecraft:textures/atlas/decorated_pot.png-atlas +[12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 512x256x4 minecraft:textures/atlas/shulker_boxes.png-atlas +[12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 512x256x4 minecraft:textures/atlas/beds.png-atlas +[12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh particle. +[12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_solid. +[12:48:00] [Render thread/INFO] [Shimmer/]: inject shader fsh rendertype_solid. +[12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_cutout_mipped. +[12:48:00] [Render thread/INFO] [Shimmer/]: inject shader fsh rendertype_cutout_mipped. +[12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_cutout. +[12:48:00] [Render thread/INFO] [Shimmer/]: inject shader fsh rendertype_cutout. +[12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_translucent. +[12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_armor_cutout_no_cull. +[12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_entity_solid. +[12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_entity_cutout. +[12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_entity_cutout_no_cull. +[12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_entity_cutout_no_cull_z_offset. +[12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_entity_translucent_cull. +[12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_entity_translucent. +[12:48:00] [Render thread/WARN] [minecraft/ShaderInstance]: Shader rendertype_entity_translucent_emissive could not find sampler named Sampler2 in the specified shader program. +[12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_entity_smooth_cutout. +[12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_entity_decal. +[12:48:00] [Render thread/INFO] [Shimmer/]: inject shader vsh rendertype_entity_no_outline. +[12:48:00] [Render thread/WARN] [minecraft/ShaderInstance]: Shader moonlight:text_alpha_color could not find sampler named Sampler2 in the specified shader program. +[12:48:00] [Render thread/WARN] [minecraft/ShaderInstance]: Shader moonlight:text_alpha_color could not find uniform named IViewRotMat in the specified shader program. +[12:48:00] [Render thread/WARN] [minecraft/ShaderInstance]: Shader moonlight:text_alpha_color could not find uniform named FogShape in the specified shader program. +[12:48:00] [Render thread/WARN] [minecraft/ShaderInstance]: Shader shimmer:rendertype_armor_cutout_no_cull could not find sampler named Sampler2 in the specified shader program. +[12:48:00] [Render thread/WARN] [minecraft/ShaderInstance]: Shader shimmer:rendertype_armor_cutout_no_cull could not find uniform named Light0_Direction in the specified shader program. +[12:48:00] [Render thread/WARN] [minecraft/ShaderInstance]: Shader shimmer:rendertype_armor_cutout_no_cull could not find uniform named Light1_Direction in the specified shader program. +[12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 2048x1024x0 minecraft:textures/atlas/particles.png-atlas +[12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 256x256x0 minecraft:textures/atlas/paintings.png-atlas +[12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 256x128x0 minecraft:textures/atlas/mob_effects.png-atlas +[12:48:00] [Render thread/INFO] [xa.ma.WorldMap/]: Successfully reloaded the world map shaders! +[12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Loading exposure filters: +[12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:light_blue_pane, exposure:shaders/light_blue_tint.json] added. +[12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:orange_pane, exposure:shaders/orange_tint.json] added. +[12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:red_pane, exposure:shaders/red_filter.json] added. +[12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:purple_pane, exposure:shaders/purple_tint.json] added. +[12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:blue_pane, exposure:shaders/blue_filter.json] added. +[12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:light_gray_pane, exposure:shaders/light_gray_tint.json] added. +[12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:magenta_pane, exposure:shaders/magenta_tint.json] added. +[12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:gray_pane, exposure:shaders/gray_tint.json] added. +[12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:lime_pane, exposure:shaders/lime_tint.json] added. +[12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:green_pane, exposure:shaders/green_filter.json] added. +[12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:pink_pane, exposure:shaders/pink_tint.json] added. +[12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:yellow_pane, exposure:shaders/yellow_tint.json] added. +[12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:white_pane, exposure:shaders/white_tint.json] added. +[12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:brown_pane, exposure:shaders/brown_tint.json] added. +[12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:glass_pane, exposure:shaders/crisp.json] added. +[12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:interplanar_projector, exposure:shaders/invert.json] added. +[12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:black_pane, exposure:shaders/black_tint.json] added. +[12:48:00] [Render thread/INFO] [io.gi.mo.ex.Exposure/]: Filter [exposure:cyan_pane, exposure:shaders/cyan_tint.json] added. +[12:48:00] [Render thread/INFO] [patchouli/]: BookContentResourceListenerLoader preloaded 1073 jsons +[12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 128x128x0 computercraft:textures/atlas/gui.png-atlas +[12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 512x256x0 polylib:textures/atlas/gui.png-atlas +[12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 256x128x0 jei:textures/atlas/gui.png-atlas +[12:48:00] [Render thread/INFO] [minecraft/TextureAtlas]: Created: 256x256x0 moonlight:textures/atlas/map_markers.png-atlas +[12:48:00] [Render thread/INFO] [xa.hu.mi.MinimapLogs/]: Successfully reloaded the minimap shaders! +[12:48:00] [Render thread/INFO] [Shimmer/]: buildIn shimmer configuration is enabled, this can be disabled by config file +[12:48:00] [Render thread/INFO] [Shimmer/]: mod jar and resource pack discovery: file managed my minecraft located in [sourceName:KubeJS Resource Pack [assets],location:KubeJS Resource Pack [assets]] +[12:48:00] [Render thread/ERROR] [Shimmer/]: can't find block framedblocks:framed_gate_door from file managed my minecraft located in [sourceName:KubeJS Resource Pack [assets],location:KubeJS Resource Pack [assets]] +[12:48:00] [Render thread/ERROR] [Shimmer/]: can't find block framedblocks:framed_iron_gate_door from file managed my minecraft located in [sourceName:KubeJS Resource Pack [assets],location:KubeJS Resource Pack [assets]] +[12:48:00] [Render thread/ERROR] [Shimmer/]: can't find block framedblocks:framed_gate_door from file managed my minecraft located in [sourceName:KubeJS Resource Pack [assets],location:KubeJS Resource Pack [assets]] +[12:48:00] [Render thread/ERROR] [Shimmer/]: can't find block framedblocks:framed_iron_gate_door from file managed my minecraft located in [sourceName:KubeJS Resource Pack [assets],location:KubeJS Resource Pack [assets]] +[12:48:00] [Render thread/INFO] [de.ke.fa.ut.re.ResourceHandlers/]: [FANCYMENU] Reloading resources.. +[12:48:00] [Render thread/INFO] [de.ke.fa.ut.re.pr.ResourcePreLoader/]: [FANCYMENU] Pre-loading resources.. +[12:48:00] [Render thread/INFO] [de.ke.fa.cu.la.ScreenCustomizationLayerHandler/]: [FANCYMENU] Updating animation sizes.. +[12:48:00] [Render thread/INFO] [de.ke.fa.cu.la.ScreenCustomizationLayerHandler/]: [FANCYMENU] Minecraft resource reload: FINISHED +[12:48:00] [Render thread/INFO] [de.ke.fa.cu.la.ScreenCustomizationLayerHandler/]: [FANCYMENU] ScreenCustomizationLayer registered: title_screen +[12:48:00] [Render thread/INFO] [Oculus/]: Creating pipeline for dimension NamespacedId{namespace='minecraft', name='overworld'} +[12:48:01] [Render thread/INFO] [ambientsounds/]: Loaded AmbientEngine 'basic' v3.1.0. 11 dimension(s), 11 features, 11 blockgroups, 2 sound collections, 37 regions, 58 sounds, 11 sound categories, 5 solids and 2 biome types +[12:48:01] [Render thread/INFO] [FirstPersonModel/]: PlayerAnimator not found! +[12:48:01] [Render thread/INFO] [FirstPersonModel/]: Loaded Vanilla Hands items: [] +[12:48:01] [Render thread/INFO] [FirstPersonModel/]: Loaded Auto Disable items: [camera] +[12:48:02] [Render thread/WARN] [ModernFix/]: Game took 40.304 seconds to start diff --git a/tests/testdata/TestLogs/TerraFirmaGreg-Modern-forge.xml.log b/tests/testdata/TestLogs/TerraFirmaGreg-Modern-forge.xml.log new file mode 100644 index 000000000..51e5ec546 --- /dev/null +++ b/tests/testdata/TestLogs/TerraFirmaGreg-Modern-forge.xml.log @@ -0,0 +1,2854 @@ +Checking: MC_SLIM +Checking: MERGED_MAPPINGS +Checking: MAPPINGS +Checking: MC_EXTRA +Checking: MOJMAPS +Checking: PATCHED +Checking: MC_SRG + + , --accessToken, ❄❄❄❄❄❄❄❄, --userType, msa, --versionType, release, --launchTarget, forgeclient, --fml.forgeVersion, 47.2.6, --fml.mcVersion, 1.20.1, --fml.forgeGroup, net.minecraftforge, --fml.mcpVersion, 20230612.114412, --width, 854, --height, 480]]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + , --username, Ryexandrite, --assetIndex, 5, --accessToken, ❄❄❄❄❄❄❄❄, --userType, msa, --versionType, release, --width, 854, --height, 480]]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Outdated (Target version: "v2.5.4")]]> + +[Mouse Tweaks] Main.initialize() +[Mouse Tweaks] Initialized. + + + + + Outdated (Target version: "v1.0.5")]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +[ALSOFT] (EE) Failed to set real-time priority for thread: Operation not permitted (1) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + (ModelBakery.java:150) + at TRANSFORMER/minecraft@1.20.1/net.minecraft.client.resources.model.ModelManager.m_246505_(ModelManager.java:83) + at java.base/java.util.concurrent.CompletableFuture.biApply(CompletableFuture.java:1311) + at java.base/java.util.concurrent.CompletableFuture$BiApply.tryFire(CompletableFuture.java:1280) + at java.base/java.util.concurrent.CompletableFuture$Completion.exec(CompletableFuture.java:483) + at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:373) + at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1182) + at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1655) + at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1622) + at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:165) +]]> + + + + (ModelBakery.java:159) + at TRANSFORMER/minecraft@1.20.1/net.minecraft.client.resources.model.ModelManager.m_246505_(ModelManager.java:83) + at java.base/java.util.concurrent.CompletableFuture.biApply(CompletableFuture.java:1311) + at java.base/java.util.concurrent.CompletableFuture$BiApply.tryFire(CompletableFuture.java:1280) + at java.base/java.util.concurrent.CompletableFuture$Completion.exec(CompletableFuture.java:483) + at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:373) + at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1182) + at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1655) + at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1622) + at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:165) +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Reloading Dynamic Lights + + + +Create Crafts & Additions Initialized! + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +[ALSOFT] (EE) Failed to set real-time priority for thread: Operation not permitted (1) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/testdata/TestLogs/vanilla-1.21.5.text.log b/tests/testdata/TestLogs/vanilla-1.21.5.text.log new file mode 100644 index 000000000..e78702858 --- /dev/null +++ b/tests/testdata/TestLogs/vanilla-1.21.5.text.log @@ -0,0 +1,25 @@ +[12:50:56] [Datafixer Bootstrap/INFO]: 263 Datafixer optimizations took 908 milliseconds +[12:50:58] [Render thread/INFO]: Environment: Environment[sessionHost=https://sessionserver.mojang.com, servicesHost=https://api.minecraftservices.com, name=PROD] +[12:50:58] [Render thread/INFO]: Setting user: Ryexandrite +[12:50:58] [Render thread/INFO]: Backend library: LWJGL version 3.3.3+5 +[12:50:58] [Render thread/INFO]: Using optional rendering extensions: GL_KHR_debug, GL_ARB_vertex_attrib_binding, GL_ARB_direct_state_access +[12:50:58] [Render thread/INFO]: Reloading ResourceManager: vanilla +[12:50:59] [Worker-Main-6/INFO]: Found unifont_all_no_pua-16.0.01.hex, loading +[12:50:59] [Worker-Main-7/INFO]: Found unifont_jp_patch-16.0.01.hex, loading +[12:50:59] [Render thread/WARN]: minecraft:pipeline/entity_translucent_emissive shader program does not use sampler Sampler2 defined in the pipeline. This might be a bug. +[12:50:59] [Render thread/INFO]: OpenAL initialized on device Starship/Matisse HD Audio Controller Analog Stereo +[12:50:59] [Render thread/INFO]: Sound engine started +[12:50:59] [Render thread/INFO]: Created: 1024x512x4 minecraft:textures/atlas/blocks.png-atlas +[12:50:59] [Render thread/INFO]: Created: 256x256x4 minecraft:textures/atlas/signs.png-atlas +[12:50:59] [Render thread/INFO]: Created: 512x512x4 minecraft:textures/atlas/banner_patterns.png-atlas +[12:50:59] [Render thread/INFO]: Created: 512x512x4 minecraft:textures/atlas/shield_patterns.png-atlas +[12:50:59] [Render thread/INFO]: Created: 2048x1024x4 minecraft:textures/atlas/armor_trims.png-atlas +[12:50:59] [Render thread/INFO]: Created: 256x256x4 minecraft:textures/atlas/chest.png-atlas +[12:50:59] [Render thread/INFO]: Created: 128x64x4 minecraft:textures/atlas/decorated_pot.png-atlas +[12:50:59] [Render thread/INFO]: Created: 512x256x4 minecraft:textures/atlas/beds.png-atlas +[12:50:59] [Render thread/INFO]: Created: 512x256x4 minecraft:textures/atlas/shulker_boxes.png-atlas +[12:50:59] [Render thread/INFO]: Created: 64x64x0 minecraft:textures/atlas/map_decorations.png-atlas +[12:50:59] [Render thread/INFO]: Created: 512x256x0 minecraft:textures/atlas/particles.png-atlas +[12:51:00] [Render thread/INFO]: Created: 512x256x0 minecraft:textures/atlas/paintings.png-atlas +[12:51:00] [Render thread/INFO]: Created: 256x128x0 minecraft:textures/atlas/mob_effects.png-atlas +[12:51:00] [Render thread/INFO]: Created: 1024x512x0 minecraft:textures/atlas/gui.png-atlas diff --git a/tests/testdata/TestLogs/vanilla-1.21.5.xml.log b/tests/testdata/TestLogs/vanilla-1.21.5.xml.log new file mode 100644 index 000000000..24bfe0b7f --- /dev/null +++ b/tests/testdata/TestLogs/vanilla-1.21.5.xml.log @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 21570a03fbb2973712f1ffe5f1bb4d6095e53e4c Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Fri, 18 Apr 2025 15:22:39 -0700 Subject: [PATCH 153/695] feat(xml-logs): finish tests Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- launcher/BaseInstance.h | 3 - launcher/MessageLevel.cpp | 17 +- launcher/launch/LaunchTask.cpp | 12 +- launcher/launch/LogModel.cpp | 8 + launcher/launch/LogModel.h | 2 + launcher/logs/LogParser.cpp | 50 +- launcher/logs/LogParser.h | 5 +- launcher/minecraft/MinecraftInstance.cpp | 43 - launcher/minecraft/MinecraftInstance.h | 3 - launcher/ui/pages/instance/OtherLogsPage.cpp | 7 +- tests/CMakeLists.txt | 4 + tests/XmlLogs_test.cpp | 104 +- .../TestLogs/TerraFirmaGreg-Modern-levels.txt | 945 ++++++++++++++++++ .../TerraFirmaGreg-Modern-xml-levels.txt | 869 ++++++++++++++++ .../TestLogs/vanilla-1.21.5-levels.txt | 25 + 15 files changed, 2006 insertions(+), 91 deletions(-) create mode 100644 tests/testdata/TestLogs/TerraFirmaGreg-Modern-levels.txt create mode 100644 tests/testdata/TestLogs/TerraFirmaGreg-Modern-xml-levels.txt create mode 100644 tests/testdata/TestLogs/vanilla-1.21.5-levels.txt diff --git a/launcher/BaseInstance.h b/launcher/BaseInstance.h index 2a2b4dc4a..99ce49a62 100644 --- a/launcher/BaseInstance.h +++ b/launcher/BaseInstance.h @@ -151,9 +151,6 @@ class BaseInstance : public QObject, public std::enable_shared_from_this -#include #include #include #include @@ -237,9 +236,9 @@ bool LaunchTask::parseXmlLogs(QString const& line, MessageLevel::Enum level) model.append(MessageLevel::Error, tr("[Log4j Parse Error] Failed to parse log4j log event: %1").arg(err.value().errMessage)); return false; } else { - if (items.has_value()) { + if (!items.isEmpty()) { auto& model = *getLogModel(); - for (auto const& item : items.value()) { + for (auto const& item : items) { if (std::holds_alternative(item)) { auto entry = std::get(item); auto msg = QString("[%1] [%2/%3] [%4]: %5") @@ -252,7 +251,7 @@ bool LaunchTask::parseXmlLogs(QString const& line, MessageLevel::Enum level) model.append(entry.level, msg); } else if (std::holds_alternative(item)) { auto msg = std::get(item).message; - level = m_instance->guessLevel(msg, level); + level = LogParser::guessLevel(msg, model.previousLevel()); msg = censorPrivateInfo(msg); model.append(level, msg); } @@ -281,15 +280,16 @@ void LaunchTask::onLogLine(QString line, MessageLevel::Enum level) level = innerLevel; } + auto& model = *getLogModel(); + // If the level is still undetermined, guess level if (level == MessageLevel::Unknown) { - level = m_instance->guessLevel(line, level); + level = LogParser::guessLevel(line, model.previousLevel()); } // censor private user info line = censorPrivateInfo(line); - auto& model = *getLogModel(); model.append(level, line); } diff --git a/launcher/launch/LogModel.cpp b/launcher/launch/LogModel.cpp index 45aac6099..5d32be9a2 100644 --- a/launcher/launch/LogModel.cpp +++ b/launcher/launch/LogModel.cpp @@ -166,3 +166,11 @@ bool LogModel::isOverFlow() { return m_numLines >= m_maxLines && m_stopOnOverflow; } + + +MessageLevel::Enum LogModel::previousLevel() { + if (!m_content.isEmpty()) { + return m_content.last().level; + } + return MessageLevel::Unknown; +} diff --git a/launcher/launch/LogModel.h b/launcher/launch/LogModel.h index ba7b14487..f4664c47c 100644 --- a/launcher/launch/LogModel.h +++ b/launcher/launch/LogModel.h @@ -31,6 +31,8 @@ class LogModel : public QAbstractListModel { void setColorLines(bool state); bool colorLines() const; + MessageLevel::Enum previousLevel(); + enum Roles { LevelRole = Qt::UserRole }; private /* types */: diff --git a/launcher/logs/LogParser.cpp b/launcher/logs/LogParser.cpp index 294036134..c5d2a8647 100644 --- a/launcher/logs/LogParser.cpp +++ b/launcher/logs/LogParser.cpp @@ -19,6 +19,9 @@ #include "LogParser.h" +#include +#include "MessageLevel.h" + void LogParser::appendLine(QAnyStringView data) { if (!m_partialData.isEmpty()) { @@ -202,7 +205,7 @@ std::optional LogParser::parseNext() } } -std::optional> LogParser::parseAvailable() +QList LogParser::parseAvailable() { QList items; bool doNext = true; @@ -320,3 +323,48 @@ std::optional LogParser::parseLog4J() throw std::runtime_error("unreachable: already verified this was a complete log4j:Event"); } + +MessageLevel::Enum LogParser::guessLevel(const QString& line, MessageLevel::Enum level) +{ + static const QRegularExpression LINE_WITH_LEVEL("^\\[(?[0-9:]+)\\] \\[[^/]+/(?[^\\]]+)\\]"); + auto match = LINE_WITH_LEVEL.match(line); + if (match.hasMatch()) { + // New style logs from log4j + QString timestamp = match.captured("timestamp"); + QString levelStr = match.captured("level"); + if (levelStr == "INFO") + level = MessageLevel::Info; + if (levelStr == "WARN") + level = MessageLevel::Warning; + if (levelStr == "ERROR") + level = MessageLevel::Error; + if (levelStr == "FATAL") + level = MessageLevel::Fatal; + if (levelStr == "TRACE" || levelStr == "DEBUG") + level = MessageLevel::Debug; + } else { + // Old style forge logs + if (line.contains("[INFO]") || line.contains("[CONFIG]") || line.contains("[FINE]") || line.contains("[FINER]") || + line.contains("[FINEST]")) + level = MessageLevel::Info; + if (line.contains("[SEVERE]") || line.contains("[STDERR]")) + level = MessageLevel::Error; + if (line.contains("[WARNING]")) + level = MessageLevel::Warning; + if (line.contains("[DEBUG]")) + level = MessageLevel::Debug; + } + if (level != MessageLevel::Unknown) + return level; + + if (line.contains("overwriting existing")) + return MessageLevel::Fatal; + + // NOTE: this diverges from the real regexp. no unicode, the first section is + instead of * + // static const QRegularExpression JAVA_EXCEPTION( + // R"(Exception in thread|...\d more$|(\s+at |Caused by: )([a-zA-Z_$][a-zA-Z\d_$]*\.)+[a-zA-Z_$][a-zA-Z\d_$]*)|([a-zA-Z_$][a-zA-Z\d_$]*\.)+[a-zA-Z_$][a-zA-Z\d_$]*(Exception|Error|Throwable)"); + // + // if (line.contains(JAVA_EXCEPTION)) + // return MessageLevel::Error; + return MessageLevel::Info; +} diff --git a/launcher/logs/LogParser.h b/launcher/logs/LogParser.h index 462ea43cf..8b23754ac 100644 --- a/launcher/logs/LogParser.h +++ b/launcher/logs/LogParser.h @@ -55,9 +55,12 @@ class LogParser { void appendLine(QAnyStringView data); std::optional parseNext(); - std::optional> parseAvailable(); + QList parseAvailable(); std::optional getError(); + /// guess log level from a line of game log + static MessageLevel::Enum guessLevel(const QString& line, MessageLevel::Enum level); + protected: MessageLevel::Enum parseLogLevel(const QString& level); std::optional parseAttributes(); diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index ec136ede0..e8db24b10 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -1004,49 +1004,6 @@ QMap MinecraftInstance::createCensorFilterFromSession(AuthSess return filter; } -MessageLevel::Enum MinecraftInstance::guessLevel(const QString& line, MessageLevel::Enum level) -{ - if (line.contains("overwriting existing")) - return MessageLevel::Fatal; - - // NOTE: this diverges from the real regexp. no unicode, the first section is + instead of * - static const QRegularExpression JAVA_EXCEPTION( - R"(Exception in thread|...\d more$|(\s+at |Caused by: )([a-zA-Z_$][a-zA-Z\d_$]*\.)+[a-zA-Z_$][a-zA-Z\d_$]*)|([a-zA-Z_$][a-zA-Z\d_$]*\.)+[a-zA-Z_$][a-zA-Z\d_$]*(Exception|Error|Throwable)"); - - if (line.contains(JAVA_EXCEPTION)) - return MessageLevel::Error; - - static const QRegularExpression LINE_WITH_LEVEL("\\[(?[0-9:]+)\\] \\[[^/]+/(?[^\\]]+)\\]"); - auto match = LINE_WITH_LEVEL.match(line); - if (match.hasMatch()) { - // New style logs from log4j - QString timestamp = match.captured("timestamp"); - QString levelStr = match.captured("level"); - if (levelStr == "INFO") - level = MessageLevel::Message; - if (levelStr == "WARN") - level = MessageLevel::Warning; - if (levelStr == "ERROR") - level = MessageLevel::Error; - if (levelStr == "FATAL") - level = MessageLevel::Fatal; - if (levelStr == "TRACE" || levelStr == "DEBUG") - level = MessageLevel::Debug; - } else { - // Old style forge logs - if (line.contains("[INFO]") || line.contains("[CONFIG]") || line.contains("[FINE]") || line.contains("[FINER]") || - line.contains("[FINEST]")) - level = MessageLevel::Message; - if (line.contains("[SEVERE]") || line.contains("[STDERR]")) - level = MessageLevel::Error; - if (line.contains("[WARNING]")) - level = MessageLevel::Warning; - if (line.contains("[DEBUG]")) - level = MessageLevel::Debug; - } - return level; -} - IPathMatcher::Ptr MinecraftInstance::getLogFileMatcher() { auto combined = std::make_shared(); diff --git a/launcher/minecraft/MinecraftInstance.h b/launcher/minecraft/MinecraftInstance.h index 5d9bb45ef..cd5cd1ddc 100644 --- a/launcher/minecraft/MinecraftInstance.h +++ b/launcher/minecraft/MinecraftInstance.h @@ -139,9 +139,6 @@ class MinecraftInstance : public BaseInstance { QProcessEnvironment createEnvironment() override; QProcessEnvironment createLaunchEnvironment() override; - /// guess log level from a line of minecraft log - MessageLevel::Enum guessLevel(const QString& line, MessageLevel::Enum level) override; - IPathMatcher::Ptr getLogFileMatcher() override; QString getLogFileRoot() override; diff --git a/launcher/ui/pages/instance/OtherLogsPage.cpp b/launcher/ui/pages/instance/OtherLogsPage.cpp index 0aeb942a8..8b8c64c6d 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.cpp +++ b/launcher/ui/pages/instance/OtherLogsPage.cpp @@ -179,7 +179,9 @@ void OtherLogsPage::on_btnReload_clicked() showTooBig(); return; } - auto handleLine = [this](QString line) { + MessageLevel::Enum last = MessageLevel::Unknown; + + auto handleLine = [this, &last](QString line) { if (line.isEmpty()) return false; if (line.back() == '\n') @@ -194,9 +196,10 @@ void OtherLogsPage::on_btnReload_clicked() // If the level is still undetermined, guess level if (level == MessageLevel::StdErr || level == MessageLevel::StdOut || level == MessageLevel::Unknown) { - level = m_instance->guessLevel(line, level); + level = LogParser::guessLevel(line, last); } + last = level; m_model->append(level, line); return m_model->isOverFlow(); }; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 2dedb47cc..31b887ff1 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -62,3 +62,7 @@ ecm_add_test(MetaComponentParse_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VE ecm_add_test(CatPack_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME CatPack) + + +ecm_add_test(XmlLogs_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test + TEST_NAME XmlLogs) diff --git a/tests/XmlLogs_test.cpp b/tests/XmlLogs_test.cpp index 072448c4e..e01238570 100644 --- a/tests/XmlLogs_test.cpp +++ b/tests/XmlLogs_test.cpp @@ -23,9 +23,11 @@ #include #include +#include #include -#include +#include +#include #include #include @@ -42,16 +44,62 @@ class XmlLogParseTest : public QObject { QString shortXml = QString::fromUtf8(FS::read(FS::PathCombine(source, "vanilla-1.21.5.xml.log"))); QString shortText = QString::fromUtf8(FS::read(FS::PathCombine(source, "vanilla-1.21.5.text.log"))); + QStringList shortTextLevels_s = QString::fromUtf8(FS::read(FS::PathCombine(source, "vanilla-1.21.5-levels.txt"))) + .split(QRegularExpression("\n|\r\n|\r"), Qt::SkipEmptyParts); + + QList shortTextLevels; + shortTextLevels.reserve(24); + std::transform(shortTextLevels_s.cbegin(), shortTextLevels_s.cend(), std::back_inserter(shortTextLevels), + [](const QString& line) { return MessageLevel::getLevel(line.trimmed()); }); QString longXml = QString::fromUtf8(FS::read(FS::PathCombine(source, "TerraFirmaGreg-Modern-forge.xml.log"))); QString longText = QString::fromUtf8(FS::read(FS::PathCombine(source, "TerraFirmaGreg-Modern-forge.text.log"))); - QTest::addColumn("text"); - QTest::addColumn("xml"); - QTest::newRow("short-vanilla") << shortText << shortXml; - QTest::newRow("long-forge") << longText << longXml; + QStringList longTextLevels_s = QString::fromUtf8(FS::read(FS::PathCombine(source, "TerraFirmaGreg-Modern-levels.txt"))) + .split(QRegularExpression("\n|\r\n|\r"), Qt::SkipEmptyParts); + QStringList longTextLevelsXml_s = QString::fromUtf8(FS::read(FS::PathCombine(source, "TerraFirmaGreg-Modern-xml-levels.txt"))) + .split(QRegularExpression("\n|\r\n|\r"), Qt::SkipEmptyParts); + + QList longTextLevelsPlain; + longTextLevelsPlain.reserve(974); + std::transform(longTextLevels_s.cbegin(), longTextLevels_s.cend(), std::back_inserter(longTextLevelsPlain), + [](const QString& line) { return MessageLevel::getLevel(line.trimmed()); }); + QList longTextLevelsXml; + longTextLevelsXml.reserve(896); + std::transform(longTextLevelsXml_s.cbegin(), longTextLevelsXml_s.cend(), std::back_inserter(longTextLevelsXml), + [](const QString& line) { return MessageLevel::getLevel(line.trimmed()); }); + + QTest::addColumn("log"); + QTest::addColumn("num_entries"); + QTest::addColumn>("entry_levels"); + + QTest::newRow("short-vanilla-plain") << shortText << 25 << shortTextLevels; + QTest::newRow("short-vanilla-xml") << shortXml << 25 << shortTextLevels; + QTest::newRow("long-forge-plain") << longText << 945 << longTextLevelsPlain; + QTest::newRow("long-forge-xml") << longXml << 869 << longTextLevelsXml; } - void parseXml() { QFETCH(QString, ) } + void parseXml() + { + QFETCH(QString, log); + QFETCH(int, num_entries); + QFETCH(QList, entry_levels); + + QList> entries = {}; + + QBENCHMARK + { + entries = parseLines(log.split(QRegularExpression("\n|\r\n|\r"))); + } + + QCOMPARE(entries.length(), num_entries); + + QList levels = {}; + + std::transform(entries.cbegin(), entries.cend(), std::back_inserter(levels), + [](std::pair entry) { return entry.first; }); + + QCOMPARE(levels, entry_levels); + } private: LogParser m_parser; @@ -59,27 +107,35 @@ class XmlLogParseTest : public QObject { QList> parseLines(const QStringList& lines) { QList> out; - for (const auto& line : lines) + MessageLevel::Enum last = MessageLevel::Unknown; + + for (const auto& line : lines) { m_parser.appendLine(line); - auto items = m_parser.parseAvailable(); - for (const auto& item : items) { - if (std::holds_alternative(item)) { - auto entry = std::get(item); - auto msg = QString("[%1] [%2/%3] [%4]: %5") - .arg(entry.timestamp.toString("HH:mm:ss")) - .arg(entry.thread) - .arg(entry.levelText) - .arg(entry.logger) - .arg(entry.message); - msg = censorPrivateInfo(msg); - out.append(std::make_pair(entry.level, msg)); - } else if (std::holds_alternative(item)) { - auto msg = std::get(item).message; - level = m_instance->guessLevel(msg, level); - msg = censorPrivateInfo(msg); - out.append(std::make_pair(entry.level, msg)); + + auto items = m_parser.parseAvailable(); + for (const auto& item : items) { + if (std::holds_alternative(item)) { + auto entry = std::get(item); + auto msg = QString("[%1] [%2/%3] [%4]: %5") + .arg(entry.timestamp.toString("HH:mm:ss")) + .arg(entry.thread) + .arg(entry.levelText) + .arg(entry.logger) + .arg(entry.message); + out.append(std::make_pair(entry.level, msg)); + last = entry.level; + } else if (std::holds_alternative(item)) { + auto msg = std::get(item).message; + auto level = LogParser::guessLevel(msg, last); + out.append(std::make_pair(level, msg)); + last = level; + } } } return out; } }; + +QTEST_GUILESS_MAIN(XmlLogParseTest) + +#include "XmlLogs_test.moc" diff --git a/tests/testdata/TestLogs/TerraFirmaGreg-Modern-levels.txt b/tests/testdata/TestLogs/TerraFirmaGreg-Modern-levels.txt new file mode 100644 index 000000000..1b3002117 --- /dev/null +++ b/tests/testdata/TestLogs/TerraFirmaGreg-Modern-levels.txt @@ -0,0 +1,945 @@ +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +WARN +WARN +WARN +WARN +INFO +ERROR +INFO +ERROR +ERROR +ERROR +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +WARN +WARN +INFO +WARN +WARN +WARN +WARN +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +WARN +INFO +INFO +WARN +WARN +WARN +INFO +WARN +INFO +INFO +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +INFO +INFO +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +INFO +INFO +WARN +INFO +WARN +WARN +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +WARN +INFO +WARN +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +WARN +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +WARN +INFO +INFO +WARN +WARN +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +INFO +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +ERROR +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +INFO +INFO +INFO +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +ERROR +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +INFO +INFO +INFO +ERROR +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +WARN +WARN +WARN +WARN +WARN +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +ERROR +ERROR +ERROR +ERROR +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN diff --git a/tests/testdata/TestLogs/TerraFirmaGreg-Modern-xml-levels.txt b/tests/testdata/TestLogs/TerraFirmaGreg-Modern-xml-levels.txt new file mode 100644 index 000000000..941e5e3fe --- /dev/null +++ b/tests/testdata/TestLogs/TerraFirmaGreg-Modern-xml-levels.txt @@ -0,0 +1,869 @@ +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +WARN +WARN +WARN +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +ERROR +INFO +ERROR +ERROR +ERROR +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +WARN +WARN +INFO +WARN +WARN +WARN +WARN +WARN +INFO +WARN +WARN +INFO +INFO +WARN +WARN +WARN +INFO +WARN +INFO +INFO +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +INFO +INFO +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +WARN +INFO +INFO +WARN +INFO +WARN +WARN +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +WARN +INFO +WARN +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +WARN +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +WARN +INFO +INFO +WARN +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +ERROR +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +WARN +WARN +INFO +INFO +INFO +INFO +ERROR +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +WARN +WARN +WARN +WARN +INFO +INFO +INFO +ERROR +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +WARN +WARN +WARN +WARN +WARN +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +ERROR +ERROR +ERROR +ERROR +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO diff --git a/tests/testdata/TestLogs/vanilla-1.21.5-levels.txt b/tests/testdata/TestLogs/vanilla-1.21.5-levels.txt new file mode 100644 index 000000000..02734e56f --- /dev/null +++ b/tests/testdata/TestLogs/vanilla-1.21.5-levels.txt @@ -0,0 +1,25 @@ +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +WARN +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO +INFO From 266031df816b432589bdb9e2905ce143cc0d9715 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Sat, 19 Apr 2025 10:45:12 +0100 Subject: [PATCH 154/695] Fix compilation on Qt 6.4.2 Is this an EOL version lol Signed-off-by: TheKodeToad --- launcher/logs/LogParser.cpp | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/launcher/logs/LogParser.cpp b/launcher/logs/LogParser.cpp index c5d2a8647..59a1ff3c3 100644 --- a/launcher/logs/LogParser.cpp +++ b/launcher/logs/LogParser.cpp @@ -22,6 +22,8 @@ #include #include "MessageLevel.h" +using namespace Qt::Literals::StringLiterals; + void LogParser::appendLine(QAnyStringView data) { if (!m_partialData.isEmpty()) { @@ -70,18 +72,18 @@ std::optional LogParser::parseAttributes() for (const auto& attr : attributes) { auto name = attr.name(); auto value = attr.value(); - if (name == "logger") { + if (name == "logger"_L1) { entry.logger = value.trimmed().toString(); - } else if (name == "timestamp") { + } else if (name == "timestamp"_L1) { if (value.trimmed().isEmpty()) { m_parser.raiseError("log4j:Event Missing required attribute: timestamp"); return {}; } entry.timestamp = QDateTime::fromSecsSinceEpoch(value.trimmed().toLongLong()); - } else if (name == "level") { + } else if (name == "level"_L1) { entry.levelText = value.trimmed().toString(); entry.level = parseLogLevel(entry.levelText); - } else if (name == "thread") { + } else if (name == "thread"_L1) { entry.thread = value.trimmed().toString(); } } @@ -135,7 +137,7 @@ std::optional LogParser::parseNext() m_parser.setNamespaceProcessing(false); m_parser.addData(m_buffer); if (m_parser.readNextStartElement()) { - if (m_parser.qualifiedName() == "log4j:Event") { + if (m_parser.qualifiedName() == "log4j:Event"_L1) { int depth = 1; bool eod = false; while (depth > 0 && !eod) { @@ -235,7 +237,7 @@ std::optional LogParser::parseLog4J() m_parser.addData(m_buffer); m_parser.readNextStartElement(); - if (m_parser.qualifiedName() == "log4j:Event") { + if (m_parser.qualifiedName() == "log4j:Event"_L1) { auto entry_ = parseAttributes(); if (!entry_.has_value()) { setError(); @@ -251,7 +253,7 @@ std::optional LogParser::parseLog4J() switch (tok) { case QXmlStreamReader::TokenType::StartElement: { depth += 1; - if (m_parser.qualifiedName() == "log4j:Message") { + if (m_parser.qualifiedName() == "log4j:Message"_L1) { QString message; bool messageComplete = false; @@ -263,7 +265,7 @@ std::optional LogParser::parseLog4J() message.append(m_parser.text()); } break; case QXmlStreamReader::TokenType::EndElement: { - if (m_parser.qualifiedName() == "log4j:Message") { + if (m_parser.qualifiedName() == "log4j:Message"_L1) { messageComplete = true; } } break; @@ -287,7 +289,7 @@ std::optional LogParser::parseLog4J() break; case QXmlStreamReader::TokenType::EndElement: { depth -= 1; - if (depth == 0 && m_parser.qualifiedName() == "log4j:Event") { + if (depth == 0 && m_parser.qualifiedName() == "log4j:Event"_L1) { if (foundMessage) { auto consumed = m_parser.characterOffset(); if (consumed > 0 && consumed <= m_buffer.length()) { @@ -362,7 +364,8 @@ MessageLevel::Enum LogParser::guessLevel(const QString& line, MessageLevel::Enum // NOTE: this diverges from the real regexp. no unicode, the first section is + instead of * // static const QRegularExpression JAVA_EXCEPTION( - // R"(Exception in thread|...\d more$|(\s+at |Caused by: )([a-zA-Z_$][a-zA-Z\d_$]*\.)+[a-zA-Z_$][a-zA-Z\d_$]*)|([a-zA-Z_$][a-zA-Z\d_$]*\.)+[a-zA-Z_$][a-zA-Z\d_$]*(Exception|Error|Throwable)"); + // R"(Exception in thread|...\d more$|(\s+at |Caused by: + // )([a-zA-Z_$][a-zA-Z\d_$]*\.)+[a-zA-Z_$][a-zA-Z\d_$]*)|([a-zA-Z_$][a-zA-Z\d_$]*\.)+[a-zA-Z_$][a-zA-Z\d_$]*(Exception|Error|Throwable)"); // // if (line.contains(JAVA_EXCEPTION)) // return MessageLevel::Error; From 11015a22d253d76f2d40838073c1b0a9c20831d4 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Sat, 19 Apr 2025 10:47:32 +0100 Subject: [PATCH 155/695] Remove commented out code Signed-off-by: TheKodeToad --- launcher/logs/LogParser.cpp | 7 ------- 1 file changed, 7 deletions(-) diff --git a/launcher/logs/LogParser.cpp b/launcher/logs/LogParser.cpp index 59a1ff3c3..4c4eae5aa 100644 --- a/launcher/logs/LogParser.cpp +++ b/launcher/logs/LogParser.cpp @@ -362,12 +362,5 @@ MessageLevel::Enum LogParser::guessLevel(const QString& line, MessageLevel::Enum if (line.contains("overwriting existing")) return MessageLevel::Fatal; - // NOTE: this diverges from the real regexp. no unicode, the first section is + instead of * - // static const QRegularExpression JAVA_EXCEPTION( - // R"(Exception in thread|...\d more$|(\s+at |Caused by: - // )([a-zA-Z_$][a-zA-Z\d_$]*\.)+[a-zA-Z_$][a-zA-Z\d_$]*)|([a-zA-Z_$][a-zA-Z\d_$]*\.)+[a-zA-Z_$][a-zA-Z\d_$]*(Exception|Error|Throwable)"); - // - // if (line.contains(JAVA_EXCEPTION)) - // return MessageLevel::Error; return MessageLevel::Info; } From 1bd1245d860e3547085533860c6c6e0725a09528 Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Sat, 19 Apr 2025 13:31:33 -0700 Subject: [PATCH 156/695] feat(xml-logs): Case insisitive xml parseing + cleaner switch Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- launcher/logs/LogParser.cpp | 182 +++++++++++++++++------------------- launcher/logs/LogParser.h | 1 - 2 files changed, 87 insertions(+), 96 deletions(-) diff --git a/launcher/logs/LogParser.cpp b/launcher/logs/LogParser.cpp index 4c4eae5aa..7e7d30ffc 100644 --- a/launcher/logs/LogParser.cpp +++ b/launcher/logs/LogParser.cpp @@ -29,11 +29,9 @@ void LogParser::appendLine(QAnyStringView data) if (!m_partialData.isEmpty()) { m_buffer = QString(m_partialData); m_buffer.append("\n"); - m_buffer.append(data.toString()); m_partialData.clear(); - } else { - m_buffer.append(data.toString()); } + m_buffer.append(data.toString()); } std::optional LogParser::getError() @@ -41,26 +39,6 @@ std::optional LogParser::getError() return m_error; } -MessageLevel::Enum LogParser::parseLogLevel(const QString& level) -{ - auto test = level.trimmed().toUpper(); - if (test == "TRACE") { - return MessageLevel::Trace; - } else if (test == "DEBUG") { - return MessageLevel::Debug; - } else if (test == "INFO") { - return MessageLevel::Info; - } else if (test == "WARN") { - return MessageLevel::Warning; - } else if (test == "ERROR") { - return MessageLevel::Error; - } else if (test == "FATAL") { - return MessageLevel::Fatal; - } else { - return MessageLevel::Unknown; - } -} - std::optional LogParser::parseAttributes() { LogParser::LogEntry entry{ @@ -82,7 +60,7 @@ std::optional LogParser::parseAttributes() entry.timestamp = QDateTime::fromSecsSinceEpoch(value.trimmed().toLongLong()); } else if (name == "level"_L1) { entry.levelText = value.trimmed().toString(); - entry.level = parseLogLevel(entry.levelText); + entry.level = MessageLevel::getLevel(entry.levelText); } else if (name == "thread"_L1) { entry.thread = value.trimmed().toString(); } @@ -137,7 +115,7 @@ std::optional LogParser::parseNext() m_parser.setNamespaceProcessing(false); m_parser.addData(m_buffer); if (m_parser.readNextStartElement()) { - if (m_parser.qualifiedName() == "log4j:Event"_L1) { + if (m_parser.qualifiedName().compare("log4j:Event"_L1, Qt::CaseInsensitive) == 0) { int depth = 1; bool eod = false; while (depth > 0 && !eod) { @@ -237,7 +215,7 @@ std::optional LogParser::parseLog4J() m_parser.addData(m_buffer); m_parser.readNextStartElement(); - if (m_parser.qualifiedName() == "log4j:Event"_L1) { + if (m_parser.qualifiedName().compare("log4j:Event"_L1, Qt::CaseInsensitive) == 0) { auto entry_ = parseAttributes(); if (!entry_.has_value()) { setError(); @@ -248,72 +226,95 @@ std::optional LogParser::parseLog4J() bool foundMessage = false; int depth = 1; - while (!m_parser.atEnd()) { - auto tok = m_parser.readNext(); - switch (tok) { - case QXmlStreamReader::TokenType::StartElement: { - depth += 1; - if (m_parser.qualifiedName() == "log4j:Message"_L1) { - QString message; - bool messageComplete = false; - - while (!messageComplete) { - auto tok = m_parser.readNext(); - - switch (tok) { - case QXmlStreamReader::TokenType::Characters: { - message.append(m_parser.text()); - } break; - case QXmlStreamReader::TokenType::EndElement: { - if (m_parser.qualifiedName() == "log4j:Message"_L1) { - messageComplete = true; - } - } break; - case QXmlStreamReader::TokenType::EndDocument: { - return {}; // parse fail - } break; - default: { - // no op - } - } + enum parseOp { noOp, entryReady, parseError }; + + auto foundStart = [&]() -> parseOp { + depth += 1; + if (m_parser.qualifiedName().compare("log4j:Message"_L1, Qt::CaseInsensitive) == 0) { + QString message; + bool messageComplete = false; + + while (!messageComplete) { + auto tok = m_parser.readNext(); - if (m_parser.hasError()) { - return {}; + switch (tok) { + case QXmlStreamReader::TokenType::Characters: { + message.append(m_parser.text()); + } break; + case QXmlStreamReader::TokenType::EndElement: { + if (m_parser.qualifiedName().compare("log4j:Message"_L1, Qt::CaseInsensitive) == 0) { + messageComplete = true; } + } break; + case QXmlStreamReader::TokenType::EndDocument: { + return parseError; // parse fail + } break; + default: { + // no op } + } - entry.message = message; - foundMessage = true; - depth -= 1; + if (m_parser.hasError()) { + return parseError; } - break; - case QXmlStreamReader::TokenType::EndElement: { - depth -= 1; - if (depth == 0 && m_parser.qualifiedName() == "log4j:Event"_L1) { - if (foundMessage) { - auto consumed = m_parser.characterOffset(); - if (consumed > 0 && consumed <= m_buffer.length()) { - m_buffer = m_buffer.right(m_buffer.length() - consumed); - - if (!m_buffer.isEmpty() && m_buffer.trimmed().isEmpty()) { - // only whitespace, dump it - m_buffer.clear(); - } - } - clearError(); - return entry; - } - m_parser.raiseError("log4j:Event Missing required attribute: message"); - setError(); - return {}; + } + + entry.message = message; + foundMessage = true; + depth -= 1; + } + return noOp; + }; + + auto foundEnd = [&]() -> parseOp { + depth -= 1; + if (depth == 0 && m_parser.qualifiedName().compare("log4j:Event"_L1, Qt::CaseInsensitive) == 0) { + if (foundMessage) { + auto consumed = m_parser.characterOffset(); + if (consumed > 0 && consumed <= m_buffer.length()) { + m_buffer = m_buffer.right(m_buffer.length() - consumed); + + if (!m_buffer.isEmpty() && m_buffer.trimmed().isEmpty()) { + // only whitespace, dump it + m_buffer.clear(); } - } break; - case QXmlStreamReader::TokenType::EndDocument: { - return {}; - } break; - default: { - // no op } + clearError(); + return entryReady; + } + m_parser.raiseError("log4j:Event Missing required attribute: message"); + setError(); + return parseError; + } + return noOp; + }; + + while (!m_parser.atEnd()) { + auto tok = m_parser.readNext(); + parseOp op = noOp; + switch (tok) { + case QXmlStreamReader::TokenType::StartElement: { + op = foundStart(); + } break; + case QXmlStreamReader::TokenType::EndElement: { + op = foundEnd(); + } break; + case QXmlStreamReader::TokenType::EndDocument: { + return {}; + } break; + default: { + // no op + } + } + + switch (op) { + case parseError: + return {}; // parse fail or error + case entryReady: + return entry; + case noOp: + default: { + // no op } } @@ -334,16 +335,7 @@ MessageLevel::Enum LogParser::guessLevel(const QString& line, MessageLevel::Enum // New style logs from log4j QString timestamp = match.captured("timestamp"); QString levelStr = match.captured("level"); - if (levelStr == "INFO") - level = MessageLevel::Info; - if (levelStr == "WARN") - level = MessageLevel::Warning; - if (levelStr == "ERROR") - level = MessageLevel::Error; - if (levelStr == "FATAL") - level = MessageLevel::Fatal; - if (levelStr == "TRACE" || levelStr == "DEBUG") - level = MessageLevel::Debug; + level = MessageLevel::getLevel(levelStr); } else { // Old style forge logs if (line.contains("[INFO]") || line.contains("[CONFIG]") || line.contains("[FINE]") || line.contains("[FINER]") || diff --git a/launcher/logs/LogParser.h b/launcher/logs/LogParser.h index 8b23754ac..1a1d86dd1 100644 --- a/launcher/logs/LogParser.h +++ b/launcher/logs/LogParser.h @@ -62,7 +62,6 @@ class LogParser { static MessageLevel::Enum guessLevel(const QString& line, MessageLevel::Enum level); protected: - MessageLevel::Enum parseLogLevel(const QString& level); std::optional parseAttributes(); void setError(); void clearError(); From 3fd5557f89de1269f024b9bd146ff019170795ee Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 20 Apr 2025 00:28:02 +0000 Subject: [PATCH 157/695] chore(nix): update lockfile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'libnbtplusplus': 'github:PrismLauncher/libnbtplusplus/23b955121b8217c1c348a9ed2483167a6f3ff4ad?narHash=sha256-yy0q%2Bbky80LtK1GWzz7qpM%2BaAGrOqLuewbid8WT1ilk%3D' (2023-11-06) → 'github:PrismLauncher/libnbtplusplus/531449ba1c930c98e0bcf5d332b237a8566f9d78?narHash=sha256-qhmjaRkt%2BO7A%2Bgu6HjUkl7QzOEb4r8y8vWZMG2R/C6o%3D' (2025-04-16) • Updated input 'nixpkgs': 'github:NixOS/nixpkgs/2631b0b7abcea6e640ce31cd78ea58910d31e650?narHash=sha256-LWqduOgLHCFxiTNYi3Uj5Lgz0SR%2BXhw3kr/3Xd0GPTM%3D' (2025-04-12) → 'github:NixOS/nixpkgs/b024ced1aac25639f8ca8fdfc2f8c4fbd66c48ef?narHash=sha256-fusHbZCyv126cyArUwwKrLdCkgVAIaa/fQJYFlCEqiU%3D' (2025-04-17) • Updated input 'qt-qrcodegenerator': 'github:nayuki/QR-Code-generator/f40366c40d8d1956081f7ec643d240c02a81df52?narHash=sha256-5%2BiYwsbX8wjKZPCy7ENj5HCYgOqzeSNLs/YrX2Vc7CQ%3D' (2024-11-18) → 'github:nayuki/QR-Code-generator/2c9044de6b049ca25cb3cd1649ed7e27aa055138?narHash=sha256-6SugPt0lp1Gz7nV23FLmsmpfzgFItkSw7jpGftsDPWc%3D' (2025-01-23) --- flake.lock | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/flake.lock b/flake.lock index 070d069e5..07fa5117a 100644 --- a/flake.lock +++ b/flake.lock @@ -3,11 +3,11 @@ "libnbtplusplus": { "flake": false, "locked": { - "lastModified": 1699286814, - "narHash": "sha256-yy0q+bky80LtK1GWzz7qpM+aAGrOqLuewbid8WT1ilk=", + "lastModified": 1744811532, + "narHash": "sha256-qhmjaRkt+O7A+gu6HjUkl7QzOEb4r8y8vWZMG2R/C6o=", "owner": "PrismLauncher", "repo": "libnbtplusplus", - "rev": "23b955121b8217c1c348a9ed2483167a6f3ff4ad", + "rev": "531449ba1c930c98e0bcf5d332b237a8566f9d78", "type": "github" }, "original": { @@ -18,11 +18,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1744463964, - "narHash": "sha256-LWqduOgLHCFxiTNYi3Uj5Lgz0SR+Xhw3kr/3Xd0GPTM=", + "lastModified": 1744932701, + "narHash": "sha256-fusHbZCyv126cyArUwwKrLdCkgVAIaa/fQJYFlCEqiU=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "2631b0b7abcea6e640ce31cd78ea58910d31e650", + "rev": "b024ced1aac25639f8ca8fdfc2f8c4fbd66c48ef", "type": "github" }, "original": { @@ -35,11 +35,11 @@ "qt-qrcodegenerator": { "flake": false, "locked": { - "lastModified": 1731907326, - "narHash": "sha256-5+iYwsbX8wjKZPCy7ENj5HCYgOqzeSNLs/YrX2Vc7CQ=", + "lastModified": 1737616857, + "narHash": "sha256-6SugPt0lp1Gz7nV23FLmsmpfzgFItkSw7jpGftsDPWc=", "owner": "nayuki", "repo": "QR-Code-generator", - "rev": "f40366c40d8d1956081f7ec643d240c02a81df52", + "rev": "2c9044de6b049ca25cb3cd1649ed7e27aa055138", "type": "github" }, "original": { From e03870d3f2455d5db7ba701187af41abcda66d73 Mon Sep 17 00:00:00 2001 From: Seth Flynn Date: Sun, 20 Apr 2025 16:44:47 -0400 Subject: [PATCH 158/695] ci(get-merge-commit): init Signed-off-by: Seth Flynn --- .github/actions/get-merge-commit.yml | 103 +++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 .github/actions/get-merge-commit.yml diff --git a/.github/actions/get-merge-commit.yml b/.github/actions/get-merge-commit.yml new file mode 100644 index 000000000..8c67fdfc9 --- /dev/null +++ b/.github/actions/get-merge-commit.yml @@ -0,0 +1,103 @@ +# This file incorporates work covered by the following copyright and +# permission notice +# +# Copyright (c) 2003-2025 Eelco Dolstra and the Nixpkgs/NixOS contributors +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +name: Get merge commit +description: Get a merge commit of a given pull request + +inputs: + repository: + description: Repository containing the pull request + required: false + pull-request-id: + description: ID of a pull request + required: true + +outputs: + merge-commit-sha: + description: Git SHA of a merge commit + value: ${{ steps.query.outputs.merge-commit-sha }} + +runs: + using: composite + + steps: + - name: Wait for GitHub to report merge commit + id: query + shell: bash + env: + GITHUB_REPO: ${{ inputs.repository || github.repository }} + PR_ID: ${{ inputs.pull-request-id }} + # https://github.com/NixOS/nixpkgs/blob/8f77f3600f1ee775b85dc2c72fd842768e486ec9/ci/get-merge-commit.sh + run: | + set -euo pipefail + + log() { + echo "$@" >&2 + } + + # Retry the API query this many times + retryCount=5 + # Start with 5 seconds, but double every retry + retryInterval=5 + + while true; do + log "Checking whether the pull request can be merged" + prInfo=$(gh api \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "/repos/$GITHUB_REPO/pulls/$PR_ID") + + # Non-open PRs won't have their mergeability computed no matter what + state=$(jq -r .state <<<"$prInfo") + if [[ "$state" != open ]]; then + log "PR is not open anymore" + exit 1 + fi + + mergeable=$(jq -r .mergeable <<<"$prInfo") + if [[ "$mergeable" == "null" ]]; then + if ((retryCount == 0)); then + log "Not retrying anymore. It's likely that GitHub is having internal issues: check https://www.githubstatus.com/" + exit 3 + else + ((retryCount -= 1)) || true + + # null indicates that GitHub is still computing whether it's mergeable + # Wait a couple seconds before trying again + log "GitHub is still computing whether this PR can be merged, waiting $retryInterval seconds before trying again ($retryCount retries left)" + sleep "$retryInterval" + + ((retryInterval *= 2)) || true + fi + else + break + fi + done + + if [[ "$mergeable" == "true" ]]; then + echo "merge-commit-sha=$(jq -r .merge_commit_sha <<<"$prInfo")" >> "$GITHUB_OUTPUT" + else + echo "# 🚨 The PR has a merge conflict!" >>> "$GITHUB_STEP_SUMMARY" + exit 2 + fi From f2a601f8153f974c73ad547198650de83889bd9d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 24 Apr 2025 16:58:13 +0000 Subject: [PATCH 159/695] chore(deps): update determinatesystems/nix-installer-action action to v17 --- .github/workflows/nix.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index fea0df6ce..765aa4ca8 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -65,7 +65,7 @@ jobs: uses: actions/checkout@v4 - name: Install Nix - uses: DeterminateSystems/nix-installer-action@v16 + uses: DeterminateSystems/nix-installer-action@v17 with: determinate: ${{ env.USE_DETERMINATE }} From 83ebb5984b47dbf393d4e2ad63bb6d9024c13f26 Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Fri, 25 Apr 2025 19:18:28 -0700 Subject: [PATCH 160/695] fix: nullptr access style can't always be created Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- launcher/ui/themes/SystemTheme.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/themes/SystemTheme.cpp b/launcher/ui/themes/SystemTheme.cpp index 7fba08026..c9b2e5cfd 100644 --- a/launcher/ui/themes/SystemTheme.cpp +++ b/launcher/ui/themes/SystemTheme.cpp @@ -54,7 +54,7 @@ SystemTheme::SystemTheme(const QString& styleName, const QPalette& defaultPalett m_colorPalette = defaultPalette; } else { auto style = QStyleFactory::create(styleName); - m_colorPalette = style->standardPalette(); + m_colorPalette = style != nullptr ? style->standardPalette() : defaultPalette; delete style; } } From abe18fb144f707b57ee56dda996ca034c47a3bd6 Mon Sep 17 00:00:00 2001 From: Seth Flynn Date: Sun, 20 Apr 2025 16:54:44 -0400 Subject: [PATCH 161/695] ci(nix): checkout merge commit of pull request Signed-off-by: Seth Flynn --- .github/workflows/nix.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index fea0df6ce..816e2a7aa 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -61,8 +61,17 @@ jobs: id-token: write steps: + - name: Get merge commit + if: ${{ github.event_name == 'pull_request_target' }} + id: merge-commit + uses: ./.github/actions/get-merge-commit.yml + with: + pull-request-id: ${{ github.event.pull_request.id }} + - name: Checkout repository uses: actions/checkout@v4 + with: + ref: ${{ steps.merge-commit.outputs.merge-commit-sha || github.sha }} - name: Install Nix uses: DeterminateSystems/nix-installer-action@v16 From a702d06cd85737728a1ec15538127e4c418bde8e Mon Sep 17 00:00:00 2001 From: Xander Date: Sat, 26 Apr 2025 21:41:14 +0100 Subject: [PATCH 162/695] Pass mainclass and gameargs to the main game via system properties Signed-off-by: Xander --- .../launcher/impl/StandardLauncher.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/libraries/launcher/org/prismlauncher/launcher/impl/StandardLauncher.java b/libraries/launcher/org/prismlauncher/launcher/impl/StandardLauncher.java index 084fbc849..af9b41533 100644 --- a/libraries/launcher/org/prismlauncher/launcher/impl/StandardLauncher.java +++ b/libraries/launcher/org/prismlauncher/launcher/impl/StandardLauncher.java @@ -97,6 +97,18 @@ public void launch() throws Throwable { gameArgs.add(worldName); } + StringBuilder joinedGameArgs = new StringBuilder(); + for (String gameArg : gameArgs) { + if (joinedGameArgs.length() > 0) { + joinedGameArgs.append(" "); + } + joinedGameArgs.append(gameArg); + } + + // pass the real main class and game arguments in so mods can access them + System.setProperty("org.prismlauncher.launch.mainclass", mainClassName); + System.setProperty("org.prismlauncher.launch.gameargs", joinedGameArgs.toString()); + // find and invoke the main method MethodHandle method = ReflectionUtils.findMainMethod(mainClassName); method.invokeExact(gameArgs.toArray(new String[0])); From a92e114236b54a23561fa4071cc1ec8486b43119 Mon Sep 17 00:00:00 2001 From: Xander Date: Sat, 26 Apr 2025 21:57:28 +0100 Subject: [PATCH 163/695] Use \u001F instead of a space as a delimeter for game args Signed-off-by: Xander --- .../org/prismlauncher/launcher/impl/StandardLauncher.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/launcher/org/prismlauncher/launcher/impl/StandardLauncher.java b/libraries/launcher/org/prismlauncher/launcher/impl/StandardLauncher.java index af9b41533..96a809dfe 100644 --- a/libraries/launcher/org/prismlauncher/launcher/impl/StandardLauncher.java +++ b/libraries/launcher/org/prismlauncher/launcher/impl/StandardLauncher.java @@ -100,7 +100,7 @@ public void launch() throws Throwable { StringBuilder joinedGameArgs = new StringBuilder(); for (String gameArg : gameArgs) { if (joinedGameArgs.length() > 0) { - joinedGameArgs.append(" "); + joinedGameArgs.append('\u001F'); // unit separator, designed for this purpose } joinedGameArgs.append(gameArg); } From 02106ab29a8761e44aab5c8e73fd8842c4802b32 Mon Sep 17 00:00:00 2001 From: Xander Date: Sat, 26 Apr 2025 23:42:22 +0100 Subject: [PATCH 164/695] comment on property about delimeter Signed-off-by: Xander --- .../org/prismlauncher/launcher/impl/StandardLauncher.java | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/launcher/org/prismlauncher/launcher/impl/StandardLauncher.java b/libraries/launcher/org/prismlauncher/launcher/impl/StandardLauncher.java index 96a809dfe..968499ff6 100644 --- a/libraries/launcher/org/prismlauncher/launcher/impl/StandardLauncher.java +++ b/libraries/launcher/org/prismlauncher/launcher/impl/StandardLauncher.java @@ -107,6 +107,7 @@ public void launch() throws Throwable { // pass the real main class and game arguments in so mods can access them System.setProperty("org.prismlauncher.launch.mainclass", mainClassName); + // unit separator ('\u001F') delimited list of game args System.setProperty("org.prismlauncher.launch.gameargs", joinedGameArgs.toString()); // find and invoke the main method From facc48d0f8e58db46f67c444a59904391cad7ce3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 27 Apr 2025 00:28:07 +0000 Subject: [PATCH 165/695] chore(nix): update lockfile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'nixpkgs': 'github:NixOS/nixpkgs/b024ced1aac25639f8ca8fdfc2f8c4fbd66c48ef?narHash=sha256-fusHbZCyv126cyArUwwKrLdCkgVAIaa/fQJYFlCEqiU%3D' (2025-04-17) → 'github:NixOS/nixpkgs/f771eb401a46846c1aebd20552521b233dd7e18b?narHash=sha256-ITSpPDwvLBZBnPRS2bUcHY3gZSwis/uTe255QgMtTLA%3D' (2025-04-24) --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 07fa5117a..5418557a3 100644 --- a/flake.lock +++ b/flake.lock @@ -18,11 +18,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1744932701, - "narHash": "sha256-fusHbZCyv126cyArUwwKrLdCkgVAIaa/fQJYFlCEqiU=", + "lastModified": 1745526057, + "narHash": "sha256-ITSpPDwvLBZBnPRS2bUcHY3gZSwis/uTe255QgMtTLA=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "b024ced1aac25639f8ca8fdfc2f8c4fbd66c48ef", + "rev": "f771eb401a46846c1aebd20552521b233dd7e18b", "type": "github" }, "original": { From 47cb58d326f5177c7337b531a8aec75b07edf1d7 Mon Sep 17 00:00:00 2001 From: Seth Flynn Date: Sun, 27 Apr 2025 07:16:20 -0400 Subject: [PATCH 166/695] ci(nix): fix get-merge-commit action call Signed-off-by: Seth Flynn --- .../{get-merge-commit.yml => get-merge-commit/action.yml} | 0 .github/workflows/nix.yml | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename .github/actions/{get-merge-commit.yml => get-merge-commit/action.yml} (100%) diff --git a/.github/actions/get-merge-commit.yml b/.github/actions/get-merge-commit/action.yml similarity index 100% rename from .github/actions/get-merge-commit.yml rename to .github/actions/get-merge-commit/action.yml diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index 816e2a7aa..03d5f2089 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -64,7 +64,7 @@ jobs: - name: Get merge commit if: ${{ github.event_name == 'pull_request_target' }} id: merge-commit - uses: ./.github/actions/get-merge-commit.yml + uses: PrismLauncher/PrismLauncher/.github/actions/get-merge-commit@develop with: pull-request-id: ${{ github.event.pull_request.id }} From 3b7b9fa03c330aa8844b380be614dce9fde90097 Mon Sep 17 00:00:00 2001 From: Seth Flynn Date: Sun, 27 Apr 2025 06:59:19 -0400 Subject: [PATCH 167/695] ci: better filter workflow runs Signed-off-by: Seth Flynn --- .github/workflows/codeql.yml | 35 ++++++++++++++++++++- .github/workflows/trigger_builds.yml | 46 +++++++++++++++++++--------- 2 files changed, 66 insertions(+), 15 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index e3243097d..a5ac537f1 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,6 +1,39 @@ name: "CodeQL Code Scanning" -on: [push, pull_request, workflow_dispatch] +on: + push: + # NOTE: `!` doesn't work with `paths-ignore` :( + # So we a catch-all glob instead + # https://github.com/orgs/community/discussions/25369#discussioncomment-3247674 + paths: + - "**" + - "!.github/**" + - ".github/workflows/codeql.yml" + - "!flatpak/" + - "!nix/" + - "!scripts/" + + - "!.git*" + - "!.envrc" + - "!**.md" + - "COPYING.md" + - "!renovate.json" + pull_request: + # See above + paths: + - "**" + - "!.github/**" + - ".github/workflows/codeql.yml" + - "!flatpak/" + - "!nix/" + - "!scripts/" + + - "!.git*" + - "!.envrc" + - "!**.md" + - "COPYING.md" + - "!renovate.json" + workflow_dispatch: jobs: CodeQL: diff --git a/.github/workflows/trigger_builds.yml b/.github/workflows/trigger_builds.yml index 9efafc8cc..e4c90ef0b 100644 --- a/.github/workflows/trigger_builds.yml +++ b/.github/workflows/trigger_builds.yml @@ -4,21 +4,39 @@ on: push: branches-ignore: - "renovate/**" - paths-ignore: - - "**.md" - - "**/LICENSE" - - "flake.lock" - - "packages/**" - - ".github/ISSUE_TEMPLATE/**" - - ".markdownlint**" + # NOTE: `!` doesn't work with `paths-ignore` :( + # So we a catch-all glob instead + # https://github.com/orgs/community/discussions/25369#discussioncomment-3247674 + paths: + - "**" + - "!.github/**" + - ".github/workflows/build.yml" + - ".github/workflows/trigger_builds.yml" + - "!flatpak/" + - "!nix/" + - "!scripts/" + + - "!.git*" + - "!.envrc" + - "!**.md" + - "COPYING.md" + - "!renovate.json" pull_request: - paths-ignore: - - "**.md" - - "**/LICENSE" - - "flake.lock" - - "packages/**" - - ".github/ISSUE_TEMPLATE/**" - - ".markdownlint**" + # See above + paths: + - "**" + - "!.github/**" + - ".github/workflows/build.yml" + - ".github/workflows/trigger_builds.yml" + - "!flatpak/" + - "!nix/" + - "!scripts/" + + - "!.git*" + - "!.envrc" + - "!**.md" + - "COPYING.md" + - "!renovate.json" workflow_dispatch: jobs: From 57a2ef1aed5a673f7772005e0dc4c4aa100e3c4d Mon Sep 17 00:00:00 2001 From: Seth Flynn Date: Sun, 27 Apr 2025 07:00:26 -0400 Subject: [PATCH 168/695] ci: fix improper paths-ignore usage Signed-off-by: Seth Flynn --- .github/workflows/flatpak.yml | 40 ++++++++++++++++++++--------- .github/workflows/nix.yml | 48 ++++++++++++++++++++--------------- 2 files changed, 55 insertions(+), 33 deletions(-) diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml index 41cc2a51d..8caba46fa 100644 --- a/.github/workflows/flatpak.yml +++ b/.github/workflows/flatpak.yml @@ -2,22 +2,38 @@ name: Flatpak on: push: - paths-ignore: - - "**.md" - - "**/LICENSE" - - ".github/ISSUE_TEMPLATE/**" - - ".markdownlint**" - - "nix/**" # We don't do anything with these artifacts on releases. They go to Flathub tags-ignore: - "*" + # NOTE: `!` doesn't work with `paths-ignore` :( + # So we a catch-all glob instead + # https://github.com/orgs/community/discussions/25369#discussioncomment-3247674 + paths: + - "**" + - "!.github/**" + - ".github/workflows/flatpak.yml" + - "!nix/" + - "!scripts/" + + - "!.git*" + - "!.envrc" + - "!**.md" + - "COPYING.md" + - "!renovate.json" pull_request: - paths-ignore: - - "**.md" - - "**/LICENSE" - - ".github/ISSUE_TEMPLATE/**" - - ".markdownlint**" - - "nix/**" + # See above + paths: + - "**" + - "!.github/**" + - ".github/workflows/flatpak.yml" + - "!nix/" + - "!scripts/" + + - "!.git*" + - "!.envrc" + - "!**.md" + - "COPYING.md" + - "!renovate.json" workflow_dispatch: permissions: diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index 816e2a7aa..b968062c9 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -4,28 +4,34 @@ on: push: tags: - "*" - paths-ignore: - - ".github/**" - - "!.github/workflows/nix.yml" - - "flatpak/" - - "scripts/" - - - ".git*" - - ".envrc" - - "**.md" - - "!COPYING.md" - - "renovate.json" + # NOTE: `!` doesn't work with `paths-ignore` :( + # So we a catch-all glob instead + # https://github.com/orgs/community/discussions/25369#discussioncomment-3247674 + paths: + - "**" + - "!.github/**" + - ".github/workflows/nix.yml" + - "!flatpak/" + - "!scripts/" + + - "!.git*" + - "!.envrc" + - "!**.md" + - "COPYING.md" + - "!renovate.json" pull_request_target: - paths-ignore: - - ".github/**" - - "flatpak/" - - "scripts/" - - - ".git*" - - ".envrc" - - "**.md" - - "!COPYING.md" - - "renovate.json" + paths: + - "**" + - "!.github/**" + - ".github/workflows/nix.yml" + - "!flatpak/" + - "!scripts/" + + - "!.git*" + - "!.envrc" + - "!**.md" + - "COPYING.md" + - "!renovate.json" workflow_dispatch: permissions: From 20626e6606ba07317fb4283f0814ecef4e1ea136 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Mon, 28 Apr 2025 10:28:57 +0100 Subject: [PATCH 169/695] Fix log sorting Signed-off-by: TheKodeToad --- launcher/minecraft/MinecraftInstance.cpp | 2 +- launcher/ui/pages/instance/OtherLogsPage.cpp | 18 ++++++++---------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index 121b8035c..1009d7c42 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -1057,7 +1057,7 @@ MessageLevel::Enum MinecraftInstance::guessLevel(const QString& line, MessageLev QStringList MinecraftInstance::getLogFileSearchPaths() { - return { FS::PathCombine(gameRoot(), "logs"), FS::PathCombine(gameRoot(), "crash-reports"), gameRoot() }; + return { FS::PathCombine(gameRoot(), "crash-reports"), FS::PathCombine(gameRoot(), "logs"), gameRoot() }; } QString MinecraftInstance::getStatusbarDescription() diff --git a/launcher/ui/pages/instance/OtherLogsPage.cpp b/launcher/ui/pages/instance/OtherLogsPage.cpp index f457195d8..80a9c0fdc 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.cpp +++ b/launcher/ui/pages/instance/OtherLogsPage.cpp @@ -146,6 +146,7 @@ void OtherLogsPage::populateSelectLogBox() ui->selectLogBox->setCurrentIndex(index); ui->selectLogBox->blockSignals(false); setControlsEnabled(true); + // don't refresh file return; } else { setControlsEnabled(false); @@ -405,20 +406,17 @@ QStringList OtherLogsPage::getPaths() QStringList result; for (QString searchPath : m_logSearchPaths) { - QDirIterator iterator(searchPath, QDir::Files | QDir::Readable); + QDir searchDir(searchPath); - const bool isRoot = searchPath == m_basePath; + QStringList filters{ "*.log", "*.log.gz" }; - while (iterator.hasNext()) { - const QString name = iterator.next(); + if (searchPath != m_basePath) + filters.append("*.txt"); - QString relativePath = baseDir.relativeFilePath(name); + QStringList entries = searchDir.entryList(filters, QDir::Files | QDir::Readable, QDir::SortFlag::Time); - if (!(name.endsWith(".log") || name.endsWith(".log.gz") || (!isRoot && name.endsWith(".txt")))) - continue; - - result.append(relativePath); - } + for (const QString& name : entries) + result.append(baseDir.relativeFilePath(searchDir.filePath(name))); } return result; From 2c943a003d798aeaa76ab630979953ac7aff51eb Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Mon, 28 Apr 2025 03:08:33 -0700 Subject: [PATCH 170/695] feat(xml-logs): preserve whitespace lines in logs Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- launcher/logs/LogParser.cpp | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/launcher/logs/LogParser.cpp b/launcher/logs/LogParser.cpp index 7e7d30ffc..6e33b24dd 100644 --- a/launcher/logs/LogParser.cpp +++ b/launcher/logs/LogParser.cpp @@ -105,8 +105,9 @@ std::optional LogParser::parseNext() } if (m_buffer.trimmed().isEmpty()) { + auto text = QString(m_buffer); m_buffer.clear(); - return {}; + return LogParser::PlainText { text }; } // check if we have a full xml log4j event @@ -177,11 +178,7 @@ std::optional LogParser::parseNext() // no log4j found, all plain text auto text = QString(m_buffer); m_buffer.clear(); - if (text.trimmed().isEmpty()) { - return {}; - } else { - return LogParser::PlainText{ text }; - } + return LogParser::PlainText{ text }; } } @@ -273,11 +270,7 @@ std::optional LogParser::parseLog4J() auto consumed = m_parser.characterOffset(); if (consumed > 0 && consumed <= m_buffer.length()) { m_buffer = m_buffer.right(m_buffer.length() - consumed); - - if (!m_buffer.isEmpty() && m_buffer.trimmed().isEmpty()) { - // only whitespace, dump it - m_buffer.clear(); - } + // potential whitespace preserved for next item } clearError(); return entryReady; From d0ccd110a15c31e3bb03454518ea2c78fe38a07c Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Sat, 19 Apr 2025 06:16:57 -0700 Subject: [PATCH 171/695] fix: use after free begone! Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- launcher/Application.cpp | 2 +- launcher/qtlogging.ini | 3 +++ launcher/ui/pages/instance/ModFolderPage.cpp | 24 ++++++++++++++----- launcher/ui/pages/instance/ModFolderPage.h | 4 ++++ .../ui/pages/instance/ResourcePackPage.cpp | 24 +++++++++++++------ launcher/ui/pages/instance/ResourcePackPage.h | 5 ++++ launcher/ui/pages/instance/ShaderPackPage.cpp | 21 +++++++++++----- launcher/ui/pages/instance/ShaderPackPage.h | 4 ++++ .../ui/pages/instance/TexturePackPage.cpp | 20 +++++++++++----- launcher/ui/pages/instance/TexturePackPage.h | 5 ++++ 10 files changed, 86 insertions(+), 26 deletions(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index cfe028279..33d700772 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -96,6 +96,7 @@ #include #include #include +#include #include #include #include @@ -125,7 +126,6 @@ #include #include -#include #include #include #include "SysInfo.h" diff --git a/launcher/qtlogging.ini b/launcher/qtlogging.ini index c12d1e109..10f724163 100644 --- a/launcher/qtlogging.ini +++ b/launcher/qtlogging.ini @@ -3,6 +3,9 @@ # prevent log spam and strange bugs # qt.qpa.drawing in particular causes theme artifacts on MacOS qt.*.debug=false +# supress image format noise +kf.imageformats.plugins.hdr=false +kf.imageformats.plugins.xcf=false # don't log credentials by default launcher.auth.credentials.debug=false # remove the debug lines, other log levels still get through diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index 026f0c140..8508e7908 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -48,6 +48,7 @@ #include #include #include +#include #include "Application.h" @@ -145,10 +146,17 @@ void ModFolderPage::downloadMods() QMessageBox::critical(this, tr("Error"), tr("Please install a mod loader first!")); return; } - auto mdownload = new ResourceDownload::ModDownloadDialog(this, m_model, m_instance); - mdownload->setAttribute(Qt::WA_DeleteOnClose); - connect(this, &QObject::destroyed, mdownload, &QDialog::close); - if (mdownload->exec()) { + + m_downloadDialog = new ResourceDownload::ModDownloadDialog(this, m_model, m_instance); + m_downloadDialog->setAttribute(Qt::WA_DeleteOnClose); + connect(this, &QObject::destroyed, m_downloadDialog, &QDialog::close); + connect(m_downloadDialog, &QDialog::finished, this, &ModFolderPage::downloadDialogFinished); + m_downloadDialog->open(); +} + +void ModFolderPage::downloadDialogFinished(int result) +{ + if (result) { auto tasks = new ConcurrentTask(tr("Download Mods"), APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); connect(tasks, &Task::failed, [this, tasks](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); @@ -166,8 +174,12 @@ void ModFolderPage::downloadMods() tasks->deleteLater(); }); - for (auto& task : mdownload->getTasks()) { - tasks->addTask(task); + if (m_downloadDialog) { + for (auto& task : m_downloadDialog->getTasks()) { + tasks->addTask(task); + } + } else { + qWarning() << "ResourceDownloadDialog vanished before we could collect tasks!"; } ProgressDialog loadDialog(this); diff --git a/launcher/ui/pages/instance/ModFolderPage.h b/launcher/ui/pages/instance/ModFolderPage.h index a7d078f50..8996b1615 100644 --- a/launcher/ui/pages/instance/ModFolderPage.h +++ b/launcher/ui/pages/instance/ModFolderPage.h @@ -38,7 +38,9 @@ #pragma once +#include #include "ExternalResourcesPage.h" +#include "ui/dialogs/ResourceDownloadDialog.h" class ModFolderPage : public ExternalResourcesPage { Q_OBJECT @@ -63,6 +65,7 @@ class ModFolderPage : public ExternalResourcesPage { void removeItems(const QItemSelection& selection) override; void downloadMods(); + void downloadDialogFinished(int result); void updateMods(bool includeDeps = false); void deleteModMetadata(); void exportModMetadata(); @@ -70,6 +73,7 @@ class ModFolderPage : public ExternalResourcesPage { protected: std::shared_ptr m_model; + QPointer m_downloadDialog; }; class CoreModFolderPage : public ModFolderPage { diff --git a/launcher/ui/pages/instance/ResourcePackPage.cpp b/launcher/ui/pages/instance/ResourcePackPage.cpp index ae5eb8fac..0d9e643b1 100644 --- a/launcher/ui/pages/instance/ResourcePackPage.cpp +++ b/launcher/ui/pages/instance/ResourcePackPage.cpp @@ -84,10 +84,16 @@ void ResourcePackPage::downloadResourcePacks() if (m_instance->typeName() != "Minecraft") return; // this is a null instance or a legacy instance - auto mdownload = new ResourceDownload::ResourcePackDownloadDialog(this, m_model, m_instance); - mdownload->setAttribute(Qt::WA_DeleteOnClose); - connect(this, &QObject::destroyed, mdownload, &QDialog::close); - if (mdownload->exec()) { + m_downloadDialog = new ResourceDownload::ResourcePackDownloadDialog(this, m_model, m_instance); + m_downloadDialog->setAttribute(Qt::WA_DeleteOnClose); + connect(this, &QObject::destroyed, m_downloadDialog, &QDialog::close); + connect(m_downloadDialog, &QDialog::finished, this, &ResourcePackPage::downloadDialogFinished); + m_downloadDialog->open(); +} + +void ResourcePackPage::downloadDialogFinished(int result) +{ + if (result) { auto tasks = new ConcurrentTask("Download Resource Pack", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); connect(tasks, &Task::failed, [this, tasks](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); @@ -105,8 +111,12 @@ void ResourcePackPage::downloadResourcePacks() tasks->deleteLater(); }); - for (auto& task : mdownload->getTasks()) { - tasks->addTask(task); + if (m_downloadDialog) { + for (auto& task : m_downloadDialog->getTasks()) { + tasks->addTask(task); + } + } else { + qWarning() << "ResourceDownloadDialog vanished before we could collect tasks!"; } ProgressDialog loadDialog(this); @@ -269,4 +279,4 @@ void ResourcePackPage::changeResourcePackVersion() m_model->update(); } -} \ No newline at end of file +} diff --git a/launcher/ui/pages/instance/ResourcePackPage.h b/launcher/ui/pages/instance/ResourcePackPage.h index 55abe007c..e39d417c9 100644 --- a/launcher/ui/pages/instance/ResourcePackPage.h +++ b/launcher/ui/pages/instance/ResourcePackPage.h @@ -37,7 +37,10 @@ #pragma once +#include + #include "ExternalResourcesPage.h" +#include "ui/dialogs/ResourceDownloadDialog.h" #include "ui_ExternalResourcesPage.h" #include "minecraft/mod/ResourcePackFolderModel.h" @@ -62,10 +65,12 @@ class ResourcePackPage : public ExternalResourcesPage { private slots: void downloadResourcePacks(); + void downloadDialogFinished(int result); void updateResourcePacks(); void deleteResourcePackMetadata(); void changeResourcePackVersion(); protected: std::shared_ptr m_model; + QPointer m_downloadDialog; }; diff --git a/launcher/ui/pages/instance/ShaderPackPage.cpp b/launcher/ui/pages/instance/ShaderPackPage.cpp index 45bb02030..829a75a72 100644 --- a/launcher/ui/pages/instance/ShaderPackPage.cpp +++ b/launcher/ui/pages/instance/ShaderPackPage.cpp @@ -81,10 +81,15 @@ void ShaderPackPage::downloadShaderPack() if (m_instance->typeName() != "Minecraft") return; // this is a null instance or a legacy instance - auto mdownload = new ResourceDownload::ShaderPackDownloadDialog(this, m_model, m_instance); - mdownload->setAttribute(Qt::WA_DeleteOnClose); - connect(this, &QObject::destroyed, mdownload, &QDialog::close); - if (mdownload->exec()) { + m_downloadDialog = new ResourceDownload::ShaderPackDownloadDialog(this, m_model, m_instance); + m_downloadDialog->setAttribute(Qt::WA_DeleteOnClose); + connect(this, &QObject::destroyed, m_downloadDialog, &QDialog::close); + connect(m_downloadDialog, &QDialog::finished, this, &ShaderPackPage::downloadDialogFinished); + m_downloadDialog->open(); +} + +void ShaderPackPage::downloadDialogFinished(int result) { + if (result) { auto tasks = new ConcurrentTask("Download Shader Packs", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); connect(tasks, &Task::failed, [this, tasks](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); @@ -102,8 +107,12 @@ void ShaderPackPage::downloadShaderPack() tasks->deleteLater(); }); - for (auto& task : mdownload->getTasks()) { - tasks->addTask(task); + if (m_downloadDialog) { + for (auto& task : m_downloadDialog->getTasks()) { + tasks->addTask(task); + } + } else { + qWarning() << "ResourceDownloadDialog vanished before we could collect tasks!"; } ProgressDialog loadDialog(this); diff --git a/launcher/ui/pages/instance/ShaderPackPage.h b/launcher/ui/pages/instance/ShaderPackPage.h index ebf7f1d58..f2b141329 100644 --- a/launcher/ui/pages/instance/ShaderPackPage.h +++ b/launcher/ui/pages/instance/ShaderPackPage.h @@ -37,7 +37,9 @@ #pragma once +#include #include "ExternalResourcesPage.h" +#include "ui/dialogs/ResourceDownloadDialog.h" class ShaderPackPage : public ExternalResourcesPage { Q_OBJECT @@ -54,10 +56,12 @@ class ShaderPackPage : public ExternalResourcesPage { public slots: void downloadShaderPack(); + void downloadDialogFinished(int result); void updateShaderPacks(); void deleteShaderPackMetadata(); void changeShaderPackVersion(); private: std::shared_ptr m_model; + QPointer m_downloadDialog; }; diff --git a/launcher/ui/pages/instance/TexturePackPage.cpp b/launcher/ui/pages/instance/TexturePackPage.cpp index 6d000a486..ada29d94b 100644 --- a/launcher/ui/pages/instance/TexturePackPage.cpp +++ b/launcher/ui/pages/instance/TexturePackPage.cpp @@ -90,10 +90,14 @@ void TexturePackPage::downloadTexturePacks() if (m_instance->typeName() != "Minecraft") return; // this is a null instance or a legacy instance - auto mdownload = new ResourceDownload::TexturePackDownloadDialog(this, m_model, m_instance); - mdownload->setAttribute(Qt::WA_DeleteOnClose); - connect(this, &QObject::destroyed, mdownload, &QDialog::close); - if (mdownload->exec()) { + auto m_downloadDialog = new ResourceDownload::TexturePackDownloadDialog(this, m_model, m_instance); + m_downloadDialog->setAttribute(Qt::WA_DeleteOnClose); + connect(this, &QObject::destroyed, m_downloadDialog, &QDialog::close); +} + +void TexturePackPage::downloadDialogFinished(int result) +{ + if (result) { auto tasks = new ConcurrentTask("Download Texture Packs", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); connect(tasks, &Task::failed, [this, tasks](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); @@ -111,8 +115,12 @@ void TexturePackPage::downloadTexturePacks() tasks->deleteLater(); }); - for (auto& task : mdownload->getTasks()) { - tasks->addTask(task); + if (m_downloadDialog) { + for (auto& task : m_downloadDialog->getTasks()) { + tasks->addTask(task); + } + } else { + qWarning() << "ResourceDownloadDialog vanished before we could collect tasks!"; } ProgressDialog loadDialog(this); diff --git a/launcher/ui/pages/instance/TexturePackPage.h b/launcher/ui/pages/instance/TexturePackPage.h index 28d7ba209..3ebca3e87 100644 --- a/launcher/ui/pages/instance/TexturePackPage.h +++ b/launcher/ui/pages/instance/TexturePackPage.h @@ -37,7 +37,10 @@ #pragma once +#include + #include "ExternalResourcesPage.h" +#include "ui/dialogs/ResourceDownloadDialog.h" #include "ui_ExternalResourcesPage.h" #include "minecraft/mod/TexturePackFolderModel.h" @@ -57,10 +60,12 @@ class TexturePackPage : public ExternalResourcesPage { public slots: void updateFrame(const QModelIndex& current, const QModelIndex& previous) override; void downloadTexturePacks(); + void downloadDialogFinished(int result); void updateTexturePacks(); void deleteTexturePackMetadata(); void changeTexturePackVersion(); private: std::shared_ptr m_model; + QPointer m_downloadDialog; }; From 07a6606c9c549b70ce91225303abe90982973a72 Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Sat, 19 Apr 2025 06:48:04 -0700 Subject: [PATCH 172/695] fix: cover both usages of the download dialog Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- launcher/ui/pages/instance/ModFolderPage.cpp | 42 ++++-------------- .../ui/pages/instance/ResourcePackPage.cpp | 40 ++++------------- launcher/ui/pages/instance/ShaderPackPage.cpp | 43 +++++-------------- .../ui/pages/instance/TexturePackPage.cpp | 43 +++++-------------- 4 files changed, 37 insertions(+), 131 deletions(-) diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index 8508e7908..dad2da8a4 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -146,11 +146,11 @@ void ModFolderPage::downloadMods() QMessageBox::critical(this, tr("Error"), tr("Please install a mod loader first!")); return; } - + m_downloadDialog = new ResourceDownload::ModDownloadDialog(this, m_model, m_instance); - m_downloadDialog->setAttribute(Qt::WA_DeleteOnClose); connect(this, &QObject::destroyed, m_downloadDialog, &QDialog::close); connect(m_downloadDialog, &QDialog::finished, this, &ModFolderPage::downloadDialogFinished); + m_downloadDialog->open(); } @@ -188,6 +188,8 @@ void ModFolderPage::downloadDialogFinished(int result) m_model->update(); } + if (m_downloadDialog) + m_downloadDialog->deleteLater(); } void ModFolderPage::updateMods(bool includeDeps) @@ -313,38 +315,12 @@ void ModFolderPage::changeModVersion() if (mods_list.length() != 1 || mods_list[0]->metadata() == nullptr) return; - auto mdownload = new ResourceDownload::ModDownloadDialog(this, m_model, m_instance); - mdownload->setAttribute(Qt::WA_DeleteOnClose); - connect(this, &QObject::destroyed, mdownload, &QDialog::close); - mdownload->setResourceMetadata((*mods_list.begin())->metadata()); - if (mdownload->exec()) { - auto tasks = new ConcurrentTask("Download Mods", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); - connect(tasks, &Task::failed, [this, tasks](QString reason) { - CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); - tasks->deleteLater(); - }); - connect(tasks, &Task::aborted, [this, tasks]() { - CustomMessageBox::selectable(this, tr("Aborted"), tr("Download stopped by user."), QMessageBox::Information)->show(); - tasks->deleteLater(); - }); - connect(tasks, &Task::succeeded, [this, tasks]() { - QStringList warnings = tasks->warnings(); - if (warnings.count()) - CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); - - tasks->deleteLater(); - }); - - for (auto& task : mdownload->getTasks()) { - tasks->addTask(task); - } - - ProgressDialog loadDialog(this); - loadDialog.setSkipButton(true, tr("Abort")); - loadDialog.execWithTask(tasks); + m_downloadDialog = new ResourceDownload::ModDownloadDialog(this, m_model, m_instance); + connect(this, &QObject::destroyed, m_downloadDialog, &QDialog::close); + connect(m_downloadDialog, &QDialog::finished, this, &ModFolderPage::downloadDialogFinished); - m_model->update(); - } + m_downloadDialog->setResourceMetadata((*mods_list.begin())->metadata()); + m_downloadDialog->open(); } void ModFolderPage::exportModMetadata() diff --git a/launcher/ui/pages/instance/ResourcePackPage.cpp b/launcher/ui/pages/instance/ResourcePackPage.cpp index 0d9e643b1..f37b3baf9 100644 --- a/launcher/ui/pages/instance/ResourcePackPage.cpp +++ b/launcher/ui/pages/instance/ResourcePackPage.cpp @@ -85,9 +85,9 @@ void ResourcePackPage::downloadResourcePacks() return; // this is a null instance or a legacy instance m_downloadDialog = new ResourceDownload::ResourcePackDownloadDialog(this, m_model, m_instance); - m_downloadDialog->setAttribute(Qt::WA_DeleteOnClose); connect(this, &QObject::destroyed, m_downloadDialog, &QDialog::close); connect(m_downloadDialog, &QDialog::finished, this, &ResourcePackPage::downloadDialogFinished); + m_downloadDialog->open(); } @@ -125,6 +125,8 @@ void ResourcePackPage::downloadDialogFinished(int result) m_model->update(); } + if (m_downloadDialog) + m_downloadDialog->deleteLater(); } void ResourcePackPage::updateResourcePacks() @@ -247,36 +249,10 @@ void ResourcePackPage::changeResourcePackVersion() if (resource.metadata() == nullptr) return; - auto mdownload = new ResourceDownload::ResourcePackDownloadDialog(this, m_model, m_instance); - mdownload->setAttribute(Qt::WA_DeleteOnClose); - connect(this, &QObject::destroyed, mdownload, &QDialog::close); - mdownload->setResourceMetadata(resource.metadata()); - if (mdownload->exec()) { - auto tasks = new ConcurrentTask("Download Resource Packs", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); - connect(tasks, &Task::failed, [this, tasks](QString reason) { - CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); - tasks->deleteLater(); - }); - connect(tasks, &Task::aborted, [this, tasks]() { - CustomMessageBox::selectable(this, tr("Aborted"), tr("Download stopped by user."), QMessageBox::Information)->show(); - tasks->deleteLater(); - }); - connect(tasks, &Task::succeeded, [this, tasks]() { - QStringList warnings = tasks->warnings(); - if (warnings.count()) - CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); - - tasks->deleteLater(); - }); - - for (auto& task : mdownload->getTasks()) { - tasks->addTask(task); - } - - ProgressDialog loadDialog(this); - loadDialog.setSkipButton(true, tr("Abort")); - loadDialog.execWithTask(tasks); + m_downloadDialog = new ResourceDownload::ResourcePackDownloadDialog(this, m_model, m_instance); + connect(this, &QObject::destroyed, m_downloadDialog, &QDialog::close); + connect(m_downloadDialog, &QDialog::finished, this, &ResourcePackPage::downloadDialogFinished); - m_model->update(); - } + m_downloadDialog->setResourceMetadata(resource.metadata()); + m_downloadDialog->open(); } diff --git a/launcher/ui/pages/instance/ShaderPackPage.cpp b/launcher/ui/pages/instance/ShaderPackPage.cpp index 829a75a72..930b0b9da 100644 --- a/launcher/ui/pages/instance/ShaderPackPage.cpp +++ b/launcher/ui/pages/instance/ShaderPackPage.cpp @@ -82,13 +82,14 @@ void ShaderPackPage::downloadShaderPack() return; // this is a null instance or a legacy instance m_downloadDialog = new ResourceDownload::ShaderPackDownloadDialog(this, m_model, m_instance); - m_downloadDialog->setAttribute(Qt::WA_DeleteOnClose); connect(this, &QObject::destroyed, m_downloadDialog, &QDialog::close); connect(m_downloadDialog, &QDialog::finished, this, &ShaderPackPage::downloadDialogFinished); + m_downloadDialog->open(); } -void ShaderPackPage::downloadDialogFinished(int result) { +void ShaderPackPage::downloadDialogFinished(int result) +{ if (result) { auto tasks = new ConcurrentTask("Download Shader Packs", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); connect(tasks, &Task::failed, [this, tasks](QString reason) { @@ -121,6 +122,8 @@ void ShaderPackPage::downloadDialogFinished(int result) { m_model->update(); } + if (m_downloadDialog) + m_downloadDialog->deleteLater(); } void ShaderPackPage::updateShaderPacks() @@ -243,36 +246,10 @@ void ShaderPackPage::changeShaderPackVersion() if (resource.metadata() == nullptr) return; - auto mdownload = new ResourceDownload::ShaderPackDownloadDialog(this, m_model, m_instance); - mdownload->setAttribute(Qt::WA_DeleteOnClose); - connect(this, &QObject::destroyed, mdownload, &QDialog::close); - mdownload->setResourceMetadata(resource.metadata()); - if (mdownload->exec()) { - auto tasks = new ConcurrentTask("Download Shader Packs", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); - connect(tasks, &Task::failed, [this, tasks](QString reason) { - CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); - tasks->deleteLater(); - }); - connect(tasks, &Task::aborted, [this, tasks]() { - CustomMessageBox::selectable(this, tr("Aborted"), tr("Download stopped by user."), QMessageBox::Information)->show(); - tasks->deleteLater(); - }); - connect(tasks, &Task::succeeded, [this, tasks]() { - QStringList warnings = tasks->warnings(); - if (warnings.count()) - CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); - - tasks->deleteLater(); - }); - - for (auto& task : mdownload->getTasks()) { - tasks->addTask(task); - } - - ProgressDialog loadDialog(this); - loadDialog.setSkipButton(true, tr("Abort")); - loadDialog.execWithTask(tasks); + m_downloadDialog = new ResourceDownload::ShaderPackDownloadDialog(this, m_model, m_instance); + connect(this, &QObject::destroyed, m_downloadDialog, &QDialog::close); + connect(m_downloadDialog, &QDialog::finished, this, &ShaderPackPage::downloadDialogFinished); - m_model->update(); - } + m_downloadDialog->setResourceMetadata(resource.metadata()); + m_downloadDialog->open(); } diff --git a/launcher/ui/pages/instance/TexturePackPage.cpp b/launcher/ui/pages/instance/TexturePackPage.cpp index ada29d94b..2886decb4 100644 --- a/launcher/ui/pages/instance/TexturePackPage.cpp +++ b/launcher/ui/pages/instance/TexturePackPage.cpp @@ -90,9 +90,10 @@ void TexturePackPage::downloadTexturePacks() if (m_instance->typeName() != "Minecraft") return; // this is a null instance or a legacy instance - auto m_downloadDialog = new ResourceDownload::TexturePackDownloadDialog(this, m_model, m_instance); - m_downloadDialog->setAttribute(Qt::WA_DeleteOnClose); + m_downloadDialog = new ResourceDownload::TexturePackDownloadDialog(this, m_model, m_instance); connect(this, &QObject::destroyed, m_downloadDialog, &QDialog::close); + connect(m_downloadDialog, &QDialog::finished, this, &TexturePackPage::downloadDialogFinished); + m_downloadDialog->open(); } void TexturePackPage::downloadDialogFinished(int result) @@ -129,6 +130,8 @@ void TexturePackPage::downloadDialogFinished(int result) m_model->update(); } + if (m_downloadDialog) + m_downloadDialog->deleteLater(); } void TexturePackPage::updateTexturePacks() @@ -251,36 +254,10 @@ void TexturePackPage::changeTexturePackVersion() if (resource.metadata() == nullptr) return; - auto mdownload = new ResourceDownload::TexturePackDownloadDialog(this, m_model, m_instance); - mdownload->setAttribute(Qt::WA_DeleteOnClose); - connect(this, &QObject::destroyed, mdownload, &QDialog::close); - mdownload->setResourceMetadata(resource.metadata()); - if (mdownload->exec()) { - auto tasks = new ConcurrentTask("Download Texture Packs", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); - connect(tasks, &Task::failed, [this, tasks](QString reason) { - CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); - tasks->deleteLater(); - }); - connect(tasks, &Task::aborted, [this, tasks]() { - CustomMessageBox::selectable(this, tr("Aborted"), tr("Download stopped by user."), QMessageBox::Information)->show(); - tasks->deleteLater(); - }); - connect(tasks, &Task::succeeded, [this, tasks]() { - QStringList warnings = tasks->warnings(); - if (warnings.count()) - CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); - - tasks->deleteLater(); - }); - - for (auto& task : mdownload->getTasks()) { - tasks->addTask(task); - } - - ProgressDialog loadDialog(this); - loadDialog.setSkipButton(true, tr("Abort")); - loadDialog.execWithTask(tasks); + m_downloadDialog = new ResourceDownload::TexturePackDownloadDialog(this, m_model, m_instance); + connect(this, &QObject::destroyed, m_downloadDialog, &QDialog::close); + connect(m_downloadDialog, &QDialog::finished, this, &TexturePackPage::downloadDialogFinished); - m_model->update(); - } + m_downloadDialog->setResourceMetadata(resource.metadata()); + m_downloadDialog->open(); } From bcdbe79c592894ab9e47908a5b06d7b3c5568430 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Mon, 28 Apr 2025 17:37:22 +0300 Subject: [PATCH 173/695] fix: add github token for gh cli Signed-off-by: Trial97 --- .github/workflows/nix.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index a11389e6c..a77b33521 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -73,7 +73,9 @@ jobs: uses: PrismLauncher/PrismLauncher/.github/actions/get-merge-commit@develop with: pull-request-id: ${{ github.event.pull_request.id }} - + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Checkout repository uses: actions/checkout@v4 with: From 0ccb4059a028615543bc232c3f87a7ba0b71e1ca Mon Sep 17 00:00:00 2001 From: Trial97 Date: Mon, 28 Apr 2025 17:44:23 +0300 Subject: [PATCH 174/695] chore: update submodules Signed-off-by: Trial97 --- flatpak/shared-modules | 2 +- libraries/extra-cmake-modules | 2 +- libraries/libnbtplusplus | 2 +- libraries/quazip | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/flatpak/shared-modules b/flatpak/shared-modules index f5d368a31..73f08ed2c 160000 --- a/flatpak/shared-modules +++ b/flatpak/shared-modules @@ -1 +1 @@ -Subproject commit f5d368a31d6ef046eb2955c74ec6f54f32ed5c4e +Subproject commit 73f08ed2c3187f6648ca04ebef030930a6c9f0be diff --git a/libraries/extra-cmake-modules b/libraries/extra-cmake-modules index a3d9394ab..1f820dc98 160000 --- a/libraries/extra-cmake-modules +++ b/libraries/extra-cmake-modules @@ -1 +1 @@ -Subproject commit a3d9394aba4b35789293378e04fb7473d65edf97 +Subproject commit 1f820dc98d0a520c175433bcbb0098327d82aac6 diff --git a/libraries/libnbtplusplus b/libraries/libnbtplusplus index 23b955121..531449ba1 160000 --- a/libraries/libnbtplusplus +++ b/libraries/libnbtplusplus @@ -1 +1 @@ -Subproject commit 23b955121b8217c1c348a9ed2483167a6f3ff4ad +Subproject commit 531449ba1c930c98e0bcf5d332b237a8566f9d78 diff --git a/libraries/quazip b/libraries/quazip index 8aeb3f7d8..3fd3b299b 160000 --- a/libraries/quazip +++ b/libraries/quazip @@ -1 +1 @@ -Subproject commit 8aeb3f7d8254f4bf1f7c6cf2a8f59c2ca141a552 +Subproject commit 3fd3b299b875fbd2beac4894b8a870d80022cad7 From c5fd5e6ac1ad69ecc753495e5da0039d5ce1ba32 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Wed, 16 Apr 2025 18:55:26 +0300 Subject: [PATCH 175/695] chore: make all the regexes static const Signed-off-by: Trial97 --- launcher/BaseInstance.cpp | 1 - launcher/InstanceImportTask.cpp | 4 ++-- launcher/JavaCommon.cpp | 6 ++++-- launcher/LaunchController.cpp | 3 ++- launcher/RecursiveFileSystemWatcher.cpp | 1 - launcher/StringUtils.cpp | 7 +++---- launcher/Version.cpp | 1 - launcher/java/JavaVersion.cpp | 8 ++++++-- launcher/launch/LaunchTask.cpp | 1 - launcher/minecraft/GradleSpecifier.h | 4 ++-- launcher/minecraft/MinecraftInstance.cpp | 5 ++--- launcher/minecraft/OneSixVersionFormat.cpp | 4 ++-- launcher/minecraft/ProfileUtils.cpp | 1 - launcher/minecraft/auth/AccountData.cpp | 1 - launcher/minecraft/auth/MinecraftAccount.cpp | 11 +++++++---- .../minecraft/launch/LauncherPartLaunch.cpp | 17 +++++++++-------- launcher/minecraft/mod/Resource.cpp | 4 ++-- launcher/minecraft/mod/ResourcePack.cpp | 2 -- launcher/minecraft/mod/ShaderPack.cpp | 2 -- launcher/minecraft/mod/TexturePack.cpp | 2 -- .../minecraft/mod/tasks/LocalModParseTask.cpp | 4 ++-- .../modplatform/atlauncher/ATLPackIndex.cpp | 3 ++- .../atlauncher/ATLPackInstallTask.cpp | 6 ++++-- .../flame/FlameInstanceCreationTask.cpp | 3 ++- launcher/net/MetaCacheSink.cpp | 4 ++-- launcher/pathmatcher/FSTreeMatcher.h | 1 - launcher/pathmatcher/MultiMatcher.h | 1 - launcher/pathmatcher/RegexpMatcher.h | 2 ++ launcher/pathmatcher/SimplePrefixMatcher.h | 1 - launcher/ui/dialogs/NewComponentDialog.cpp | 3 ++- launcher/ui/dialogs/ProfileSetupDialog.cpp | 5 ++--- launcher/ui/pages/global/APIPage.cpp | 14 +++++++------- launcher/ui/pages/instance/ScreenshotsPage.cpp | 3 ++- launcher/updater/prismupdater/PrismUpdater.cpp | 13 ++++++------- libraries/LocalPeer/src/LocalPeer.cpp | 3 ++- libraries/qdcss/src/qdcss.cpp | 8 ++++---- libraries/systeminfo/src/distroutils.cpp | 12 +++++++----- 37 files changed, 87 insertions(+), 84 deletions(-) diff --git a/launcher/BaseInstance.cpp b/launcher/BaseInstance.cpp index eab91a5eb..6fbe5eea6 100644 --- a/launcher/BaseInstance.cpp +++ b/launcher/BaseInstance.cpp @@ -42,7 +42,6 @@ #include #include #include -#include #include "settings/INISettingsObject.h" #include "settings/OverrideSetting.h" diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index 71630656d..633382404 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -378,8 +378,8 @@ void InstanceImportTask::processModrinth() } else { QString pack_id; if (!m_sourceUrl.isEmpty()) { - QRegularExpression regex(R"(data\/([^\/]*)\/versions)"); - pack_id = regex.match(m_sourceUrl.toString()).captured(1); + static const QRegularExpression s_regex(R"(data\/([^\/]*)\/versions)"); + pack_id = s_regex.match(m_sourceUrl.toString()).captured(1); } // FIXME: Find a way to get the ID in directly imported ZIPs diff --git a/launcher/JavaCommon.cpp b/launcher/JavaCommon.cpp index 188edb943..b71000054 100644 --- a/launcher/JavaCommon.cpp +++ b/launcher/JavaCommon.cpp @@ -41,7 +41,9 @@ bool JavaCommon::checkJVMArgs(QString jvmargs, QWidget* parent) { - if (jvmargs.contains("-XX:PermSize=") || jvmargs.contains(QRegularExpression("-Xm[sx]")) || jvmargs.contains("-XX-MaxHeapSize") || + static const QRegularExpression s_memRegex("-Xm[sx]"); + static const QRegularExpression s_versionRegex("-version:.*"); + if (jvmargs.contains("-XX:PermSize=") || jvmargs.contains(s_memRegex) || jvmargs.contains("-XX-MaxHeapSize") || jvmargs.contains("-XX:InitialHeapSize")) { auto warnStr = QObject::tr( "You tried to manually set a JVM memory option (using \"-XX:PermSize\", \"-XX-MaxHeapSize\", \"-XX:InitialHeapSize\", \"-Xmx\" " @@ -52,7 +54,7 @@ bool JavaCommon::checkJVMArgs(QString jvmargs, QWidget* parent) return false; } // block lunacy with passing required version to the JVM - if (jvmargs.contains(QRegularExpression("-version:.*"))) { + if (jvmargs.contains(s_versionRegex)) { auto warnStr = QObject::tr( "You tried to pass required Java version argument to the JVM (using \"-version:xxx\"). This is not safe and will not be " "allowed.\n" diff --git a/launcher/LaunchController.cpp b/launcher/LaunchController.cpp index 07047bf67..b1a956b49 100644 --- a/launcher/LaunchController.cpp +++ b/launcher/LaunchController.cpp @@ -182,7 +182,8 @@ void LaunchController::login() auto name = askOfflineName("Player", m_demo, ok); if (ok) { m_session = std::make_shared(); - m_session->MakeDemo(name, MinecraftAccount::uuidFromUsername(name).toString().remove(QRegularExpression("[{}-]"))); + static const QRegularExpression s_removeChars("[{}-]"); + m_session->MakeDemo(name, MinecraftAccount::uuidFromUsername(name).toString().remove(s_removeChars)); launchInstance(); return; } diff --git a/launcher/RecursiveFileSystemWatcher.cpp b/launcher/RecursiveFileSystemWatcher.cpp index 8b28a03f1..5cb3cd0be 100644 --- a/launcher/RecursiveFileSystemWatcher.cpp +++ b/launcher/RecursiveFileSystemWatcher.cpp @@ -1,7 +1,6 @@ #include "RecursiveFileSystemWatcher.h" #include -#include RecursiveFileSystemWatcher::RecursiveFileSystemWatcher(QObject* parent) : QObject(parent), m_watcher(new QFileSystemWatcher(this)) { diff --git a/launcher/StringUtils.cpp b/launcher/StringUtils.cpp index 2ea67762e..b9e875482 100644 --- a/launcher/StringUtils.cpp +++ b/launcher/StringUtils.cpp @@ -213,11 +213,10 @@ QPair StringUtils::splitFirst(const QString& s, const QRegular return qMakePair(left, right); } -static const QRegularExpression ulMatcher("<\\s*/\\s*ul\\s*>"); - QString StringUtils::htmlListPatch(QString htmlStr) { - int pos = htmlStr.indexOf(ulMatcher); + static const QRegularExpression s_ulMatcher("<\\s*/\\s*ul\\s*>"); + int pos = htmlStr.indexOf(s_ulMatcher); int imgPos; while (pos != -1) { pos = htmlStr.indexOf(">", pos) + 1; // Get the size of the tag. Add one for zeroeth index @@ -230,7 +229,7 @@ QString StringUtils::htmlListPatch(QString htmlStr) if (textBetween.isEmpty()) htmlStr.insert(pos, "
"); - pos = htmlStr.indexOf(ulMatcher, pos); + pos = htmlStr.indexOf(s_ulMatcher, pos); } return htmlStr; } \ No newline at end of file diff --git a/launcher/Version.cpp b/launcher/Version.cpp index 03a16e8a0..bffe5d58a 100644 --- a/launcher/Version.cpp +++ b/launcher/Version.cpp @@ -1,7 +1,6 @@ #include "Version.h" #include -#include #include #include diff --git a/launcher/java/JavaVersion.cpp b/launcher/java/JavaVersion.cpp index bca50f2c9..e9a160ea7 100644 --- a/launcher/java/JavaVersion.cpp +++ b/launcher/java/JavaVersion.cpp @@ -19,9 +19,13 @@ JavaVersion& JavaVersion::operator=(const QString& javaVersionString) QRegularExpression pattern; if (javaVersionString.startsWith("1.")) { - pattern = QRegularExpression("1[.](?[0-9]+)([.](?[0-9]+))?(_(?[0-9]+)?)?(-(?[a-zA-Z0-9]+))?"); + static const QRegularExpression s_withOne( + "1[.](?[0-9]+)([.](?[0-9]+))?(_(?[0-9]+)?)?(-(?[a-zA-Z0-9]+))?"); + pattern = s_withOne; } else { - pattern = QRegularExpression("(?[0-9]+)([.](?[0-9]+))?([.](?[0-9]+))?(-(?[a-zA-Z0-9]+))?"); + static const QRegularExpression s_withoutOne( + "(?[0-9]+)([.](?[0-9]+))?([.](?[0-9]+))?(-(?[a-zA-Z0-9]+))?"); + pattern = s_withoutOne; } auto match = pattern.match(m_string); diff --git a/launcher/launch/LaunchTask.cpp b/launcher/launch/LaunchTask.cpp index e3e8eaec2..b67df7631 100644 --- a/launcher/launch/LaunchTask.cpp +++ b/launcher/launch/LaunchTask.cpp @@ -41,7 +41,6 @@ #include #include #include -#include #include #include #include "MessageLevel.h" diff --git a/launcher/minecraft/GradleSpecifier.h b/launcher/minecraft/GradleSpecifier.h index 22db7d641..a2588064f 100644 --- a/launcher/minecraft/GradleSpecifier.h +++ b/launcher/minecraft/GradleSpecifier.h @@ -54,11 +54,11 @@ struct GradleSpecifier { 4 "jdk15" 5 "jar" */ - QRegularExpression matcher( + static const QRegularExpression s_matcher( QRegularExpression::anchoredPattern("([^:@]+):([^:@]+):([^:@]+)" "(?::([^:@]+))?" "(?:@([^:@]+))?")); - QRegularExpressionMatch match = matcher.match(value); + QRegularExpressionMatch match = s_matcher.match(value); m_valid = match.hasMatch(); if (!m_valid) { m_invalidValue = value; diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index 83141158e..b8ab93d5b 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -53,7 +53,6 @@ #include "MMCTime.h" #include "java/JavaVersion.h" #include "pathmatcher/MultiMatcher.h" -#include "pathmatcher/RegexpMatcher.h" #include "launch/LaunchTask.h" #include "launch/TaskStepWrapper.h" @@ -689,9 +688,9 @@ static QString replaceTokensIn(QString text, QMap with) { // TODO: does this still work?? QString result; - QRegularExpression token_regexp("\\$\\{(.+)\\}", QRegularExpression::InvertedGreedinessOption); + static const QRegularExpression s_token_regexp("\\$\\{(.+)\\}", QRegularExpression::InvertedGreedinessOption); QStringList list; - QRegularExpressionMatchIterator i = token_regexp.globalMatch(text); + QRegularExpressionMatchIterator i = s_token_regexp.globalMatch(text); int lastCapturedEnd = 0; while (i.hasNext()) { QRegularExpressionMatch match = i.next(); diff --git a/launcher/minecraft/OneSixVersionFormat.cpp b/launcher/minecraft/OneSixVersionFormat.cpp index 684869c8d..32dd1875c 100644 --- a/launcher/minecraft/OneSixVersionFormat.cpp +++ b/launcher/minecraft/OneSixVersionFormat.cpp @@ -114,9 +114,9 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument& doc out->uid = root.value("fileId").toString(); } - const QRegularExpression valid_uid_regex{ QRegularExpression::anchoredPattern( + static const QRegularExpression s_validUidRegex{ QRegularExpression::anchoredPattern( QStringLiteral(R"([a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]+)*)")) }; - if (!valid_uid_regex.match(out->uid).hasMatch()) { + if (!s_validUidRegex.match(out->uid).hasMatch()) { qCritical() << "The component's 'uid' contains illegal characters! UID:" << out->uid; out->addProblem(ProblemSeverity::Error, QObject::tr("The component's 'uid' contains illegal characters! This can cause security issues.")); diff --git a/launcher/minecraft/ProfileUtils.cpp b/launcher/minecraft/ProfileUtils.cpp index 08ec0fac3..a79f89529 100644 --- a/launcher/minecraft/ProfileUtils.cpp +++ b/launcher/minecraft/ProfileUtils.cpp @@ -41,7 +41,6 @@ #include #include -#include #include namespace ProfileUtils { diff --git a/launcher/minecraft/auth/AccountData.cpp b/launcher/minecraft/auth/AccountData.cpp index fd2082035..9dbe7ffc0 100644 --- a/launcher/minecraft/auth/AccountData.cpp +++ b/launcher/minecraft/auth/AccountData.cpp @@ -38,7 +38,6 @@ #include #include #include -#include #include namespace { diff --git a/launcher/minecraft/auth/MinecraftAccount.cpp b/launcher/minecraft/auth/MinecraftAccount.cpp index 1613a42b1..86e9cc511 100644 --- a/launcher/minecraft/auth/MinecraftAccount.cpp +++ b/launcher/minecraft/auth/MinecraftAccount.cpp @@ -55,7 +55,8 @@ MinecraftAccount::MinecraftAccount(QObject* parent) : QObject(parent) { - data.internalId = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]")); + static const QRegularExpression s_removeChars("[{}-]"); + data.internalId = QUuid::createUuid().toString().remove(s_removeChars); } MinecraftAccountPtr MinecraftAccount::loadFromJsonV3(const QJsonObject& json) @@ -76,14 +77,15 @@ MinecraftAccountPtr MinecraftAccount::createBlankMSA() MinecraftAccountPtr MinecraftAccount::createOffline(const QString& username) { + static const QRegularExpression s_removeChars("[{}-]"); auto account = makeShared(); account->data.type = AccountType::Offline; account->data.yggdrasilToken.token = "0"; account->data.yggdrasilToken.validity = Validity::Certain; account->data.yggdrasilToken.issueInstant = QDateTime::currentDateTimeUtc(); account->data.yggdrasilToken.extra["userName"] = username; - account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]")); - account->data.minecraftProfile.id = uuidFromUsername(username).toString().remove(QRegularExpression("[{}-]")); + account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(s_removeChars); + account->data.minecraftProfile.id = uuidFromUsername(username).toString().remove(s_removeChars); account->data.minecraftProfile.name = username; account->data.minecraftProfile.validity = Validity::Certain; return account; @@ -231,6 +233,7 @@ bool MinecraftAccount::shouldRefresh() const void MinecraftAccount::fillSession(AuthSessionPtr session) { + static const QRegularExpression s_removeChars("[{}-]"); if (ownsMinecraft() && !hasProfile()) { session->status = AuthSession::RequiresProfileSetup; } else { @@ -248,7 +251,7 @@ void MinecraftAccount::fillSession(AuthSessionPtr session) // profile ID session->uuid = data.profileId(); if (session->uuid.isEmpty()) - session->uuid = uuidFromUsername(session->player_name).toString().remove(QRegularExpression("[{}-]")); + session->uuid = uuidFromUsername(session->player_name).toString().remove(s_removeChars); // 'legacy' or 'mojang', depending on account type session->user_type = typeString(); if (!session->access_token.isEmpty()) { diff --git a/launcher/minecraft/launch/LauncherPartLaunch.cpp b/launcher/minecraft/launch/LauncherPartLaunch.cpp index 49d91e433..388d55628 100644 --- a/launcher/minecraft/launch/LauncherPartLaunch.cpp +++ b/launcher/minecraft/launch/LauncherPartLaunch.cpp @@ -53,15 +53,16 @@ LauncherPartLaunch::LauncherPartLaunch(LaunchTask* parent) , m_process(parent->instance()->getJavaVersion().defaultsToUtf8() ? QTextCodec::codecForName("UTF-8") : QTextCodec::codecForLocale()) { if (parent->instance()->settings()->get("CloseAfterLaunch").toBool()) { + static const QRegularExpression s_settingUser(".*Setting user.+", QRegularExpression::CaseInsensitiveOption); std::shared_ptr connection{ new QMetaObject::Connection }; - *connection = connect( - &m_process, &LoggedProcess::log, this, [connection](const QStringList& lines, [[maybe_unused]] MessageLevel::Enum level) { - qDebug() << lines; - if (lines.filter(QRegularExpression(".*Setting user.+", QRegularExpression::CaseInsensitiveOption)).length() != 0) { - APPLICATION->closeAllWindows(); - disconnect(*connection); - } - }); + *connection = connect(&m_process, &LoggedProcess::log, this, + [connection](const QStringList& lines, [[maybe_unused]] MessageLevel::Enum level) { + qDebug() << lines; + if (lines.filter(s_settingUser).length() != 0) { + APPLICATION->closeAllWindows(); + disconnect(*connection); + } + }); } connect(&m_process, &LoggedProcess::log, this, &LauncherPartLaunch::logLines); diff --git a/launcher/minecraft/mod/Resource.cpp b/launcher/minecraft/mod/Resource.cpp index d1a7b8f9c..6f2efe740 100644 --- a/launcher/minecraft/mod/Resource.cpp +++ b/launcher/minecraft/mod/Resource.cpp @@ -82,8 +82,8 @@ auto Resource::name() const -> QString static void removeThePrefix(QString& string) { - QRegularExpression regex(QStringLiteral("^(?:the|teh) +"), QRegularExpression::CaseInsensitiveOption); - string.remove(regex); + static const QRegularExpression s_regex(QStringLiteral("^(?:the|teh) +"), QRegularExpression::CaseInsensitiveOption); + string.remove(s_regex); string = string.trimmed(); } diff --git a/launcher/minecraft/mod/ResourcePack.cpp b/launcher/minecraft/mod/ResourcePack.cpp index 4ed3c67e3..cf8ec871c 100644 --- a/launcher/minecraft/mod/ResourcePack.cpp +++ b/launcher/minecraft/mod/ResourcePack.cpp @@ -3,8 +3,6 @@ #include #include #include -#include - #include "MTPixmapCache.h" #include "Version.h" diff --git a/launcher/minecraft/mod/ShaderPack.cpp b/launcher/minecraft/mod/ShaderPack.cpp index ccb344cb5..99e51fcae 100644 --- a/launcher/minecraft/mod/ShaderPack.cpp +++ b/launcher/minecraft/mod/ShaderPack.cpp @@ -22,8 +22,6 @@ #include "ShaderPack.h" -#include - void ShaderPack::setPackFormat(ShaderPackFormat new_format) { QMutexLocker locker(&m_data_lock); diff --git a/launcher/minecraft/mod/TexturePack.cpp b/launcher/minecraft/mod/TexturePack.cpp index 04cc36310..a1ef7f525 100644 --- a/launcher/minecraft/mod/TexturePack.cpp +++ b/launcher/minecraft/mod/TexturePack.cpp @@ -21,8 +21,6 @@ #include #include -#include - #include "MTPixmapCache.h" #include "minecraft/mod/tasks/LocalTexturePackParseTask.h" diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp index b0e8eb101..952115bed 100644 --- a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp @@ -16,7 +16,7 @@ #include "minecraft/mod/ModDetails.h" #include "settings/INIFile.h" -static QRegularExpression newlineRegex("\r\n|\n|\r"); +static const QRegularExpression s_newlineRegex("\r\n|\n|\r"); namespace ModUtils { @@ -494,7 +494,7 @@ bool processZIP(Mod& mod, [[maybe_unused]] ProcessingLevel level) } // quick and dirty line-by-line parser - auto manifestLines = QString(file.readAll()).split(newlineRegex); + auto manifestLines = QString(file.readAll()).split(s_newlineRegex); QString manifestVersion = ""; for (auto& line : manifestLines) { if (line.startsWith("Implementation-Version: ", Qt::CaseInsensitive)) { diff --git a/launcher/modplatform/atlauncher/ATLPackIndex.cpp b/launcher/modplatform/atlauncher/ATLPackIndex.cpp index 678db63cc..c35569d45 100644 --- a/launcher/modplatform/atlauncher/ATLPackIndex.cpp +++ b/launcher/modplatform/atlauncher/ATLPackIndex.cpp @@ -43,5 +43,6 @@ void ATLauncher::loadIndexedPack(ATLauncher::IndexedPack& m, QJsonObject& obj) m.system = Json::ensureBoolean(obj, QString("system"), false); m.description = Json::ensureString(obj, "description", ""); - m.safeName = Json::requireString(obj, "name").replace(QRegularExpression("[^A-Za-z0-9]"), "").toLower() + ".png"; + static const QRegularExpression s_regex("[^A-Za-z0-9]"); + m.safeName = Json::requireString(obj, "name").replace(s_regex, "").toLower() + ".png"; } diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp index a9706a768..e22e6135d 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp @@ -69,7 +69,8 @@ PackInstallTask::PackInstallTask(UserInteractionSupport* support, QString packNa { m_support = support; m_pack_name = packName; - m_pack_safe_name = packName.replace(QRegularExpression("[^A-Za-z0-9]"), ""); + static const QRegularExpression s_regex("[^A-Za-z0-9]"); + m_pack_safe_name = packName.replace(s_regex, ""); m_version_name = version; m_install_mode = installMode; } @@ -938,7 +939,8 @@ bool PackInstallTask::extractMods(const QMap& toExtract, QString folderToExtract = ""; if (mod.type == ModType::Extract) { folderToExtract = mod.extractFolder; - folderToExtract.remove(QRegularExpression("^/")); + static const QRegularExpression s_regex("^/"); + folderToExtract.remove(s_regex); } qDebug() << "Extracting " + mod.file + " to " + extractToDir; diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp index 22c9e603b..c30ba5249 100644 --- a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp +++ b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp @@ -391,7 +391,8 @@ bool FlameCreationTask::createInstance() // Hack to correct some 'special sauce'... if (mcVersion.endsWith('.')) { - mcVersion.remove(QRegularExpression("[.]+$")); + static const QRegularExpression s_regex("[.]+$"); + mcVersion.remove(s_regex); logWarning(tr("Mysterious trailing dots removed from Minecraft version while importing pack.")); } diff --git a/launcher/net/MetaCacheSink.cpp b/launcher/net/MetaCacheSink.cpp index 432c0c84b..8896f10e3 100644 --- a/launcher/net/MetaCacheSink.cpp +++ b/launcher/net/MetaCacheSink.cpp @@ -98,8 +98,8 @@ Task::State MetaCacheSink::finalizeCache(QNetworkReply& reply) auto cache_control_header = reply.rawHeader("Cache-Control"); qCDebug(taskMetaCacheLogC) << "Parsing 'Cache-Control' header with" << cache_control_header; - QRegularExpression max_age_expr("max-age=([0-9]+)"); - qint64 max_age = max_age_expr.match(cache_control_header).captured(1).toLongLong(); + static const QRegularExpression s_maxAgeExpr("max-age=([0-9]+)"); + qint64 max_age = s_maxAgeExpr.match(cache_control_header).captured(1).toLongLong(); m_entry->setMaximumAge(max_age); } else if (reply.hasRawHeader("Expires")) { diff --git a/launcher/pathmatcher/FSTreeMatcher.h b/launcher/pathmatcher/FSTreeMatcher.h index 689f11979..d8d36d2c3 100644 --- a/launcher/pathmatcher/FSTreeMatcher.h +++ b/launcher/pathmatcher/FSTreeMatcher.h @@ -1,7 +1,6 @@ #pragma once #include -#include #include "IPathMatcher.h" class FSTreeMatcher : public IPathMatcher { diff --git a/launcher/pathmatcher/MultiMatcher.h b/launcher/pathmatcher/MultiMatcher.h index ccd5a9163..3ad07b643 100644 --- a/launcher/pathmatcher/MultiMatcher.h +++ b/launcher/pathmatcher/MultiMatcher.h @@ -1,7 +1,6 @@ #pragma once #include -#include #include "IPathMatcher.h" class MultiMatcher : public IPathMatcher { diff --git a/launcher/pathmatcher/RegexpMatcher.h b/launcher/pathmatcher/RegexpMatcher.h index 18c42f887..e36516386 100644 --- a/launcher/pathmatcher/RegexpMatcher.h +++ b/launcher/pathmatcher/RegexpMatcher.h @@ -12,6 +12,8 @@ class RegexpMatcher : public IPathMatcher { m_onlyFilenamePart = !regexp.contains('/'); } + RegexpMatcher(const QRegularExpression& regex) : m_regexp(regex) { m_onlyFilenamePart = !regex.pattern().contains('/'); } + RegexpMatcher& caseSensitive(bool cs = true) { if (cs) { diff --git a/launcher/pathmatcher/SimplePrefixMatcher.h b/launcher/pathmatcher/SimplePrefixMatcher.h index ff3805179..57bf63a30 100644 --- a/launcher/pathmatcher/SimplePrefixMatcher.h +++ b/launcher/pathmatcher/SimplePrefixMatcher.h @@ -2,7 +2,6 @@ // // SPDX-License-Identifier: GPL-3.0-only -#include #include "IPathMatcher.h" class SimplePrefixMatcher : public IPathMatcher { diff --git a/launcher/ui/dialogs/NewComponentDialog.cpp b/launcher/ui/dialogs/NewComponentDialog.cpp index b5f8ff889..d1e420864 100644 --- a/launcher/ui/dialogs/NewComponentDialog.cpp +++ b/launcher/ui/dialogs/NewComponentDialog.cpp @@ -83,7 +83,8 @@ NewComponentDialog::~NewComponentDialog() void NewComponentDialog::updateDialogState() { auto protoUid = ui->nameTextBox->text().toLower(); - protoUid.remove(QRegularExpression("[^a-z]")); + static const QRegularExpression s_removeChars("[^a-z]"); + protoUid.remove(s_removeChars); if (protoUid.isEmpty()) { ui->uidTextBox->setPlaceholderText(originalPlaceholderText); } else { diff --git a/launcher/ui/dialogs/ProfileSetupDialog.cpp b/launcher/ui/dialogs/ProfileSetupDialog.cpp index dd87b249c..0b5e1a784 100644 --- a/launcher/ui/dialogs/ProfileSetupDialog.cpp +++ b/launcher/ui/dialogs/ProfileSetupDialog.cpp @@ -59,9 +59,9 @@ ProfileSetupDialog::ProfileSetupDialog(MinecraftAccountPtr accountToSetup, QWidg yellowIcon = APPLICATION->getThemedIcon("status-yellow"); badIcon = APPLICATION->getThemedIcon("status-bad"); - QRegularExpression permittedNames("[a-zA-Z0-9_]{3,16}"); + static const QRegularExpression s_permittedNames("[a-zA-Z0-9_]{3,16}"); auto nameEdit = ui->nameEdit; - nameEdit->setValidator(new QRegularExpressionValidator(permittedNames)); + nameEdit->setValidator(new QRegularExpressionValidator(s_permittedNames)); nameEdit->setClearButtonEnabled(true); validityAction = nameEdit->addAction(yellowIcon, QLineEdit::LeadingPosition); connect(nameEdit, &QLineEdit::textEdited, this, &ProfileSetupDialog::nameEdited); @@ -268,7 +268,6 @@ void ProfileSetupDialog::setupProfileFinished() QString errorMessage = tr("Network Error: %1\nHTTP Status: %2").arg(m_profile_task->errorString(), QString::number(m_profile_task->replyStatusCode())); - if (parsedError.fullyParsed) { errorMessage += "Path: " + parsedError.path + "\n"; errorMessage += "Error: " + parsedError.error + "\n"; diff --git a/launcher/ui/pages/global/APIPage.cpp b/launcher/ui/pages/global/APIPage.cpp index a137c4cde..a030bf316 100644 --- a/launcher/ui/pages/global/APIPage.cpp +++ b/launcher/ui/pages/global/APIPage.cpp @@ -59,10 +59,10 @@ APIPage::APIPage(QWidget* parent) : QWidget(parent), ui(new Ui::APIPage) int comboBoxEntries[] = { PasteUpload::PasteType::Mclogs, PasteUpload::PasteType::NullPointer, PasteUpload::PasteType::PasteGG, PasteUpload::PasteType::Hastebin }; - static QRegularExpression validUrlRegExp("https?://.+"); - static QRegularExpression validMSAClientID( + static const QRegularExpression s_validUrlRegExp("https?://.+"); + static const QRegularExpression s_validMSAClientID( QRegularExpression::anchoredPattern("[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}")); - static QRegularExpression validFlameKey(QRegularExpression::anchoredPattern("\\$2[ayb]\\$.{56}")); + static const QRegularExpression s_validFlameKey(QRegularExpression::anchoredPattern("\\$2[ayb]\\$.{56}")); ui->setupUi(this); @@ -75,10 +75,10 @@ APIPage::APIPage(QWidget* parent) : QWidget(parent), ui(new Ui::APIPage) // This function needs to be called even when the ComboBox's index is still in its default state. updateBaseURLPlaceholder(ui->pasteTypeComboBox->currentIndex()); // NOTE: this allows http://, but we replace that with https later anyway - ui->metaURL->setValidator(new QRegularExpressionValidator(validUrlRegExp, ui->metaURL)); - ui->baseURLEntry->setValidator(new QRegularExpressionValidator(validUrlRegExp, ui->baseURLEntry)); - ui->msaClientID->setValidator(new QRegularExpressionValidator(validMSAClientID, ui->msaClientID)); - ui->flameKey->setValidator(new QRegularExpressionValidator(validFlameKey, ui->flameKey)); + ui->metaURL->setValidator(new QRegularExpressionValidator(s_validUrlRegExp, ui->metaURL)); + ui->baseURLEntry->setValidator(new QRegularExpressionValidator(s_validUrlRegExp, ui->baseURLEntry)); + ui->msaClientID->setValidator(new QRegularExpressionValidator(s_validMSAClientID, ui->msaClientID)); + ui->flameKey->setValidator(new QRegularExpressionValidator(s_validFlameKey, ui->flameKey)); ui->metaURL->setPlaceholderText(BuildConfig.META_URL); ui->userAgentLineEdit->setPlaceholderText(BuildConfig.USER_AGENT); diff --git a/launcher/ui/pages/instance/ScreenshotsPage.cpp b/launcher/ui/pages/instance/ScreenshotsPage.cpp index fa568c794..e59002a15 100644 --- a/launcher/ui/pages/instance/ScreenshotsPage.cpp +++ b/launcher/ui/pages/instance/ScreenshotsPage.cpp @@ -150,7 +150,8 @@ class FilterModel : public QIdentityProxyModel { return QVariant(); if (role == Qt::DisplayRole || role == Qt::EditRole) { QVariant result = sourceModel()->data(mapToSource(proxyIndex), role); - return result.toString().remove(QRegularExpression("\\.png$")); + static const QRegularExpression s_removeChars("\\.png$"); + return result.toString().remove(s_removeChars); } if (role == Qt::DecorationRole) { QVariant result = sourceModel()->data(mapToSource(proxyIndex), QFileSystemModel::FilePathRole); diff --git a/launcher/updater/prismupdater/PrismUpdater.cpp b/launcher/updater/prismupdater/PrismUpdater.cpp index 365647db9..815924d52 100644 --- a/launcher/updater/prismupdater/PrismUpdater.cpp +++ b/launcher/updater/prismupdater/PrismUpdater.cpp @@ -721,8 +721,8 @@ QList PrismUpdaterApp::validReleaseArtifacts(const GitHubRel for_platform = false; } - auto qt_pattern = QRegularExpression("-qt(\\d+)"); - auto qt_match = qt_pattern.match(asset_name); + static const QRegularExpression s_qtPattern("-qt(\\d+)"); + auto qt_match = s_qtPattern.match(asset_name); if (for_platform && qt_match.hasMatch()) { if (platform_qt_ver.isEmpty() || platform_qt_ver.toInt() != qt_match.captured(1).toInt()) { qDebug() << "Rejecting" << asset.name << "because it is not for the correct qt version" << platform_qt_ver.toInt() << "vs" @@ -1018,12 +1018,11 @@ void PrismUpdaterApp::backupAppDir() logUpdate("manifest.txt empty or missing. making best guess at files to back up."); } logUpdate(tr("Backing up:\n %1").arg(file_list.join(",\n "))); + static const QRegularExpression s_replaceRegex("[" + QRegularExpression::escape("\\/:*?\"<>|") + "]"); auto app_dir = QDir(m_rootPath); - auto backup_dir = FS::PathCombine( - app_dir.absolutePath(), - QStringLiteral("backup_") + - QString(m_prismVersion).replace(QRegularExpression("[" + QRegularExpression::escape("\\/:*?\"<>|") + "]"), QString("_")) + "-" + - m_prismGitCommit); + auto backup_dir = + FS::PathCombine(app_dir.absolutePath(), + QStringLiteral("backup_") + QString(m_prismVersion).replace(s_replaceRegex, QString("_")) + "-" + m_prismGitCommit); FS::ensureFolderPathExists(backup_dir); auto backup_marker_path = FS::PathCombine(m_dataPath, ".prism_launcher_update_backup_path.txt"); FS::write(backup_marker_path, backup_dir.toUtf8()); diff --git a/libraries/LocalPeer/src/LocalPeer.cpp b/libraries/LocalPeer/src/LocalPeer.cpp index 3761c109e..cb74c031b 100644 --- a/libraries/LocalPeer/src/LocalPeer.cpp +++ b/libraries/LocalPeer/src/LocalPeer.cpp @@ -72,7 +72,8 @@ ApplicationId ApplicationId::fromTraditionalApp() protoId = protoId.toLower(); #endif auto prefix = protoId.section(QLatin1Char('/'), -1); - prefix.remove(QRegularExpression("[^a-zA-Z]")); + static const QRegularExpression s_removeChars("[^a-zA-Z]"); + prefix.remove(s_removeChars); prefix.truncate(6); QByteArray idc = protoId.toUtf8(); quint16 idNum = qChecksum(idc); diff --git a/libraries/qdcss/src/qdcss.cpp b/libraries/qdcss/src/qdcss.cpp index c531fb63d..bf0ef63cb 100644 --- a/libraries/qdcss/src/qdcss.cpp +++ b/libraries/qdcss/src/qdcss.cpp @@ -8,19 +8,19 @@ #include #include -QRegularExpression ruleset_re = QRegularExpression(R"([#.]?(@?\w+?)\s*\{(.*?)\})", QRegularExpression::DotMatchesEverythingOption); -QRegularExpression rule_re = QRegularExpression(R"((\S+?)\s*:\s*(?:\"(.*?)(? +static const QRegularExpression s_distoSplitRegex("\\s+"); + Sys::DistributionInfo Sys::read_os_release() { Sys::DistributionInfo out; @@ -145,7 +147,7 @@ void Sys::lsb_postprocess(Sys::LsbInfo& lsb, Sys::DistributionInfo& out) vers = lsb.codename; } else { // ubuntu, debian, gentoo, scientific, slackware, ... ? - auto parts = dist.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts); + auto parts = dist.split(s_distoSplitRegex, Qt::SkipEmptyParts); if (parts.size()) { dist = parts[0]; } @@ -178,7 +180,7 @@ QString Sys::_extract_distribution(const QString& x) if (release.startsWith("suse linux enterprise")) { return "sles"; } - QStringList list = release.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts); + QStringList list = release.split(s_distoSplitRegex, Qt::SkipEmptyParts); if (list.size()) { return list[0]; } @@ -187,11 +189,11 @@ QString Sys::_extract_distribution(const QString& x) QString Sys::_extract_version(const QString& x) { - QRegularExpression versionish_string(QRegularExpression::anchoredPattern("\\d+(?:\\.\\d+)*$")); - QStringList list = x.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts); + static const QRegularExpression s_versionishString(QRegularExpression::anchoredPattern("\\d+(?:\\.\\d+)*$")); + QStringList list = x.split(s_distoSplitRegex, Qt::SkipEmptyParts); for (int i = list.size() - 1; i >= 0; --i) { QString chunk = list[i]; - if (versionish_string.match(chunk).hasMatch()) { + if (s_versionishString.match(chunk).hasMatch()) { return chunk; } } From 71da130fe4cfbc64ced56fe27a89af49b6715494 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Mon, 28 Apr 2025 23:11:52 +0300 Subject: [PATCH 176/695] ci(nix): fix the PR number Signed-off-by: Trial97 --- .github/workflows/nix.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index a77b33521..75ef7c65a 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -72,7 +72,7 @@ jobs: id: merge-commit uses: PrismLauncher/PrismLauncher/.github/actions/get-merge-commit@develop with: - pull-request-id: ${{ github.event.pull_request.id }} + pull-request-id: ${{ github.event.number }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 1c223997db10c5459b58fa93be541edfac925ac5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 28 Apr 2025 21:33:12 +0000 Subject: [PATCH 177/695] chore(deps): update hendrikmuhs/ccache-action action to v1.2.18 --- .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 5d5cbc893..952b7c515 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -152,7 +152,7 @@ jobs: - name: Setup ccache if: (runner.os != 'Windows' || matrix.msystem == '') && inputs.build_type == 'Debug' - uses: hendrikmuhs/ccache-action@v1.2.17 + uses: hendrikmuhs/ccache-action@v1.2.18 with: key: ${{ matrix.os }}-qt${{ matrix.qt_ver }}-${{ matrix.architecture }} From 93552277fe52c31eb03f4eb8ae08969f61a01891 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Tue, 29 Apr 2025 09:02:58 +0300 Subject: [PATCH 178/695] fix: build error introduced in #3516 Signed-off-by: Trial97 --- launcher/ui/widgets/ProjectItem.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/widgets/ProjectItem.cpp b/launcher/ui/widgets/ProjectItem.cpp index 950c5fe0a..03fa659c9 100644 --- a/launcher/ui/widgets/ProjectItem.cpp +++ b/launcher/ui/widgets/ProjectItem.cpp @@ -152,7 +152,7 @@ bool ProjectItemDelegate::editorEvent(QEvent* event, const QStyleOptionViewItem checkboxOpt = makeCheckboxStyleOption(opt, style); - if (!checkboxOpt.rect.contains(mouseEvent->x(), mouseEvent->y())) + if (!checkboxOpt.rect.contains(mouseEvent->pos().x(), mouseEvent->pos().y())) return false; // swallow other events From 7da32af1b26ff9695e52c19622989e1dc755b498 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Tue, 29 Apr 2025 10:42:42 +0300 Subject: [PATCH 179/695] ci(nix): remove addtional > Signed-off-by: Trial97 --- .github/actions/get-merge-commit/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/get-merge-commit/action.yml b/.github/actions/get-merge-commit/action.yml index 8c67fdfc9..534d138e1 100644 --- a/.github/actions/get-merge-commit/action.yml +++ b/.github/actions/get-merge-commit/action.yml @@ -98,6 +98,6 @@ runs: if [[ "$mergeable" == "true" ]]; then echo "merge-commit-sha=$(jq -r .merge_commit_sha <<<"$prInfo")" >> "$GITHUB_OUTPUT" else - echo "# 🚨 The PR has a merge conflict!" >>> "$GITHUB_STEP_SUMMARY" + echo "# 🚨 The PR has a merge conflict!" >> "$GITHUB_STEP_SUMMARY" exit 2 fi From 5c8481a118c8fefbfe901001d7828eaf6866eac4 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Tue, 29 Apr 2025 10:34:42 +0300 Subject: [PATCH 180/695] chore: reformat Signed-off-by: Trial97 --- launcher/Application.cpp | 1 + launcher/launch/LogModel.cpp | 4 +- launcher/logs/LogParser.cpp | 2 +- launcher/minecraft/MinecraftInstance.cpp | 1 - launcher/minecraft/mod/Mod.h | 2 +- .../minecraft/mod/TexturePackFolderModel.cpp | 3 +- launcher/net/HttpMetaCache.cpp | 4 +- launcher/tasks/Task.h | 5 +- launcher/ui/MainWindow.cpp | 2 +- launcher/ui/pages/global/JavaPage.cpp | 2 +- launcher/ui/pages/global/JavaPage.h | 2 +- .../ui/pages/instance/InstanceSettingsPage.h | 5 +- .../ui/pages/instance/ManagedPackPage.cpp | 15 ++- launcher/ui/pages/instance/McClient.cpp | 93 ++++++++++--------- launcher/ui/pages/instance/McClient.h | 27 +++--- launcher/ui/pages/instance/McResolver.cpp | 31 ++++--- launcher/ui/pages/instance/McResolver.h | 14 +-- launcher/ui/pages/instance/ServerPingTask.cpp | 32 +++---- launcher/ui/pages/instance/ServerPingTask.h | 7 +- launcher/ui/themes/HintOverrideProxyStyle.cpp | 3 +- 20 files changed, 135 insertions(+), 120 deletions(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 33d700772..0daab026c 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -128,6 +128,7 @@ #include #include +#include #include "SysInfo.h" #ifdef Q_OS_LINUX diff --git a/launcher/launch/LogModel.cpp b/launcher/launch/LogModel.cpp index 53a450ff7..90af9787d 100644 --- a/launcher/launch/LogModel.cpp +++ b/launcher/launch/LogModel.cpp @@ -167,8 +167,8 @@ bool LogModel::isOverFlow() return m_numLines >= m_maxLines && m_stopOnOverflow; } - -MessageLevel::Enum LogModel::previousLevel() { +MessageLevel::Enum LogModel::previousLevel() +{ if (!m_content.isEmpty()) { return m_content.last().level; } diff --git a/launcher/logs/LogParser.cpp b/launcher/logs/LogParser.cpp index 6e33b24dd..0790dec4d 100644 --- a/launcher/logs/LogParser.cpp +++ b/launcher/logs/LogParser.cpp @@ -107,7 +107,7 @@ std::optional LogParser::parseNext() if (m_buffer.trimmed().isEmpty()) { auto text = QString(m_buffer); m_buffer.clear(); - return LogParser::PlainText { text }; + return LogParser::PlainText{ text }; } // check if we have a full xml log4j event diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index b8ab93d5b..991afce89 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -1003,7 +1003,6 @@ QMap MinecraftInstance::createCensorFilterFromSession(AuthSess return filter; } - QStringList MinecraftInstance::getLogFileSearchPaths() { return { FS::PathCombine(gameRoot(), "crash-reports"), FS::PathCombine(gameRoot(), "logs"), gameRoot() }; diff --git a/launcher/minecraft/mod/Mod.h b/launcher/minecraft/mod/Mod.h index deb1859de..553af92f3 100644 --- a/launcher/minecraft/mod/Mod.h +++ b/launcher/minecraft/mod/Mod.h @@ -84,7 +84,7 @@ class Mod : public Resource { bool valid() const override; - [[nodiscard]] int compare(const Resource & other, SortType type) const override; + [[nodiscard]] int compare(const Resource& other, SortType type) const override; [[nodiscard]] bool applyFilter(QRegularExpression filter) const override; // Delete all the files of this mod diff --git a/launcher/minecraft/mod/TexturePackFolderModel.cpp b/launcher/minecraft/mod/TexturePackFolderModel.cpp index 073ea7ca7..4d7c71359 100644 --- a/launcher/minecraft/mod/TexturePackFolderModel.cpp +++ b/launcher/minecraft/mod/TexturePackFolderModel.cpp @@ -48,7 +48,8 @@ TexturePackFolderModel::TexturePackFolderModel(const QDir& dir, BaseInstance* in m_column_names = QStringList({ "Enable", "Image", "Name", "Last Modified", "Provider", "Size" }); m_column_names_translated = QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Last Modified"), tr("Provider"), tr("Size") }); m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::DATE, SortType::PROVIDER, SortType::SIZE }; - m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive }; + m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, + QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive }; m_columnsHideable = { false, true, false, true, true, true }; m_columnsHiddenByDefault = { false, false, false, false, false, true }; } diff --git a/launcher/net/HttpMetaCache.cpp b/launcher/net/HttpMetaCache.cpp index 5a3a451b7..77c1de47d 100644 --- a/launcher/net/HttpMetaCache.cpp +++ b/launcher/net/HttpMetaCache.cpp @@ -166,7 +166,7 @@ auto HttpMetaCache::evictEntry(MetaEntryPtr entry) -> bool return true; } -//returns true on success, false otherwise +// returns true on success, false otherwise auto HttpMetaCache::evictAll() -> bool { bool ret = true; @@ -178,7 +178,7 @@ auto HttpMetaCache::evictAll() -> bool qCWarning(taskHttpMetaCacheLogC) << "Unexpected missing cache entry" << entry->m_basePath; } map.entry_list.clear(); - //AND all return codes together so the result is true iff all runs of deletePath() are true + // AND all return codes together so the result is true iff all runs of deletePath() are true ret &= FS::deletePath(map.base_path); } return ret; diff --git a/launcher/tasks/Task.h b/launcher/tasks/Task.h index 503d6a6b6..fcd075150 100644 --- a/launcher/tasks/Task.h +++ b/launcher/tasks/Task.h @@ -79,7 +79,6 @@ Q_DECLARE_METATYPE(TaskStepProgress) using TaskStepProgressList = QList>; - /*! * Represents a task that has to be done. * To create a task, you need to subclass this class, implement the executeTask() method and call @@ -177,9 +176,9 @@ class Task : public QObject, public QRunnable { virtual void executeTask() = 0; protected slots: - //! The Task subclass must call this method when the task has succeeded + //! The Task subclass must call this method when the task has succeeded virtual void emitSucceeded(); - //! **The Task subclass** must call this method when the task has aborted. External code should call abort() instead. + //! **The Task subclass** must call this method when the task has aborted. External code should call abort() instead. virtual void emitAborted(); //! The Task subclass must call this method when the task has failed virtual void emitFailed(QString reason = ""); diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index d190c6a02..d9275a7ab 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -1311,7 +1311,7 @@ void MainWindow::on_actionReportBug_triggered() void MainWindow::on_actionClearMetadata_triggered() { - //This if contains side effects! + // This if contains side effects! if (!APPLICATION->metacache()->evictAll()) { CustomMessageBox::selectable(this, tr("Error"), tr("Metadata cache clear Failed!\nTo clear the metadata cache manually, press Folders -> View " diff --git a/launcher/ui/pages/global/JavaPage.cpp b/launcher/ui/pages/global/JavaPage.cpp index b99d0c63e..6a44c9290 100644 --- a/launcher/ui/pages/global/JavaPage.cpp +++ b/launcher/ui/pages/global/JavaPage.cpp @@ -62,7 +62,7 @@ JavaPage::JavaPage(QWidget* parent) : QWidget(parent), ui(new Ui::JavaPage) { ui->setupUi(this); - + if (BuildConfig.JAVA_DOWNLOADER_ENABLED) { ui->managedJavaList->initialize(new JavaInstallList(this, true)); ui->managedJavaList->setResizeOn(2); diff --git a/launcher/ui/pages/global/JavaPage.h b/launcher/ui/pages/global/JavaPage.h index ea7724c1d..b30fa22e3 100644 --- a/launcher/ui/pages/global/JavaPage.h +++ b/launcher/ui/pages/global/JavaPage.h @@ -37,11 +37,11 @@ #include #include -#include "ui/widgets/JavaSettingsWidget.h" #include #include #include "JavaCommon.h" #include "ui/pages/BasePage.h" +#include "ui/widgets/JavaSettingsWidget.h" class SettingsObject; diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.h b/launcher/ui/pages/instance/InstanceSettingsPage.h index fa1dce3dc..de173937b 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.h +++ b/launcher/ui/pages/instance/InstanceSettingsPage.h @@ -35,17 +35,18 @@ #pragma once +#include #include "Application.h" #include "BaseInstance.h" #include "ui/pages/BasePage.h" #include "ui/widgets/MinecraftSettingsWidget.h" -#include class InstanceSettingsPage : public MinecraftSettingsWidget, public BasePage { Q_OBJECT public: - explicit InstanceSettingsPage(MinecraftInstancePtr instance, QWidget* parent = nullptr) : MinecraftSettingsWidget(std::move(instance), parent) + explicit InstanceSettingsPage(MinecraftInstancePtr instance, QWidget* parent = nullptr) + : MinecraftSettingsWidget(std::move(instance), parent) { connect(APPLICATION, &Application::globalSettingsAboutToOpen, this, &InstanceSettingsPage::saveSettings); connect(APPLICATION, &Application::globalSettingsClosed, this, &InstanceSettingsPage::loadSettings); diff --git a/launcher/ui/pages/instance/ManagedPackPage.cpp b/launcher/ui/pages/instance/ManagedPackPage.cpp index 0fccd1d33..1738c9cde 100644 --- a/launcher/ui/pages/instance/ManagedPackPage.cpp +++ b/launcher/ui/pages/instance/ManagedPackPage.cpp @@ -347,13 +347,18 @@ void ManagedPackPage::onUpdateTaskCompleted(bool did_succeed) const if (m_instance_window != nullptr) m_instance_window->close(); - CustomMessageBox::selectable(nullptr, tr("Update Successful"), tr("The instance updated to pack version %1 successfully.").arg(m_inst->getManagedPackVersionName()), QMessageBox::Information) - ->show(); + CustomMessageBox::selectable(nullptr, tr("Update Successful"), + tr("The instance updated to pack version %1 successfully.").arg(m_inst->getManagedPackVersionName()), + QMessageBox::Information) + ->show(); } else { - CustomMessageBox::selectable(nullptr, tr("Update Failed"), tr("The instance failed to update to pack version %1. Please check launcher logs for more information.").arg(m_inst->getManagedPackVersionName()), QMessageBox::Critical) - ->show(); + CustomMessageBox::selectable( + nullptr, tr("Update Failed"), + tr("The instance failed to update to pack version %1. Please check launcher logs for more information.") + .arg(m_inst->getManagedPackVersionName()), + QMessageBox::Critical) + ->show(); } - } void ModrinthManagedPackPage::update() diff --git a/launcher/ui/pages/instance/McClient.cpp b/launcher/ui/pages/instance/McClient.cpp index 90813ac18..11b3a22d1 100644 --- a/launcher/ui/pages/instance/McClient.cpp +++ b/launcher/ui/pages/instance/McClient.cpp @@ -1,21 +1,22 @@ -#include -#include +#include #include #include -#include +#include +#include #include -#include "McClient.h" #include "Json.h" +#include "McClient.h" -// 7 first bits +// 7 first bits #define SEGMENT_BITS 0x7F // last bit #define CONTINUE_BIT 0x80 -McClient::McClient(QObject *parent, QString domain, QString ip, short port): QObject(parent), m_domain(domain), m_ip(ip), m_port(port) {} +McClient::McClient(QObject* parent, QString domain, QString ip, short port) : QObject(parent), m_domain(domain), m_ip(ip), m_port(port) {} -void McClient::getStatusData() { +void McClient::getStatusData() +{ qDebug() << "Connecting to socket.."; connect(&m_socket, &QTcpSocket::connected, this, [this]() { @@ -25,28 +26,28 @@ void McClient::getStatusData() { connect(&m_socket, &QTcpSocket::readyRead, this, &McClient::readRawResponse); }); - connect(&m_socket, &QTcpSocket::errorOccurred, this, [this]() { - emitFail("Socket disconnected: " + m_socket.errorString()); - }); + connect(&m_socket, &QTcpSocket::errorOccurred, this, [this]() { emitFail("Socket disconnected: " + m_socket.errorString()); }); m_socket.connectToHost(m_ip, m_port); } -void McClient::sendRequest() { +void McClient::sendRequest() +{ QByteArray data; - writeVarInt(data, 0x00); // packet ID - writeVarInt(data, 763); // hardcoded protocol version (763 = 1.20.1) - writeVarInt(data, m_domain.size()); // server address length - writeString(data, m_domain.toStdString()); // server address - writeFixedInt(data, m_port, 2); // server port - writeVarInt(data, 0x01); // next state - writePacketToSocket(data); // send handshake packet - - writeVarInt(data, 0x00); // packet ID - writePacketToSocket(data); // send status packet + writeVarInt(data, 0x00); // packet ID + writeVarInt(data, 763); // hardcoded protocol version (763 = 1.20.1) + writeVarInt(data, m_domain.size()); // server address length + writeString(data, m_domain.toStdString()); // server address + writeFixedInt(data, m_port, 2); // server port + writeVarInt(data, 0x01); // next state + writePacketToSocket(data); // send handshake packet + + writeVarInt(data, 0x00); // packet ID + writePacketToSocket(data); // send status packet } -void McClient::readRawResponse() { +void McClient::readRawResponse() +{ if (m_responseReadState == 2) { return; } @@ -56,28 +57,27 @@ void McClient::readRawResponse() { m_wantedRespLength = readVarInt(m_resp); m_responseReadState = 1; } - + if (m_responseReadState == 1 && m_resp.size() >= m_wantedRespLength) { if (m_resp.size() > m_wantedRespLength) { - qDebug() << "Warning: Packet length doesn't match actual packet size (" << m_wantedRespLength << " expected vs " << m_resp.size() << " received)"; + qDebug() << "Warning: Packet length doesn't match actual packet size (" << m_wantedRespLength << " expected vs " + << m_resp.size() << " received)"; } parseResponse(); m_responseReadState = 2; } } -void McClient::parseResponse() { +void McClient::parseResponse() +{ qDebug() << "Received response successfully"; int packetID = readVarInt(m_resp); if (packetID != 0x00) { - throw Exception( - QString("Packet ID doesn't match expected value (0x00 vs 0x%1)") - .arg(packetID, 0, 16) - ); + throw Exception(QString("Packet ID doesn't match expected value (0x00 vs 0x%1)").arg(packetID, 0, 16)); } - Q_UNUSED(readVarInt(m_resp)); // json length + Q_UNUSED(readVarInt(m_resp)); // json length // 'resp' should now be the JSON string QJsonDocument doc = QJsonDocument::fromJson(m_resp); @@ -85,8 +85,9 @@ void McClient::parseResponse() { } // From https://wiki.vg/Protocol#VarInt_and_VarLong -void McClient::writeVarInt(QByteArray &data, int value) { - while ((value & ~SEGMENT_BITS)) { // check if the value is too big to fit in 7 bits +void McClient::writeVarInt(QByteArray& data, int value) +{ + while ((value & ~SEGMENT_BITS)) { // check if the value is too big to fit in 7 bits // Write 7 bits data.append((value & SEGMENT_BITS) | CONTINUE_BIT); @@ -98,7 +99,8 @@ void McClient::writeVarInt(QByteArray &data, int value) { } // From https://wiki.vg/Protocol#VarInt_and_VarLong -int McClient::readVarInt(QByteArray &data) { +int McClient::readVarInt(QByteArray& data) +{ int value = 0; int position = 0; char currentByte; @@ -107,17 +109,20 @@ int McClient::readVarInt(QByteArray &data) { currentByte = readByte(data); value |= (currentByte & SEGMENT_BITS) << position; - if ((currentByte & CONTINUE_BIT) == 0) break; + if ((currentByte & CONTINUE_BIT) == 0) + break; position += 7; } - if (position >= 32) throw Exception("VarInt is too big"); + if (position >= 32) + throw Exception("VarInt is too big"); return value; } -char McClient::readByte(QByteArray &data) { +char McClient::readByte(QByteArray& data) +{ if (data.isEmpty()) { throw Exception("No more bytes to read"); } @@ -128,17 +133,20 @@ char McClient::readByte(QByteArray &data) { } // write number with specified size in big endian format -void McClient::writeFixedInt(QByteArray &data, int value, int size) { +void McClient::writeFixedInt(QByteArray& data, int value, int size) +{ for (int i = size - 1; i >= 0; i--) { data.append((value >> (i * 8)) & 0xFF); } } -void McClient::writeString(QByteArray &data, const std::string &value) { +void McClient::writeString(QByteArray& data, const std::string& value) +{ data.append(value.c_str()); } -void McClient::writePacketToSocket(QByteArray &data) { +void McClient::writePacketToSocket(QByteArray& data) +{ // we prefix the packet with its length QByteArray dataWithSize; writeVarInt(dataWithSize, data.size()); @@ -151,14 +159,15 @@ void McClient::writePacketToSocket(QByteArray &data) { data.clear(); } - -void McClient::emitFail(QString error) { +void McClient::emitFail(QString error) +{ qDebug() << "Minecraft server ping for status error:" << error; emit failed(error); emit finished(); } -void McClient::emitSucceed(QJsonObject data) { +void McClient::emitSucceed(QJsonObject data) +{ emit succeeded(data); emit finished(); } diff --git a/launcher/ui/pages/instance/McClient.h b/launcher/ui/pages/instance/McClient.h index 59834dfb7..832b70d40 100644 --- a/launcher/ui/pages/instance/McClient.h +++ b/launcher/ui/pages/instance/McClient.h @@ -1,8 +1,8 @@ -#include -#include +#include #include #include -#include +#include +#include #include @@ -22,29 +22,30 @@ class McClient : public QObject { unsigned m_wantedRespLength = 0; QByteArray m_resp; -public: - explicit McClient(QObject *parent, QString domain, QString ip, short port); + public: + explicit McClient(QObject* parent, QString domain, QString ip, short port); //! Read status data of the server, and calls the succeeded() signal with the parsed JSON data void getStatusData(); -private: + + private: void sendRequest(); //! Accumulate data until we have a full response, then call parseResponse() once void readRawResponse(); void parseResponse(); - void writeVarInt(QByteArray &data, int value); - int readVarInt(QByteArray &data); - char readByte(QByteArray &data); + void writeVarInt(QByteArray& data, int value); + int readVarInt(QByteArray& data); + char readByte(QByteArray& data); //! write number with specified size in big endian format - void writeFixedInt(QByteArray &data, int value, int size); - void writeString(QByteArray &data, const std::string &value); + void writeFixedInt(QByteArray& data, int value, int size); + void writeString(QByteArray& data, const std::string& value); - void writePacketToSocket(QByteArray &data); + void writePacketToSocket(QByteArray& data); void emitFail(QString error); void emitSucceed(QJsonObject data); -signals: + signals: void succeeded(QJsonObject data); void failed(QString error); void finished(); diff --git a/launcher/ui/pages/instance/McResolver.cpp b/launcher/ui/pages/instance/McResolver.cpp index 48c2a72fd..2a769762c 100644 --- a/launcher/ui/pages/instance/McResolver.cpp +++ b/launcher/ui/pages/instance/McResolver.cpp @@ -1,23 +1,25 @@ -#include -#include #include +#include #include +#include #include "McResolver.h" -McResolver::McResolver(QObject *parent, QString domain, int port): QObject(parent), m_constrDomain(domain), m_constrPort(port) {} +McResolver::McResolver(QObject* parent, QString domain, int port) : QObject(parent), m_constrDomain(domain), m_constrPort(port) {} -void McResolver::ping() { +void McResolver::ping() +{ pingWithDomainSRV(m_constrDomain, m_constrPort); } -void McResolver::pingWithDomainSRV(QString domain, int port) { - QDnsLookup *lookup = new QDnsLookup(this); +void McResolver::pingWithDomainSRV(QString domain, int port) +{ + QDnsLookup* lookup = new QDnsLookup(this); lookup->setName(QString("_minecraft._tcp.%1").arg(domain)); lookup->setType(QDnsLookup::SRV); connect(lookup, &QDnsLookup::finished, this, [this, domain, port]() { - QDnsLookup *lookup = qobject_cast(sender()); + QDnsLookup* lookup = qobject_cast(sender()); lookup->deleteLater(); @@ -43,8 +45,9 @@ void McResolver::pingWithDomainSRV(QString domain, int port) { lookup->lookup(); } -void McResolver::pingWithDomainA(QString domain, int port) { - QHostInfo::lookupHost(domain, this, [this, port](const QHostInfo &hostInfo){ +void McResolver::pingWithDomainA(QString domain, int port) +{ + QHostInfo::lookupHost(domain, this, [this, port](const QHostInfo& hostInfo) { if (hostInfo.error() != QHostInfo::NoError) { emitFail("A record lookup failed"); return; @@ -55,19 +58,21 @@ void McResolver::pingWithDomainA(QString domain, int port) { emitFail("No A entries found for domain"); return; } - + const auto& firstRecord = records.at(0); emitSucceed(firstRecord.toString(), port); - }); + }); } -void McResolver::emitFail(QString error) { +void McResolver::emitFail(QString error) +{ qDebug() << "DNS resolver error:" << error; emit failed(error); emit finished(); } -void McResolver::emitSucceed(QString ip, int port) { +void McResolver::emitSucceed(QString ip, int port) +{ emit succeeded(ip, port); emit finished(); } diff --git a/launcher/ui/pages/instance/McResolver.h b/launcher/ui/pages/instance/McResolver.h index 06b4b7b38..3dfeddc6a 100644 --- a/launcher/ui/pages/instance/McResolver.h +++ b/launcher/ui/pages/instance/McResolver.h @@ -1,8 +1,8 @@ -#include -#include -#include #include +#include #include +#include +#include // resolve the IP and port of a Minecraft server class McResolver : public QObject { @@ -11,17 +11,17 @@ class McResolver : public QObject { QString m_constrDomain; int m_constrPort; -public: - explicit McResolver(QObject *parent, QString domain, int port); + public: + explicit McResolver(QObject* parent, QString domain, int port); void ping(); -private: + private: void pingWithDomainSRV(QString domain, int port); void pingWithDomainA(QString domain, int port); void emitFail(QString error); void emitSucceed(QString ip, int port); -signals: + signals: void succeeded(QString ip, int port); void failed(QString error); void finished(); diff --git a/launcher/ui/pages/instance/ServerPingTask.cpp b/launcher/ui/pages/instance/ServerPingTask.cpp index 3ec9308ca..b39f3d117 100644 --- a/launcher/ui/pages/instance/ServerPingTask.cpp +++ b/launcher/ui/pages/instance/ServerPingTask.cpp @@ -1,47 +1,41 @@ #include -#include "ServerPingTask.h" -#include "McResolver.h" -#include "McClient.h" #include +#include "McClient.h" +#include "McResolver.h" +#include "ServerPingTask.h" -unsigned getOnlinePlayers(QJsonObject data) { +unsigned getOnlinePlayers(QJsonObject data) +{ return Json::requireInteger(Json::requireObject(data, "players"), "online"); } -void ServerPingTask::executeTask() { +void ServerPingTask::executeTask() +{ qDebug() << "Querying status of " << QString("%1:%2").arg(m_domain).arg(m_port); // Resolve the actual IP and port for the server - McResolver *resolver = new McResolver(nullptr, m_domain, m_port); + McResolver* resolver = new McResolver(nullptr, m_domain, m_port); QObject::connect(resolver, &McResolver::succeeded, this, [this, resolver](QString ip, int port) { qDebug() << "Resolved Address for" << m_domain << ": " << ip << ":" << port; // Now that we have the IP and port, query the server - McClient *client = new McClient(nullptr, m_domain, ip, port); + McClient* client = new McClient(nullptr, m_domain, ip, port); QObject::connect(client, &McClient::succeeded, this, [this](QJsonObject data) { m_outputOnlinePlayers = getOnlinePlayers(data); qDebug() << "Online players: " << m_outputOnlinePlayers; emitSucceeded(); }); - QObject::connect(client, &McClient::failed, this, [this](QString error) { - emitFailed(error); - }); + QObject::connect(client, &McClient::failed, this, [this](QString error) { emitFailed(error); }); // Delete McClient object when done - QObject::connect(client, &McClient::finished, this, [this, client]() { - client->deleteLater(); - }); + QObject::connect(client, &McClient::finished, this, [this, client]() { client->deleteLater(); }); client->getStatusData(); }); - QObject::connect(resolver, &McResolver::failed, this, [this](QString error) { - emitFailed(error); - }); + QObject::connect(resolver, &McResolver::failed, this, [this](QString error) { emitFailed(error); }); // Delete McResolver object when done - QObject::connect(resolver, &McResolver::finished, [resolver]() { - resolver->deleteLater(); - }); + QObject::connect(resolver, &McResolver::finished, [resolver]() { resolver->deleteLater(); }); resolver->ping(); } \ No newline at end of file diff --git a/launcher/ui/pages/instance/ServerPingTask.h b/launcher/ui/pages/instance/ServerPingTask.h index 0956a4f63..6f03b92ad 100644 --- a/launcher/ui/pages/instance/ServerPingTask.h +++ b/launcher/ui/pages/instance/ServerPingTask.h @@ -5,18 +5,17 @@ #include - class ServerPingTask : public Task { Q_OBJECT - public: + public: explicit ServerPingTask(QString domain, int port) : Task(), m_domain(domain), m_port(port) {} ~ServerPingTask() override = default; int m_outputOnlinePlayers = -1; - private: + private: QString m_domain; int m_port; - protected: + protected: virtual void executeTask() override; }; diff --git a/launcher/ui/themes/HintOverrideProxyStyle.cpp b/launcher/ui/themes/HintOverrideProxyStyle.cpp index f31969fce..f5b8232a8 100644 --- a/launcher/ui/themes/HintOverrideProxyStyle.cpp +++ b/launcher/ui/themes/HintOverrideProxyStyle.cpp @@ -18,7 +18,8 @@ #include "HintOverrideProxyStyle.h" -HintOverrideProxyStyle::HintOverrideProxyStyle(QStyle* style) : QProxyStyle(style) { +HintOverrideProxyStyle::HintOverrideProxyStyle(QStyle* style) : QProxyStyle(style) +{ setObjectName(style->objectName()); } From 8bb79cefacef4616304eae958dcef5b8281b0d23 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Tue, 29 Apr 2025 10:47:00 +0300 Subject: [PATCH 181/695] chore: add format commits to the git-blame-ignore Signed-off-by: Trial97 --- .git-blame-ignore-revs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 528b128b1..c7d36db27 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -5,3 +5,9 @@ bbb3b3e6f6e3c0f95873f22e6d0a4aaf350f49d9 # (nix) alejandra -> nixfmt 4c81d8c53d09196426568c4a31a4e752ed05397a + +# reformat codebase +1d468ac35ad88d8c77cc83f25e3704d9bd7df01b + +# format a part of codebase +5c8481a118c8fefbfe901001d7828eaf6866eac4 From 476054ba19f1858b1ac1effb84f0e7415e7de0c3 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Sun, 20 Apr 2025 22:17:41 +0300 Subject: [PATCH 182/695] feat: move qr code glue in MSALoginDialog Signed-off-by: Trial97 --- .gitmodules | 4 +-- CMakeLists.txt | 10 +++++- COPYING.md | 2 +- flake.lock | 4 +-- flake.nix | 6 ++-- launcher/CMakeLists.txt | 2 +- launcher/ui/dialogs/MSALoginDialog.cpp | 33 ++++++++++++++++++- libraries/README.md | 2 +- libraries/qrcodegenerator | 1 + libraries/qt-qrcodegenerator/CMakeLists.txt | 32 ------------------ .../qt-qrcodegenerator/QR-Code-generator | 1 - libraries/qt-qrcodegenerator/qr.cpp | 29 ---------------- libraries/qt-qrcodegenerator/qr.h | 8 ----- nix/unwrapped.nix | 6 ++-- 14 files changed, 55 insertions(+), 85 deletions(-) create mode 160000 libraries/qrcodegenerator delete mode 100644 libraries/qt-qrcodegenerator/CMakeLists.txt delete mode 160000 libraries/qt-qrcodegenerator/QR-Code-generator delete mode 100644 libraries/qt-qrcodegenerator/qr.cpp delete mode 100644 libraries/qt-qrcodegenerator/qr.h diff --git a/.gitmodules b/.gitmodules index 0c56d8768..0a0a50bee 100644 --- a/.gitmodules +++ b/.gitmodules @@ -19,6 +19,6 @@ [submodule "flatpak/shared-modules"] path = flatpak/shared-modules url = https://github.com/flathub/shared-modules.git -[submodule "libraries/qt-qrcodegenerator/QR-Code-generator"] - path = libraries/qt-qrcodegenerator/QR-Code-generator +[submodule "libraries/qrcodegenerator"] + path = libraries/qrcodegenerator url = https://github.com/nayuki/QR-Code-generator diff --git a/CMakeLists.txt b/CMakeLists.txt index 68d900c27..ce3d433fb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -475,7 +475,6 @@ add_subdirectory(libraries/libnbtplusplus) add_subdirectory(libraries/systeminfo) # system information library add_subdirectory(libraries/launcher) # java based launcher part for Minecraft add_subdirectory(libraries/javacheck) # java compatibility checker -add_subdirectory(libraries/qt-qrcodegenerator) # qr code generator if(FORCE_BUNDLED_ZLIB) message(STATUS "Using bundled zlib") @@ -533,6 +532,15 @@ add_subdirectory(libraries/gamemode) add_subdirectory(libraries/murmur2) # Hash for usage with the CurseForge API add_subdirectory(libraries/qdcss) # css parser +# qr code generator +set(QRCODE_SOURCES + libraries/qrcodegenerator/cpp/qrcodegen.cpp + libraries/qrcodegenerator/cpp/qrcodegen.hpp +) +add_library(qrcodegenerator STATIC ${QRCODE_SOURCES}) +target_include_directories(qrcodegenerator PUBLIC "libraries/qrcodegenerator/cpp/" ) +generate_export_header(qrcodegenerator) + ############################### Built Artifacts ############################### add_subdirectory(buildconfig) diff --git a/COPYING.md b/COPYING.md index 1ebde116f..f9b905351 100644 --- a/COPYING.md +++ b/COPYING.md @@ -404,7 +404,7 @@ You should have received a copy of the GNU Lesser General Public License along with this library. If not, see . -## qt-qrcodegenerator (`libraries/qt-qrcodegenerator`) +## QR-Code-generator (`libraries/qrcodegenerator`) Copyright © 2024 Project Nayuki. (MIT License) https://www.nayuki.io/page/qr-code-generator-library diff --git a/flake.lock b/flake.lock index 5418557a3..a0057327e 100644 --- a/flake.lock +++ b/flake.lock @@ -32,7 +32,7 @@ "type": "github" } }, - "qt-qrcodegenerator": { + "qrcodegenerator": { "flake": false, "locked": { "lastModified": 1737616857, @@ -52,7 +52,7 @@ "inputs": { "libnbtplusplus": "libnbtplusplus", "nixpkgs": "nixpkgs", - "qt-qrcodegenerator": "qt-qrcodegenerator" + "qrcodegenerator": "qrcodegenerator" } } }, diff --git a/flake.nix b/flake.nix index 69abd78dd..751ef2eeb 100644 --- a/flake.nix +++ b/flake.nix @@ -16,7 +16,7 @@ flake = false; }; - qt-qrcodegenerator = { + qrcodegenerator = { url = "github:nayuki/QR-Code-generator"; flake = false; }; @@ -27,7 +27,7 @@ self, nixpkgs, libnbtplusplus, - qt-qrcodegenerator, + qrcodegenerator, }: let @@ -175,7 +175,7 @@ prismlauncher-unwrapped = prev.callPackage ./nix/unwrapped.nix { inherit libnbtplusplus - qt-qrcodegenerator + qrcodegenerator self ; }; diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 4f8b9018a..aa26b3544 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -1302,7 +1302,7 @@ target_link_libraries(Launcher_logic qdcss BuildConfig Qt${QT_VERSION_MAJOR}::Widgets - qrcode + qrcodegenerator ) if (UNIX AND NOT CYGWIN AND NOT APPLE) diff --git a/launcher/ui/dialogs/MSALoginDialog.cpp b/launcher/ui/dialogs/MSALoginDialog.cpp index 83f46294d..14ec672e0 100644 --- a/launcher/ui/dialogs/MSALoginDialog.cpp +++ b/launcher/ui/dialogs/MSALoginDialog.cpp @@ -36,7 +36,6 @@ #include "MSALoginDialog.h" #include "Application.h" -#include "qr.h" #include "ui_MSALoginDialog.h" #include "DesktopServices.h" @@ -44,10 +43,15 @@ #include #include +#include +#include #include +#include #include #include +#include "qrcodegen.hpp" + MSALoginDialog::MSALoginDialog(QWidget* parent) : QDialog(parent), ui(new Ui::MSALoginDialog) { ui->setupUi(this); @@ -139,6 +143,33 @@ void MSALoginDialog::authorizeWithBrowser(const QUrl& url) m_url = url; } +// https://stackoverflow.com/questions/21400254/how-to-draw-a-qr-code-with-qt-in-native-c-c +void paintQR(QPainter& painter, const QSize sz, const QString& data, QColor fg) +{ + // NOTE: At this point you will use the API to get the encoding and format you want, instead of my hardcoded stuff: + qrcodegen::QrCode qr = qrcodegen::QrCode::encodeText(data.toUtf8().constData(), qrcodegen::QrCode::Ecc::LOW); + const int s = qr.getSize() > 0 ? qr.getSize() : 1; + const double w = sz.width(); + const double h = sz.height(); + const double aspect = w / h; + const double size = ((aspect > 1.0) ? h : w); + const double scale = size / (s + 2); + // NOTE: For performance reasons my implementation only draws the foreground parts in supplied color. + // It expects background to be prepared already (in white or whatever is preferred). + painter.setPen(Qt::NoPen); + painter.setBrush(fg); + for (int y = 0; y < s; y++) { + for (int x = 0; x < s; x++) { + const int color = qr.getModule(x, y); // 0 for white, 1 for black + if (0 != color) { + const double rx1 = (x + 1) * scale, ry1 = (y + 1) * scale; + QRectF r(rx1, ry1, scale, scale); + painter.drawRects(&r, 1); + } + } + } +} + void MSALoginDialog::authorizeWithBrowserWithExtra(QString url, QString code, [[maybe_unused]] int expiresIn) { ui->stackedWidget->setCurrentIndex(1); diff --git a/libraries/README.md b/libraries/README.md index 5f7b685e5..be41e549f 100644 --- a/libraries/README.md +++ b/libraries/README.md @@ -99,7 +99,7 @@ Canonical implementation of the murmur2 hash, taken from [SMHasher](https://gith Public domain (the author disclaimed the copyright). -## qt-qrcodegenerator +## QR-Code-generator A simple library for generating QR codes diff --git a/libraries/qrcodegenerator b/libraries/qrcodegenerator new file mode 160000 index 000000000..2c9044de6 --- /dev/null +++ b/libraries/qrcodegenerator @@ -0,0 +1 @@ +Subproject commit 2c9044de6b049ca25cb3cd1649ed7e27aa055138 diff --git a/libraries/qt-qrcodegenerator/CMakeLists.txt b/libraries/qt-qrcodegenerator/CMakeLists.txt deleted file mode 100644 index e18da0e71..000000000 --- a/libraries/qt-qrcodegenerator/CMakeLists.txt +++ /dev/null @@ -1,32 +0,0 @@ -cmake_minimum_required(VERSION 3.6) - -project(qrcode) - -set(CMAKE_AUTOMOC ON) -set(CMAKE_INCLUDE_CURRENT_DIR ON) - -set(CMAKE_CXX_STANDARD_REQUIRED true) -set(CMAKE_C_STANDARD_REQUIRED true) -set(CMAKE_CXX_STANDARD 11) -set(CMAKE_C_STANDARD 11) - - -if(QT_VERSION_MAJOR EQUAL 5) - find_package(Qt5 COMPONENTS Core Gui REQUIRED) -elseif(Launcher_QT_VERSION_MAJOR EQUAL 6) - find_package(Qt6 COMPONENTS Core Gui Core5Compat REQUIRED) - list(APPEND systeminfo_LIBS Qt${QT_VERSION_MAJOR}::Core5Compat) -endif() - -add_library(qrcode STATIC qr.h qr.cpp QR-Code-generator/cpp/qrcodegen.cpp QR-Code-generator/cpp/qrcodegen.hpp ) - -target_link_libraries(qrcode Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Gui ${systeminfo_LIBS}) - - -# needed for statically linked qrcode in shared libs on x86_64 -set_target_properties(qrcode - PROPERTIES POSITION_INDEPENDENT_CODE TRUE -) - -target_include_directories(qrcode PUBLIC ./ PRIVATE QR-Code-generator/cpp/) - diff --git a/libraries/qt-qrcodegenerator/QR-Code-generator b/libraries/qt-qrcodegenerator/QR-Code-generator deleted file mode 160000 index f40366c40..000000000 --- a/libraries/qt-qrcodegenerator/QR-Code-generator +++ /dev/null @@ -1 +0,0 @@ -Subproject commit f40366c40d8d1956081f7ec643d240c02a81df52 diff --git a/libraries/qt-qrcodegenerator/qr.cpp b/libraries/qt-qrcodegenerator/qr.cpp deleted file mode 100644 index 69bfb6da5..000000000 --- a/libraries/qt-qrcodegenerator/qr.cpp +++ /dev/null @@ -1,29 +0,0 @@ - -#include "qr.h" -#include "qrcodegen.hpp" - -void paintQR(QPainter& painter, const QSize sz, const QString& data, QColor fg) -{ - // NOTE: At this point you will use the API to get the encoding and format you want, instead of my hardcoded stuff: - qrcodegen::QrCode qr = qrcodegen::QrCode::encodeText(data.toUtf8().constData(), qrcodegen::QrCode::Ecc::LOW); - const int s = qr.getSize() > 0 ? qr.getSize() : 1; - const double w = sz.width(); - const double h = sz.height(); - const double aspect = w / h; - const double size = ((aspect > 1.0) ? h : w); - const double scale = size / (s + 2); - // NOTE: For performance reasons my implementation only draws the foreground parts in supplied color. - // It expects background to be prepared already (in white or whatever is preferred). - painter.setPen(Qt::NoPen); - painter.setBrush(fg); - for (int y = 0; y < s; y++) { - for (int x = 0; x < s; x++) { - const int color = qr.getModule(x, y); // 0 for white, 1 for black - if (0 != color) { - const double rx1 = (x + 1) * scale, ry1 = (y + 1) * scale; - QRectF r(rx1, ry1, scale, scale); - painter.drawRects(&r, 1); - } - } - } -} \ No newline at end of file diff --git a/libraries/qt-qrcodegenerator/qr.h b/libraries/qt-qrcodegenerator/qr.h deleted file mode 100644 index 290d49001..000000000 --- a/libraries/qt-qrcodegenerator/qr.h +++ /dev/null @@ -1,8 +0,0 @@ -#pragma once - -#include -#include -#include - -// https://stackoverflow.com/questions/21400254/how-to-draw-a-qr-code-with-qt-in-native-c-c -void paintQR(QPainter& painter, const QSize sz, const QString& data, QColor fg); diff --git a/nix/unwrapped.nix b/nix/unwrapped.nix index b5b02b101..d9144410f 100644 --- a/nix/unwrapped.nix +++ b/nix/unwrapped.nix @@ -9,7 +9,7 @@ jdk17, kdePackages, libnbtplusplus, - qt-qrcodegenerator, + qrcodegenerator, ninja, self, stripJavaArchivesHook, @@ -64,8 +64,8 @@ stdenv.mkDerivation { rm -rf source/libraries/libnbtplusplus ln -s ${libnbtplusplus} source/libraries/libnbtplusplus - rm -rf source/libraries/qt-qrcodegenerator/QR-Code-generator - ln -s ${qt-qrcodegenerator} source/libraries/qt-qrcodegenerator/QR-Code-generator + rm -rf source/libraries/qrcodegenerator + ln -s ${qrcodegenerator} source/libraries/qrcodegenerator ''; nativeBuildInputs = [ From 80fb1a8f4ea67d32b401378c3a5112bee62720de Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Tue, 29 Apr 2025 12:15:58 +0100 Subject: [PATCH 183/695] Fixes for new changes Signed-off-by: TheKodeToad --- launcher/ui/pages/instance/DataPackPage.cpp | 34 +++++++++++++++---- launcher/ui/pages/instance/DataPackPage.h | 8 +++-- .../ui/pages/modplatform/DataPackModel.cpp | 4 +-- launcher/ui/pages/modplatform/DataPackModel.h | 4 +-- .../ui/pages/modplatform/DataPackPage.cpp | 6 +--- launcher/ui/pages/modplatform/DataPackPage.h | 5 +-- 6 files changed, 41 insertions(+), 20 deletions(-) diff --git a/launcher/ui/pages/instance/DataPackPage.cpp b/launcher/ui/pages/instance/DataPackPage.cpp index 1723bc37f..2fc4ec31d 100644 --- a/launcher/ui/pages/instance/DataPackPage.cpp +++ b/launcher/ui/pages/instance/DataPackPage.cpp @@ -17,11 +17,13 @@ */ #include "DataPackPage.h" -#include +#include "minecraft/PackProfile.h" +#include "ui_ExternalResourcesPage.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/ProgressDialog.h" #include "ui/dialogs/ResourceDownloadDialog.h" +#include "ui/dialogs/ResourceUpdateDialog.h" DataPackPage::DataPackPage(BaseInstance* instance, std::shared_ptr model, QWidget* parent) : ExternalResourcesPage(instance, model, parent), m_model(model) @@ -65,9 +67,23 @@ void DataPackPage::downloadDataPacks() if (m_instance->typeName() != "Minecraft") return; // this is a null instance or a legacy instance - ResourceDownload::DataPackDownloadDialog mdownload(this, std::static_pointer_cast(m_model), m_instance); - if (mdownload.exec()) { - auto tasks = new ConcurrentTask("Download Data Pack", APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); + auto profile = static_cast(m_instance)->getPackProfile(); + if (!profile->getModLoaders().has_value()) { + QMessageBox::critical(this, tr("Error"), tr("Please install a mod loader first!")); + return; + } + + m_downloadDialog = new ResourceDownload::DataPackDownloadDialog(this, m_model, m_instance); + connect(this, &QObject::destroyed, m_downloadDialog, &QDialog::close); + connect(m_downloadDialog, &QDialog::finished, this, &DataPackPage::downloadDialogFinished); + + m_downloadDialog->open(); +} + +void DataPackPage::downloadDialogFinished(int result) +{ + if (result) { + auto tasks = new ConcurrentTask(tr("Download Data Packs"), APPLICATION->settings()->get("NumberOfConcurrentDownloads").toInt()); connect(tasks, &Task::failed, [this, tasks](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); tasks->deleteLater(); @@ -84,8 +100,12 @@ void DataPackPage::downloadDataPacks() tasks->deleteLater(); }); - for (auto& task : mdownload.getTasks()) { - tasks->addTask(task); + if (m_downloadDialog) { + for (auto& task : m_downloadDialog->getTasks()) { + tasks->addTask(task); + } + } else { + qWarning() << "ResourceDownloadDialog vanished before we could collect tasks!"; } ProgressDialog loadDialog(this); @@ -94,6 +114,8 @@ void DataPackPage::downloadDataPacks() m_model->update(); } + if (m_downloadDialog) + m_downloadDialog->deleteLater(); } void DataPackPage::updateDataPacks() diff --git a/launcher/ui/pages/instance/DataPackPage.h b/launcher/ui/pages/instance/DataPackPage.h index 80eda1602..6676c165a 100644 --- a/launcher/ui/pages/instance/DataPackPage.h +++ b/launcher/ui/pages/instance/DataPackPage.h @@ -18,9 +18,10 @@ #pragma once +#include #include "ExternalResourcesPage.h" #include "minecraft/mod/DataPackFolderModel.h" -#include "ui_ExternalResourcesPage.h" +#include "ui/dialogs/ResourceDownloadDialog.h" class DataPackPage : public ExternalResourcesPage { Q_OBJECT @@ -36,13 +37,14 @@ class DataPackPage : public ExternalResourcesPage { public slots: void updateFrame(const QModelIndex& current, const QModelIndex& previous) override; void downloadDataPacks(); + void downloadDialogFinished(int result); void updateDataPacks(); void deleteDataPackMetadata(); void changeDataPackVersion(); private: std::shared_ptr m_model; - + QPointer m_downloadDialog; }; /** @@ -63,7 +65,7 @@ class GlobalDataPackPage : public QWidget, public BasePage { void openedImpl() override; void closedImpl() override; - void setParentContainer(BasePageContainer *container) override; + void setParentContainer(BasePageContainer* container) override; private: void updateContent(); diff --git a/launcher/ui/pages/modplatform/DataPackModel.cpp b/launcher/ui/pages/modplatform/DataPackModel.cpp index 4b537cda9..085bd2d53 100644 --- a/launcher/ui/pages/modplatform/DataPackModel.cpp +++ b/launcher/ui/pages/modplatform/DataPackModel.cpp @@ -21,13 +21,13 @@ ResourceAPI::SearchArgs DataPackResourceModel::createSearchArguments() return { ModPlatform::ResourceType::DATA_PACK, m_next_search_offset, m_search_term, sort, ModPlatform::ModLoaderType::DataPack }; } -ResourceAPI::VersionSearchArgs DataPackResourceModel::createVersionsArguments(QModelIndex& entry) +ResourceAPI::VersionSearchArgs DataPackResourceModel::createVersionsArguments(const QModelIndex& entry) { auto& pack = m_packs[entry.row()]; return { *pack, {}, ModPlatform::ModLoaderType::DataPack }; } -ResourceAPI::ProjectInfoArgs DataPackResourceModel::createInfoArguments(QModelIndex& entry) +ResourceAPI::ProjectInfoArgs DataPackResourceModel::createInfoArguments(const QModelIndex& entry) { auto& pack = m_packs[entry.row()]; return { *pack }; diff --git a/launcher/ui/pages/modplatform/DataPackModel.h b/launcher/ui/pages/modplatform/DataPackModel.h index 54da2404c..89e83969c 100644 --- a/launcher/ui/pages/modplatform/DataPackModel.h +++ b/launcher/ui/pages/modplatform/DataPackModel.h @@ -32,8 +32,8 @@ class DataPackResourceModel : public ResourceModel { public slots: ResourceAPI::SearchArgs createSearchArguments() override; - ResourceAPI::VersionSearchArgs createVersionsArguments(QModelIndex&) override; - ResourceAPI::ProjectInfoArgs createInfoArguments(QModelIndex&) override; + ResourceAPI::VersionSearchArgs createVersionsArguments(const QModelIndex&) override; + ResourceAPI::ProjectInfoArgs createInfoArguments(const QModelIndex&) override; protected: const BaseInstance& m_base_instance; diff --git a/launcher/ui/pages/modplatform/DataPackPage.cpp b/launcher/ui/pages/modplatform/DataPackPage.cpp index 2b506ca67..82892b318 100644 --- a/launcher/ui/pages/modplatform/DataPackPage.cpp +++ b/launcher/ui/pages/modplatform/DataPackPage.cpp @@ -4,7 +4,6 @@ // SPDX-License-Identifier: GPL-3.0-only #include "DataPackPage.h" -#include "modplatform/ModIndex.h" #include "ui_ResourcePage.h" #include "DataPackModel.h" @@ -15,10 +14,7 @@ namespace ResourceDownload { -DataPackResourcePage::DataPackResourcePage(DataPackDownloadDialog* dialog, BaseInstance& instance) : ResourcePage(dialog, instance) -{ - connect(m_ui->packView, &QListView::doubleClicked, this, &DataPackResourcePage::onResourceSelected); -} +DataPackResourcePage::DataPackResourcePage(DataPackDownloadDialog* dialog, BaseInstance& instance) : ResourcePage(dialog, instance) {} /******** Callbacks to events in the UI (set up in the derived classes) ********/ diff --git a/launcher/ui/pages/modplatform/DataPackPage.h b/launcher/ui/pages/modplatform/DataPackPage.h index 2e622ebd4..cf78df96c 100644 --- a/launcher/ui/pages/modplatform/DataPackPage.h +++ b/launcher/ui/pages/modplatform/DataPackPage.h @@ -5,8 +5,8 @@ #pragma once -#include "ui/pages/modplatform/ResourcePage.h" #include "ui/pages/modplatform/DataPackModel.h" +#include "ui/pages/modplatform/ResourcePage.h" namespace Ui { class ResourcePage; @@ -26,8 +26,9 @@ class DataPackResourcePage : public ResourcePage { auto page = new T(dialog, instance); auto model = static_cast(page->getModel()); - connect(model, &ResourceModel::versionListUpdated, page, &ResourcePage::updateVersionList); + connect(model, &ResourceModel::versionListUpdated, page, &ResourcePage::versionListUpdated); connect(model, &ResourceModel::projectInfoUpdated, page, &ResourcePage::updateUi); + connect(model, &QAbstractListModel::modelReset, page, &ResourcePage::modelReset); return page; } From 497e0cbfdc68a8ffd386e0fd5ab9c11eb8eb2903 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Tue, 29 Apr 2025 14:42:31 +0100 Subject: [PATCH 184/695] =?UTF-8?q?Tweak=20pack=20export=20UI=20=E2=80=93?= =?UTF-8?q?=20add=20recommended=20RAM?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: TheKodeToad --- launcher/ui/dialogs/ExportPackDialog.ui | 176 +++++++++++++++++------- 1 file changed, 130 insertions(+), 46 deletions(-) diff --git a/launcher/ui/dialogs/ExportPackDialog.ui b/launcher/ui/dialogs/ExportPackDialog.ui index a4a174212..bda8b8dd0 100644 --- a/launcher/ui/dialogs/ExportPackDialog.ui +++ b/launcher/ui/dialogs/ExportPackDialog.ui @@ -19,36 +19,56 @@ &Description - - - - - &Name - - - name - - - - - - + - - - &Version + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - version - - - - - - - 1.0.0 - - + + + + &Name: + + + name + + + + + + + + + + &Version: + + + version + + + + + + + 1.0.0 + + + + + + + &Author: + + + author + + + + + + +
@@ -62,24 +82,29 @@ - - true + + + 0 + 0 + - - - - - - &Author + + + 0 + 100 + - - author + + + 16777215 + 100 + + + + true - - -
@@ -88,7 +113,70 @@ &Options - + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + &Recommended Memory: + + + + + + + false + + + + 0 + 0 + + + + MiB + + + 8 + + + 32768 + + + 128 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + @@ -138,10 +226,6 @@
- name - version - summary - author files optionalFiles From 75d4ef18288700992eb3c08b3e2120bea26427b0 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Tue, 29 Apr 2025 14:42:40 +0100 Subject: [PATCH 185/695] Implement UI for setting recommended RAM Signed-off-by: TheKodeToad --- launcher/minecraft/MinecraftInstance.cpp | 1 + launcher/ui/dialogs/ExportPackDialog.cpp | 25 +++++++++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index 991afce89..635cecfac 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -248,6 +248,7 @@ void MinecraftInstance::loadSpecificSettings() m_settings->registerSetting("ExportSummary", ""); m_settings->registerSetting("ExportAuthor", ""); m_settings->registerSetting("ExportOptionalFiles", true); + m_settings->registerSetting("ExportRecommendedRAM"); qDebug() << "Instance-type specific settings were loaded!"; diff --git a/launcher/ui/dialogs/ExportPackDialog.cpp b/launcher/ui/dialogs/ExportPackDialog.cpp index 303df94a1..e5edaf8a7 100644 --- a/launcher/ui/dialogs/ExportPackDialog.cpp +++ b/launcher/ui/dialogs/ExportPackDialog.cpp @@ -44,12 +44,16 @@ ExportPackDialog::ExportPackDialog(InstancePtr instance, QWidget* parent, ModPla m_ui->version->setText(instance->settings()->get("ExportVersion").toString()); m_ui->optionalFiles->setChecked(instance->settings()->get("ExportOptionalFiles").toBool()); + connect(m_ui->recommendedMemoryCheckBox, &QCheckBox::toggled, m_ui->recommendedMemory, &QWidget::setEnabled); + if (m_provider == ModPlatform::ResourceProvider::MODRINTH) { setWindowTitle(tr("Export Modrinth Pack")); m_ui->authorLabel->hide(); m_ui->author->hide(); + m_ui->recommendedMemoryWidget->hide(); + m_ui->summary->setPlainText(instance->settings()->get("ExportSummary").toString()); } else { setWindowTitle(tr("Export CurseForge Pack")); @@ -57,6 +61,19 @@ ExportPackDialog::ExportPackDialog(InstancePtr instance, QWidget* parent, ModPla m_ui->summaryLabel->hide(); m_ui->summary->hide(); + const int recommendedRAM = instance->settings()->get("ExportRecommendedRAM").toInt(); + + if (recommendedRAM > 0) { + m_ui->recommendedMemoryCheckBox->setChecked(true); + m_ui->recommendedMemory->setValue(recommendedRAM); + } else { + m_ui->recommendedMemoryCheckBox->setChecked(false); + + // recommend based on setting - limited to 12 GiB (CurseForge warns above this amount) + const int defaultRecommendation = qMin(m_instance->settings()->get("MaxMemAlloc").toInt(), 1024 * 12); + m_ui->recommendedMemory->setValue(defaultRecommendation); + } + m_ui->author->setText(instance->settings()->get("ExportAuthor").toString()); } @@ -120,9 +137,15 @@ void ExportPackDialog::done(int result) if (m_provider == ModPlatform::ResourceProvider::MODRINTH) settings->set("ExportSummary", m_ui->summary->toPlainText()); - else + else { settings->set("ExportAuthor", m_ui->author->text()); + if (m_ui->recommendedMemoryCheckBox->isChecked()) + settings->set("ExportRecommendedRAM", m_ui->recommendedMemory->value()); + else + settings->reset("ExportRecommendedRAM"); + } + if (result == Accepted) { const QString name = m_ui->name->text().isEmpty() ? m_instance->name() : m_ui->name->text(); const QString filename = FS::RemoveInvalidFilenameChars(name); From ee52127044a170437c8d6161b26fcf8f8a4c82e5 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Tue, 29 Apr 2025 14:40:20 +0100 Subject: [PATCH 186/695] Implement recommendedRam in CurseForge export Signed-off-by: TheKodeToad --- launcher/modplatform/flame/FlamePackExportTask.cpp | 8 +++++++- launcher/modplatform/flame/FlamePackExportTask.h | 4 +++- launcher/ui/dialogs/ExportPackDialog.cpp | 4 +++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/launcher/modplatform/flame/FlamePackExportTask.cpp b/launcher/modplatform/flame/FlamePackExportTask.cpp index 3405b702f..952f30c11 100644 --- a/launcher/modplatform/flame/FlamePackExportTask.cpp +++ b/launcher/modplatform/flame/FlamePackExportTask.cpp @@ -47,7 +47,8 @@ FlamePackExportTask::FlamePackExportTask(const QString& name, bool optionalFiles, InstancePtr instance, const QString& output, - MMCZip::FilterFunction filter) + MMCZip::FilterFunction filter, + int recommendedRAM) : name(name) , version(version) , author(author) @@ -57,6 +58,7 @@ FlamePackExportTask::FlamePackExportTask(const QString& name, , gameRoot(instance->gameRoot()) , output(output) , filter(filter) + , m_recommendedRAM(recommendedRAM) {} void FlamePackExportTask::executeTask() @@ -411,6 +413,10 @@ QByteArray FlamePackExportTask::generateIndex() loader["primary"] = true; version["modLoaders"] = QJsonArray({ loader }); } + + if (m_recommendedRAM > 0) + version["recommendedRam"] = m_recommendedRAM; + obj["minecraft"] = version; } diff --git a/launcher/modplatform/flame/FlamePackExportTask.h b/launcher/modplatform/flame/FlamePackExportTask.h index b11eb17fa..dfd2ea941 100644 --- a/launcher/modplatform/flame/FlamePackExportTask.h +++ b/launcher/modplatform/flame/FlamePackExportTask.h @@ -34,7 +34,8 @@ class FlamePackExportTask : public Task { bool optionalFiles, InstancePtr instance, const QString& output, - MMCZip::FilterFunction filter); + MMCZip::FilterFunction filter, + int recommendedRAM); protected: void executeTask() override; @@ -52,6 +53,7 @@ class FlamePackExportTask : public Task { const QDir gameRoot; const QString output; const MMCZip::FilterFunction filter; + const int m_recommendedRAM; struct ResolvedFile { int addonId; diff --git a/launcher/ui/dialogs/ExportPackDialog.cpp b/launcher/ui/dialogs/ExportPackDialog.cpp index e5edaf8a7..675f0d158 100644 --- a/launcher/ui/dialogs/ExportPackDialog.cpp +++ b/launcher/ui/dialogs/ExportPackDialog.cpp @@ -172,8 +172,10 @@ void ExportPackDialog::done(int result) task = new ModrinthPackExportTask(name, m_ui->version->text(), m_ui->summary->toPlainText(), m_ui->optionalFiles->isChecked(), m_instance, output, std::bind(&FileIgnoreProxy::filterFile, m_proxy, std::placeholders::_1)); } else { + int recommendedRAM = m_ui->recommendedMemoryCheckBox->isChecked() ? m_ui->recommendedMemory->value() : 0; + task = new FlamePackExportTask(name, m_ui->version->text(), m_ui->author->text(), m_ui->optionalFiles->isChecked(), m_instance, - output, std::bind(&FileIgnoreProxy::filterFile, m_proxy, std::placeholders::_1)); + output, std::bind(&FileIgnoreProxy::filterFile, m_proxy, std::placeholders::_1), recommendedRAM); } connect(task, &Task::failed, From 147159be2c98a334a88a8f3fcda42de1bdda7e7c Mon Sep 17 00:00:00 2001 From: Trial97 Date: Tue, 29 Apr 2025 19:02:35 +0300 Subject: [PATCH 187/695] fix: file filtering on modpack export Signed-off-by: Trial97 --- launcher/FileIgnoreProxy.cpp | 4 ++-- launcher/FileIgnoreProxy.h | 2 +- launcher/MMCZip.cpp | 6 +++--- launcher/MMCZip.h | 3 ++- launcher/modplatform/flame/FlamePackExportTask.cpp | 2 +- launcher/modplatform/flame/FlamePackExportTask.h | 4 ++-- launcher/modplatform/modrinth/ModrinthPackExportTask.cpp | 2 +- launcher/modplatform/modrinth/ModrinthPackExportTask.h | 4 ++-- 8 files changed, 14 insertions(+), 13 deletions(-) diff --git a/launcher/FileIgnoreProxy.cpp b/launcher/FileIgnoreProxy.cpp index 0314057d1..cebe82eda 100644 --- a/launcher/FileIgnoreProxy.cpp +++ b/launcher/FileIgnoreProxy.cpp @@ -269,9 +269,9 @@ bool FileIgnoreProxy::ignoreFile(QFileInfo fileInfo) const return m_ignoreFiles.contains(fileInfo.fileName()) || m_ignoreFilePaths.covers(relPath(fileInfo.absoluteFilePath())); } -bool FileIgnoreProxy::filterFile(const QString& fileName) const +bool FileIgnoreProxy::filterFile(const QFileInfo& file) const { - return m_blocked.covers(fileName) || ignoreFile(QFileInfo(QDir(m_root), fileName)); + return m_blocked.covers(relPath(file.absoluteFilePath())) || ignoreFile(file); } void FileIgnoreProxy::loadBlockedPathsFromFile(const QString& fileName) diff --git a/launcher/FileIgnoreProxy.h b/launcher/FileIgnoreProxy.h index 25d85ab60..5184fc354 100644 --- a/launcher/FileIgnoreProxy.h +++ b/launcher/FileIgnoreProxy.h @@ -69,7 +69,7 @@ class FileIgnoreProxy : public QSortFilterProxyModel { // list of relative paths that need to be removed completely from model inline SeparatorPrefixTree<'/'>& ignoreFilesWithPath() { return m_ignoreFilePaths; } - bool filterFile(const QString& fileName) const; + bool filterFile(const QFileInfo& fileName) const; void loadBlockedPathsFromFile(const QString& fileName); diff --git a/launcher/MMCZip.cpp b/launcher/MMCZip.cpp index b38aca17a..0b1a2b39e 100644 --- a/launcher/MMCZip.cpp +++ b/launcher/MMCZip.cpp @@ -418,7 +418,7 @@ bool extractFile(QString fileCompressed, QString file, QString target) return extractRelFile(&zip, file, target); } -bool collectFileListRecursively(const QString& rootDir, const QString& subDir, QFileInfoList* files, FilterFunction excludeFilter) +bool collectFileListRecursively(const QString& rootDir, const QString& subDir, QFileInfoList* files, FilterFileFunction excludeFilter) { QDir rootDirectory(rootDir); if (!rootDirectory.exists()) @@ -443,8 +443,8 @@ bool collectFileListRecursively(const QString& rootDir, const QString& subDir, Q // collect files entries = directory.entryInfoList(QDir::Files); for (const auto& e : entries) { - QString relativeFilePath = rootDirectory.relativeFilePath(e.absoluteFilePath()); - if (excludeFilter && excludeFilter(relativeFilePath)) { + if (excludeFilter && excludeFilter(e)) { + QString relativeFilePath = rootDirectory.relativeFilePath(e.absoluteFilePath()); qDebug() << "Skipping file " << relativeFilePath; continue; } diff --git a/launcher/MMCZip.h b/launcher/MMCZip.h index d81df9d81..fe0c79de2 100644 --- a/launcher/MMCZip.h +++ b/launcher/MMCZip.h @@ -56,6 +56,7 @@ namespace MMCZip { using FilterFunction = std::function; +using FilterFileFunction = std::function; /** * Merge two zip files, using a filter function @@ -149,7 +150,7 @@ bool extractFile(QString fileCompressed, QString file, QString dir); * \param excludeFilter function to excludeFilter which files shouldn't be included (returning true means to excude) * \return true for success or false for failure */ -bool collectFileListRecursively(const QString& rootDir, const QString& subDir, QFileInfoList* files, FilterFunction excludeFilter); +bool collectFileListRecursively(const QString& rootDir, const QString& subDir, QFileInfoList* files, FilterFileFunction excludeFilter); #if defined(LAUNCHER_APPLICATION) class ExportToZipTask : public Task { diff --git a/launcher/modplatform/flame/FlamePackExportTask.cpp b/launcher/modplatform/flame/FlamePackExportTask.cpp index 3405b702f..4e1a3722d 100644 --- a/launcher/modplatform/flame/FlamePackExportTask.cpp +++ b/launcher/modplatform/flame/FlamePackExportTask.cpp @@ -47,7 +47,7 @@ FlamePackExportTask::FlamePackExportTask(const QString& name, bool optionalFiles, InstancePtr instance, const QString& output, - MMCZip::FilterFunction filter) + MMCZip::FilterFileFunction filter) : name(name) , version(version) , author(author) diff --git a/launcher/modplatform/flame/FlamePackExportTask.h b/launcher/modplatform/flame/FlamePackExportTask.h index b11eb17fa..38acdf518 100644 --- a/launcher/modplatform/flame/FlamePackExportTask.h +++ b/launcher/modplatform/flame/FlamePackExportTask.h @@ -34,7 +34,7 @@ class FlamePackExportTask : public Task { bool optionalFiles, InstancePtr instance, const QString& output, - MMCZip::FilterFunction filter); + MMCZip::FilterFileFunction filter); protected: void executeTask() override; @@ -51,7 +51,7 @@ class FlamePackExportTask : public Task { MinecraftInstance* mcInstance; const QDir gameRoot; const QString output; - const MMCZip::FilterFunction filter; + const MMCZip::FilterFileFunction filter; struct ResolvedFile { int addonId; diff --git a/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp b/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp index d103170af..8e582e456 100644 --- a/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp @@ -40,7 +40,7 @@ ModrinthPackExportTask::ModrinthPackExportTask(const QString& name, bool optionalFiles, InstancePtr instance, const QString& output, - MMCZip::FilterFunction filter) + MMCZip::FilterFileFunction filter) : name(name) , version(version) , summary(summary) diff --git a/launcher/modplatform/modrinth/ModrinthPackExportTask.h b/launcher/modplatform/modrinth/ModrinthPackExportTask.h index ee740a456..ec4730de5 100644 --- a/launcher/modplatform/modrinth/ModrinthPackExportTask.h +++ b/launcher/modplatform/modrinth/ModrinthPackExportTask.h @@ -35,7 +35,7 @@ class ModrinthPackExportTask : public Task { bool optionalFiles, InstancePtr instance, const QString& output, - MMCZip::FilterFunction filter); + MMCZip::FilterFileFunction filter); protected: void executeTask() override; @@ -58,7 +58,7 @@ class ModrinthPackExportTask : public Task { MinecraftInstance* mcInstance; const QDir gameRoot; const QString output; - const MMCZip::FilterFunction filter; + const MMCZip::FilterFileFunction filter; ModrinthAPI api; QFileInfoList files; From acdb8c5578374652ce8a505051e1c2e6fbe06673 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Tue, 29 Apr 2025 16:25:19 +0100 Subject: [PATCH 188/695] Implement recommendedRam in CurseForge import Signed-off-by: TheKodeToad --- .../flame/FlameInstanceCreationTask.cpp | 19 +++++++++++++++++++ launcher/modplatform/flame/PackManifest.cpp | 1 + launcher/modplatform/flame/PackManifest.h | 1 + 3 files changed, 21 insertions(+) diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp index c30ba5249..5d9c74ccf 100644 --- a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp +++ b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp @@ -54,6 +54,7 @@ #include "settings/INISettingsObject.h" +#include "sys.h" #include "tasks/ConcurrentTask.h" #include "ui/dialogs/BlockedModsDialog.h" #include "ui/dialogs/CustomMessageBox.h" @@ -418,6 +419,24 @@ bool FlameCreationTask::createInstance() } } + int recommendedRAM = m_pack.minecraft.recommendedRAM; + + // only set memory if this is a fresh instance + if (m_instance == nullptr && recommendedRAM > 0) { + const uint64_t sysMiB = Sys::getSystemRam() / Sys::mebibyte; + const uint64_t max = sysMiB * 0.9; + + if (recommendedRAM > max) { + logWarning(tr("The recommended memory of the modpack exceeds 90% of your system RAM—reducing it from %1 MiB to %2 MiB!") + .arg(recommendedRAM) + .arg(max)); + recommendedRAM = max; + } + + instance.settings()->set("OverrideMemory", true); + instance.settings()->set("MaxMemAlloc", recommendedRAM); + } + QString jarmodsPath = FS::PathCombine(m_stagingPath, "minecraft", "jarmods"); QFileInfo jarmodsInfo(jarmodsPath); if (jarmodsInfo.isDir()) { diff --git a/launcher/modplatform/flame/PackManifest.cpp b/launcher/modplatform/flame/PackManifest.cpp index 278105f4a..641fb5d9a 100644 --- a/launcher/modplatform/flame/PackManifest.cpp +++ b/launcher/modplatform/flame/PackManifest.cpp @@ -27,6 +27,7 @@ static void loadMinecraftV1(Flame::Minecraft& m, QJsonObject& minecraft) loadModloaderV1(loader, obj); m.modLoaders.append(loader); } + m.recommendedRAM = Json::ensureInteger(minecraft, "recommendedRam", 0); } static void loadManifestV1(Flame::Manifest& pack, QJsonObject& manifest) diff --git a/launcher/modplatform/flame/PackManifest.h b/launcher/modplatform/flame/PackManifest.h index ebb3ed5cc..6b911ffb4 100644 --- a/launcher/modplatform/flame/PackManifest.h +++ b/launcher/modplatform/flame/PackManifest.h @@ -67,6 +67,7 @@ struct Minecraft { QString version; QString libraries; QList modLoaders; + int recommendedRAM; }; struct Manifest { From 24036021bbd3833b8dfc66a8a3162a087ea16b37 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Tue, 29 Apr 2025 16:46:11 +0100 Subject: [PATCH 189/695] Propagate task warnings Signed-off-by: TheKodeToad --- launcher/InstanceImportTask.cpp | 5 +++++ launcher/tasks/Task.cpp | 2 ++ launcher/tasks/Task.h | 1 + 3 files changed, 8 insertions(+) diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index 633382404..b5a41b590 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -212,6 +212,7 @@ void InstanceImportTask::processZipPack() progressStep->status = status; stepProgress(*progressStep); }); + connect(zipTask.get(), &Task::warningLogged, this, [this](const QString& line) { m_Warnings.append(line); }); m_task.reset(zipTask); zipTask->start(); } @@ -308,6 +309,8 @@ void InstanceImportTask::processFlame() connect(inst_creation_task.get(), &Task::aborted, this, &Task::abort); connect(inst_creation_task.get(), &Task::abortStatusChanged, this, &Task::setAbortable); + connect(inst_creation_task.get(), &Task::warningLogged, this, [this](const QString& line) { m_Warnings.append(line); }); + m_task.reset(inst_creation_task); setAbortable(true); m_task->start(); @@ -407,6 +410,8 @@ void InstanceImportTask::processModrinth() connect(inst_creation_task.get(), &Task::aborted, this, &Task::abort); connect(inst_creation_task.get(), &Task::abortStatusChanged, this, &Task::setAbortable); + connect(inst_creation_task.get(), &Task::warningLogged, this, [this](const QString& line) { m_Warnings.append(line); }); + m_task.reset(inst_creation_task); setAbortable(true); m_task->start(); diff --git a/launcher/tasks/Task.cpp b/launcher/tasks/Task.cpp index 1871c5df8..92b345c8d 100644 --- a/launcher/tasks/Task.cpp +++ b/launcher/tasks/Task.cpp @@ -196,6 +196,8 @@ void Task::logWarning(const QString& line) { qWarning() << line; m_Warnings.append(line); + + emit warningLogged(line); } QStringList Task::warnings() const diff --git a/launcher/tasks/Task.h b/launcher/tasks/Task.h index fcd075150..43e71c8ab 100644 --- a/launcher/tasks/Task.h +++ b/launcher/tasks/Task.h @@ -145,6 +145,7 @@ class Task : public QObject, public QRunnable { void failed(QString reason); void status(QString status); void details(QString details); + void warningLogged(const QString& warning); void stepProgress(TaskStepProgress const& task_progress); //! Emitted when the canAbort() status has changed. */ From 053b57c21ff813e70915668ed29d80c16f0a43c2 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Tue, 29 Apr 2025 19:24:04 +0300 Subject: [PATCH 190/695] fix: crash when task was canceled and abort signal was fired early Signed-off-by: Trial97 --- launcher/InstanceImportTask.cpp | 5 ++--- launcher/java/download/ArchiveDownloadTask.cpp | 2 +- launcher/modplatform/flame/FlamePackExportTask.cpp | 4 +++- launcher/modplatform/modrinth/ModrinthPackExportTask.cpp | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index 633382404..e2735385b 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -72,7 +72,6 @@ bool InstanceImportTask::abort() bool wasAborted = false; if (m_task) wasAborted = m_task->abort(); - Task::abort(); return wasAborted; } @@ -305,7 +304,7 @@ void InstanceImportTask::processFlame() connect(inst_creation_task.get(), &Task::status, this, &InstanceImportTask::setStatus); connect(inst_creation_task.get(), &Task::details, this, &InstanceImportTask::setDetails); - connect(inst_creation_task.get(), &Task::aborted, this, &Task::abort); + connect(inst_creation_task.get(), &Task::aborted, this, &InstanceImportTask::emitAborted); connect(inst_creation_task.get(), &Task::abortStatusChanged, this, &Task::setAbortable); m_task.reset(inst_creation_task); @@ -404,7 +403,7 @@ void InstanceImportTask::processModrinth() connect(inst_creation_task.get(), &Task::status, this, &InstanceImportTask::setStatus); connect(inst_creation_task.get(), &Task::details, this, &InstanceImportTask::setDetails); - connect(inst_creation_task.get(), &Task::aborted, this, &Task::abort); + connect(inst_creation_task.get(), &Task::aborted, this, &InstanceImportTask::emitAborted); connect(inst_creation_task.get(), &Task::abortStatusChanged, this, &Task::setAbortable); m_task.reset(inst_creation_task); diff --git a/launcher/java/download/ArchiveDownloadTask.cpp b/launcher/java/download/ArchiveDownloadTask.cpp index bb7cc568d..bb31ca1e2 100644 --- a/launcher/java/download/ArchiveDownloadTask.cpp +++ b/launcher/java/download/ArchiveDownloadTask.cpp @@ -55,6 +55,7 @@ void ArchiveDownloadTask::executeTask() connect(download.get(), &Task::stepProgress, this, &ArchiveDownloadTask::propagateStepProgress); connect(download.get(), &Task::status, this, &ArchiveDownloadTask::setStatus); connect(download.get(), &Task::details, this, &ArchiveDownloadTask::setDetails); + connect(download.get(), &Task::aborted, this, &ArchiveDownloadTask::emitAborted); connect(download.get(), &Task::succeeded, [this, fullPath] { // This should do all of the extracting and creating folders extractJava(fullPath); @@ -135,7 +136,6 @@ bool ArchiveDownloadTask::abort() auto aborted = canAbort(); if (m_task) aborted = m_task->abort(); - emitAborted(); return aborted; }; } // namespace Java \ No newline at end of file diff --git a/launcher/modplatform/flame/FlamePackExportTask.cpp b/launcher/modplatform/flame/FlamePackExportTask.cpp index 3405b702f..08b01e1e9 100644 --- a/launcher/modplatform/flame/FlamePackExportTask.cpp +++ b/launcher/modplatform/flame/FlamePackExportTask.cpp @@ -70,7 +70,6 @@ bool FlamePackExportTask::abort() { if (task) { task->abort(); - emitAborted(); return true; } return false; @@ -171,6 +170,7 @@ void FlamePackExportTask::collectHashes() progressStep->status = status; stepProgress(*progressStep); }); + connect(hashingTask.get(), &Task::aborted, this, &FlamePackExportTask::emitAborted); hashingTask->start(); } @@ -246,6 +246,7 @@ void FlamePackExportTask::makeApiRequest() getProjectsInfo(); }); connect(task.get(), &Task::failed, this, &FlamePackExportTask::getProjectsInfo); + connect(task.get(), &Task::aborted, this, &FlamePackExportTask::emitAborted); task->start(); } @@ -324,6 +325,7 @@ void FlamePackExportTask::getProjectsInfo() buildZip(); }); connect(projTask.get(), &Task::failed, this, &FlamePackExportTask::emitFailed); + connect(task.get(), &Task::aborted, this, &FlamePackExportTask::emitAborted); task.reset(projTask); task->start(); } diff --git a/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp b/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp index d103170af..a03ae2122 100644 --- a/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp @@ -63,7 +63,6 @@ bool ModrinthPackExportTask::abort() { if (task) { task->abort(); - emitAborted(); return true; } return false; @@ -158,6 +157,7 @@ void ModrinthPackExportTask::makeApiRequest() task = api.currentVersions(pendingHashes.values(), "sha512", response); connect(task.get(), &Task::succeeded, [this, response]() { parseApiResponse(response); }); connect(task.get(), &Task::failed, this, &ModrinthPackExportTask::emitFailed); + connect(task.get(), &Task::aborted, this, &ModrinthPackExportTask::emitAborted); task->start(); } } From 9131f79cc0bb967ddca85564b74cabc6e074124b Mon Sep 17 00:00:00 2001 From: Seth Flynn Date: Tue, 8 Apr 2025 14:59:33 -0400 Subject: [PATCH 191/695] feat: add CMakePresets.json Signed-off-by: Seth Flynn --- CMakePresets.json | 14 +++ cmake/commonPresets.json | 70 +++++++++++++++ cmake/linuxPreset.json | 95 +++++++++++++++++++++ cmake/macosPreset.json | 152 +++++++++++++++++++++++++++++++++ cmake/windowsMSVCPreset.json | 155 ++++++++++++++++++++++++++++++++++ cmake/windowsMinGWPreset.json | 99 ++++++++++++++++++++++ 6 files changed, 585 insertions(+) create mode 100644 CMakePresets.json create mode 100644 cmake/commonPresets.json create mode 100644 cmake/linuxPreset.json create mode 100644 cmake/macosPreset.json create mode 100644 cmake/windowsMSVCPreset.json create mode 100644 cmake/windowsMinGWPreset.json diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 000000000..f8e688b89 --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://cmake.org/cmake/help/latest/_downloads/3e2d73bff478d88a7de0de736ba5e361/schema.json", + "version": 8, + "cmakeMinimumRequired": { + "major": 3, + "minor": 28 + }, + "include": [ + "cmake/linuxPreset.json", + "cmake/macosPreset.json", + "cmake/windowsMinGWPreset.json", + "cmake/windowsMSVCPreset.json" + ] +} diff --git a/cmake/commonPresets.json b/cmake/commonPresets.json new file mode 100644 index 000000000..7acbf8a31 --- /dev/null +++ b/cmake/commonPresets.json @@ -0,0 +1,70 @@ +{ + "$schema": "https://cmake.org/cmake/help/latest/_downloads/3e2d73bff478d88a7de0de736ba5e361/schema.json", + "version": 8, + "configurePresets": [ + { + "name": "base", + "hidden": true, + "binaryDir": "build", + "installDir": "install", + "cacheVariables": { + "Launcher_BUILD_PLATFORM": "custom" + } + }, + { + "name": "base_debug", + "hidden": true, + "inherits": [ + "base" + ], + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } + }, + { + "name": "base_release", + "hidden": true, + "inherits": [ + "base" + ], + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "ENABLE_LTO": "ON" + } + } + ], + "testPresets": [ + { + "name": "base", + "hidden": true, + "output": { + "outputOnFailure": true + }, + "execution": { + "noTestsAction": "error" + }, + "filter": { + "exclude": { + "name": "^example64|example$" + } + } + }, + { + "name": "base_debug", + "hidden": true, + "inherits": [ + "base" + ], + "output": { + "debug": true + } + }, + { + "name": "base_release", + "hidden": true, + "inherits": [ + "base" + ] + } + ] +} diff --git a/cmake/linuxPreset.json b/cmake/linuxPreset.json new file mode 100644 index 000000000..81ae95c2c --- /dev/null +++ b/cmake/linuxPreset.json @@ -0,0 +1,95 @@ +{ + "$schema": "https://cmake.org/cmake/help/latest/_downloads/3e2d73bff478d88a7de0de736ba5e361/schema.json", + "version": 8, + "include": [ + "commonPresets.json" + ], + "configurePresets": [ + { + "name": "linux_base", + "hidden": true, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Linux" + }, + "generator": "Ninja", + "cacheVariables": { + "Launcher_BUILD_ARTIFACT": "Linux-Qt6", + "Launcher_ENABLE_JAVA_DOWNLOADER": "ON" + } + }, + { + "name": "linux_debug", + "inherits": [ + "base_debug", + "linux_base" + ], + "displayName": "Linux (Debug)" + }, + { + "name": "linux_release", + "inherits": [ + "base_release", + "linux_base" + ], + "displayName": "Linux (Release)" + } + ], + "buildPresets": [ + { + "name": "linux_base", + "hidden": true, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Linux" + } + }, + { + "name": "linux_debug", + "inherits": [ + "linux_base" + ], + "displayName": "Linux (Debug)", + "configurePreset": "linux_debug" + }, + { + "name": "linux_release", + "inherits": [ + "linux_base" + ], + "displayName": "Linux (Release)", + "configurePreset": "linux_release" + } + ], + "testPresets": [ + { + "name": "linux_base", + "hidden": true, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Linux" + } + }, + { + "name": "linux_debug", + "inherits": [ + "base_debug", + "linux_base" + ], + "displayName": "Linux (Debug)", + "configurePreset": "linux_debug" + }, + { + "name": "linux_release", + "inherits": [ + "base_release", + "linux_base" + ], + "displayName": "Linux (Release)", + "configurePreset": "linux_release" + } + ] +} diff --git a/cmake/macosPreset.json b/cmake/macosPreset.json new file mode 100644 index 000000000..8cecfbe71 --- /dev/null +++ b/cmake/macosPreset.json @@ -0,0 +1,152 @@ +{ + "$schema": "https://cmake.org/cmake/help/latest/_downloads/3e2d73bff478d88a7de0de736ba5e361/schema.json", + "version": 8, + "include": [ + "commonPresets.json" + ], + "configurePresets": [ + { + "name": "macos_base", + "hidden": true, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Darwin" + }, + "generator": "Ninja" + }, + { + "name": "macos_universal_base", + "hidden": true, + "inherits": [ + "macos_base" + ], + "cacheVariables": { + "CMAKE_OSX_ARCHITECTURES": "x86_64;arm64", + "Launcher_BUILD_ARTIFACT": "macOS-Qt6" + } + }, + { + "name": "macos_debug", + "inherits": [ + "base_debug", + "macos_base" + ], + "displayName": "macOS (Debug)" + }, + { + "name": "macos_release", + "inherits": [ + "base_release", + "macos_base" + ], + "displayName": "macOS (Release)" + }, + { + "name": "macos_universal_debug", + "inherits": [ + "base_debug", + "macos_universal_base" + ], + "displayName": "macOS (Universal Binary, Debug)" + }, + { + "name": "macos_universal_release", + "inherits": [ + "base_release", + "macos_universal_base" + ], + "displayName": "macOS (Universal Binary, Release)" + } + ], + "buildPresets": [ + { + "name": "macos_base", + "hidden": true, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Darwin" + } + }, + { + "name": "macos_debug", + "inherits": [ + "macos_base" + ], + "displayName": "macOS (Debug)", + "configurePreset": "macos_debug" + }, + { + "name": "macos_release", + "inherits": [ + "macos_base" + ], + "displayName": "macOS (Release)", + "configurePreset": "macos_release" + }, + { + "name": "macos_universal_debug", + "inherits": [ + "macos_base" + ], + "displayName": "macOS (Universal Binary, Debug)", + "configurePreset": "macos_universal_debug" + }, + { + "name": "macos_universal_release", + "inherits": [ + "macos_base" + ], + "displayName": "macOS (Universal Binary, Release)", + "configurePreset": "macos_universal_release" + } + ], + "testPresets": [ + { + "name": "macos_base", + "hidden": true, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Darwin" + } + }, + { + "name": "macos_debug", + "inherits": [ + "base_debug", + "macos_base" + ], + "displayName": "MacOS (Debug)", + "configurePreset": "macos_debug" + }, + { + "name": "macos_release", + "inherits": [ + "base_release", + "macos_base" + ], + "displayName": "macOS (Release)", + "configurePreset": "macos_release" + }, + { + "name": "macos_universal_debug", + "inherits": [ + "base_debug", + "macos_base" + ], + "displayName": "MacOS (Universal Binary, Debug)", + "configurePreset": "macos_universal_debug" + }, + { + "name": "macos_universal_release", + "inherits": [ + "base_release", + "macos_base" + ], + "displayName": "macOS (Universal Binary, Release)", + "configurePreset": "macos_universal_release" + } + ] +} diff --git a/cmake/windowsMSVCPreset.json b/cmake/windowsMSVCPreset.json new file mode 100644 index 000000000..f7e9d09ec --- /dev/null +++ b/cmake/windowsMSVCPreset.json @@ -0,0 +1,155 @@ +{ + "$schema": "https://cmake.org/cmake/help/latest/_downloads/3e2d73bff478d88a7de0de736ba5e361/schema.json", + "version": 8, + "include": [ + "commonPresets.json" + ], + "configurePresets": [ + { + "name": "windows_msvc_base", + "hidden": true, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + }, + "cacheVariables": { + "Launcher_BUILD_ARTIFACT": "Windows-MSVC-Qt6" + } + }, + { + "name": "windows_msvc_arm64_cross_base", + "hidden": true, + "inherits": [ + "windows_msvc_base" + ], + "architecture": "arm64", + "cacheVariables": { + "Launcher_BUILD_ARTIFACT": "Windows-MSVC-arm64-Qt6" + } + }, + { + "name": "windows_msvc_debug", + "inherits": [ + "base_debug", + "windows_msvc_base" + ], + "displayName": "Windows MSVC (Debug)", + "generator": "Ninja" + }, + { + "name": "windows_msvc_release", + "inherits": [ + "base_release", + "windows_msvc_base" + ], + "displayName": "Windows MSVC (Release)" + }, + { + "name": "windows_msvc_arm64_cross_debug", + "inherits": [ + "base_debug", + "windows_msvc_arm64_cross_base" + ], + "displayName": "Windows MSVC (ARM64 cross, Debug)" + }, + { + "name": "windows_msvc_arm64_cross_release", + "inherits": [ + "base_release", + "windows_msvc_arm64_cross_base" + ], + "displayName": "Windows MSVC (ARM64 cross, Release)" + } + ], + "buildPresets": [ + { + "name": "windows_msvc_base", + "hidden": true, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + } + }, + { + "name": "windows_msvc_debug", + "inherits": [ + "windows_msvc_base" + ], + "displayName": "Windows MSVC (Debug)", + "configurePreset": "windows_msvc_debug", + "configuration": "Debug" + }, + { + "name": "windows_msvc_release", + "inherits": [ + "windows_msvc_base" + ], + "displayName": "Windows MSVC (Release)", + "configurePreset": "windows_msvc_release", + "configuration": "Release", + "nativeToolOptions": [ + "/p:UseMultiToolTask=true", + "/p:EnforceProcessCountAcrossBuilds=true" + ] + }, + { + "name": "windows_msvc_arm64_cross_debug", + "inherits": [ + "windows_msvc_base" + ], + "displayName": "Windows MSVC (ARM64 cross, Debug)", + "configurePreset": "windows_msvc_arm64_cross_debug", + "configuration": "Debug", + "nativeToolOptions": [ + "/p:UseMultiToolTask=true", + "/p:EnforceProcessCountAcrossBuilds=true" + ] + }, + { + "name": "windows_msvc_arm64_cross_release", + "inherits": [ + "windows_msvc_base" + ], + "displayName": "Windows MSVC (ARM64 cross, Release)", + "configurePreset": "windows_msvc_arm64_cross_release", + "configuration": "Release", + "nativeToolOptions": [ + "/p:UseMultiToolTask=true", + "/p:EnforceProcessCountAcrossBuilds=true" + ] + } + ], + "testPresets": [ + { + "name": "windows_msvc_base", + "hidden": true, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + } + }, + { + "name": "windows_msvc_debug", + "inherits": [ + "base_debug", + "windows_msvc_base" + ], + "displayName": "Windows MSVC (Debug)", + "configurePreset": "windows_msvc_debug", + "configuration": "Debug" + }, + { + "name": "windows_msvc_release", + "inherits": [ + "base_release", + "windows_msvc_base" + ], + "displayName": "Windows MSVC (Release)", + "configurePreset": "windows_msvc_release", + "configuration": "Release" + } + ] +} diff --git a/cmake/windowsMinGWPreset.json b/cmake/windowsMinGWPreset.json new file mode 100644 index 000000000..40273f81b --- /dev/null +++ b/cmake/windowsMinGWPreset.json @@ -0,0 +1,99 @@ +{ + "$schema": "https://cmake.org/cmake/help/latest/_downloads/3e2d73bff478d88a7de0de736ba5e361/schema.json", + "version": 8, + "include": [ + "commonPresets.json" + ], + "configurePresets": [ + { + "name": "windows_mingw_base", + "hidden": true, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + }, + "generator": "Ninja", + "cacheVariables": { + "Launcher_BUILD_ARTIFACT": "Windows-MinGW-w64-Qt6" + } + }, + { + "name": "windows_mingw_debug", + "inherits": [ + "base_debug", + "windows_mingw_base" + ], + "displayName": "Windows MinGW (Debug)" + }, + { + "name": "windows_mingw_release", + "inherits": [ + "base_release", + "windows_mingw_base" + ], + "displayName": "Windows MinGW (Release)" + } + ], + "buildPresets": [ + { + "name": "windows_mingw_base", + "hidden": true, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + } + }, + { + "name": "windows_mingw_debug", + "inherits": [ + "windows_mingw_base" + ], + "displayName": "Windows MinGW (Debug)", + "configurePreset": "windows_mingw_debug" + }, + { + "name": "windows_mingw_release", + "inherits": [ + "windows_mingw_base" + ], + "displayName": "Windows MinGW (Release)", + "configurePreset": "windows_mingw_release" + } + ], + "testPresets": [ + { + "name": "windows_mingw_base", + "hidden": true, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + }, + "filter": { + "exclude": { + "name": "^example64|example$" + } + } + }, + { + "name": "windows_mingw_debug", + "inherits": [ + "base_debug", + "windows_mingw_base" + ], + "displayName": "Windows MinGW (Debug)", + "configurePreset": "windows_mingw_debug" + }, + { + "name": "windows_mingw_release", + "inherits": [ + "base_release", + "windows_mingw_base" + ], + "displayName": "Windows MinGW (Release)", + "configurePreset": "windows_mingw_release" + } + ] +} From 70500af2a2029fd783a80131a47e000bf24dfbb4 Mon Sep 17 00:00:00 2001 From: Seth Flynn Date: Tue, 8 Apr 2025 14:59:54 -0400 Subject: [PATCH 192/695] ci: use cmake workflow presets Signed-off-by: Seth Flynn --- .github/workflows/build.yml | 87 ++++++------------- .github/workflows/codeql.yml | 5 +- cmake/commonPresets.json | 11 +++ cmake/linuxPreset.json | 85 ++++++++++++++++++ cmake/macosPreset.json | 120 ++++++++++++++++++++++++++ cmake/windowsMSVCPreset.json | 156 ++++++++++++++++++++++++++++++++++ cmake/windowsMinGWPreset.json | 84 ++++++++++++++++++ 7 files changed, 483 insertions(+), 65 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 952b7c515..6172dc3ae 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -53,6 +53,7 @@ jobs: matrix: include: - os: ubuntu-22.04 + cmake_preset: linux qt_ver: 6 qt_host: linux qt_arch: "" @@ -64,11 +65,13 @@ jobs: - os: windows-2022 name: "Windows-MinGW-w64" + cmake_preset: windows_mingw msystem: clang64 vcvars_arch: "amd64_x86" - os: windows-2022 name: "Windows-MSVC" + cmake_preset: windows_msvc msystem: "" architecture: "x64" vcvars_arch: "amd64" @@ -82,6 +85,7 @@ jobs: - os: windows-2022 name: "Windows-MSVC-arm64" + cmake_preset: windows_msvc_arm64_cross msystem: "" architecture: "arm64" vcvars_arch: "amd64_arm64" @@ -95,6 +99,7 @@ jobs: - os: macos-14 name: macOS + cmake_preset: ${{ inputs.build_type == 'Debug' && 'macos_universal' || 'macos' }} macosx_deployment_target: 11.0 qt_ver: 6 qt_host: mac @@ -275,75 +280,30 @@ jobs: with: distribution: "temurin" java-version: "17" - ## - # CONFIGURE - ## - - - 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 -DLauncher_ENABLE_JAVA_DOWNLOADER=ON -DLauncher_BUILD_PLATFORM=official -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DCMAKE_OSX_ARCHITECTURES="x86_64;arm64" -G Ninja - - - name: Configure CMake (Windows MinGW-w64) - if: runner.os == 'Windows' && matrix.msystem != '' - 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_ENABLE_JAVA_DOWNLOADER=ON -DLauncher_BUILD_PLATFORM=official -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=6 -DCMAKE_OBJDUMP=/mingw64/bin/objdump.exe -DLauncher_BUILD_ARTIFACT=${{ matrix.name }}-Qt${{ matrix.qt_ver }} -G Ninja - - - name: Configure CMake (Windows MSVC) - if: runner.os == 'Windows' && matrix.msystem == '' && matrix.architecture != 'arm64' - run: | - cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_ENABLE_JAVA_DOWNLOADER=ON -DLauncher_BUILD_PLATFORM=official -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DCMAKE_MSVC_RUNTIME_LIBRARY="MultiThreadedDLL" -DLauncher_FORCE_BUNDLED_LIBS=ON -DLauncher_BUILD_ARTIFACT=${{ matrix.name }}-Qt${{ matrix.qt_ver }} -G Ninja - - - name: Configure CMake (Windows MSVC arm64) - if: runner.os == 'Windows' && matrix.msystem == '' && matrix.architecture == 'arm64' - run: | - cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_ENABLE_JAVA_DOWNLOADER=ON -DLauncher_BUILD_PLATFORM=official -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DCMAKE_MSVC_RUNTIME_LIBRARY="MultiThreadedDLL" -A${{ matrix.architecture}} -DLauncher_FORCE_BUNDLED_LIBS=ON -DLauncher_BUILD_ARTIFACT=${{ matrix.name }}-Qt${{ matrix.qt_ver }} - - - 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_ENABLE_JAVA_DOWNLOADER=ON -DLauncher_BUILD_PLATFORM=official -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DLauncher_BUILD_ARTIFACT=Linux-Qt${{ matrix.qt_ver }} -G Ninja ## - # BUILD + # SOURCE BUILD ## - - name: Build - if: runner.os != 'Windows' - run: | - cmake --build ${{ env.BUILD_DIR }} - - - name: Build (Windows MinGW-w64) - if: runner.os == 'Windows' && matrix.msystem != '' - shell: msys2 {0} - run: | - cmake --build ${{ env.BUILD_DIR }} - - - name: Build (Windows MSVC) - if: runner.os == 'Windows' && matrix.msystem == '' - run: | - cmake --build ${{ env.BUILD_DIR }} --config ${{ inputs.build_type }} - - ## - # TEST - ## - - - name: Test - if: runner.os != 'Windows' + - name: Run CMake workflow + if: ${{ runner.os != 'Windows' || matrix.msystem == '' }} + shell: bash + env: + CMAKE_PRESET: ${{ matrix.cmake_preset }} + PRESET_TYPE: ${{ inputs.build_type == 'Debug' && 'debug' || 'ci' }} run: | - ctest -E "^example64|example$" --test-dir build --output-on-failure + cmake --workflow --preset "$CMAKE_PRESET"_"$PRESET_TYPE" - - name: Test (Windows MinGW-w64) - if: runner.os == 'Windows' && matrix.msystem != '' + # NOTE: Split due to the `shell` requirement for msys2 + # TODO(@getchoo): Get ccache working! + - name: Run CMake workflow (Windows MinGW-w64) + if: ${{ runner.os == 'Windows' && matrix.msystem != '' }} shell: msys2 {0} + env: + CMAKE_PRESET: ${{ matrix.cmake_preset }} + PRESET_TYPE: ${{ inputs.build_type == 'Debug' && 'debug' || 'ci' }} run: | - ctest -E "^example64|example$" --test-dir build --output-on-failure - - - name: Test (Windows MSVC) - if: runner.os == 'Windows' && matrix.msystem == '' && matrix.architecture != 'arm64' - run: | - ctest -E "^example64|example$" --test-dir build --output-on-failure -C ${{ inputs.build_type }} + cmake --workflow --preset "$CMAKE_PRESET"_"$PRESET_TYPE" ## # PACKAGE BUILDS @@ -542,8 +502,11 @@ jobs: - name: Package (Linux, portable) if: runner.os == 'Linux' + env: + CMAKE_PRESET: ${{ matrix.cmake_preset }} + PRESET_TYPE: ${{ inputs.build_type == 'Debug' && 'debug' || 'ci' }} run: | - cmake -S . -B ${{ env.BUILD_DIR }} -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_PORTABLE_DIR }} -DCMAKE_BUILD_TYPE=${{ inputs.build_type }} -DENABLE_LTO=ON -DLauncher_BUILD_PLATFORM=official -DCMAKE_C_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DCMAKE_CXX_COMPILER_LAUNCHER=${{ env.CCACHE_VAR }} -DLauncher_QT_VERSION_MAJOR=${{ matrix.qt_ver }} -DLauncher_BUILD_ARTIFACT=Linux-Qt${{ matrix.qt_ver }} -DINSTALL_BUNDLE=full -G Ninja + cmake --preset "$CMAKE_PRESET"_"$PRESET_TYPE" -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_PORTABLE_DIR }} -DINSTALL_BUNDLE=full cmake --install ${{ env.BUILD_DIR }} cmake --install ${{ env.BUILD_DIR }} --component portable diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index a5ac537f1..285125185 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -71,9 +71,8 @@ jobs: - name: Configure and Build run: | - cmake -S . -B build -DCMAKE_INSTALL_PREFIX=/usr -G Ninja - - cmake --build build + cmake --preset linux_debug + cmake --build --preset linux_debug - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 diff --git a/cmake/commonPresets.json b/cmake/commonPresets.json index 7acbf8a31..9cdf51649 100644 --- a/cmake/commonPresets.json +++ b/cmake/commonPresets.json @@ -31,6 +31,17 @@ "CMAKE_BUILD_TYPE": "Release", "ENABLE_LTO": "ON" } + }, + { + "name": "base_ci", + "hidden": true, + "inherits": [ + "base_release" + ], + "cacheVariables": { + "Launcher_BUILD_PLATFORM": "official", + "Launcher_FORCE_BUNDLED_LIBS": "ON" + } } ], "testPresets": [ diff --git a/cmake/linuxPreset.json b/cmake/linuxPreset.json index 81ae95c2c..b8bfe4ff0 100644 --- a/cmake/linuxPreset.json +++ b/cmake/linuxPreset.json @@ -34,6 +34,18 @@ "linux_base" ], "displayName": "Linux (Release)" + }, + { + "name": "linux_ci", + "inherits": [ + "base_ci", + "linux_base" + ], + "displayName": "Linux (CI)", + "cacheVariables": { + "Launcher_BUILD_ARTIFACT": "Linux-Qt6" + }, + "installDir": "/usr" } ], "buildPresets": [ @@ -61,6 +73,14 @@ ], "displayName": "Linux (Release)", "configurePreset": "linux_release" + }, + { + "name": "linux_ci", + "inherits": [ + "linux_base" + ], + "displayName": "Linux (CI)", + "configurePreset": "linux_ci" } ], "testPresets": [ @@ -90,6 +110,71 @@ ], "displayName": "Linux (Release)", "configurePreset": "linux_release" + }, + { + "name": "linux_ci", + "inherits": [ + "base_release", + "linux_base" + ], + "displayName": "Linux (CI)", + "configurePreset": "linux_ci" + } + ], + "workflowPresets": [ + { + "name": "linux_debug", + "displayName": "Linux (Debug)", + "steps": [ + { + "type": "configure", + "name": "linux_debug" + }, + { + "type": "build", + "name": "linux_debug" + }, + { + "type": "test", + "name": "linux_debug" + } + ] + }, + { + "name": "linux", + "displayName": "Linux (Release)", + "steps": [ + { + "type": "configure", + "name": "linux_release" + }, + { + "type": "build", + "name": "linux_release" + }, + { + "type": "test", + "name": "linux_release" + } + ] + }, + { + "name": "linux_ci", + "displayName": "Linux (CI)", + "steps": [ + { + "type": "configure", + "name": "linux_ci" + }, + { + "type": "build", + "name": "linux_ci" + }, + { + "type": "test", + "name": "linux_ci" + } + ] } ] } diff --git a/cmake/macosPreset.json b/cmake/macosPreset.json index 8cecfbe71..726949934 100644 --- a/cmake/macosPreset.json +++ b/cmake/macosPreset.json @@ -57,6 +57,17 @@ "macos_universal_base" ], "displayName": "macOS (Universal Binary, Release)" + }, + { + "name": "macos_ci", + "inherits": [ + "base_ci", + "macos_universal_base" + ], + "displayName": "macOS (CI)", + "cacheVariables": { + "Launcher_BUILD_ARTIFACT": "macOS-Qt6" + } } ], "buildPresets": [ @@ -100,6 +111,14 @@ ], "displayName": "macOS (Universal Binary, Release)", "configurePreset": "macos_universal_release" + }, + { + "name": "macos_ci", + "inherits": [ + "macos_base" + ], + "displayName": "macOS (CI)", + "configurePreset": "macos_ci" } ], "testPresets": [ @@ -147,6 +166,107 @@ ], "displayName": "macOS (Universal Binary, Release)", "configurePreset": "macos_universal_release" + }, + { + "name": "macos_ci", + "inherits": [ + "base_release", + "macos_base" + ], + "displayName": "macOS (CI)", + "configurePreset": "macos_ci" + } + ], + "workflowPresets": [ + { + "name": "macos_debug", + "displayName": "macOS (Debug)", + "steps": [ + { + "type": "configure", + "name": "macos_debug" + }, + { + "type": "build", + "name": "macos_debug" + }, + { + "type": "test", + "name": "macos_debug" + } + ] + }, + { + "name": "macos", + "displayName": "macOS (Release)", + "steps": [ + { + "type": "configure", + "name": "macos_release" + }, + { + "type": "build", + "name": "macos_release" + }, + { + "type": "test", + "name": "macos_release" + } + ] + }, + { + "name": "macos_universal_debug", + "displayName": "macOS (Universal Binary, Debug)", + "steps": [ + { + "type": "configure", + "name": "macos_universal_debug" + }, + { + "type": "build", + "name": "macos_universal_debug" + }, + { + "type": "test", + "name": "macos_universal_debug" + } + ] + }, + { + "name": "macos_universal", + "displayName": "macOS (Universal Binary, Release)", + "steps": [ + { + "type": "configure", + "name": "macos_universal_release" + }, + { + "type": "build", + "name": "macos_universal_release" + }, + { + "type": "test", + "name": "macos_universal_release" + } + ] + }, + { + "name": "macos_ci", + "displayName": "macOS (CI)", + "steps": [ + { + "type": "configure", + "name": "macos_ci" + }, + { + "type": "build", + "name": "macos_ci" + }, + { + "type": "test", + "name": "macos_ci" + } + ] } ] } diff --git a/cmake/windowsMSVCPreset.json b/cmake/windowsMSVCPreset.json index f7e9d09ec..eb6a38b19 100644 --- a/cmake/windowsMSVCPreset.json +++ b/cmake/windowsMSVCPreset.json @@ -60,6 +60,28 @@ "windows_msvc_arm64_cross_base" ], "displayName": "Windows MSVC (ARM64 cross, Release)" + }, + { + "name": "windows_msvc_ci", + "inherits": [ + "base_ci", + "windows_msvc_base" + ], + "displayName": "Windows MSVC (CI)", + "cacheVariables": { + "Launcher_BUILD_ARTIFACT": "Windows-MSVC-Qt6" + } + }, + { + "name": "windows_msvc_arm64_cross_ci", + "inherits": [ + "base_ci", + "windows_msvc_arm64_cross_base" + ], + "displayName": "Windows MSVC (ARM64 cross, CI)", + "cacheVariables": { + "Launcher_BUILD_ARTIFACT": "Windows-MSVC-arm64-Qt6" + } } ], "buildPresets": [ @@ -119,6 +141,32 @@ "/p:UseMultiToolTask=true", "/p:EnforceProcessCountAcrossBuilds=true" ] + }, + { + "name": "windows_msvc_ci", + "inherits": [ + "windows_msvc_base" + ], + "displayName": "Windows MSVC (CI)", + "configurePreset": "windows_msvc_ci", + "configuration": "Release", + "nativeToolOptions": [ + "/p:UseMultiToolTask=true", + "/p:EnforceProcessCountAcrossBuilds=true" + ] + }, + { + "name": "windows_msvc_arm64_cross_ci", + "inherits": [ + "windows_msvc_base" + ], + "displayName": "Windows MSVC (ARM64 cross, CI)", + "configurePreset": "windows_msvc_arm64_cross_ci", + "configuration": "Release", + "nativeToolOptions": [ + "/p:UseMultiToolTask=true", + "/p:EnforceProcessCountAcrossBuilds=true" + ] } ], "testPresets": [ @@ -150,6 +198,114 @@ "displayName": "Windows MSVC (Release)", "configurePreset": "windows_msvc_release", "configuration": "Release" + }, + { + "name": "windows_msvc_ci", + "inherits": [ + "base_release", + "windows_msvc_base" + ], + "displayName": "Windows MSVC (CI)", + "configurePreset": "windows_msvc_ci", + "configuration": "Release" + } + ], + "workflowPresets": [ + { + "name": "windows_msvc_debug", + "displayName": "Windows MSVC (Debug)", + "steps": [ + { + "type": "configure", + "name": "windows_msvc_debug" + }, + { + "type": "build", + "name": "windows_msvc_debug" + }, + { + "type": "test", + "name": "windows_msvc_debug" + } + ] + }, + { + "name": "windows_msvc", + "displayName": "Windows MSVC (Release)", + "steps": [ + { + "type": "configure", + "name": "windows_msvc_release" + }, + { + "type": "build", + "name": "windows_msvc_release" + }, + { + "type": "test", + "name": "windows_msvc_release" + } + ] + }, + { + "name": "windows_msvc_arm64_cross_debug", + "displayName": "Windows MSVC (ARM64 cross, Debug)", + "steps": [ + { + "type": "configure", + "name": "windows_msvc_arm64_cross_debug" + }, + { + "type": "build", + "name": "windows_msvc_arm64_cross_debug" + } + ] + }, + { + "name": "windows_msvc_arm64_cross", + "displayName": "Windows MSVC (ARM64 cross, Release)", + "steps": [ + { + "type": "configure", + "name": "windows_msvc_arm64_cross_release" + }, + { + "type": "build", + "name": "windows_msvc_arm64_cross_release" + } + ] + }, + { + "name": "windows_msvc_ci", + "displayName": "Windows MSVC (CI)", + "steps": [ + { + "type": "configure", + "name": "windows_msvc_ci" + }, + { + "type": "build", + "name": "windows_msvc_ci" + }, + { + "type": "test", + "name": "windows_msvc_ci" + } + ] + }, + { + "name": "windows_msvc_arm64_cross_ci", + "displayName": "Windows MSVC (ARM64 cross, CI)", + "steps": [ + { + "type": "configure", + "name": "windows_msvc_arm64_cross_ci" + }, + { + "type": "build", + "name": "windows_msvc_arm64_cross_ci" + } + ] } ] } diff --git a/cmake/windowsMinGWPreset.json b/cmake/windowsMinGWPreset.json index 40273f81b..984caadd6 100644 --- a/cmake/windowsMinGWPreset.json +++ b/cmake/windowsMinGWPreset.json @@ -33,6 +33,17 @@ "windows_mingw_base" ], "displayName": "Windows MinGW (Release)" + }, + { + "name": "windows_mingw_ci", + "inherits": [ + "base_ci", + "windows_mingw_base" + ], + "displayName": "Windows MinGW (CI)", + "cacheVariables": { + "Launcher_BUILD_ARTIFACT": "Windows-MinGW-w64-Qt6" + } } ], "buildPresets": [ @@ -60,6 +71,14 @@ ], "displayName": "Windows MinGW (Release)", "configurePreset": "windows_mingw_release" + }, + { + "name": "windows_mingw_ci", + "inherits": [ + "windows_mingw_base" + ], + "displayName": "Windows MinGW (CI)", + "configurePreset": "windows_mingw_ci" } ], "testPresets": [ @@ -94,6 +113,71 @@ ], "displayName": "Windows MinGW (Release)", "configurePreset": "windows_mingw_release" + }, + { + "name": "windows_mingw_ci", + "inherits": [ + "base_release", + "windows_mingw_base" + ], + "displayName": "Windows MinGW (CI)", + "configurePreset": "windows_mingw_ci" + } + ], + "workflowPresets": [ + { + "name": "windows_mingw_debug", + "displayName": "Windows MinGW (Debug)", + "steps": [ + { + "type": "configure", + "name": "windows_mingw_debug" + }, + { + "type": "build", + "name": "windows_mingw_debug" + }, + { + "type": "test", + "name": "windows_mingw_debug" + } + ] + }, + { + "name": "windows_mingw", + "displayName": "Windows MinGW (Release)", + "steps": [ + { + "type": "configure", + "name": "windows_mingw_release" + }, + { + "type": "build", + "name": "windows_mingw_release" + }, + { + "type": "test", + "name": "windows_mingw_release" + } + ] + }, + { + "name": "windows_mingw_ci", + "displayName": "Windows MinGW (CI)", + "steps": [ + { + "type": "configure", + "name": "windows_mingw_ci" + }, + { + "type": "build", + "name": "windows_mingw_ci" + }, + { + "type": "test", + "name": "windows_mingw_ci" + } + ] } ] } From 6c45ff915aa7a5b190d3f0e03bd40ed1c18d16c5 Mon Sep 17 00:00:00 2001 From: Seth Flynn Date: Mon, 28 Apr 2025 13:22:54 -0400 Subject: [PATCH 193/695] chore(gitignore): add CMakeUserPresets.json Signed-off-by: Seth Flynn --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index b563afbc7..00afabbfa 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ CMakeLists.txt.user.* CMakeSettings.json /CMakeFiles CMakeCache.txt +CMakeUserPresets.json /.project /.settings /.idea From b438236a6467605feabc10266a13602079a797e4 Mon Sep 17 00:00:00 2001 From: Seth Flynn Date: Tue, 29 Apr 2025 09:15:07 -0400 Subject: [PATCH 194/695] ci: use symlink for ccache when possible Signed-off-by: Seth Flynn --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6172dc3ae..5de1c44fb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -159,6 +159,7 @@ jobs: if: (runner.os != 'Windows' || matrix.msystem == '') && inputs.build_type == 'Debug' uses: hendrikmuhs/ccache-action@v1.2.18 with: + create-symlink: ${{ runner.os != 'Windows' }} key: ${{ matrix.os }}-qt${{ matrix.qt_ver }}-${{ matrix.architecture }} - name: Use ccache on Debug builds only From a465af45dc62904ea7b874aea2534b4c4ce4effd Mon Sep 17 00:00:00 2001 From: Trial97 Date: Wed, 30 Apr 2025 10:15:59 +0300 Subject: [PATCH 195/695] fix: task typo in flame export task Signed-off-by: Trial97 --- launcher/modplatform/flame/FlamePackExportTask.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/modplatform/flame/FlamePackExportTask.cpp b/launcher/modplatform/flame/FlamePackExportTask.cpp index 98cdfffc9..c9a35f3cf 100644 --- a/launcher/modplatform/flame/FlamePackExportTask.cpp +++ b/launcher/modplatform/flame/FlamePackExportTask.cpp @@ -325,7 +325,7 @@ void FlamePackExportTask::getProjectsInfo() buildZip(); }); connect(projTask.get(), &Task::failed, this, &FlamePackExportTask::emitFailed); - connect(task.get(), &Task::aborted, this, &FlamePackExportTask::emitAborted); + connect(projTask.get(), &Task::aborted, this, &FlamePackExportTask::emitAborted); task.reset(projTask); task->start(); } From dc3a8dcfed67d3e3a7808fe06fe7e7b6bfff7300 Mon Sep 17 00:00:00 2001 From: Seth Flynn Date: Tue, 29 Apr 2025 23:48:42 -0400 Subject: [PATCH 196/695] ci: only run on specific paths This avoids the previously applied paths-ignore exception workaround, and makes runs as strict as (reasonably) possible. Only directories known to affect builds will trigger builds, as well as any `.cpp` or `.h` files to account for any new folders created - though these should still be added to the workflow later Signed-off-by: Seth Flynn --- .github/workflows/codeql.yml | 59 ++++++++++++++---------- .github/workflows/flatpak.yml | 61 ++++++++++++++++--------- .github/workflows/nix.yml | 68 ++++++++++++++++++---------- .github/workflows/trigger_builds.yml | 61 ++++++++++++++----------- 4 files changed, 154 insertions(+), 95 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index a5ac537f1..5ce0741dd 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -2,37 +2,48 @@ name: "CodeQL Code Scanning" on: push: - # NOTE: `!` doesn't work with `paths-ignore` :( - # So we a catch-all glob instead - # https://github.com/orgs/community/discussions/25369#discussioncomment-3247674 paths: - - "**" - - "!.github/**" - - ".github/workflows/codeql.yml" - - "!flatpak/" - - "!nix/" - - "!scripts/" + # File types + - "**.cpp" + - "**.h" + - "**.java" + + # Directories + - "buildconfig/" + - "cmake/" + - "launcher/" + - "libraries/" + - "program_info/" + - "tests/" - - "!.git*" - - "!.envrc" - - "!**.md" + # Files + - "CMakeLists.txt" - "COPYING.md" - - "!renovate.json" + + # Workflows + - ".github/codeql" + - ".github/workflows/codeql.yml" pull_request: - # See above paths: - - "**" - - "!.github/**" - - ".github/workflows/codeql.yml" - - "!flatpak/" - - "!nix/" - - "!scripts/" + # File types + - "**.cpp" + - "**.h" + + # Directories + - "buildconfig/" + - "cmake/" + - "launcher/" + - "libraries/" + - "program_info/" + - "tests/" - - "!.git*" - - "!.envrc" - - "!**.md" + # Files + - "CMakeLists.txt" - "COPYING.md" - - "!renovate.json" + + # Workflows + - ".github/codeql" + - ".github/workflows/codeql.yml" workflow_dispatch: jobs: diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml index 8caba46fa..cab0edeb7 100644 --- a/.github/workflows/flatpak.yml +++ b/.github/workflows/flatpak.yml @@ -5,35 +5,52 @@ on: # We don't do anything with these artifacts on releases. They go to Flathub tags-ignore: - "*" - # NOTE: `!` doesn't work with `paths-ignore` :( - # So we a catch-all glob instead - # https://github.com/orgs/community/discussions/25369#discussioncomment-3247674 paths: - - "**" - - "!.github/**" - - ".github/workflows/flatpak.yml" - - "!nix/" - - "!scripts/" + # File types + - "**.cpp" + - "**.h" + - "**.java" + + # Build files + - "flatpak/" + + # Directories + - "buildconfig/" + - "cmake/" + - "launcher/" + - "libraries/" + - "program_info/" + - "tests/" - - "!.git*" - - "!.envrc" - - "!**.md" + # Files + - "CMakeLists.txt" - "COPYING.md" - - "!renovate.json" + + # Workflows + - ".github/workflows/flatpak.yml" pull_request: - # See above paths: - - "**" - - "!.github/**" - - ".github/workflows/flatpak.yml" - - "!nix/" - - "!scripts/" + # File types + - "**.cpp" + - "**.h" + + # Build files + - "flatpak/" + + # Directories + - "buildconfig/" + - "cmake/" + - "launcher/" + - "libraries/" + - "program_info/" + - "tests/" - - "!.git*" - - "!.envrc" - - "!**.md" + # Files + - "CMakeLists.txt" - "COPYING.md" - - "!renovate.json" + + # Workflows + - ".github/workflows/flatpak.yml" workflow_dispatch: permissions: diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index 75ef7c65a..5a40ebb1f 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -4,34 +4,56 @@ on: push: tags: - "*" - # NOTE: `!` doesn't work with `paths-ignore` :( - # So we a catch-all glob instead - # https://github.com/orgs/community/discussions/25369#discussioncomment-3247674 paths: - - "**" - - "!.github/**" - - ".github/workflows/nix.yml" - - "!flatpak/" - - "!scripts/" - - - "!.git*" - - "!.envrc" - - "!**.md" + # File types + - "**.cpp" + - "**.h" + - "**.java" + + # Build files + - "**.nix" + - "nix/" + - "flake.lock" + + # Directories + - "buildconfig/" + - "cmake/" + - "launcher/" + - "libraries/" + - "program_info/" + - "tests/" + + # Files + - "CMakeLists.txt" - "COPYING.md" - - "!renovate.json" + + # Workflows + - ".github/workflows/nix.yml" pull_request_target: paths: - - "**" - - "!.github/**" - - ".github/workflows/nix.yml" - - "!flatpak/" - - "!scripts/" - - - "!.git*" - - "!.envrc" - - "!**.md" + # File types + - "**.cpp" + - "**.h" + + # Build files + - "**.nix" + - "nix/" + - "flake.lock" + + # Directories + - "buildconfig/" + - "cmake/" + - "launcher/" + - "libraries/" + - "program_info/" + - "tests/" + + # Files + - "CMakeLists.txt" - "COPYING.md" - - "!renovate.json" + + # Workflows + - ".github/workflows/nix.yml" workflow_dispatch: permissions: diff --git a/.github/workflows/trigger_builds.yml b/.github/workflows/trigger_builds.yml index e4c90ef0b..4be03f46f 100644 --- a/.github/workflows/trigger_builds.yml +++ b/.github/workflows/trigger_builds.yml @@ -4,39 +4,48 @@ on: push: branches-ignore: - "renovate/**" - # NOTE: `!` doesn't work with `paths-ignore` :( - # So we a catch-all glob instead - # https://github.com/orgs/community/discussions/25369#discussioncomment-3247674 paths: - - "**" - - "!.github/**" - - ".github/workflows/build.yml" - - ".github/workflows/trigger_builds.yml" - - "!flatpak/" - - "!nix/" - - "!scripts/" + # File types + - "**.cpp" + - "**.h" + - "**.java" + + # Directories + - "buildconfig/" + - "cmake/" + - "launcher/" + - "libraries/" + - "program_info/" + - "tests/" - - "!.git*" - - "!.envrc" - - "!**.md" + # Files + - "CMakeLists.txt" - "COPYING.md" - - "!renovate.json" - pull_request: - # See above - paths: - - "**" - - "!.github/**" + + # Workflows - ".github/workflows/build.yml" - ".github/workflows/trigger_builds.yml" - - "!flatpak/" - - "!nix/" - - "!scripts/" + pull_request: + paths: + # File types + - "**.cpp" + - "**.h" + + # Directories + - "buildconfig/" + - "cmake/" + - "launcher/" + - "libraries/" + - "program_info/" + - "tests/" - - "!.git*" - - "!.envrc" - - "!**.md" + # Files + - "CMakeLists.txt" - "COPYING.md" - - "!renovate.json" + + # Workflows + - ".github/workflows/build.yml" + - ".github/workflows/trigger_builds.yml" workflow_dispatch: jobs: From eb911389f855881fee9983d002fc434db7aa685a Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Wed, 30 Apr 2025 02:45:23 -0700 Subject: [PATCH 197/695] Distinguish between stacked and blocked pr distinguish between stacked and blocked pr stacks need merge blocks just need a close Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- .github/workflows/blocked-prs.yml | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/.github/workflows/blocked-prs.yml b/.github/workflows/blocked-prs.yml index bd49b7230..a8b2a3fe6 100644 --- a/.github/workflows/blocked-prs.yml +++ b/.github/workflows/blocked-prs.yml @@ -125,6 +125,7 @@ jobs: "type": $type, "number": .number, "merged": .merged, + "state": if .state == "open" then "Open" elif .merged then "Merged" else "Closed" end, "labels": (reduce .labels[].name as $l ([]; . + [$l])), "basePrUrl": .html_url, "baseRepoName": .head.repo.name, @@ -138,11 +139,16 @@ jobs: ) { echo "data=$blocked_pr_data"; - echo "all_merged=$(jq -r 'all(.[].merged; .)' <<< "$blocked_pr_data")"; - echo "current_blocking=$(jq -c 'map( select( .merged | not ) | .number )' <<< "$blocked_pr_data" )"; + echo "all_merged=$(jq -r 'all(.[] | (.type == "Stacked on" and .merged) or (.type == "Blocked on" and (.state != "Open")); .)' <<< "$blocked_pr_data")"; + echo "current_blocking=$(jq -c 'map( + select( + (.type == "Stacked on" and (.merged | not)) or + (.type == "Blocked on" and (.state == "Open")) + ) | .number + )' <<< "$blocked_pr_data" )"; } >> "$GITHUB_OUTPUT" - - name: Add 'blocked' Label is Missing + - name: Add 'blocked' Label if Missing id: label_blocked if: (fromJSON(steps.pr_ids.outputs.prs).numBlocking > 0) && !contains(fromJSON(env.JOB_DATA).prLabels, 'blocked') && !fromJSON(steps.blocking_data.outputs.all_merged) continue-on-error: true @@ -184,14 +190,18 @@ jobs: # create commit Status, overwrites previous identical context while read -r pr_data ; do DESC=$( - jq -r ' "Blocking PR #" + (.number | tostring) + " is " + (if .merged then "" else "not yet " end) + "merged"' <<< "$pr_data" + jq -r 'if .type == "Stacked on" then + "Stacked PR #" + (.number | tostring) + " is " + (if .merged then "" else "not yet " end) + "merged" + else + "Blocking PR #" + (.number | tostring) + " is " + (if .state == "Open" then "" else "not yet " end) + "merged or closed" + end ' <<< "$pr_data" ) gh api \ --method POST \ -H "Accept: application/vnd.github+json" \ -H "X-GitHub-Api-Version: 2022-11-28" \ "/repos/${OWNER}/${REPO}/statuses/${pr_head_sha}" \ - -f "state=$(jq -r 'if .merged then "success" else "failure" end' <<< "$pr_data")" \ + -f "state=$(jq -r 'if (.type == "Stacked on" and .merged) or (.type == "Blocked on" and (.state != "Open")) then "success" else "failure" end' <<< "$pr_data")" \ -f "target_url=$(jq -r '.basePrUrl' <<< "$pr_data" )" \ -f "description=$DESC" \ -f "context=ci/blocking-pr-check:$(jq '.number' <<< "$pr_data")" @@ -214,7 +224,13 @@ jobs: base_repo_owner=$(jq -r '.baseRepoOwner' <<< "$pr_data") base_repo_name=$(jq -r '.baseRepoName' <<< "$pr_data") compare_url="https://github.com/$base_repo_owner/$base_repo_name/compare/$base_ref_name...$pr_head_label" - status=$(jq -r 'if .merged then ":heavy_check_mark: Merged" else ":x: Not Merged" end' <<< "$pr_data") + status=$(jq -r ' + if .type == "Stacked on" then + if .merged then ":heavy_check_mark: Merged" else ":x: Not Merged (" + .state + ")" end + else + if .state != "Open" then ":white_check_mark: " + .state else ":x: Open" end + end + ' <<< "$pr_data") type=$(jq -r '.type' <<< "$pr_data") echo " - $type #$base_pr $status [(compare)]($compare_url)" >> "$COMMENT_PATH" done < <(jq -c '.[]' <<< "$BLOCKING_DATA") From 19d69994554edf7027a3a7fe591956a85b75480a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 30 Apr 2025 16:35:22 +0000 Subject: [PATCH 198/695] chore(deps): update cachix/install-nix-action digest to 5261181 --- .github/workflows/update-flake.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/update-flake.yml b/.github/workflows/update-flake.yml index f4b1c4f5d..62852171b 100644 --- a/.github/workflows/update-flake.yml +++ b/.github/workflows/update-flake.yml @@ -17,7 +17,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: cachix/install-nix-action@754537aaedb35f72ab11a60cc162c49ef3016495 # v31 + - uses: cachix/install-nix-action@526118121621777ccd86f79b04685a9319637641 # v31 - uses: DeterminateSystems/update-flake-lock@v24 with: From 2dfb674e443b5510e6a6c48a5ce9ec249d6b9bcd Mon Sep 17 00:00:00 2001 From: Seth Flynn Date: Tue, 29 Apr 2025 01:07:34 -0400 Subject: [PATCH 199/695] ci: split build workflow into composite actions Signed-off-by: Seth Flynn --- .github/actions/package/linux/action.yml | 124 ++++ .github/actions/package/macos/action.yml | 121 ++++ .github/actions/package/windows/action.yml | 143 +++++ .github/actions/setup-dependencies/action.yml | 67 ++ .../setup-dependencies/linux/action.yml | 26 + .../setup-dependencies/macos/action.yml | 16 + .../setup-dependencies/windows/action.yml | 85 +++ .github/workflows/build.yml | 603 +++--------------- .github/workflows/trigger_builds.yml | 16 +- .github/workflows/trigger_release.yml | 16 +- 10 files changed, 666 insertions(+), 551 deletions(-) create mode 100644 .github/actions/package/linux/action.yml create mode 100644 .github/actions/package/macos/action.yml create mode 100644 .github/actions/package/windows/action.yml create mode 100644 .github/actions/setup-dependencies/action.yml create mode 100644 .github/actions/setup-dependencies/linux/action.yml create mode 100644 .github/actions/setup-dependencies/macos/action.yml create mode 100644 .github/actions/setup-dependencies/windows/action.yml diff --git a/.github/actions/package/linux/action.yml b/.github/actions/package/linux/action.yml new file mode 100644 index 000000000..b71e62592 --- /dev/null +++ b/.github/actions/package/linux/action.yml @@ -0,0 +1,124 @@ +name: Package for Linux +description: Create Linux packages for Prism Launcher + +inputs: + version: + description: Launcher version + required: true + build-type: + description: Type for the build + required: true + default: Debug + artifact-name: + description: Name of the uploaded artifact + required: true + default: Linux + cmake-preset: + description: Base CMake preset previously used for the build + required: true + default: linux + qt-version: + description: Version of Qt to use + required: true + gpg-private-key: + description: Private key for AppImage signing + required: false + gpg-private-key-id: + description: ID for the gpg-private-key, to select the signing key + required: false + +runs: + using: composite + + steps: + - name: Package AppImage + shell: bash + env: + VERSION: ${{ inputs.version }} + BUILD_DIR: build + INSTALL_APPIMAGE_DIR: install-appdir + + GPG_PRIVATE_KEY: ${{ inputs.gpg-private-key }} + run: | + cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_APPIMAGE_DIR }}/usr + + mv ${{ env.INSTALL_APPIMAGE_DIR }}/usr/share/metainfo/org.prismlauncher.PrismLauncher.metainfo.xml ${{ env.INSTALL_APPIMAGE_DIR }}/usr/share/metainfo/org.prismlauncher.PrismLauncher.appdata.xml + export "NO_APPSTREAM=1" # we have to skip appstream checking because appstream on ubuntu 20.04 is outdated + + export OUTPUT="PrismLauncher-Linux-x86_64.AppImage" + + chmod +x linuxdeploy-*.AppImage + + mkdir -p ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib + mkdir -p ${{ env.INSTALL_APPIMAGE_DIR }}/usr/plugins/iconengines + + cp -r ${{ runner.workspace }}/Qt/${{ inputs.qt-version }}/gcc_64/plugins/iconengines/* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/plugins/iconengines + + cp /usr/lib/x86_64-linux-gnu/libcrypto.so.* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/ + cp /usr/lib/x86_64-linux-gnu/libssl.so.* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/ + cp /usr/lib/x86_64-linux-gnu/libOpenGL.so.0* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/ + + LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib" + export LD_LIBRARY_PATH + + chmod +x AppImageUpdate-x86_64.AppImage + cp AppImageUpdate-x86_64.AppImage ${{ env.INSTALL_APPIMAGE_DIR }}/usr/bin + + export UPDATE_INFORMATION="gh-releases-zsync|${{ github.repository_owner }}|${{ github.event.repository.name }}|latest|PrismLauncher-Linux-x86_64.AppImage.zsync" + + if [ '${{ inputs.gpg-private-key-id }}' != '' ]; then + export SIGN=1 + export SIGN_KEY=${{ inputs.gpg-private-key-id }} + mkdir -p ~/.gnupg/ + echo "$GPG_PRIVATE_KEY" > ~/.gnupg/private.key + gpg --import ~/.gnupg/private.key + else + echo ":warning: Skipped code signing for Linux AppImage, as gpg key was not present." >> $GITHUB_STEP_SUMMARY + fi + + ./linuxdeploy-x86_64.AppImage --appdir ${{ env.INSTALL_APPIMAGE_DIR }} --output appimage --plugin qt -i ${{ env.INSTALL_APPIMAGE_DIR }}/usr/share/icons/hicolor/scalable/apps/org.prismlauncher.PrismLauncher.svg + + mv "PrismLauncher-Linux-x86_64.AppImage" "PrismLauncher-Linux-${{ env.VERSION }}-${{ inputs.build-type }}-x86_64.AppImage" + + - name: Package portable tarball + shell: bash + env: + BUILD_DIR: build + + CMAKE_PRESET: ${{ inputs.cmake-preset }} + + INSTALL_PORTABLE_DIR: install-portable + run: | + cmake --preset "$CMAKE_PRESET" -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_PORTABLE_DIR }} -DINSTALL_BUNDLE=full + cmake --install ${{ env.BUILD_DIR }} + cmake --install ${{ env.BUILD_DIR }} --component portable + + mkdir ${{ env.INSTALL_PORTABLE_DIR }}/lib + cp /lib/x86_64-linux-gnu/libbz2.so.1.0 ${{ env.INSTALL_PORTABLE_DIR }}/lib + cp /usr/lib/x86_64-linux-gnu/libgobject-2.0.so.0 ${{ env.INSTALL_PORTABLE_DIR }}/lib + cp /usr/lib/x86_64-linux-gnu/libcrypto.so.* ${{ env.INSTALL_PORTABLE_DIR }}/lib + cp /usr/lib/x86_64-linux-gnu/libssl.so.* ${{ env.INSTALL_PORTABLE_DIR }}/lib + cp /usr/lib/x86_64-linux-gnu/libffi.so.*.* ${{ env.INSTALL_PORTABLE_DIR }}/lib + mv ${{ env.INSTALL_PORTABLE_DIR }}/bin/*.so* ${{ env.INSTALL_PORTABLE_DIR }}/lib + + for l in $(find ${{ env.INSTALL_PORTABLE_DIR }} -type f); do l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_PORTABLE_DIR }}/}; l=${l#./}; echo $l; done > ${{ env.INSTALL_PORTABLE_DIR }}/manifest.txt + cd ${{ env.INSTALL_PORTABLE_DIR }} + tar -czf ../PrismLauncher-portable.tar.gz * + + - name: Upload binary tarball + uses: actions/upload-artifact@v4 + with: + name: PrismLauncher-${{ inputs.artifact-name }}-Qt6-Portable-${{ inputs.version }}-${{ inputs.build-type }} + path: PrismLauncher-portable.tar.gz + + - name: Upload AppImage + uses: actions/upload-artifact@v4 + with: + name: PrismLauncher-${{ inputs.artifact-name }}-${{ inputs.version }}-${{ inputs.build-type }}-x86_64.AppImage + path: PrismLauncher-${{ runner.os }}-${{ inputs.version }}-${{ inputs.build-type }}-x86_64.AppImage + + - name: Upload AppImage Zsync + uses: actions/upload-artifact@v4 + with: + name: PrismLauncher-${{ inputs.artifact-name }}-${{ inputs.version }}-${{ inputs.build-type }}-x86_64.AppImage.zsync + path: PrismLauncher-Linux-x86_64.AppImage.zsync diff --git a/.github/actions/package/macos/action.yml b/.github/actions/package/macos/action.yml new file mode 100644 index 000000000..42181953c --- /dev/null +++ b/.github/actions/package/macos/action.yml @@ -0,0 +1,121 @@ +name: Package for macOS +description: Create a macOS package for Prism Launcher + +inputs: + version: + description: Launcher version + required: true + build-type: + description: Type for the build + required: true + default: Debug + artifact-name: + description: Name of the uploaded artifact + required: true + default: macOS + apple-codesign-cert: + description: Certificate for signing macOS builds + required: false + apple-codesign-password: + description: Password for signing macOS builds + required: false + apple-codesign-id: + description: Certificate ID for signing macOS builds + required: false + apple-notarize-apple-id: + description: Apple ID used for notarizing macOS builds + required: false + apple-notarize-team-id: + description: Team ID used for notarizing macOS builds + required: false + apple-notarize-password: + description: Password used for notarizing macOS builds + required: false + sparkle-ed25519-key: + description: Private key for signing Sparkle updates + required: false + +runs: + using: composite + + steps: + - name: Fetch codesign certificate + shell: bash + run: | + echo '${{ inputs.apple-codesign-cert }}' | base64 --decode > codesign.p12 + if [ -n '${{ inputs.apple-codesign-id }}' ]; then + security create-keychain -p '${{ inputs.apple-codesign-password }}' build.keychain + security default-keychain -s build.keychain + security unlock-keychain -p '${{ inputs.apple-codesign-password }}' build.keychain + security import codesign.p12 -k build.keychain -P '${{ inputs.apple-codesign-password }}' -T /usr/bin/codesign + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k '${{ inputs.apple-codesign-password }}' build.keychain + else + echo ":warning: Using ad-hoc code signing for macOS, as certificate was not present." >> $GITHUB_STEP_SUMMARY + fi + + - name: Package + shell: bash + env: + BUILD_DIR: build + INSTALL_DIR: install + run: | + cmake --install ${{ env.BUILD_DIR }} + + cd ${{ env.INSTALL_DIR }} + chmod +x "PrismLauncher.app/Contents/MacOS/prismlauncher" + + if [ -n '${{ inputs.apple-codesign-id }}' ]; then + APPLE_CODESIGN_ID='${{ inputs.apple-codesign-id }}' + ENTITLEMENTS_FILE='../program_info/App.entitlements' + else + APPLE_CODESIGN_ID='-' + ENTITLEMENTS_FILE='../program_info/AdhocSignedApp.entitlements' + fi + + sudo codesign --sign "$APPLE_CODESIGN_ID" --deep --force --entitlements "$ENTITLEMENTS_FILE" --options runtime "PrismLauncher.app/Contents/MacOS/prismlauncher" + mv "PrismLauncher.app" "Prism Launcher.app" + + - name: Notarize + shell: bash + env: + INSTALL_DIR: install + run: | + cd ${{ env.INSTALL_DIR }} + + if [ -n '${{ inputs.apple-notarize-password }}' ]; then + ditto -c -k --sequesterRsrc --keepParent "Prism Launcher.app" ../PrismLauncher.zip + xcrun notarytool submit ../PrismLauncher.zip \ + --wait --progress \ + --apple-id '${{ inputs.apple-notarize-apple-id }}' \ + --team-id '${{ inputs.apple-notarize-team-id }}' \ + --password '${{ inputs.apple-notarize-password }}' + + xcrun stapler staple "Prism Launcher.app" + else + echo ":warning: Skipping notarization as credentials are not present." >> $GITHUB_STEP_SUMMARY + fi + ditto -c -k --sequesterRsrc --keepParent "Prism Launcher.app" ../PrismLauncher.zip + + - name: Make Sparkle signature + shell: bash + run: | + if [ '${{ inputs.sparkle-ed25519-key }}' != '' ]; then + echo '${{ inputs.sparkle-ed25519-key }}' > ed25519-priv.pem + signature=$(/opt/homebrew/opt/openssl@3/bin/openssl pkeyutl -sign -rawin -in ${{ github.workspace }}/PrismLauncher.zip -inkey ed25519-priv.pem | openssl base64 | tr -d \\n) + rm ed25519-priv.pem + cat >> $GITHUB_STEP_SUMMARY << EOF + ### Artifact Information :information_source: + - :memo: Sparkle Signature (ed25519): \`$signature\` + EOF + else + cat >> $GITHUB_STEP_SUMMARY << EOF + ### Artifact Information :information_source: + - :warning: Sparkle Signature (ed25519): No private key available (likely a pull request or fork) + EOF + fi + + - name: Upload binary tarball + uses: actions/upload-artifact@v4 + with: + name: PrismLauncher-${{ inputs.artifact-name }}-${{ inputs.version }}-${{ inputs.build-type }} + path: PrismLauncher.zip diff --git a/.github/actions/package/windows/action.yml b/.github/actions/package/windows/action.yml new file mode 100644 index 000000000..60b2c75d1 --- /dev/null +++ b/.github/actions/package/windows/action.yml @@ -0,0 +1,143 @@ +name: Package for Windows +description: Create a Windows package for Prism Launcher + +inputs: + version: + description: Launcher version + required: true + build-type: + description: Type for the build + required: true + default: Debug + artifact-name: + description: Name of the uploaded artifact + required: true + msystem: + description: MSYS2 subsystem to use + required: true + default: false + windows-codesign-cert: + description: Certificate for signing Windows builds + required: false + windows-codesign-password: + description: Password for signing Windows builds + required: false + +runs: + using: composite + + steps: + - name: Package (MinGW) + if: ${{ inputs.msystem != '' }} + shell: msys2 {0} + env: + BUILD_DIR: build + INSTALL_DIR: install + run: | + cmake --install ${{ env.BUILD_DIR }} + touch ${{ env.INSTALL_DIR }}/manifest.txt + for l in $(find ${{ env.INSTALL_DIR }} -type f); do l=$(cygpath -u $l); l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_DIR }}/}; l=${l#./}; echo $l; done >> ${{ env.INSTALL_DIR }}/manifest.txt + + - name: Package (MSVC) + if: ${{ inputs.msystem == '' }} + shell: pwsh + env: + BUILD_DIR: build + INSTALL_DIR: install + run: | + cmake --install ${{ env.BUILD_DIR }} --config ${{ inputs.build-type }} + + cd ${{ github.workspace }} + + Get-ChildItem ${{ env.INSTALL_DIR }} -Recurse | ForEach FullName | Resolve-Path -Relative | %{ $_.TrimStart('.\') } | %{ $_.TrimStart('${{ env.INSTALL_DIR }}') } | %{ $_.TrimStart('\') } | Out-File -FilePath ${{ env.INSTALL_DIR }}/manifest.txt + + - name: Fetch codesign certificate + shell: bash # yes, we are not using MSYS2 or PowerShell here + run: | + echo '${{ inputs.windows-codesign-cert }}' | base64 --decode > codesign.pfx + + - name: Sign executable + shell: pwsh + env: + INSTALL_DIR: install + run: | + if (Get-Content ./codesign.pfx){ + cd ${{ env.INSTALL_DIR }} + # We ship the exact same executable for portable and non-portable editions, so signing just once is fine + SignTool sign /fd sha256 /td sha256 /f ../codesign.pfx /p '${{ inputs.windows-codesign-password }}' /tr http://timestamp.digicert.com prismlauncher.exe prismlauncher_updater.exe prismlauncher_filelink.exe + } else { + ":warning: Skipped code signing for Windows, as certificate was not present." >> $env:GITHUB_STEP_SUMMARY + } + + - name: Package (MinGW, portable) + if: ${{ inputs.msystem != '' }} + shell: msys2 {0} + env: + BUILD_DIR: build + INSTALL_DIR: install + INSTALL_PORTABLE_DIR: install-portable + run: | + 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 + for l in $(find ${{ env.INSTALL_PORTABLE_DIR }} -type f); do l=$(cygpath -u $l); l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_PORTABLE_DIR }}/}; l=${l#./}; echo $l; done >> ${{ env.INSTALL_PORTABLE_DIR }}/manifest.txt + + - name: Package (MSVC, portable) + if: ${{ inputs.msystem == '' }} + shell: pwsh + env: + BUILD_DIR: build + INSTALL_DIR: install + INSTALL_PORTABLE_DIR: install-portable + run: | + 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 + + Get-ChildItem ${{ env.INSTALL_PORTABLE_DIR }} -Recurse | ForEach FullName | Resolve-Path -Relative | %{ $_.TrimStart('.\') } | %{ $_.TrimStart('${{ env.INSTALL_PORTABLE_DIR }}') } | %{ $_.TrimStart('\') } | Out-File -FilePath ${{ env.INSTALL_DIR }}/manifest.txt + + - name: Package (installer) + shell: pwsh + env: + BUILD_DIR: build + INSTALL_DIR: install + + NSCURL_VERSION: "v24.9.26.122" + NSCURL_SHA256: "AEE6C4BE3CB6455858E9C1EE4B3AFE0DB9960FA03FE99CCDEDC28390D57CCBB0" + run: | + New-Item -Name NSISPlugins -ItemType Directory + Invoke-Webrequest https://github.com/negrutiu/nsis-nscurl/releases/download/"${{ env.NSCURL_VERSION }}"/NScurl.zip -OutFile NSISPlugins\NScurl.zip + $nscurl_hash = Get-FileHash NSISPlugins\NScurl.zip -Algorithm Sha256 | Select-Object -ExpandProperty Hash + if ( $nscurl_hash -ne "${{ env.nscurl_sha256 }}") { + echo "::error:: NSCurl.zip sha256 mismatch" + exit 1 + } + Expand-Archive -Path NSISPlugins\NScurl.zip -DestinationPath NSISPlugins\NScurl + + cd ${{ env.INSTALL_DIR }} + makensis -NOCD "${{ github.workspace }}/${{ env.BUILD_DIR }}/program_info/win_install.nsi" + + - name: Sign installer + shell: pwsh + run: | + if (Get-Content ./codesign.pfx){ + SignTool sign /fd sha256 /td sha256 /f codesign.pfx /p '${{ inputs.windows-codesign-password }}' /tr http://timestamp.digicert.com PrismLauncher-Setup.exe + } else { + ":warning: Skipped code signing for Windows, as certificate was not present." >> $env:GITHUB_STEP_SUMMARY + } + + - name: Upload binary zip + uses: actions/upload-artifact@v4 + with: + name: PrismLauncher-${{ inputs.artifact-name }}-${{ inputs.version }}-${{ inputs.build-type }} + path: install/** + + - name: Upload portable zip + uses: actions/upload-artifact@v4 + with: + name: PrismLauncher-${{ inputs.artifact-name }}-Portable-${{ inputs.version }}-${{ inputs.build-type }} + path: install-portable/** + + - name: Upload installer + uses: actions/upload-artifact@v4 + with: + name: PrismLauncher-${{ inputs.artifact-name }}-Setup-${{ inputs.version }}-${{ inputs.build-type }} + path: PrismLauncher-Setup.exe diff --git a/.github/actions/setup-dependencies/action.yml b/.github/actions/setup-dependencies/action.yml new file mode 100644 index 000000000..e583989b0 --- /dev/null +++ b/.github/actions/setup-dependencies/action.yml @@ -0,0 +1,67 @@ +name: Setup Dependencies +description: Install and setup dependencies for building Prism Launcher + +inputs: + build-type: + description: Type for the build + required: true + default: Debug + msystem: + description: MSYS2 subsystem to use + required: false + vcvars-arch: + description: Visual Studio architecture to use + required: false + qt-architecture: + description: Qt architecture + required: false + qt-version: + description: Version of Qt to use + required: true + default: 6.8.1 + +outputs: + build-type: + description: Type of build used + value: ${{ inputs.build-type }} + qt-version: + description: Version of Qt used + value: ${{ inputs.qt-version }} + +runs: + using: composite + + steps: + - name: Setup Linux dependencies + if: ${{ runner.os == 'Linux' }} + uses: ./.github/actions/setup-dependencies/linux + + - name: Setup macOS dependencies + if: ${{ runner.os == 'macOS' }} + uses: ./.github/actions/setup-dependencies/macos + + - name: Setup Windows dependencies + if: ${{ runner.os == 'Windows' }} + uses: ./.github/actions/setup-dependencies/windows + with: + build-type: ${{ inputs.build-type }} + msystem: ${{ inputs.msystem }} + vcvars-arch: ${{ inputs.vcvars-arch }} + + # TODO(@getchoo): Get this working on MSYS2! + - name: Setup ccache + if: ${{ (runner.os != 'Windows' || inputs.msystem == '') && inputs.build-type == 'Debug' }} + uses: hendrikmuhs/ccache-action@v1.2.17 + with: + create-symlink: ${{ runner.os != 'Windows' }} + key: ${{ runner.os }}-qt${{ inputs.qt_ver }}-${{ inputs.architecture }} + + - name: Install Qt + if: ${{ inputs.msystem == '' }} + uses: jurplel/install-qt-action@v4 + with: + aqtversion: "==3.1.*" + version: ${{ inputs.qt-version }} + arch: ${{ inputs.qt-architecture }} + modules: qt5compat qtimageformats qtnetworkauth + cache: ${{ inputs.build-type == 'Debug' }} diff --git a/.github/actions/setup-dependencies/linux/action.yml b/.github/actions/setup-dependencies/linux/action.yml new file mode 100644 index 000000000..dd0d28364 --- /dev/null +++ b/.github/actions/setup-dependencies/linux/action.yml @@ -0,0 +1,26 @@ +name: Setup Linux dependencies + +runs: + using: composite + + steps: + - name: Install host dependencies + shell: bash + run: | + sudo apt-get -y update + sudo apt-get -y install ninja-build extra-cmake-modules scdoc appstream libxcb-cursor-dev + + - name: Setup AppImage tooling + shell: bash + run: | + declare -A appimage_deps + appimage_deps["https://github.com/linuxdeploy/linuxdeploy/releases/download/1-alpha-20250213-2/linuxdeploy-x86_64.AppImage"]="4648f278ab3ef31f819e67c30d50f462640e5365a77637d7e6f2ad9fd0b4522a linuxdeploy-x86_64.AppImage" + appimage_deps["https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/1-alpha-20250213-1/linuxdeploy-plugin-qt-x86_64.AppImage"]="15106be885c1c48a021198e7e1e9a48ce9d02a86dd0a1848f00bdbf3c1c92724 linuxdeploy-plugin-qt-x86_64.AppImage" + appimage_deps["https://github.com/AppImageCommunity/AppImageUpdate/releases/download/2.0.0-alpha-1-20241225/AppImageUpdate-x86_64.AppImage"]="f1747cf60058e99f1bb9099ee9787d16c10241313b7acec81810ea1b1e568c11 AppImageUpdate-x86_64.AppImage" + + for url in "${!appimage_deps[@]}"; do + curl -LO "$url" + sha256sum -c - <<< "${appimage_deps[$url]}" + done + + sudo apt -y install libopengl0 diff --git a/.github/actions/setup-dependencies/macos/action.yml b/.github/actions/setup-dependencies/macos/action.yml new file mode 100644 index 000000000..dcbb308c2 --- /dev/null +++ b/.github/actions/setup-dependencies/macos/action.yml @@ -0,0 +1,16 @@ +name: Setup macOS dependencies + +runs: + using: composite + + steps: + - name: Install dependencies + shell: bash + run: | + brew update + brew install ninja extra-cmake-modules temurin@17 + + - name: Set JAVA_HOME + shell: bash + run: | + echo "JAVA_HOME=$(/usr/libexec/java_home -v 17)" >> "$GITHUB_ENV" diff --git a/.github/actions/setup-dependencies/windows/action.yml b/.github/actions/setup-dependencies/windows/action.yml new file mode 100644 index 000000000..782d02348 --- /dev/null +++ b/.github/actions/setup-dependencies/windows/action.yml @@ -0,0 +1,85 @@ +name: Setup Windows Dependencies + +inputs: + build-type: + description: Type for the build + required: true + default: Debug + msystem: + description: MSYS2 subsystem to use + required: false + vcvars-arch: + description: Visual Studio architecture to use + required: true + default: amd64 + +runs: + using: composite + + steps: + # NOTE: Installed on MinGW as well for SignTool + - name: Enter VS Developer shell + if: ${{ runner.os == 'Windows' }} + uses: ilammy/msvc-dev-cmd@v1 + with: + arch: ${{ inputs.vcvars-arch }} + vsversion: 2022 + + - name: Setup MSYS2 (MinGW-64) + if: ${{ inputs.msystem != '' }} + uses: msys2/setup-msys2@v2 + with: + msystem: ${{ inputs.msystem }} + update: true + install: >- + git + mingw-w64-x86_64-binutils + pacboy: >- + toolchain:p + cmake:p + extra-cmake-modules:p + ninja:p + qt6-base:p + qt6-svg:p + qt6-imageformats:p + quazip-qt6:p + ccache:p + qt6-5compat:p + qt6-networkauth:p + cmark:p + + - name: Force newer ccache (MSVC) + if: ${{ inputs.msystem == '' && inputs.build-type == 'Debug' }} + shell: bash + run: | + choco install ccache --version 4.7.1 + + - name: Configure ccache (MSVC) + if: ${{ inputs.msystem == '' && inputs.build-type == 'Debug' }} + shell: pwsh + run: | + # https://github.com/ccache/ccache/wiki/MS-Visual-Studio (I coudn't figure out the compiler prefix) + Copy-Item C:/ProgramData/chocolatey/lib/ccache/tools/ccache-4.7.1-windows-x86_64/ccache.exe -Destination C:/ProgramData/chocolatey/lib/ccache/tools/ccache-4.7.1-windows-x86_64/cl.exe + echo "CLToolExe=cl.exe" >> $env:GITHUB_ENV + echo "CLToolPath=C:/ProgramData/chocolatey/lib/ccache/tools/ccache-4.7.1-windows-x86_64/" >> $env:GITHUB_ENV + echo "TrackFileAccess=false" >> $env:GITHUB_ENV + # Needed for ccache, but also speeds up compile + echo "UseMultiToolTask=true" >> $env:GITHUB_ENV + + - name: Retrieve ccache cache (MinGW) + if: ${{ inputs.msystem != '' && inputs.build-type == 'Debug' }} + uses: actions/cache@v4.2.3 + with: + path: '${{ github.workspace }}\.ccache' + key: ${{ runner.os }}-mingw-w64-ccache-${{ github.run_id }} + restore-keys: | + ${{ runner.os }}-mingw-w64-ccache + + - name: Setup ccache (MinGW) + if: ${{ inputs.msystem != '' && inputs.build-type == 'Debug' }} + 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 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5de1c44fb..3408cf624 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,581 +3,138 @@ name: Build on: workflow_call: inputs: - build_type: - description: Type of build (Debug, Release, RelWithDebInfo, MinSizeRel) + build-type: + description: Type of build (Debug or Release) type: string default: Debug - is_qt_cached: - description: Enable Qt caching or not - type: string - default: true - secrets: - SPARKLE_ED25519_KEY: - description: Private key for signing Sparkle updates - required: false - WINDOWS_CODESIGN_CERT: - description: Certificate for signing Windows builds - required: false - WINDOWS_CODESIGN_PASSWORD: - description: Password for signing Windows builds - required: false - APPLE_CODESIGN_CERT: - description: Certificate for signing macOS builds - required: false - APPLE_CODESIGN_PASSWORD: - description: Password for signing macOS builds - required: false - APPLE_CODESIGN_ID: - description: Certificate ID for signing macOS builds - required: false - APPLE_NOTARIZE_APPLE_ID: - description: Apple ID used for notarizing macOS builds - required: false - APPLE_NOTARIZE_TEAM_ID: - description: Team ID used for notarizing macOS builds - required: false - APPLE_NOTARIZE_PASSWORD: - description: Password used for notarizing macOS builds - required: false - GPG_PRIVATE_KEY: - description: Private key for AppImage signing - required: false - GPG_PRIVATE_KEY_ID: - description: ID for the GPG_PRIVATE_KEY, to select the signing key - required: false jobs: build: + name: Build (${{ matrix.artifact-name }}) + strategy: fail-fast: false matrix: include: - os: ubuntu-22.04 - cmake_preset: linux - qt_ver: 6 - qt_host: linux - qt_arch: "" - qt_version: "6.8.1" - qt_modules: "qt5compat qtimageformats qtnetworkauth" - linuxdeploy_hash: "4648f278ab3ef31f819e67c30d50f462640e5365a77637d7e6f2ad9fd0b4522a linuxdeploy-x86_64.AppImage" - linuxdeploy_qt_hash: "15106be885c1c48a021198e7e1e9a48ce9d02a86dd0a1848f00bdbf3c1c92724 linuxdeploy-plugin-qt-x86_64.AppImage" - appimageupdate_hash: "f1747cf60058e99f1bb9099ee9787d16c10241313b7acec81810ea1b1e568c11 AppImageUpdate-x86_64.AppImage" + artifact-name: Linux + base-cmake-preset: linux - os: windows-2022 - name: "Windows-MinGW-w64" - cmake_preset: windows_mingw + artifact-name: Windows-MinGW-w64 + base-cmake-preset: windows_mingw msystem: clang64 - vcvars_arch: "amd64_x86" + vcvars-arch: amd64_x86 - os: windows-2022 - name: "Windows-MSVC" - cmake_preset: windows_msvc - msystem: "" - architecture: "x64" - vcvars_arch: "amd64" - qt_ver: 6 - qt_host: "windows" - qt_arch: "win64_msvc2022_64" - qt_version: "6.8.1" - qt_modules: "qt5compat qtimageformats qtnetworkauth" - nscurl_tag: "v24.9.26.122" - nscurl_sha256: "AEE6C4BE3CB6455858E9C1EE4B3AFE0DB9960FA03FE99CCDEDC28390D57CCBB0" + artifact-name: Windows-MSVC + base-cmake-preset: windows_msvc + # TODO(@getchoo): This is the default in setup-dependencies/windows. Why isn't it working?!?! + vcvars-arch: amd64 - os: windows-2022 - name: "Windows-MSVC-arm64" - cmake_preset: windows_msvc_arm64_cross - msystem: "" - architecture: "arm64" - vcvars_arch: "amd64_arm64" - qt_ver: 6 - qt_host: "windows" - qt_arch: "win64_msvc2022_arm64_cross_compiled" - qt_version: "6.8.1" - qt_modules: "qt5compat qtimageformats qtnetworkauth" - nscurl_tag: "v24.9.26.122" - nscurl_sha256: "AEE6C4BE3CB6455858E9C1EE4B3AFE0DB9960FA03FE99CCDEDC28390D57CCBB0" + artifact-name: Windows-MSVC-arm64 + base-cmake-preset: windows_msvc_arm64_cross + vcvars-arch: amd64_arm64 + qt-architecture: win64_msvc2022_arm64_cross_compiled - os: macos-14 - name: macOS - cmake_preset: ${{ inputs.build_type == 'Debug' && 'macos_universal' || 'macos' }} - macosx_deployment_target: 11.0 - qt_ver: 6 - qt_host: mac - qt_arch: "" - qt_version: "6.8.1" - qt_modules: "qt5compat qtimageformats qtnetworkauth" + artifact-name: macOS + base-cmake-preset: ${{ inputs.build-type == 'Debug' && 'macos_universal' || 'macos' }} + macosx-deployment-target: 11.0 runs-on: ${{ matrix.os }} + defaults: + run: + shell: ${{ matrix.msystem != '' && 'msys2 {0}' || 'bash' }} + env: - MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }} - INSTALL_DIR: "install" - INSTALL_PORTABLE_DIR: "install-portable" - INSTALL_APPIMAGE_DIR: "install-appdir" - BUILD_DIR: "build" - CCACHE_VAR: "" - HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: 1 + MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx-deployment-target }} steps: ## - # PREPARE + # SETUP ## + - name: Checkout uses: actions/checkout@v4 with: - submodules: "true" + submodules: true - - name: "Setup MSYS2" - if: runner.os == 'Windows' && matrix.msystem != '' - uses: msys2/setup-msys2@v2 + - name: Setup dependencies + id: setup-dependencies + uses: ./.github/actions/setup-dependencies with: + build-type: ${{ inputs.build-type || 'Debug' }} msystem: ${{ matrix.msystem }} - update: true - install: >- - git - mingw-w64-x86_64-binutils - pacboy: >- - toolchain:p - cmake:p - extra-cmake-modules:p - ninja:p - qt6-base:p - qt6-svg:p - qt6-imageformats:p - quazip-qt6:p - ccache:p - qt6-5compat:p - qt6-networkauth:p - cmark:p - - - name: Force newer ccache - if: runner.os == 'Windows' && matrix.msystem == '' && inputs.build_type == 'Debug' - run: | - choco install ccache --version 4.7.1 - - - name: Setup ccache - if: (runner.os != 'Windows' || matrix.msystem == '') && inputs.build_type == 'Debug' - uses: hendrikmuhs/ccache-action@v1.2.18 - with: - create-symlink: ${{ runner.os != 'Windows' }} - key: ${{ matrix.os }}-qt${{ matrix.qt_ver }}-${{ matrix.architecture }} - - - 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 MinGW-w64) - if: runner.os == 'Windows' && matrix.msystem != '' && inputs.build_type == 'Debug' - uses: actions/cache@v4.2.3 - with: - path: '${{ github.workspace }}\.ccache' - key: ${{ matrix.os }}-mingw-w64-ccache-${{ github.run_id }} - restore-keys: | - ${{ matrix.os }}-mingw-w64-ccache - - - name: Setup ccache (Windows MinGW-w64) - if: runner.os == 'Windows' && matrix.msystem != '' && inputs.build_type == 'Debug' - 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: Configure ccache (Windows MSVC) - if: ${{ runner.os == 'Windows' && matrix.msystem == '' && inputs.build_type == 'Debug' }} - run: | - # https://github.com/ccache/ccache/wiki/MS-Visual-Studio (I coudn't figure out the compiler prefix) - Copy-Item C:/ProgramData/chocolatey/lib/ccache/tools/ccache-4.7.1-windows-x86_64/ccache.exe -Destination C:/ProgramData/chocolatey/lib/ccache/tools/ccache-4.7.1-windows-x86_64/cl.exe - echo "CLToolExe=cl.exe" >> $env:GITHUB_ENV - echo "CLToolPath=C:/ProgramData/chocolatey/lib/ccache/tools/ccache-4.7.1-windows-x86_64/" >> $env:GITHUB_ENV - echo "TrackFileAccess=false" >> $env:GITHUB_ENV - # Needed for ccache, but also speeds up compile - echo "UseMultiToolTask=true" >> $env:GITHUB_ENV - - - name: Set short version - shell: bash - run: | - ver_short=`git rev-parse --short HEAD` - echo "VERSION=$ver_short" >> $GITHUB_ENV - - - name: Install Dependencies (Linux) - if: runner.os == 'Linux' - run: | - sudo apt-get -y update - sudo apt-get -y install ninja-build extra-cmake-modules scdoc appstream libxcb-cursor-dev - - - name: Install Dependencies (macOS) - if: runner.os == 'macOS' - run: | - brew update - brew install ninja extra-cmake-modules - - - name: Install host Qt (Windows MSVC arm64) - if: runner.os == 'Windows' && matrix.architecture == 'arm64' - uses: jurplel/install-qt-action@v4 - with: - aqtversion: "==3.1.*" - py7zrversion: ">=0.20.2" - version: ${{ matrix.qt_version }} - host: "windows" - target: "desktop" - arch: ${{ matrix.qt_arch }} - modules: ${{ matrix.qt_modules }} - cache: ${{ inputs.is_qt_cached }} - cache-key-prefix: host-qt-arm64-windows - dir: ${{ github.workspace }}\HostQt - set-env: false - - - name: Install Qt (macOS, Linux & Windows MSVC) - if: matrix.msystem == '' - uses: jurplel/install-qt-action@v4 - with: - aqtversion: "==3.1.*" - py7zrversion: ">=0.20.2" - version: ${{ matrix.qt_version }} - target: "desktop" - arch: ${{ matrix.qt_arch }} - modules: ${{ matrix.qt_modules }} - tools: ${{ matrix.qt_tools }} - cache: ${{ inputs.is_qt_cached }} - - - name: Install MSVC (Windows MSVC) - if: runner.os == 'Windows' # We want this for MinGW builds as well, as we need SignTool - uses: ilammy/msvc-dev-cmd@v1 - with: - vsversion: 2022 - arch: ${{ matrix.vcvars_arch }} - - - name: Prepare AppImage (Linux) - if: runner.os == 'Linux' - env: - APPIMAGEUPDATE_HASH: ${{ matrix.appimageupdate_hash }} - LINUXDEPLOY_HASH: ${{ matrix.linuxdeploy_hash }} - LINUXDEPLOY_QT_HASH: ${{ matrix.linuxdeploy_qt_hash }} - run: | - wget "https://github.com/linuxdeploy/linuxdeploy/releases/download/1-alpha-20250213-2/linuxdeploy-x86_64.AppImage" - wget "https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/1-alpha-20250213-1/linuxdeploy-plugin-qt-x86_64.AppImage" - - wget "https://github.com/AppImageCommunity/AppImageUpdate/releases/download/2.0.0-alpha-1-20241225/AppImageUpdate-x86_64.AppImage" - - sha256sum -c - <<< "$LINUXDEPLOY_HASH" - sha256sum -c - <<< "$LINUXDEPLOY_QT_HASH" - sha256sum -c - <<< "$APPIMAGEUPDATE_HASH" - - sudo apt install libopengl0 - - - name: Add QT_HOST_PATH var (Windows MSVC arm64) - if: runner.os == 'Windows' && matrix.architecture == 'arm64' - run: | - echo "QT_HOST_PATH=${{ github.workspace }}\HostQt\Qt\${{ matrix.qt_version }}\msvc2022_64" >> $env:GITHUB_ENV - - - name: Setup java (macOS) - if: runner.os == 'macOS' - uses: actions/setup-java@v4 - with: - distribution: "temurin" - java-version: "17" + vcvars-arch: ${{ matrix.vcvars-arch }} + qt-architecture: ${{ matrix.qt-architecture }} ## - # SOURCE BUILD + # BUILD ## - - name: Run CMake workflow - if: ${{ runner.os != 'Windows' || matrix.msystem == '' }} - shell: bash + - name: Get CMake preset + id: cmake-preset env: - CMAKE_PRESET: ${{ matrix.cmake_preset }} - PRESET_TYPE: ${{ inputs.build_type == 'Debug' && 'debug' || 'ci' }} + BASE_CMAKE_PRESET: ${{ matrix.base-cmake-preset }} + PRESET_TYPE: ${{ inputs.build-type == 'Debug' && 'debug' || 'ci' }} run: | - cmake --workflow --preset "$CMAKE_PRESET"_"$PRESET_TYPE" + echo preset="$BASE_CMAKE_PRESET"_"$PRESET_TYPE" >> "$GITHUB_OUTPUT" - # NOTE: Split due to the `shell` requirement for msys2 - # TODO(@getchoo): Get ccache working! - - name: Run CMake workflow (Windows MinGW-w64) - if: ${{ runner.os == 'Windows' && matrix.msystem != '' }} - shell: msys2 {0} + - name: Run CMake workflow env: - CMAKE_PRESET: ${{ matrix.cmake_preset }} - PRESET_TYPE: ${{ inputs.build_type == 'Debug' && 'debug' || 'ci' }} + CMAKE_PRESET: ${{ steps.cmake-preset.outputs.preset }} run: | - cmake --workflow --preset "$CMAKE_PRESET"_"$PRESET_TYPE" + cmake --workflow --preset "$CMAKE_PRESET" ## - # PACKAGE BUILDS + # PACKAGE ## - - name: Fetch codesign certificate (macOS) - if: runner.os == 'macOS' - run: | - echo '${{ secrets.APPLE_CODESIGN_CERT }}' | base64 --decode > codesign.p12 - if [ -n '${{ secrets.APPLE_CODESIGN_ID }}' ]; then - security create-keychain -p '${{ secrets.APPLE_CODESIGN_PASSWORD }}' build.keychain - security default-keychain -s build.keychain - security unlock-keychain -p '${{ secrets.APPLE_CODESIGN_PASSWORD }}' build.keychain - security import codesign.p12 -k build.keychain -P '${{ secrets.APPLE_CODESIGN_PASSWORD }}' -T /usr/bin/codesign - security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k '${{ secrets.APPLE_CODESIGN_PASSWORD }}' build.keychain - else - echo ":warning: Using ad-hoc code signing for macOS, as certificate was not present." >> $GITHUB_STEP_SUMMARY - fi - - - name: Package (macOS) - if: runner.os == 'macOS' - run: | - cmake --install ${{ env.BUILD_DIR }} - - cd ${{ env.INSTALL_DIR }} - chmod +x "PrismLauncher.app/Contents/MacOS/prismlauncher" - - if [ -n '${{ secrets.APPLE_CODESIGN_ID }}' ]; then - APPLE_CODESIGN_ID='${{ secrets.APPLE_CODESIGN_ID }}' - ENTITLEMENTS_FILE='../program_info/App.entitlements' - else - APPLE_CODESIGN_ID='-' - ENTITLEMENTS_FILE='../program_info/AdhocSignedApp.entitlements' - fi - - sudo codesign --sign "$APPLE_CODESIGN_ID" --deep --force --entitlements "$ENTITLEMENTS_FILE" --options runtime "PrismLauncher.app/Contents/MacOS/prismlauncher" - mv "PrismLauncher.app" "Prism Launcher.app" - - - name: Notarize (macOS) - if: runner.os == 'macOS' - run: | - cd ${{ env.INSTALL_DIR }} - - if [ -n '${{ secrets.APPLE_NOTARIZE_PASSWORD }}' ]; then - ditto -c -k --sequesterRsrc --keepParent "Prism Launcher.app" ../PrismLauncher.zip - xcrun notarytool submit ../PrismLauncher.zip \ - --wait --progress \ - --apple-id '${{ secrets.APPLE_NOTARIZE_APPLE_ID }}' \ - --team-id '${{ secrets.APPLE_NOTARIZE_TEAM_ID }}' \ - --password '${{ secrets.APPLE_NOTARIZE_PASSWORD }}' - - xcrun stapler staple "Prism Launcher.app" - else - echo ":warning: Skipping notarization as credentials are not present." >> $GITHUB_STEP_SUMMARY - fi - ditto -c -k --sequesterRsrc --keepParent "Prism Launcher.app" ../PrismLauncher.zip - - - name: Make Sparkle signature (macOS) - if: matrix.name == 'macOS' - run: | - if [ '${{ secrets.SPARKLE_ED25519_KEY }}' != '' ]; then - echo '${{ secrets.SPARKLE_ED25519_KEY }}' > ed25519-priv.pem - signature=$(/opt/homebrew/opt/openssl@3/bin/openssl pkeyutl -sign -rawin -in ${{ github.workspace }}/PrismLauncher.zip -inkey ed25519-priv.pem | openssl base64 | tr -d \\n) - rm ed25519-priv.pem - cat >> $GITHUB_STEP_SUMMARY << EOF - ### Artifact Information :information_source: - - :memo: Sparkle Signature (ed25519): \`$signature\` - EOF - else - cat >> $GITHUB_STEP_SUMMARY << EOF - ### Artifact Information :information_source: - - :warning: Sparkle Signature (ed25519): No private key available (likely a pull request or fork) - EOF - fi - - - name: Package (Windows MinGW-w64) - if: runner.os == 'Windows' && matrix.msystem != '' - shell: msys2 {0} - run: | - cmake --install ${{ env.BUILD_DIR }} - touch ${{ env.INSTALL_DIR }}/manifest.txt - for l in $(find ${{ env.INSTALL_DIR }} -type f); do l=$(cygpath -u $l); l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_DIR }}/}; l=${l#./}; echo $l; done >> ${{ env.INSTALL_DIR }}/manifest.txt - - - name: Package (Windows MSVC) - if: runner.os == 'Windows' && matrix.msystem == '' - run: | - cmake --install ${{ env.BUILD_DIR }} --config ${{ inputs.build_type }} - - cd ${{ github.workspace }} - - Get-ChildItem ${{ env.INSTALL_DIR }} -Recurse | ForEach FullName | Resolve-Path -Relative | %{ $_.TrimStart('.\') } | %{ $_.TrimStart('${{ env.INSTALL_DIR }}') } | %{ $_.TrimStart('\') } | Out-File -FilePath ${{ env.INSTALL_DIR }}/manifest.txt - - - name: Fetch codesign certificate (Windows) - if: runner.os == 'Windows' - shell: bash # yes, we are not using MSYS2 or PowerShell here - run: | - echo '${{ secrets.WINDOWS_CODESIGN_CERT }}' | base64 --decode > codesign.pfx - - - name: Sign executable (Windows) - if: runner.os == 'Windows' - run: | - if (Get-Content ./codesign.pfx){ - cd ${{ env.INSTALL_DIR }} - # We ship the exact same executable for portable and non-portable editions, so signing just once is fine - SignTool sign /fd sha256 /td sha256 /f ../codesign.pfx /p '${{ secrets.WINDOWS_CODESIGN_PASSWORD }}' /tr http://timestamp.digicert.com prismlauncher.exe prismlauncher_updater.exe prismlauncher_filelink.exe - } else { - ":warning: Skipped code signing for Windows, as certificate was not present." >> $env:GITHUB_STEP_SUMMARY - } - - - name: Package (Windows MinGW-w64, portable) - if: runner.os == 'Windows' && matrix.msystem != '' - shell: msys2 {0} - run: | - 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 - for l in $(find ${{ env.INSTALL_PORTABLE_DIR }} -type f); do l=$(cygpath -u $l); l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_PORTABLE_DIR }}/}; l=${l#./}; echo $l; done >> ${{ env.INSTALL_PORTABLE_DIR }}/manifest.txt - - - name: Package (Windows MSVC, portable) - if: runner.os == 'Windows' && matrix.msystem == '' - run: | - 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 - - Get-ChildItem ${{ env.INSTALL_PORTABLE_DIR }} -Recurse | ForEach FullName | Resolve-Path -Relative | %{ $_.TrimStart('.\') } | %{ $_.TrimStart('${{ env.INSTALL_PORTABLE_DIR }}') } | %{ $_.TrimStart('\') } | Out-File -FilePath ${{ env.INSTALL_DIR }}/manifest.txt - - - name: Package (Windows, installer) - if: runner.os == 'Windows' - run: | - if ('${{ matrix.nscurl_tag }}') { - New-Item -Name NSISPlugins -ItemType Directory - Invoke-Webrequest https://github.com/negrutiu/nsis-nscurl/releases/download/${{ matrix.nscurl_tag }}/NScurl.zip -OutFile NSISPlugins\NScurl.zip - $nscurl_hash = Get-FileHash NSISPlugins\NScurl.zip -Algorithm Sha256 | Select-Object -ExpandProperty Hash - if ( $nscurl_hash -ne "${{ matrix.nscurl_sha256 }}") { - echo "::error:: NSCurl.zip sha256 mismatch" - exit 1 - } - Expand-Archive -Path NSISPlugins\NScurl.zip -DestinationPath NSISPlugins\NScurl - } - cd ${{ env.INSTALL_DIR }} - makensis -NOCD "${{ github.workspace }}/${{ env.BUILD_DIR }}/program_info/win_install.nsi" - - - name: Sign installer (Windows) - if: runner.os == 'Windows' - run: | - if (Get-Content ./codesign.pfx){ - SignTool sign /fd sha256 /td sha256 /f codesign.pfx /p '${{ secrets.WINDOWS_CODESIGN_PASSWORD }}' /tr http://timestamp.digicert.com PrismLauncher-Setup.exe - } else { - ":warning: Skipped code signing for Windows, as certificate was not present." >> $env:GITHUB_STEP_SUMMARY - } - - - name: Package AppImage (Linux) - if: runner.os == 'Linux' + - name: Get short version + id: short-version shell: bash - env: - GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} - run: | - cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_APPIMAGE_DIR }}/usr - - mv ${{ env.INSTALL_APPIMAGE_DIR }}/usr/share/metainfo/org.prismlauncher.PrismLauncher.metainfo.xml ${{ env.INSTALL_APPIMAGE_DIR }}/usr/share/metainfo/org.prismlauncher.PrismLauncher.appdata.xml - export "NO_APPSTREAM=1" # we have to skip appstream checking because appstream on ubuntu 20.04 is outdated - - export OUTPUT="PrismLauncher-Linux-x86_64.AppImage" - - chmod +x linuxdeploy-*.AppImage - - mkdir -p ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib - mkdir -p ${{ env.INSTALL_APPIMAGE_DIR }}/usr/plugins/iconengines - - cp -r ${{ runner.workspace }}/Qt/${{ matrix.qt_version }}/gcc_64/plugins/iconengines/* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/plugins/iconengines - - cp /usr/lib/x86_64-linux-gnu/libcrypto.so.* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/ - cp /usr/lib/x86_64-linux-gnu/libssl.so.* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/ - cp /usr/lib/x86_64-linux-gnu/libOpenGL.so.0* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/ - - LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib" - export LD_LIBRARY_PATH - - chmod +x AppImageUpdate-x86_64.AppImage - cp AppImageUpdate-x86_64.AppImage ${{ env.INSTALL_APPIMAGE_DIR }}/usr/bin - - export UPDATE_INFORMATION="gh-releases-zsync|${{ github.repository_owner }}|${{ github.event.repository.name }}|latest|PrismLauncher-Linux-x86_64.AppImage.zsync" - - if [ '${{ secrets.GPG_PRIVATE_KEY_ID }}' != '' ]; then - export SIGN=1 - export SIGN_KEY=${{ secrets.GPG_PRIVATE_KEY_ID }} - mkdir -p ~/.gnupg/ - echo "$GPG_PRIVATE_KEY" > ~/.gnupg/private.key - gpg --import ~/.gnupg/private.key - else - echo ":warning: Skipped code signing for Linux AppImage, as gpg key was not present." >> $GITHUB_STEP_SUMMARY - fi - - ./linuxdeploy-x86_64.AppImage --appdir ${{ env.INSTALL_APPIMAGE_DIR }} --output appimage --plugin qt -i ${{ env.INSTALL_APPIMAGE_DIR }}/usr/share/icons/hicolor/scalable/apps/org.prismlauncher.PrismLauncher.svg - - mv "PrismLauncher-Linux-x86_64.AppImage" "PrismLauncher-Linux-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage" - - - name: Package (Linux, portable) - if: runner.os == 'Linux' - env: - CMAKE_PRESET: ${{ matrix.cmake_preset }} - PRESET_TYPE: ${{ inputs.build_type == 'Debug' && 'debug' || 'ci' }} run: | - cmake --preset "$CMAKE_PRESET"_"$PRESET_TYPE" -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_PORTABLE_DIR }} -DINSTALL_BUNDLE=full - cmake --install ${{ env.BUILD_DIR }} - cmake --install ${{ env.BUILD_DIR }} --component portable + echo "version=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT" - mkdir ${{ env.INSTALL_PORTABLE_DIR }}/lib - cp /lib/x86_64-linux-gnu/libbz2.so.1.0 ${{ env.INSTALL_PORTABLE_DIR }}/lib - cp /usr/lib/x86_64-linux-gnu/libgobject-2.0.so.0 ${{ env.INSTALL_PORTABLE_DIR }}/lib - cp /usr/lib/x86_64-linux-gnu/libcrypto.so.* ${{ env.INSTALL_PORTABLE_DIR }}/lib - cp /usr/lib/x86_64-linux-gnu/libssl.so.* ${{ env.INSTALL_PORTABLE_DIR }}/lib - cp /usr/lib/x86_64-linux-gnu/libffi.so.*.* ${{ env.INSTALL_PORTABLE_DIR }}/lib - mv ${{ env.INSTALL_PORTABLE_DIR }}/bin/*.so* ${{ env.INSTALL_PORTABLE_DIR }}/lib - - for l in $(find ${{ env.INSTALL_PORTABLE_DIR }} -type f); do l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_PORTABLE_DIR }}/}; l=${l#./}; echo $l; done > ${{ env.INSTALL_PORTABLE_DIR }}/manifest.txt - cd ${{ env.INSTALL_PORTABLE_DIR }} - tar -czf ../PrismLauncher-portable.tar.gz * - - ## - # UPLOAD BUILDS - ## - - - name: Upload binary tarball (macOS) - if: runner.os == 'macOS' - uses: actions/upload-artifact@v4 - with: - name: PrismLauncher-${{ matrix.name }}-${{ env.VERSION }}-${{ inputs.build_type }} - path: PrismLauncher.zip - - - name: Upload binary zip (Windows) - if: runner.os == 'Windows' - uses: actions/upload-artifact@v4 + - name: Package (Linux) + if: ${{ runner.os == 'Linux' }} + uses: ./.github/actions/package/linux with: - name: PrismLauncher-${{ matrix.name }}-${{ env.VERSION }}-${{ inputs.build_type }} - path: ${{ env.INSTALL_DIR }}/** + version: ${{ steps.short-version.outputs.version }} + build-type: ${{ steps.setup-dependencies.outputs.build-type }} + cmake-preset: ${{ steps.cmake-preset.outputs.preset }} + qt-version: ${{ steps.setup-dependencies.outputs.qt-version }} - - name: Upload binary zip (Windows, portable) - if: runner.os == 'Windows' - uses: actions/upload-artifact@v4 - with: - name: PrismLauncher-${{ 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@v4 - with: - name: PrismLauncher-${{ matrix.name }}-Setup-${{ env.VERSION }}-${{ inputs.build_type }} - path: PrismLauncher-Setup.exe + gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} + gpg-private-key-id: ${{ secrets.GPG_PRIVATE_KEY_ID }} - - name: Upload binary tarball (Linux, portable) - if: runner.os == 'Linux' - uses: actions/upload-artifact@v4 - with: - name: PrismLauncher-${{ runner.os }}-Qt6-Portable-${{ env.VERSION }}-${{ inputs.build_type }} - path: PrismLauncher-portable.tar.gz - - - name: Upload AppImage (Linux) - if: runner.os == 'Linux' - uses: actions/upload-artifact@v4 + - name: Package (macOS) + if: ${{ runner.os == 'macOS' }} + uses: ./.github/actions/package/macos with: - name: PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage - path: PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage - - - name: Upload AppImage Zsync (Linux) - if: runner.os == 'Linux' - uses: actions/upload-artifact@v4 + version: ${{ steps.short-version.outputs.version }} + build-type: ${{ steps.setup-dependencies.outputs.build-type }} + artifact-name: ${{ matrix.artifact-name }} + + apple-codesign-cert: ${{ secrets.APPLE-CODESIGN-CERT }} + apple-codesign-password: ${{ secrets.APPLE-CODESIGN_PASSWORD }} + apple-codesign-id: ${{ secrets.APPLE-CODESIGN_ID }} + apple-notarize-apple-id: ${{ secrets.APPLE_NOTARIZE_APPLE_ID }} + apple-notarize-team-id: ${{ secrets.APPLE_NOTARIZE_TEAM_ID }} + apple-notarize-password: ${{ secrets.APPLE-NOTARIZE_PASSWORD }} + sparkle-ed25519-key: ${{ secrets.SPARKLE-ED25519_KEY }} + + - name: Package (Windows) + if: ${{ runner.os == 'Windows' }} + uses: ./.github/actions/package/windows with: - name: PrismLauncher-${{ runner.os }}-${{ env.VERSION }}-${{ inputs.build_type }}-x86_64.AppImage.zsync - path: PrismLauncher-Linux-x86_64.AppImage.zsync + version: ${{ steps.short-version.outputs.version }} + build-type: ${{ steps.setup-dependencies.outputs.build-type }} + artifact-name: ${{ matrix.artifact-name }} + msystem: ${{ matrix.msystem }} - - name: ccache stats (Windows MinGW-w64) - if: runner.os == 'Windows' && matrix.msystem != '' - shell: msys2 {0} - run: | - ccache -s + windows-codesign-cert: ${{ secrets.WINDOWS_CODESIGN_CERT }} + windows-codesign-password: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }} diff --git a/.github/workflows/trigger_builds.yml b/.github/workflows/trigger_builds.yml index 4be03f46f..e0d6f83ce 100644 --- a/.github/workflows/trigger_builds.yml +++ b/.github/workflows/trigger_builds.yml @@ -53,17 +53,5 @@ jobs: name: Build Debug uses: ./.github/workflows/build.yml with: - build_type: Debug - is_qt_cached: true - secrets: - SPARKLE_ED25519_KEY: ${{ secrets.SPARKLE_ED25519_KEY }} - WINDOWS_CODESIGN_CERT: ${{ secrets.WINDOWS_CODESIGN_CERT }} - WINDOWS_CODESIGN_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }} - APPLE_CODESIGN_CERT: ${{ secrets.APPLE_CODESIGN_CERT }} - APPLE_CODESIGN_PASSWORD: ${{ secrets.APPLE_CODESIGN_PASSWORD }} - APPLE_CODESIGN_ID: ${{ secrets.APPLE_CODESIGN_ID }} - APPLE_NOTARIZE_APPLE_ID: ${{ secrets.APPLE_NOTARIZE_APPLE_ID }} - APPLE_NOTARIZE_TEAM_ID: ${{ secrets.APPLE_NOTARIZE_TEAM_ID }} - APPLE_NOTARIZE_PASSWORD: ${{ secrets.APPLE_NOTARIZE_PASSWORD }} - GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} - GPG_PRIVATE_KEY_ID: ${{ secrets.GPG_PRIVATE_KEY_ID }} + build-type: Debug + secrets: inherit diff --git a/.github/workflows/trigger_release.yml b/.github/workflows/trigger_release.yml index 96f616a43..ae205895d 100644 --- a/.github/workflows/trigger_release.yml +++ b/.github/workflows/trigger_release.yml @@ -10,20 +10,8 @@ jobs: name: Build Release uses: ./.github/workflows/build.yml with: - build_type: Release - is_qt_cached: false - secrets: - SPARKLE_ED25519_KEY: ${{ secrets.SPARKLE_ED25519_KEY }} - WINDOWS_CODESIGN_CERT: ${{ secrets.WINDOWS_CODESIGN_CERT }} - WINDOWS_CODESIGN_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }} - APPLE_CODESIGN_CERT: ${{ secrets.APPLE_CODESIGN_CERT }} - APPLE_CODESIGN_PASSWORD: ${{ secrets.APPLE_CODESIGN_PASSWORD }} - APPLE_CODESIGN_ID: ${{ secrets.APPLE_CODESIGN_ID }} - APPLE_NOTARIZE_APPLE_ID: ${{ secrets.APPLE_NOTARIZE_APPLE_ID }} - APPLE_NOTARIZE_TEAM_ID: ${{ secrets.APPLE_NOTARIZE_TEAM_ID }} - APPLE_NOTARIZE_PASSWORD: ${{ secrets.APPLE_NOTARIZE_PASSWORD }} - GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} - GPG_PRIVATE_KEY_ID: ${{ secrets.GPG_PRIVATE_KEY_ID }} + build-type: Release + secrets: inherit create_release: needs: build_release From 77b88fc7ec468d43d154e5fa464bada1774cbd19 Mon Sep 17 00:00:00 2001 From: Seth Flynn Date: Tue, 29 Apr 2025 01:08:33 -0400 Subject: [PATCH 200/695] ci: run build workflow directly on push/prs Calling this from another workflow only for these events doesn't make much sense now Signed-off-by: Seth Flynn --- .github/workflows/build.yml | 53 +++++++++++++++++++++++++- .github/workflows/trigger_builds.yml | 57 ---------------------------- 2 files changed, 51 insertions(+), 59 deletions(-) delete mode 100644 .github/workflows/trigger_builds.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3408cf624..c393d4767 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,12 +1,61 @@ name: Build on: + push: + branches-ignore: + - "renovate/**" + paths: + # File types + - "**.cpp" + - "**.h" + - "**.java" + + # Directories + - "buildconfig/" + - "cmake/" + - "launcher/" + - "libraries/" + - "program_info/" + - "tests/" + + # Files + - "CMakeLists.txt" + - "COPYING.md" + + # Workflows + - ".github/workflows/build.yml" + pull_request: + paths: + # File types + - "**.cpp" + - "**.h" + + # Directories + - "buildconfig/" + - "cmake/" + - "launcher/" + - "libraries/" + - "program_info/" + - "tests/" + + # Files + - "CMakeLists.txt" + - "COPYING.md" + + # Workflows + - ".github/workflows/build.yml" workflow_call: inputs: build-type: description: Type of build (Debug or Release) type: string default: Debug + workflow_dispatch: + inputs: + build-type: + description: Type of build (Debug or Release) + type: string + default: Debug jobs: build: @@ -40,7 +89,7 @@ jobs: - os: macos-14 artifact-name: macOS - base-cmake-preset: ${{ inputs.build-type == 'Debug' && 'macos_universal' || 'macos' }} + base-cmake-preset: ${{ (inputs.build-type || 'Debug') == 'Debug' && 'macos_universal' || 'macos' }} macosx-deployment-target: 11.0 runs-on: ${{ matrix.os }} @@ -79,7 +128,7 @@ jobs: id: cmake-preset env: BASE_CMAKE_PRESET: ${{ matrix.base-cmake-preset }} - PRESET_TYPE: ${{ inputs.build-type == 'Debug' && 'debug' || 'ci' }} + PRESET_TYPE: ${{ (inputs.build-type || 'Debug') == 'Debug' && 'debug' || 'ci' }} run: | echo preset="$BASE_CMAKE_PRESET"_"$PRESET_TYPE" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/trigger_builds.yml b/.github/workflows/trigger_builds.yml deleted file mode 100644 index e0d6f83ce..000000000 --- a/.github/workflows/trigger_builds.yml +++ /dev/null @@ -1,57 +0,0 @@ -name: Build Application - -on: - push: - branches-ignore: - - "renovate/**" - paths: - # File types - - "**.cpp" - - "**.h" - - "**.java" - - # Directories - - "buildconfig/" - - "cmake/" - - "launcher/" - - "libraries/" - - "program_info/" - - "tests/" - - # Files - - "CMakeLists.txt" - - "COPYING.md" - - # Workflows - - ".github/workflows/build.yml" - - ".github/workflows/trigger_builds.yml" - pull_request: - paths: - # File types - - "**.cpp" - - "**.h" - - # Directories - - "buildconfig/" - - "cmake/" - - "launcher/" - - "libraries/" - - "program_info/" - - "tests/" - - # Files - - "CMakeLists.txt" - - "COPYING.md" - - # Workflows - - ".github/workflows/build.yml" - - ".github/workflows/trigger_builds.yml" - workflow_dispatch: - -jobs: - build_debug: - name: Build Debug - uses: ./.github/workflows/build.yml - with: - build-type: Debug - secrets: inherit From efa3392632890ba3bff4abc5b95bc85ab591fad6 Mon Sep 17 00:00:00 2001 From: Seth Flynn Date: Tue, 29 Apr 2025 01:09:19 -0400 Subject: [PATCH 201/695] ci: trigger_release -> release Signed-off-by: Seth Flynn --- .github/workflows/{trigger_release.yml => release.yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/workflows/{trigger_release.yml => release.yml} (100%) diff --git a/.github/workflows/trigger_release.yml b/.github/workflows/release.yml similarity index 100% rename from .github/workflows/trigger_release.yml rename to .github/workflows/release.yml From 1e617392ad24a3283205e89a792309c4744fd45f Mon Sep 17 00:00:00 2001 From: Seth Flynn Date: Wed, 30 Apr 2025 02:22:03 -0400 Subject: [PATCH 202/695] ci(codeql): use setup-dependencies action Signed-off-by: Seth Flynn --- .github/workflows/codeql.yml | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index a16738c9c..5a2ecbd6d 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -63,22 +63,10 @@ jobs: queries: security-and-quality languages: cpp, java - - name: Install Dependencies - run: sudo apt-get -y update - - sudo apt-get -y install ninja-build extra-cmake-modules scdoc - - - name: Install Qt - uses: jurplel/install-qt-action@v3 + - name: Setup dependencies + uses: ./.github/actions/setup-dependencies with: - aqtversion: "==3.1.*" - py7zrversion: ">=0.20.2" - version: "6.8.1" - host: "linux" - target: "desktop" - arch: "" - modules: "qt5compat qtimageformats qtnetworkauth" - tools: "" + build-type: Debug - name: Configure and Build run: | From 8c5333a5da67795e5d53a320b0147ebb4470a2a4 Mon Sep 17 00:00:00 2001 From: Seth Flynn Date: Wed, 30 Apr 2025 03:54:47 -0400 Subject: [PATCH 203/695] ci: use sccache on windows This apparently works with less hacks, and is actually suggested by the action we're using Signed-off-by: Seth Flynn --- .github/actions/setup-dependencies/action.yml | 11 +++++++++++ .../setup-dependencies/windows/action.yml | 18 ------------------ 2 files changed, 11 insertions(+), 18 deletions(-) diff --git a/.github/actions/setup-dependencies/action.yml b/.github/actions/setup-dependencies/action.yml index e583989b0..760ee3e87 100644 --- a/.github/actions/setup-dependencies/action.yml +++ b/.github/actions/setup-dependencies/action.yml @@ -53,9 +53,20 @@ runs: if: ${{ (runner.os != 'Windows' || inputs.msystem == '') && inputs.build-type == 'Debug' }} uses: hendrikmuhs/ccache-action@v1.2.17 with: + variant: ${{ runner.os == 'Windows' && 'sccache' || 'ccache' }} create-symlink: ${{ runner.os != 'Windows' }} key: ${{ runner.os }}-qt${{ inputs.qt_ver }}-${{ inputs.architecture }} + - name: Use ccache on debug builds + if: ${{ inputs.build-type == 'Debug' }} + shell: bash + env: + # Only use sccache on MSVC + CCACHE_VARIANT: ${{ (runner.os == 'Windows' && inputs.msystem == '') && 'sccache' || 'ccache' }} + run: | + echo "CMAKE_C_COMPILER_LAUNCHER=$CCACHE_VARIANT" >> "$GITHUB_ENV" + echo "CMAKE_CXX_COMPILER_LAUNCHER=$CCACHE_VARIANT" >> "$GITHUB_ENV" + - name: Install Qt if: ${{ inputs.msystem == '' }} uses: jurplel/install-qt-action@v4 diff --git a/.github/actions/setup-dependencies/windows/action.yml b/.github/actions/setup-dependencies/windows/action.yml index 782d02348..cac1698cb 100644 --- a/.github/actions/setup-dependencies/windows/action.yml +++ b/.github/actions/setup-dependencies/windows/action.yml @@ -48,24 +48,6 @@ runs: qt6-networkauth:p cmark:p - - name: Force newer ccache (MSVC) - if: ${{ inputs.msystem == '' && inputs.build-type == 'Debug' }} - shell: bash - run: | - choco install ccache --version 4.7.1 - - - name: Configure ccache (MSVC) - if: ${{ inputs.msystem == '' && inputs.build-type == 'Debug' }} - shell: pwsh - run: | - # https://github.com/ccache/ccache/wiki/MS-Visual-Studio (I coudn't figure out the compiler prefix) - Copy-Item C:/ProgramData/chocolatey/lib/ccache/tools/ccache-4.7.1-windows-x86_64/ccache.exe -Destination C:/ProgramData/chocolatey/lib/ccache/tools/ccache-4.7.1-windows-x86_64/cl.exe - echo "CLToolExe=cl.exe" >> $env:GITHUB_ENV - echo "CLToolPath=C:/ProgramData/chocolatey/lib/ccache/tools/ccache-4.7.1-windows-x86_64/" >> $env:GITHUB_ENV - echo "TrackFileAccess=false" >> $env:GITHUB_ENV - # Needed for ccache, but also speeds up compile - echo "UseMultiToolTask=true" >> $env:GITHUB_ENV - - name: Retrieve ccache cache (MinGW) if: ${{ inputs.msystem != '' && inputs.build-type == 'Debug' }} uses: actions/cache@v4.2.3 From be33aa356708765b5210894f1d9674b4cb814955 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 1 May 2025 14:19:24 +0000 Subject: [PATCH 204/695] chore(deps): update hendrikmuhs/ccache-action action to v1.2.18 --- .github/actions/setup-dependencies/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/setup-dependencies/action.yml b/.github/actions/setup-dependencies/action.yml index 760ee3e87..e97abd1df 100644 --- a/.github/actions/setup-dependencies/action.yml +++ b/.github/actions/setup-dependencies/action.yml @@ -51,7 +51,7 @@ runs: # TODO(@getchoo): Get this working on MSYS2! - name: Setup ccache if: ${{ (runner.os != 'Windows' || inputs.msystem == '') && inputs.build-type == 'Debug' }} - uses: hendrikmuhs/ccache-action@v1.2.17 + uses: hendrikmuhs/ccache-action@v1.2.18 with: variant: ${{ runner.os == 'Windows' && 'sccache' || 'ccache' }} create-symlink: ${{ runner.os != 'Windows' }} From c7aef20b1e7a6d3ef3e88e87f6d59ffd255203b7 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Tue, 8 Apr 2025 20:46:18 +0300 Subject: [PATCH 205/695] deperecate macos 11 Signed-off-by: Trial97 --- .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 c393d4767..574db521f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -90,7 +90,7 @@ jobs: - os: macos-14 artifact-name: macOS base-cmake-preset: ${{ (inputs.build-type || 'Debug') == 'Debug' && 'macos_universal' || 'macos' }} - macosx-deployment-target: 11.0 + macosx-deployment-target: 12.0 runs-on: ${{ matrix.os }} From ee81c7a6f4f77c3983f8df10bae442b083b96c47 Mon Sep 17 00:00:00 2001 From: Seth Flynn Date: Thu, 1 May 2025 09:17:09 -0400 Subject: [PATCH 206/695] feat: build mingw binaries for arm64 Signed-off-by: Seth Flynn --- .github/workflows/build.yml | 8 +++++++- .github/workflows/release.yml | 3 +++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c393d4767..d7d2e97b8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -72,9 +72,15 @@ jobs: - os: windows-2022 artifact-name: Windows-MinGW-w64 base-cmake-preset: windows_mingw - msystem: clang64 + msystem: CLANG64 vcvars-arch: amd64_x86 + - os: windows-11-arm + artifact-name: Windows-MinGW-arm64 + base-cmake-preset: windows_mingw + msystem: CLANGARM64 + vcvars-arch: arm64 + - os: windows-2022 artifact-name: Windows-MSVC base-cmake-preset: windows_msvc diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ae205895d..a93233dab 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -82,6 +82,9 @@ jobs: PrismLauncher-Windows-MinGW-w64-${{ env.VERSION }}.zip PrismLauncher-Windows-MinGW-w64-Portable-${{ env.VERSION }}.zip PrismLauncher-Windows-MinGW-w64-Setup-${{ env.VERSION }}.exe + PrismLauncher-Windows-MinGW-arm64-${{ env.VERSION }}.zip + PrismLauncher-Windows-MinGW-arm64-Portable-${{ env.VERSION }}.zip + PrismLauncher-Windows-MinGW-arm64-Setup-${{ env.VERSION }}.exe PrismLauncher-Windows-MSVC-arm64-${{ env.VERSION }}.zip PrismLauncher-Windows-MSVC-arm64-Portable-${{ env.VERSION }}.zip PrismLauncher-Windows-MSVC-arm64-Setup-${{ env.VERSION }}.exe From a7c5959b7e7dfbe58bca207fe73fd10e3570cd1e Mon Sep 17 00:00:00 2001 From: Seth Flynn Date: Thu, 1 May 2025 09:24:34 -0400 Subject: [PATCH 207/695] ci(setup-deps): dont force x64 binutils for msys2 Signed-off-by: Seth Flynn --- .github/actions/setup-dependencies/windows/action.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/actions/setup-dependencies/windows/action.yml b/.github/actions/setup-dependencies/windows/action.yml index cac1698cb..78717ddf4 100644 --- a/.github/actions/setup-dependencies/windows/action.yml +++ b/.github/actions/setup-dependencies/windows/action.yml @@ -33,20 +33,19 @@ runs: update: true install: >- git - mingw-w64-x86_64-binutils pacboy: >- toolchain:p + ccache:p cmake:p extra-cmake-modules:p ninja:p qt6-base:p qt6-svg:p qt6-imageformats:p - quazip-qt6:p - ccache:p qt6-5compat:p qt6-networkauth:p cmark:p + quazip-qt6:p - name: Retrieve ccache cache (MinGW) if: ${{ inputs.msystem != '' && inputs.build-type == 'Debug' }} From 13f533801b5ae19923c58bb6723431569398d271 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Sat, 3 May 2025 13:32:11 +0100 Subject: [PATCH 208/695] Use options struct for FlamePackExportTask Signed-off-by: TheKodeToad --- .../modplatform/flame/FlamePackExportTask.cpp | 119 ++++++++---------- .../modplatform/flame/FlamePackExportTask.h | 32 +++-- launcher/ui/MainWindow.cpp | 12 +- launcher/ui/dialogs/ExportPackDialog.cpp | 20 ++- launcher/ui/dialogs/ExportPackDialog.h | 5 +- 5 files changed, 90 insertions(+), 98 deletions(-) diff --git a/launcher/modplatform/flame/FlamePackExportTask.cpp b/launcher/modplatform/flame/FlamePackExportTask.cpp index 701c75308..b74aa08ac 100644 --- a/launcher/modplatform/flame/FlamePackExportTask.cpp +++ b/launcher/modplatform/flame/FlamePackExportTask.cpp @@ -41,24 +41,8 @@ const QString FlamePackExportTask::TEMPLATE = "
  • {name}{authors}
  • \n"; const QStringList FlamePackExportTask::FILE_EXTENSIONS({ "jar", "zip" }); -FlamePackExportTask::FlamePackExportTask(const QString& name, - const QString& version, - const QString& author, - bool optionalFiles, - InstancePtr instance, - const QString& output, - MMCZip::FilterFileFunction filter, - int recommendedRAM) - : name(name) - , version(version) - , author(author) - , optionalFiles(optionalFiles) - , instance(instance) - , mcInstance(dynamic_cast(instance.get())) - , gameRoot(instance->gameRoot()) - , output(output) - , filter(filter) - , m_recommendedRAM(recommendedRAM) +FlamePackExportTask::FlamePackExportTask(FlamePackExportOptions&& options) + : m_options(std::move(options)), m_gameRoot(m_options.instance->gameRoot()) {} void FlamePackExportTask::executeTask() @@ -83,7 +67,7 @@ void FlamePackExportTask::collectFiles() QCoreApplication::processEvents(); files.clear(); - if (!MMCZip::collectFileListRecursively(instance->gameRoot(), nullptr, &files, filter)) { + if (!MMCZip::collectFileListRecursively(m_options.instance->gameRoot(), nullptr, &files, m_options.filter)) { emitFailed(tr("Could not search for files")); return; } @@ -91,11 +75,8 @@ void FlamePackExportTask::collectFiles() pendingHashes.clear(); resolvedFiles.clear(); - if (mcInstance != nullptr) { - mcInstance->loaderModList()->update(); - connect(mcInstance->loaderModList().get(), &ModFolderModel::updateFinished, this, &FlamePackExportTask::collectHashes); - } else - collectHashes(); + m_options.instance->loaderModList()->update(); + connect(m_options.instance->loaderModList().get(), &ModFolderModel::updateFinished, this, &FlamePackExportTask::collectHashes); } void FlamePackExportTask::collectHashes() @@ -103,11 +84,11 @@ void FlamePackExportTask::collectHashes() setAbortable(true); setStatus(tr("Finding file hashes...")); setProgress(1, 5); - auto allMods = mcInstance->loaderModList()->allMods(); + auto allMods = m_options.instance->loaderModList()->allMods(); ConcurrentTask::Ptr hashingTask(new ConcurrentTask("MakeHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt())); task.reset(hashingTask); for (const QFileInfo& file : files) { - const QString relative = gameRoot.relativeFilePath(file.absoluteFilePath()); + const QString relative = m_gameRoot.relativeFilePath(file.absoluteFilePath()); // require sensible file types if (!std::any_of(FILE_EXTENSIONS.begin(), FILE_EXTENSIONS.end(), [&relative](const QString& extension) { return relative.endsWith('.' + extension) || relative.endsWith('.' + extension + ".disabled"); @@ -337,13 +318,13 @@ void FlamePackExportTask::buildZip() setStatus(tr("Adding files...")); setProgress(4, 5); - auto zipTask = makeShared(output, gameRoot, files, "overrides/", true, false); + auto zipTask = makeShared(m_options.output, m_gameRoot, files, "overrides/", true, false); zipTask->addExtraFile("manifest.json", generateIndex()); zipTask->addExtraFile("modlist.html", generateHTML()); QStringList exclude; std::transform(resolvedFiles.keyBegin(), resolvedFiles.keyEnd(), std::back_insert_iterator(exclude), - [this](QString file) { return gameRoot.relativeFilePath(file); }); + [this](QString file) { return m_gameRoot.relativeFilePath(file); }); zipTask->setExcludeFiles(exclude); auto progressStep = std::make_shared(); @@ -378,56 +359,56 @@ QByteArray FlamePackExportTask::generateIndex() QJsonObject obj; obj["manifestType"] = "minecraftModpack"; obj["manifestVersion"] = 1; - obj["name"] = name; - obj["version"] = version; - obj["author"] = author; + obj["name"] = m_options.name; + obj["version"] = m_options.version; + obj["author"] = m_options.author; obj["overrides"] = "overrides"; - if (mcInstance) { - QJsonObject version; - auto profile = mcInstance->getPackProfile(); - // collect all supported components - const ComponentPtr minecraft = profile->getComponent("net.minecraft"); - const ComponentPtr quilt = profile->getComponent("org.quiltmc.quilt-loader"); - const ComponentPtr fabric = profile->getComponent("net.fabricmc.fabric-loader"); - const ComponentPtr forge = profile->getComponent("net.minecraftforge"); - const ComponentPtr neoforge = profile->getComponent("net.neoforged"); - - // convert all available components to mrpack dependencies - if (minecraft != nullptr) - version["version"] = minecraft->m_version; - QString id; - if (quilt != nullptr) - id = "quilt-" + quilt->m_version; - else if (fabric != nullptr) - id = "fabric-" + fabric->m_version; - else if (forge != nullptr) - id = "forge-" + forge->m_version; - else if (neoforge != nullptr) { - id = "neoforge-"; - if (minecraft->m_version == "1.20.1") - id += "1.20.1-"; - id += neoforge->m_version; - } - version["modLoaders"] = QJsonArray(); - if (!id.isEmpty()) { - QJsonObject loader; - loader["id"] = id; - loader["primary"] = true; - version["modLoaders"] = QJsonArray({ loader }); - } - - if (m_recommendedRAM > 0) - version["recommendedRam"] = m_recommendedRAM; - obj["minecraft"] = version; + QJsonObject version; + + auto profile = m_options.instance->getPackProfile(); + // collect all supported components + const ComponentPtr minecraft = profile->getComponent("net.minecraft"); + const ComponentPtr quilt = profile->getComponent("org.quiltmc.quilt-loader"); + const ComponentPtr fabric = profile->getComponent("net.fabricmc.fabric-loader"); + const ComponentPtr forge = profile->getComponent("net.minecraftforge"); + const ComponentPtr neoforge = profile->getComponent("net.neoforged"); + + // convert all available components to mrpack dependencies + if (minecraft != nullptr) + version["version"] = minecraft->m_version; + QString id; + if (quilt != nullptr) + id = "quilt-" + quilt->m_version; + else if (fabric != nullptr) + id = "fabric-" + fabric->m_version; + else if (forge != nullptr) + id = "forge-" + forge->m_version; + else if (neoforge != nullptr) { + id = "neoforge-"; + if (minecraft->m_version == "1.20.1") + id += "1.20.1-"; + id += neoforge->m_version; } + version["modLoaders"] = QJsonArray(); + if (!id.isEmpty()) { + QJsonObject loader; + loader["id"] = id; + loader["primary"] = true; + version["modLoaders"] = QJsonArray({ loader }); + } + + if (m_options.recommendedRAM > 0) + version["recommendedRam"] = m_options.recommendedRAM; + + obj["minecraft"] = version; QJsonArray files; for (auto mod : resolvedFiles) { QJsonObject file; file["projectID"] = mod.addonId; file["fileID"] = mod.version; - file["required"] = mod.enabled || !optionalFiles; + file["required"] = mod.enabled || !m_options.optionalFiles; files << file; } obj["files"] = files; diff --git a/launcher/modplatform/flame/FlamePackExportTask.h b/launcher/modplatform/flame/FlamePackExportTask.h index d3c35de77..e3d4c74a7 100644 --- a/launcher/modplatform/flame/FlamePackExportTask.h +++ b/launcher/modplatform/flame/FlamePackExportTask.h @@ -19,23 +19,26 @@ #pragma once -#include "BaseInstance.h" #include "MMCZip.h" #include "minecraft/MinecraftInstance.h" #include "modplatform/flame/FlameAPI.h" #include "tasks/Task.h" +struct FlamePackExportOptions { + QString name; + QString version; + QString author; + bool optionalFiles; + MinecraftInstancePtr instance; + QString output; + MMCZip::FilterFileFunction filter; + int recommendedRAM; +}; + class FlamePackExportTask : public Task { Q_OBJECT public: - FlamePackExportTask(const QString& name, - const QString& version, - const QString& author, - bool optionalFiles, - InstancePtr instance, - const QString& output, - MMCZip::FilterFileFunction filter, - int recommendedRAM); + FlamePackExportTask(FlamePackExportOptions&& options); protected: void executeTask() override; @@ -46,14 +49,6 @@ class FlamePackExportTask : public Task { static const QStringList FILE_EXTENSIONS; // inputs - const QString name, version, author; - const bool optionalFiles; - const InstancePtr instance; - MinecraftInstance* mcInstance; - const QDir gameRoot; - const QString output; - const MMCZip::FilterFileFunction filter; - const int m_recommendedRAM; struct ResolvedFile { int addonId; @@ -72,6 +67,9 @@ class FlamePackExportTask : public Task { bool isMod; }; + FlamePackExportOptions m_options; + QDir m_gameRoot; + FlameAPI api; QFileInfoList files; diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index d9275a7ab..629ec6aa7 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -71,6 +71,7 @@ #include #include #include +#include #include #include @@ -1419,15 +1420,18 @@ void MainWindow::on_actionExportInstanceZip_triggered() void MainWindow::on_actionExportInstanceMrPack_triggered() { if (m_selectedInstance) { - ExportPackDialog dlg(m_selectedInstance, this); - dlg.exec(); + auto instance = std::dynamic_pointer_cast(m_selectedInstance); + if (instance != nullptr) { + ExportPackDialog dlg(instance, this); + dlg.exec(); + } } } void MainWindow::on_actionExportInstanceFlamePack_triggered() { if (m_selectedInstance) { - auto instance = dynamic_cast(m_selectedInstance.get()); + auto instance = std::dynamic_pointer_cast(m_selectedInstance); if (instance) { if (auto cmp = instance->getPackProfile()->getComponent("net.minecraft"); cmp && cmp->getVersionFile() && cmp->getVersionFile()->type == "snapshot") { @@ -1436,7 +1440,7 @@ void MainWindow::on_actionExportInstanceFlamePack_triggered() msgBox.exec(); return; } - ExportPackDialog dlg(m_selectedInstance, this, ModPlatform::ResourceProvider::FLAME); + ExportPackDialog dlg(instance, this, ModPlatform::ResourceProvider::FLAME); dlg.exec(); } } diff --git a/launcher/ui/dialogs/ExportPackDialog.cpp b/launcher/ui/dialogs/ExportPackDialog.cpp index 675f0d158..15420616e 100644 --- a/launcher/ui/dialogs/ExportPackDialog.cpp +++ b/launcher/ui/dialogs/ExportPackDialog.cpp @@ -17,7 +17,7 @@ */ #include "ExportPackDialog.h" -#include "minecraft/mod/ModFolderModel.h" +#include "minecraft/mod/ResourceFolderModel.h" #include "modplatform/ModIndex.h" #include "modplatform/flame/FlamePackExportTask.h" #include "ui/dialogs/CustomMessageBox.h" @@ -33,7 +33,7 @@ #include "MMCZip.h" #include "modplatform/modrinth/ModrinthPackExportTask.h" -ExportPackDialog::ExportPackDialog(InstancePtr instance, QWidget* parent, ModPlatform::ResourceProvider provider) +ExportPackDialog::ExportPackDialog(MinecraftInstancePtr instance, QWidget* parent, ModPlatform::ResourceProvider provider) : QDialog(parent), m_instance(instance), m_ui(new Ui::ExportPackDialog), m_provider(provider) { Q_ASSERT(m_provider == ModPlatform::ResourceProvider::MODRINTH || m_provider == ModPlatform::ResourceProvider::FLAME); @@ -172,10 +172,18 @@ void ExportPackDialog::done(int result) task = new ModrinthPackExportTask(name, m_ui->version->text(), m_ui->summary->toPlainText(), m_ui->optionalFiles->isChecked(), m_instance, output, std::bind(&FileIgnoreProxy::filterFile, m_proxy, std::placeholders::_1)); } else { - int recommendedRAM = m_ui->recommendedMemoryCheckBox->isChecked() ? m_ui->recommendedMemory->value() : 0; - - task = new FlamePackExportTask(name, m_ui->version->text(), m_ui->author->text(), m_ui->optionalFiles->isChecked(), m_instance, - output, std::bind(&FileIgnoreProxy::filterFile, m_proxy, std::placeholders::_1), recommendedRAM); + FlamePackExportOptions options{}; + + options.name = name; + options.version = m_ui->version->text(); + options.author = m_ui->author->text(); + options.optionalFiles = m_ui->optionalFiles->isChecked(); + options.instance = m_instance; + options.output = output; + options.filter = std::bind(&FileIgnoreProxy::filterFile, m_proxy, std::placeholders::_1); + options.recommendedRAM = m_ui->recommendedMemoryCheckBox->isChecked() ? m_ui->recommendedMemory->value() : 0; + + task = new FlamePackExportTask(std::move(options)); } connect(task, &Task::failed, diff --git a/launcher/ui/dialogs/ExportPackDialog.h b/launcher/ui/dialogs/ExportPackDialog.h index 092288d49..e93055d8d 100644 --- a/launcher/ui/dialogs/ExportPackDialog.h +++ b/launcher/ui/dialogs/ExportPackDialog.h @@ -22,6 +22,7 @@ #include "BaseInstance.h" #include "FastFileIconProvider.h" #include "FileIgnoreProxy.h" +#include "minecraft/MinecraftInstance.h" #include "modplatform/ModIndex.h" namespace Ui { @@ -32,7 +33,7 @@ class ExportPackDialog : public QDialog { Q_OBJECT public: - explicit ExportPackDialog(InstancePtr instance, + explicit ExportPackDialog(MinecraftInstancePtr instance, QWidget* parent = nullptr, ModPlatform::ResourceProvider provider = ModPlatform::ResourceProvider::MODRINTH); ~ExportPackDialog(); @@ -44,7 +45,7 @@ class ExportPackDialog : public QDialog { QString ignoreFileName(); private: - const InstancePtr m_instance; + const MinecraftInstancePtr m_instance; Ui::ExportPackDialog* m_ui; FileIgnoreProxy* m_proxy; FastFileIconProvider m_icons; From fe0c52ff78c5d8bd705b0ce11b1c936ed770156d Mon Sep 17 00:00:00 2001 From: Trial97 Date: Mon, 5 May 2025 19:57:24 +0300 Subject: [PATCH 209/695] remove: unused files Signed-off-by: Trial97 --- launcher/CMakeLists.txt | 5 - launcher/ui/widgets/ErrorFrame.cpp | 116 ---------------------- launcher/ui/widgets/ErrorFrame.h | 47 --------- launcher/ui/widgets/ErrorFrame.ui | 92 ----------------- launcher/ui/widgets/FocusLineEdit.cpp | 24 ----- launcher/ui/widgets/FocusLineEdit.h | 16 --- launcher/ui/widgets/InstanceCardWidget.ui | 58 ----------- launcher/ui/widgets/LineSeparator.cpp | 35 ------- launcher/ui/widgets/LineSeparator.h | 18 ---- 9 files changed, 411 deletions(-) delete mode 100644 launcher/ui/widgets/ErrorFrame.cpp delete mode 100644 launcher/ui/widgets/ErrorFrame.h delete mode 100644 launcher/ui/widgets/ErrorFrame.ui delete mode 100644 launcher/ui/widgets/FocusLineEdit.cpp delete mode 100644 launcher/ui/widgets/FocusLineEdit.h delete mode 100644 launcher/ui/widgets/InstanceCardWidget.ui delete mode 100644 launcher/ui/widgets/LineSeparator.cpp delete mode 100644 launcher/ui/widgets/LineSeparator.h diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 4f8b9018a..7cdab2394 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -1106,8 +1106,6 @@ SET(LAUNCHER_SOURCES ui/widgets/CustomCommands.h ui/widgets/EnvironmentVariables.cpp ui/widgets/EnvironmentVariables.h - ui/widgets/FocusLineEdit.cpp - ui/widgets/FocusLineEdit.h ui/widgets/IconLabel.cpp ui/widgets/IconLabel.h ui/widgets/JavaWizardWidget.cpp @@ -1116,8 +1114,6 @@ SET(LAUNCHER_SOURCES ui/widgets/LabeledToolButton.h ui/widgets/LanguageSelectionWidget.cpp ui/widgets/LanguageSelectionWidget.h - ui/widgets/LineSeparator.cpp - ui/widgets/LineSeparator.h ui/widgets/LogView.cpp ui/widgets/LogView.h ui/widgets/InfoFrame.cpp @@ -1216,7 +1212,6 @@ qt_wrap_ui(LAUNCHER_UI ui/pages/modplatform/OptionalModDialog.ui ui/pages/modplatform/modrinth/ModrinthPage.ui ui/pages/modplatform/technic/TechnicPage.ui - ui/widgets/InstanceCardWidget.ui ui/widgets/CustomCommands.ui ui/widgets/EnvironmentVariables.ui ui/widgets/InfoFrame.ui diff --git a/launcher/ui/widgets/ErrorFrame.cpp b/launcher/ui/widgets/ErrorFrame.cpp deleted file mode 100644 index 213c26b76..000000000 --- a/launcher/ui/widgets/ErrorFrame.cpp +++ /dev/null @@ -1,116 +0,0 @@ -/* 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 -#include - -#include "ErrorFrame.h" -#include "ui_ErrorFrame.h" - -#include "ui/dialogs/CustomMessageBox.h" - -void ErrorFrame::clear() -{ - setTitle(QString()); - setDescription(QString()); -} - -ErrorFrame::ErrorFrame(QWidget* parent) : QFrame(parent), ui(new Ui::ErrorFrame) -{ - ui->setupUi(this); - ui->label_Description->setHidden(true); - ui->label_Title->setHidden(true); - updateHiddenState(); -} - -ErrorFrame::~ErrorFrame() -{ - delete ui; -} - -void ErrorFrame::updateHiddenState() -{ - if (ui->label_Description->isHidden() && ui->label_Title->isHidden()) { - setHidden(true); - } else { - setHidden(false); - } -} - -void ErrorFrame::setTitle(QString text) -{ - if (text.isEmpty()) { - ui->label_Title->setHidden(true); - } else { - ui->label_Title->setText(text); - ui->label_Title->setHidden(false); - } - updateHiddenState(); -} - -void ErrorFrame::setDescription(QString text) -{ - if (text.isEmpty()) { - ui->label_Description->setHidden(true); - updateHiddenState(); - return; - } else { - ui->label_Description->setHidden(false); - updateHiddenState(); - } - ui->label_Description->setToolTip(""); - QString intermediatetext = text.trimmed(); - bool prev(false); - QChar rem('\n'); - QString finaltext; - finaltext.reserve(intermediatetext.size()); - foreach (const QChar& c, intermediatetext) { - if (c == rem && prev) { - continue; - } - prev = c == rem; - finaltext += c; - } - QString labeltext; - labeltext.reserve(300); - if (finaltext.length() > 290) { - ui->label_Description->setOpenExternalLinks(false); - ui->label_Description->setTextFormat(Qt::TextFormat::RichText); - desc = text; - // This allows injecting HTML here. - labeltext.append("" + finaltext.left(287) + "..."); - QObject::connect(ui->label_Description, &QLabel::linkActivated, this, &ErrorFrame::ellipsisHandler); - } else { - ui->label_Description->setTextFormat(Qt::TextFormat::PlainText); - labeltext.append(finaltext); - } - ui->label_Description->setText(labeltext); -} - -void ErrorFrame::ellipsisHandler(const QString& link) -{ - if (!currentBox) { - currentBox = CustomMessageBox::selectable(this, QString(), desc); - connect(currentBox, &QMessageBox::finished, this, &ErrorFrame::boxClosed); - currentBox->show(); - } else { - currentBox->setText(desc); - } -} - -void ErrorFrame::boxClosed(int result) -{ - currentBox = nullptr; -} diff --git a/launcher/ui/widgets/ErrorFrame.h b/launcher/ui/widgets/ErrorFrame.h deleted file mode 100644 index 1aea6a1d8..000000000 --- a/launcher/ui/widgets/ErrorFrame.h +++ /dev/null @@ -1,47 +0,0 @@ -/* 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 - -namespace Ui { -class ErrorFrame; -} - -class ErrorFrame : public QFrame { - Q_OBJECT - - public: - explicit ErrorFrame(QWidget* parent = 0); - ~ErrorFrame(); - - void setTitle(QString text); - void setDescription(QString text); - - void clear(); - - public slots: - void ellipsisHandler(const QString& link); - void boxClosed(int result); - - private: - void updateHiddenState(); - - private: - Ui::ErrorFrame* ui; - QString desc; - class QMessageBox* currentBox = nullptr; -}; diff --git a/launcher/ui/widgets/ErrorFrame.ui b/launcher/ui/widgets/ErrorFrame.ui deleted file mode 100644 index 0bb567439..000000000 --- a/launcher/ui/widgets/ErrorFrame.ui +++ /dev/null @@ -1,92 +0,0 @@ - - - ErrorFrame - - - - 0 - 0 - 527 - 113 - - - - - 0 - 0 - - - - - 16777215 - 120 - - - - - 6 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - - - Qt::RichText - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - - - true - - - true - - - Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - - - - - - - Qt::RichText - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - - - true - - - true - - - Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - - - - - - diff --git a/launcher/ui/widgets/FocusLineEdit.cpp b/launcher/ui/widgets/FocusLineEdit.cpp deleted file mode 100644 index 6570227bb..000000000 --- a/launcher/ui/widgets/FocusLineEdit.cpp +++ /dev/null @@ -1,24 +0,0 @@ -#include "FocusLineEdit.h" -#include - -FocusLineEdit::FocusLineEdit(QWidget* parent) : QLineEdit(parent) -{ - _selectOnMousePress = false; -} - -void FocusLineEdit::focusInEvent(QFocusEvent* e) -{ - QLineEdit::focusInEvent(e); - selectAll(); - _selectOnMousePress = true; -} - -void FocusLineEdit::mousePressEvent(QMouseEvent* me) -{ - QLineEdit::mousePressEvent(me); - if (_selectOnMousePress) { - selectAll(); - _selectOnMousePress = false; - } - qDebug() << selectedText(); -} diff --git a/launcher/ui/widgets/FocusLineEdit.h b/launcher/ui/widgets/FocusLineEdit.h deleted file mode 100644 index 797969406..000000000 --- a/launcher/ui/widgets/FocusLineEdit.h +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once - -#include - -class FocusLineEdit : public QLineEdit { - Q_OBJECT - public: - FocusLineEdit(QWidget* parent); - virtual ~FocusLineEdit() {} - - protected: - void focusInEvent(QFocusEvent* e); - void mousePressEvent(QMouseEvent* me); - - bool _selectOnMousePress; -}; diff --git a/launcher/ui/widgets/InstanceCardWidget.ui b/launcher/ui/widgets/InstanceCardWidget.ui deleted file mode 100644 index 6eeeb0769..000000000 --- a/launcher/ui/widgets/InstanceCardWidget.ui +++ /dev/null @@ -1,58 +0,0 @@ - - - InstanceCardWidget - - - - 0 - 0 - 473 - 118 - - - - - - - - 80 - 80 - - - - - - - - &Name: - - - instNameTextBox - - - - - - - - - - &Group: - - - groupBox - - - - - - - true - - - - - - - - diff --git a/launcher/ui/widgets/LineSeparator.cpp b/launcher/ui/widgets/LineSeparator.cpp deleted file mode 100644 index 2d6239a2f..000000000 --- a/launcher/ui/widgets/LineSeparator.cpp +++ /dev/null @@ -1,35 +0,0 @@ -#include "LineSeparator.h" - -#include -#include -#include -#include - -void LineSeparator::initStyleOption(QStyleOption* option) const -{ - option->initFrom(this); - // in a horizontal layout, the line is vertical (and vice versa) - if (m_orientation == Qt::Vertical) - option->state |= QStyle::State_Horizontal; -} - -LineSeparator::LineSeparator(QWidget* parent, Qt::Orientation orientation) : QWidget(parent), m_orientation(orientation) -{ - setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); -} - -QSize LineSeparator::sizeHint() const -{ - QStyleOption opt; - initStyleOption(&opt); - const int extent = style()->pixelMetric(QStyle::PM_ToolBarSeparatorExtent, &opt, parentWidget()); - return QSize(extent, extent); -} - -void LineSeparator::paintEvent(QPaintEvent*) -{ - QPainter p(this); - QStyleOption opt; - initStyleOption(&opt); - style()->drawPrimitive(QStyle::PE_IndicatorToolBarSeparator, &opt, &p, parentWidget()); -} diff --git a/launcher/ui/widgets/LineSeparator.h b/launcher/ui/widgets/LineSeparator.h deleted file mode 100644 index 719facb99..000000000 --- a/launcher/ui/widgets/LineSeparator.h +++ /dev/null @@ -1,18 +0,0 @@ -#pragma once -#include - -class QStyleOption; - -class LineSeparator : public QWidget { - Q_OBJECT - - public: - /// Create a line separator. orientation is the orientation of the line. - explicit LineSeparator(QWidget* parent, Qt::Orientation orientation = Qt::Horizontal); - QSize sizeHint() const; - void paintEvent(QPaintEvent*); - void initStyleOption(QStyleOption* option) const; - - private: - Qt::Orientation m_orientation = Qt::Horizontal; -}; From a55bffc963bc285936d0e5739c2cffa617581f69 Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Mon, 5 May 2025 13:25:29 -0700 Subject: [PATCH 210/695] ci(blocked-prs): jq if statements need parens Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- .github/workflows/blocked-prs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/blocked-prs.yml b/.github/workflows/blocked-prs.yml index a8b2a3fe6..e518c709d 100644 --- a/.github/workflows/blocked-prs.yml +++ b/.github/workflows/blocked-prs.yml @@ -125,7 +125,7 @@ jobs: "type": $type, "number": .number, "merged": .merged, - "state": if .state == "open" then "Open" elif .merged then "Merged" else "Closed" end, + "state": (if .state == "open" then "Open" elif .merged then "Merged" else "Closed" end), "labels": (reduce .labels[].name as $l ([]; . + [$l])), "basePrUrl": .html_url, "baseRepoName": .head.repo.name, From d1234198a1506564617fb8da8437e540d6131f67 Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Mon, 5 May 2025 14:01:58 -0700 Subject: [PATCH 211/695] ci(blocked-pr): default pr body as empty string Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- .github/workflows/blocked-prs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/blocked-prs.yml b/.github/workflows/blocked-prs.yml index e518c709d..3ebe9f5cf 100644 --- a/.github/workflows/blocked-prs.yml +++ b/.github/workflows/blocked-prs.yml @@ -64,7 +64,7 @@ jobs: "prNumber": .number, "prHeadSha": .head.sha, "prHeadLabel": .head.label, - "prBody": .body, + "prBody": .body // "", "prLabels": (reduce .labels[].name as $l ([]; . + [$l])) } ' <<< "$PR_JSON")" From f379c5ef34718f15a190614665f59250886ee19c Mon Sep 17 00:00:00 2001 From: Rachel Powers <508861+Ryex@users.noreply.github.com> Date: Mon, 5 May 2025 15:40:21 -0700 Subject: [PATCH 212/695] ci(blocked-pr): another jq syntax fix Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com> --- .github/workflows/blocked-prs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/blocked-prs.yml b/.github/workflows/blocked-prs.yml index 3ebe9f5cf..ecbaf755d 100644 --- a/.github/workflows/blocked-prs.yml +++ b/.github/workflows/blocked-prs.yml @@ -64,7 +64,7 @@ jobs: "prNumber": .number, "prHeadSha": .head.sha, "prHeadLabel": .head.label, - "prBody": .body // "", + "prBody": (.body // ""), "prLabels": (reduce .labels[].name as $l ([]; . + [$l])) } ' <<< "$PR_JSON")" From 476f3edce0985ddebe04190a8d566d5abf60e5b2 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Tue, 6 May 2025 00:06:56 +0300 Subject: [PATCH 213/695] Revert "fix: 6.2 deprecation warning regard the QScopedPointer::swap function (#3655)" This reverts commit ca258109c5d49f27f5de5cb5100aa8e74eaf71e2, reversing changes made to 693d9d02bca3348b9a59e013867c3f6c8c5a9f98. Signed-off-by: Trial97 --- launcher/ui/pages/modplatform/ModPage.cpp | 16 +++++++--------- launcher/ui/pages/modplatform/ModPage.h | 6 +++--- .../ui/pages/modplatform/flame/FlamePage.cpp | 13 +++++-------- launcher/ui/pages/modplatform/flame/FlamePage.h | 2 +- .../modplatform/flame/FlameResourcePages.cpp | 4 ++-- .../pages/modplatform/flame/FlameResourcePages.h | 2 +- .../pages/modplatform/modrinth/ModrinthPage.cpp | 12 +++++------- .../ui/pages/modplatform/modrinth/ModrinthPage.h | 2 +- .../modrinth/ModrinthResourcePages.cpp | 4 ++-- .../modplatform/modrinth/ModrinthResourcePages.h | 2 +- launcher/ui/widgets/ModFilterWidget.cpp | 5 +++++ launcher/ui/widgets/ModFilterWidget.h | 4 +++- 12 files changed, 36 insertions(+), 36 deletions(-) diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index 80d2bcb73..8b4919015 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -61,24 +61,22 @@ ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance& instance) : ResourcePa connect(m_ui->resourceFilterButton, &QPushButton::clicked, this, &ModPage::filterMods); } -void ModPage::setFilterWidget(ModFilterWidget* widget) +void ModPage::setFilterWidget(unique_qobject_ptr& widget) { if (m_filter_widget) - disconnect(m_filter_widget, nullptr, nullptr, nullptr); + disconnect(m_filter_widget.get(), nullptr, nullptr, nullptr); - auto old = m_ui->splitter->replaceWidget(0, widget); + auto old = m_ui->splitter->replaceWidget(0, widget.get()); // because we replaced the widget we also need to delete it if (old) { - old->deleteLater(); + delete old; } - m_filter_widget = widget; - if (m_filter_widget) { - m_filter_widget->deleteLater(); - } + m_filter_widget.swap(widget); + m_filter = m_filter_widget->getFilter(); - connect(m_filter_widget, &ModFilterWidget::filterChanged, this, &ModPage::triggerSearch); + connect(m_filter_widget.get(), &ModFilterWidget::filterChanged, this, &ModPage::triggerSearch); prepareProviderCategories(); } diff --git a/launcher/ui/pages/modplatform/ModPage.h b/launcher/ui/pages/modplatform/ModPage.h index 87865dc83..47fe21e0f 100644 --- a/launcher/ui/pages/modplatform/ModPage.h +++ b/launcher/ui/pages/modplatform/ModPage.h @@ -51,11 +51,11 @@ class ModPage : public ResourcePage { void addResourceToPage(ModPlatform::IndexedPack::Ptr, ModPlatform::IndexedVersion&, std::shared_ptr) override; - virtual ModFilterWidget* createFilterWidget() = 0; + virtual unique_qobject_ptr createFilterWidget() = 0; [[nodiscard]] bool supportsFiltering() const override { return true; }; auto getFilter() const -> const std::shared_ptr { return m_filter; } - void setFilterWidget(ModFilterWidget*); + void setFilterWidget(unique_qobject_ptr&); protected: ModPage(ModDownloadDialog* dialog, BaseInstance& instance); @@ -67,7 +67,7 @@ class ModPage : public ResourcePage { void triggerSearch() override; protected: - ModFilterWidget* m_filter_widget = nullptr; + unique_qobject_ptr m_filter_widget; std::shared_ptr m_filter; }; diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.cpp b/launcher/ui/pages/modplatform/flame/FlamePage.cpp index bcbae0d76..de6b3d633 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlamePage.cpp @@ -341,20 +341,17 @@ void FlamePage::setSearchTerm(QString term) void FlamePage::createFilterWidget() { - auto widget = new ModFilterWidget(nullptr, false, this); - if (m_filterWidget) { - m_filterWidget->deleteLater(); - } - m_filterWidget = (widget); - auto old = ui->splitter->replaceWidget(0, m_filterWidget); + auto widget = ModFilterWidget::create(nullptr, false, this); + m_filterWidget.swap(widget); + auto old = ui->splitter->replaceWidget(0, m_filterWidget.get()); // because we replaced the widget we also need to delete it if (old) { - old->deleteLater(); + delete old; } connect(ui->filterButton, &QPushButton::clicked, this, [this] { m_filterWidget->setHidden(!m_filterWidget->isHidden()); }); - connect(m_filterWidget, &ModFilterWidget::filterChanged, this, &FlamePage::triggerSearch); + connect(m_filterWidget.get(), &ModFilterWidget::filterChanged, this, &FlamePage::triggerSearch); auto response = std::make_shared(); m_categoriesTask = FlameAPI::getCategories(response, ModPlatform::ResourceType::MODPACK); QObject::connect(m_categoriesTask.get(), &Task::succeeded, [this, response]() { diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.h b/launcher/ui/pages/modplatform/flame/FlamePage.h index a828a2a29..27c96d2f1 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.h +++ b/launcher/ui/pages/modplatform/flame/FlamePage.h @@ -100,6 +100,6 @@ class FlamePage : public QWidget, public ModpackProviderBasePage { // Used to do instant searching with a delay to cache quick changes QTimer m_search_timer; - ModFilterWidget* m_filterWidget; + unique_qobject_ptr m_filterWidget; Task::Ptr m_categoriesTask; }; diff --git a/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp index bfe873ac8..4e01f3a65 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp @@ -207,9 +207,9 @@ auto FlameShaderPackPage::shouldDisplay() const -> bool return true; } -ModFilterWidget* FlameModPage::createFilterWidget() +unique_qobject_ptr FlameModPage::createFilterWidget() { - return new ModFilterWidget(&static_cast(m_baseInstance), false, this); + return ModFilterWidget::create(&static_cast(m_baseInstance), false, this); } void FlameModPage::prepareProviderCategories() diff --git a/launcher/ui/pages/modplatform/flame/FlameResourcePages.h b/launcher/ui/pages/modplatform/flame/FlameResourcePages.h index 3117851a5..052706549 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourcePages.h +++ b/launcher/ui/pages/modplatform/flame/FlameResourcePages.h @@ -96,7 +96,7 @@ class FlameModPage : public ModPage { [[nodiscard]] inline auto helpPage() const -> QString override { return "Mod-platform"; } void openUrl(const QUrl& url) override; - ModFilterWidget* createFilterWidget() override; + unique_qobject_ptr createFilterWidget() override; protected: virtual void prepareProviderCategories() override; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index 784b656a1..7d70abec4 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -391,19 +391,17 @@ QString ModrinthPage::getSerachTerm() const void ModrinthPage::createFilterWidget() { - auto widget = new ModFilterWidget(nullptr, true, this); - if (m_filterWidget) - m_filterWidget->deleteLater(); - m_filterWidget = widget; - auto old = ui->splitter->replaceWidget(0, m_filterWidget); + auto widget = ModFilterWidget::create(nullptr, true, this); + m_filterWidget.swap(widget); + auto old = ui->splitter->replaceWidget(0, m_filterWidget.get()); // because we replaced the widget we also need to delete it if (old) { - old->deleteLater(); + delete old; } connect(ui->filterButton, &QPushButton::clicked, this, [this] { m_filterWidget->setHidden(!m_filterWidget->isHidden()); }); - connect(m_filterWidget, &ModFilterWidget::filterChanged, this, &ModrinthPage::triggerSearch); + connect(m_filterWidget.get(), &ModFilterWidget::filterChanged, this, &ModrinthPage::triggerSearch); auto response = std::make_shared(); m_categoriesTask = ModrinthAPI::getModCategories(response); QObject::connect(m_categoriesTask.get(), &Task::succeeded, [this, response]() { diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h index c90402f52..7f504cdbd 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h @@ -103,6 +103,6 @@ class ModrinthPage : public QWidget, public ModpackProviderBasePage { // Used to do instant searching with a delay to cache quick changes QTimer m_search_timer; - ModFilterWidget* m_filterWidget; + unique_qobject_ptr m_filterWidget; Task::Ptr m_categoriesTask; }; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp index 38a750622..4ee620677 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp @@ -142,9 +142,9 @@ auto ModrinthShaderPackPage::shouldDisplay() const -> bool return true; } -ModFilterWidget* ModrinthModPage::createFilterWidget() +unique_qobject_ptr ModrinthModPage::createFilterWidget() { - return new ModFilterWidget(&static_cast(m_baseInstance), true, this); + return ModFilterWidget::create(&static_cast(m_baseInstance), true, this); } void ModrinthModPage::prepareProviderCategories() diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h index e2ad60b51..eaf6129a5 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h @@ -94,7 +94,7 @@ class ModrinthModPage : public ModPage { [[nodiscard]] inline auto helpPage() const -> QString override { return "Mod-platform"; } - ModFilterWidget* createFilterWidget() override; + unique_qobject_ptr createFilterWidget() override; protected: virtual void prepareProviderCategories() override; diff --git a/launcher/ui/widgets/ModFilterWidget.cpp b/launcher/ui/widgets/ModFilterWidget.cpp index 8be3ce8d3..03522bc19 100644 --- a/launcher/ui/widgets/ModFilterWidget.cpp +++ b/launcher/ui/widgets/ModFilterWidget.cpp @@ -49,6 +49,11 @@ #include "Application.h" #include "minecraft/PackProfile.h" +unique_qobject_ptr ModFilterWidget::create(MinecraftInstance* instance, bool extended, QWidget* parent) +{ + return unique_qobject_ptr(new ModFilterWidget(instance, extended, parent)); +} + class VersionBasicModel : public QIdentityProxyModel { Q_OBJECT diff --git a/launcher/ui/widgets/ModFilterWidget.h b/launcher/ui/widgets/ModFilterWidget.h index c7192a0d6..41a2f1bbd 100644 --- a/launcher/ui/widgets/ModFilterWidget.h +++ b/launcher/ui/widgets/ModFilterWidget.h @@ -83,7 +83,7 @@ class ModFilterWidget : public QTabWidget { } }; - ModFilterWidget(MinecraftInstance* instance, bool extendedSupport, QWidget* parent = nullptr); + static unique_qobject_ptr create(MinecraftInstance* instance, bool extended, QWidget* parent = nullptr); virtual ~ModFilterWidget(); auto getFilter() -> std::shared_ptr; @@ -96,6 +96,8 @@ class ModFilterWidget : public QTabWidget { void setCategories(const QList&); private: + ModFilterWidget(MinecraftInstance* instance, bool extendedSupport, QWidget* parent = nullptr); + void loadVersionList(); void prepareBasicFilter(); From 7523bc192532c7e7c4078e9539864f80cb72af81 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Tue, 6 May 2025 00:17:38 +0300 Subject: [PATCH 214/695] fix: replaced deprecated unique_qobject_ptr::swap with unique_ptr Signed-off-by: Trial97 --- launcher/ui/pages/modplatform/ModPage.cpp | 2 +- launcher/ui/pages/modplatform/ModPage.h | 6 +++--- launcher/ui/pages/modplatform/flame/FlamePage.cpp | 2 +- launcher/ui/pages/modplatform/flame/FlamePage.h | 2 +- .../ui/pages/modplatform/flame/FlameResourcePages.cpp | 4 ++-- launcher/ui/pages/modplatform/flame/FlameResourcePages.h | 2 +- launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp | 2 +- launcher/ui/pages/modplatform/modrinth/ModrinthPage.h | 2 +- .../pages/modplatform/modrinth/ModrinthResourcePages.cpp | 4 ++-- .../ui/pages/modplatform/modrinth/ModrinthResourcePages.h | 2 +- launcher/ui/widgets/ModFilterWidget.cpp | 8 ++++---- launcher/ui/widgets/ModFilterWidget.h | 4 ++-- 12 files changed, 20 insertions(+), 20 deletions(-) diff --git a/launcher/ui/pages/modplatform/ModPage.cpp b/launcher/ui/pages/modplatform/ModPage.cpp index 8b4919015..803ba6d5c 100644 --- a/launcher/ui/pages/modplatform/ModPage.cpp +++ b/launcher/ui/pages/modplatform/ModPage.cpp @@ -61,7 +61,7 @@ ModPage::ModPage(ModDownloadDialog* dialog, BaseInstance& instance) : ResourcePa connect(m_ui->resourceFilterButton, &QPushButton::clicked, this, &ModPage::filterMods); } -void ModPage::setFilterWidget(unique_qobject_ptr& widget) +void ModPage::setFilterWidget(std::unique_ptr& widget) { if (m_filter_widget) disconnect(m_filter_widget.get(), nullptr, nullptr, nullptr); diff --git a/launcher/ui/pages/modplatform/ModPage.h b/launcher/ui/pages/modplatform/ModPage.h index 47fe21e0f..fb9f3f9d3 100644 --- a/launcher/ui/pages/modplatform/ModPage.h +++ b/launcher/ui/pages/modplatform/ModPage.h @@ -51,11 +51,11 @@ class ModPage : public ResourcePage { void addResourceToPage(ModPlatform::IndexedPack::Ptr, ModPlatform::IndexedVersion&, std::shared_ptr) override; - virtual unique_qobject_ptr createFilterWidget() = 0; + virtual std::unique_ptr createFilterWidget() = 0; [[nodiscard]] bool supportsFiltering() const override { return true; }; auto getFilter() const -> const std::shared_ptr { return m_filter; } - void setFilterWidget(unique_qobject_ptr&); + void setFilterWidget(std::unique_ptr&); protected: ModPage(ModDownloadDialog* dialog, BaseInstance& instance); @@ -67,7 +67,7 @@ class ModPage : public ResourcePage { void triggerSearch() override; protected: - unique_qobject_ptr m_filter_widget; + std::unique_ptr m_filter_widget; std::shared_ptr m_filter; }; diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.cpp b/launcher/ui/pages/modplatform/flame/FlamePage.cpp index de6b3d633..bb91e5a64 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlamePage.cpp @@ -341,7 +341,7 @@ void FlamePage::setSearchTerm(QString term) void FlamePage::createFilterWidget() { - auto widget = ModFilterWidget::create(nullptr, false, this); + auto widget = ModFilterWidget::create(nullptr, false); m_filterWidget.swap(widget); auto old = ui->splitter->replaceWidget(0, m_filterWidget.get()); // because we replaced the widget we also need to delete it diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.h b/launcher/ui/pages/modplatform/flame/FlamePage.h index 27c96d2f1..32b752bbe 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.h +++ b/launcher/ui/pages/modplatform/flame/FlamePage.h @@ -100,6 +100,6 @@ class FlamePage : public QWidget, public ModpackProviderBasePage { // Used to do instant searching with a delay to cache quick changes QTimer m_search_timer; - unique_qobject_ptr m_filterWidget; + std::unique_ptr m_filterWidget; Task::Ptr m_categoriesTask; }; diff --git a/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp index 4e01f3a65..4bea52fc0 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp @@ -207,9 +207,9 @@ auto FlameShaderPackPage::shouldDisplay() const -> bool return true; } -unique_qobject_ptr FlameModPage::createFilterWidget() +std::unique_ptr FlameModPage::createFilterWidget() { - return ModFilterWidget::create(&static_cast(m_baseInstance), false, this); + return ModFilterWidget::create(&static_cast(m_baseInstance), false); } void FlameModPage::prepareProviderCategories() diff --git a/launcher/ui/pages/modplatform/flame/FlameResourcePages.h b/launcher/ui/pages/modplatform/flame/FlameResourcePages.h index 052706549..3518e7c24 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourcePages.h +++ b/launcher/ui/pages/modplatform/flame/FlameResourcePages.h @@ -96,7 +96,7 @@ class FlameModPage : public ModPage { [[nodiscard]] inline auto helpPage() const -> QString override { return "Mod-platform"; } void openUrl(const QUrl& url) override; - unique_qobject_ptr createFilterWidget() override; + std::unique_ptr createFilterWidget() override; protected: virtual void prepareProviderCategories() override; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index 7d70abec4..701bb9f72 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -391,7 +391,7 @@ QString ModrinthPage::getSerachTerm() const void ModrinthPage::createFilterWidget() { - auto widget = ModFilterWidget::create(nullptr, true, this); + auto widget = ModFilterWidget::create(nullptr, true); m_filterWidget.swap(widget); auto old = ui->splitter->replaceWidget(0, m_filterWidget.get()); // because we replaced the widget we also need to delete it diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h index 7f504cdbd..d22a72e4e 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h @@ -103,6 +103,6 @@ class ModrinthPage : public QWidget, public ModpackProviderBasePage { // Used to do instant searching with a delay to cache quick changes QTimer m_search_timer; - unique_qobject_ptr m_filterWidget; + std::unique_ptr m_filterWidget; Task::Ptr m_categoriesTask; }; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp index 4ee620677..064cb28bf 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp @@ -142,9 +142,9 @@ auto ModrinthShaderPackPage::shouldDisplay() const -> bool return true; } -unique_qobject_ptr ModrinthModPage::createFilterWidget() +std::unique_ptr ModrinthModPage::createFilterWidget() { - return ModFilterWidget::create(&static_cast(m_baseInstance), true, this); + return ModFilterWidget::create(&static_cast(m_baseInstance), true); } void ModrinthModPage::prepareProviderCategories() diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h index eaf6129a5..f6a789cc3 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h @@ -94,7 +94,7 @@ class ModrinthModPage : public ModPage { [[nodiscard]] inline auto helpPage() const -> QString override { return "Mod-platform"; } - unique_qobject_ptr createFilterWidget() override; + std::unique_ptr createFilterWidget() override; protected: virtual void prepareProviderCategories() override; diff --git a/launcher/ui/widgets/ModFilterWidget.cpp b/launcher/ui/widgets/ModFilterWidget.cpp index 03522bc19..da41b990a 100644 --- a/launcher/ui/widgets/ModFilterWidget.cpp +++ b/launcher/ui/widgets/ModFilterWidget.cpp @@ -49,9 +49,9 @@ #include "Application.h" #include "minecraft/PackProfile.h" -unique_qobject_ptr ModFilterWidget::create(MinecraftInstance* instance, bool extended, QWidget* parent) +std::unique_ptr ModFilterWidget::create(MinecraftInstance* instance, bool extended) { - return unique_qobject_ptr(new ModFilterWidget(instance, extended, parent)); + return std::unique_ptr(new ModFilterWidget(instance, extended)); } class VersionBasicModel : public QIdentityProxyModel { @@ -107,8 +107,8 @@ class AllVersionProxyModel : public QSortFilterProxyModel { } }; -ModFilterWidget::ModFilterWidget(MinecraftInstance* instance, bool extended, QWidget* parent) - : QTabWidget(parent), ui(new Ui::ModFilterWidget), m_instance(instance), m_filter(new Filter()) +ModFilterWidget::ModFilterWidget(MinecraftInstance* instance, bool extended) + : QTabWidget(), ui(new Ui::ModFilterWidget), m_instance(instance), m_filter(new Filter()) { ui->setupUi(this); diff --git a/launcher/ui/widgets/ModFilterWidget.h b/launcher/ui/widgets/ModFilterWidget.h index 41a2f1bbd..88f2593dd 100644 --- a/launcher/ui/widgets/ModFilterWidget.h +++ b/launcher/ui/widgets/ModFilterWidget.h @@ -83,7 +83,7 @@ class ModFilterWidget : public QTabWidget { } }; - static unique_qobject_ptr create(MinecraftInstance* instance, bool extended, QWidget* parent = nullptr); + static std::unique_ptr create(MinecraftInstance* instance, bool extended); virtual ~ModFilterWidget(); auto getFilter() -> std::shared_ptr; @@ -96,7 +96,7 @@ class ModFilterWidget : public QTabWidget { void setCategories(const QList&); private: - ModFilterWidget(MinecraftInstance* instance, bool extendedSupport, QWidget* parent = nullptr); + ModFilterWidget(MinecraftInstance* instance, bool extendedSupport); void loadVersionList(); void prepareBasicFilter(); From 9b07c6948cbd02f79af69a7d92b3a215d61a6377 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Tue, 6 May 2025 23:27:29 +0300 Subject: [PATCH 215/695] fix: modrinth categories not loading Signed-off-by: Trial97 --- .../ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp | 6 +++--- .../ui/pages/modplatform/modrinth/ModrinthResourcePages.h | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp index 064cb28bf..398bf0455 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp @@ -150,11 +150,11 @@ std::unique_ptr ModrinthModPage::createFilterWidget() void ModrinthModPage::prepareProviderCategories() { auto response = std::make_shared(); - auto task = ModrinthAPI::getModCategories(response); - QObject::connect(task.get(), &Task::succeeded, [this, response]() { + m_categoriesTask = ModrinthAPI::getModCategories(response); + QObject::connect(m_categoriesTask.get(), &Task::succeeded, [this, response]() { auto categories = ModrinthAPI::loadModCategories(response); m_filter_widget->setCategories(categories); }); - task->start(); + m_categoriesTask->start(); }; } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h index f6a789cc3..7f8d9d571 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h @@ -98,6 +98,7 @@ class ModrinthModPage : public ModPage { protected: virtual void prepareProviderCategories() override; + Task::Ptr m_categoriesTask; }; class ModrinthResourcePackPage : public ResourcePackResourcePage { From cb01d5c46ef41998d6905d43850746e4a3190ae4 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Sun, 4 May 2025 09:11:43 +0300 Subject: [PATCH 216/695] feat: refactor logs upload to use the NetJob Signed-off-by: Trial97 --- buildconfig/BuildConfig.cpp.in | 1 - buildconfig/BuildConfig.h | 3 - launcher/Application.cpp | 11 -- launcher/Application.h | 1 - launcher/net/PasteUpload.cpp | 221 +++++++++++++++------------------ launcher/net/PasteUpload.h | 60 +++++---- launcher/ui/GuiUtil.cpp | 126 +++++++++++-------- launcher/ui/GuiUtil.h | 4 +- 8 files changed, 215 insertions(+), 212 deletions(-) diff --git a/buildconfig/BuildConfig.cpp.in b/buildconfig/BuildConfig.cpp.in index 6bebcb80e..3637e7369 100644 --- a/buildconfig/BuildConfig.cpp.in +++ b/buildconfig/BuildConfig.cpp.in @@ -53,7 +53,6 @@ Config::Config() LAUNCHER_SVGFILENAME = "@Launcher_SVGFileName@"; USER_AGENT = "@Launcher_UserAgent@"; - USER_AGENT_UNCACHED = USER_AGENT + " (Uncached)"; // Version information VERSION_MAJOR = @Launcher_VERSION_MAJOR@; diff --git a/buildconfig/BuildConfig.h b/buildconfig/BuildConfig.h index b59adcb57..10c38e3d6 100644 --- a/buildconfig/BuildConfig.h +++ b/buildconfig/BuildConfig.h @@ -107,9 +107,6 @@ class Config { /// User-Agent to use. QString USER_AGENT; - /// User-Agent to use for uncached requests. - QString USER_AGENT_UNCACHED; - /// The git commit hash of this build QString GIT_COMMIT; diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 0daab026c..772e685eb 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -1883,17 +1883,6 @@ QString Application::getUserAgent() return BuildConfig.USER_AGENT; } -QString Application::getUserAgentUncached() -{ - QString uaOverride = m_settings->get("UserAgentOverride").toString(); - if (!uaOverride.isEmpty()) { - uaOverride += " (Uncached)"; - return uaOverride.replace("$LAUNCHER_VER", BuildConfig.printableVersionString()); - } - - return BuildConfig.USER_AGENT_UNCACHED; -} - bool Application::handleDataMigration(const QString& currentData, const QString& oldData, const QString& name, diff --git a/launcher/Application.h b/launcher/Application.h index 12f41509c..fefb32292 100644 --- a/launcher/Application.h +++ b/launcher/Application.h @@ -160,7 +160,6 @@ class Application : public QApplication { QString getFlameAPIKey(); QString getModrinthAPIToken(); QString getUserAgent(); - QString getUserAgentUncached(); /// this is the root of the 'installation'. Used for automatic updates const QString& root() { return m_rootPath; } diff --git a/launcher/net/PasteUpload.cpp b/launcher/net/PasteUpload.cpp index 86a44669e..8df47d006 100644 --- a/launcher/net/PasteUpload.cpp +++ b/launcher/net/PasteUpload.cpp @@ -36,74 +36,42 @@ */ #include "PasteUpload.h" -#include "Application.h" -#include "BuildConfig.h" -#include -#include #include #include #include #include #include -#include "net/Logging.h" +const std::array PasteUpload::PasteTypes = { { { "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" } } }; -std::array PasteUpload::PasteTypes = { { { "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()) -{ - 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() {} - -void PasteUpload::executeTask() +QNetworkReply* PasteUpload::getReply(QNetworkRequest& request) { - QNetworkRequest request{ QUrl(m_uploadUrl) }; - QNetworkReply* rep{}; - - request.setHeader(QNetworkRequest::UserAgentHeader, APPLICATION->getUserAgentUncached().toUtf8()); - - switch (m_pasteType) { - case NullPointer: { - QHttpMultiPart* multiPart = new QHttpMultiPart{ QHttpMultiPart::FormDataType }; + switch (m_paste_type) { + case PasteUpload::NullPointer: { + QHttpMultiPart* multiPart = new QHttpMultiPart{ QHttpMultiPart::FormDataType, this }; QHttpPart filePart; - filePart.setBody(m_text); + filePart.setBody(m_log.toUtf8()); filePart.setHeader(QNetworkRequest::ContentTypeHeader, "text/plain"); filePart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data; name=\"file\"; filename=\"log.txt\""); multiPart->append(filePart); - rep = APPLICATION->network()->post(request, multiPart); - multiPart->setParent(rep); - - break; + return m_network->post(request, multiPart); } - case Hastebin: { - request.setHeader(QNetworkRequest::UserAgentHeader, APPLICATION->getUserAgentUncached().toUtf8()); - rep = APPLICATION->network()->post(request, m_text); - break; + case PasteUpload::Hastebin: { + return m_network->post(request, m_log.toUtf8()); } - case Mclogs: { + case PasteUpload::Mclogs: { QUrlQuery postData; - postData.addQueryItem("content", m_text); + postData.addQueryItem("content", m_log); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); - rep = APPLICATION->network()->post(request, postData.toString().toUtf8()); - break; + return m_network->post(request, postData.toString().toUtf8()); } - case PasteGG: { + case PasteUpload::PasteGG: { QJsonObject obj; QJsonDocument doc; request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); @@ -114,7 +82,7 @@ void PasteUpload::executeTask() QJsonObject logFileInfo; QJsonObject logFileContentInfo; logFileContentInfo.insert("format", "text"); - logFileContentInfo.insert("value", QString::fromUtf8(m_text)); + logFileContentInfo.insert("value", m_log); logFileInfo.insert("name", "log.txt"); logFileInfo.insert("content", logFileContentInfo); files.append(logFileInfo); @@ -122,108 +90,115 @@ void PasteUpload::executeTask() obj.insert("files", files); doc.setObject(obj); - rep = APPLICATION->network()->post(request, doc.toJson()); - break; + return m_network->post(request, doc.toJson()); } } - connect(rep, &QNetworkReply::uploadProgress, this, &Task::setProgress); - connect(rep, &QNetworkReply::finished, this, &PasteUpload::downloadFinished); + return nullptr; +}; - connect(rep, &QNetworkReply::errorOccurred, this, &PasteUpload::downloadError); - - m_reply = std::shared_ptr(rep); +auto PasteUpload::Sink::init(QNetworkRequest&) -> Task::State +{ + m_output.clear(); + return Task::State::Running; +}; - setStatus(tr("Uploading to %1").arg(m_uploadUrl)); +auto PasteUpload::Sink::write(QByteArray& data) -> Task::State +{ + m_output.append(data); + return Task::State::Running; } -void PasteUpload::downloadError(QNetworkReply::NetworkError error) +auto PasteUpload::Sink::abort() -> Task::State { - // error happened during download. - qCCritical(taskUploadLogC) << getUid().toString() << "Network error: " << error; - emitFailed(m_reply->errorString()); + m_output.clear(); + return Task::State::Failed; } -void PasteUpload::downloadFinished() +auto PasteUpload::Sink::finalize(QNetworkReply&) -> Task::State { - QByteArray data = m_reply->readAll(); - int statusCode = m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - - if (m_reply->error() != QNetworkReply::NetworkError::NoError) { - emitFailed(tr("Network error: %1").arg(m_reply->errorString())); - m_reply.reset(); - return; - } else if (statusCode != 200 && statusCode != 201) { - QString reasonPhrase = m_reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString(); - emitFailed(tr("Error: %1 returned unexpected status code %2 %3").arg(m_uploadUrl).arg(statusCode).arg(reasonPhrase)); - qCCritical(taskUploadLogC) << getUid().toString() << m_uploadUrl << " returned unexpected status code " << statusCode - << " with body: " << data; - m_reply.reset(); - return; - } - - switch (m_pasteType) { - case NullPointer: - m_pasteLink = QString::fromUtf8(data).trimmed(); + switch (m_paste_type) { + case PasteUpload::NullPointer: + m_result->link = QString::fromUtf8(m_output).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; + case PasteUpload::Hastebin: { + QJsonParseError jsonError; + auto doc = QJsonDocument::fromJson(m_output, &jsonError); + if (jsonError.error != QJsonParseError::NoError) { + qDebug() << "hastebin server did not reply with JSON" << jsonError.errorString(); + return Task::State::Failed; + } + auto obj = doc.object(); + if (obj.contains("key") && obj["key"].isString()) { + QString key = doc.object()["key"].toString(); + m_result->link = m_base_url + "/" + key; } else { - emitFailed(tr("Error: %1 returned a malformed response body").arg(m_uploadUrl)); - qCCritical(taskUploadLogC) << getUid().toString() << getUid().toString() << m_uploadUrl - << " returned malformed response body: " << data; - return; + qDebug() << "Log upload failed:" << doc.toJson(); + return Task::State::Failed; } break; } - case Mclogs: { - QJsonDocument jsonDoc{ QJsonDocument::fromJson(data) }; - QJsonObject jsonObj{ jsonDoc.object() }; - if (jsonObj.contains("success") && jsonObj["success"].isBool()) { - bool success = jsonObj["success"].toBool(); + case PasteUpload::Mclogs: { + QJsonParseError jsonError; + auto doc = QJsonDocument::fromJson(m_output, &jsonError); + if (jsonError.error != QJsonParseError::NoError) { + qDebug() << "mclogs server did not reply with JSON" << jsonError.errorString(); + return Task::State::Failed; + } + auto obj = doc.object(); + if (obj.contains("success") && obj["success"].isBool()) { + bool success = obj["success"].toBool(); if (success) { - m_pasteLink = jsonObj["url"].toString(); + m_result->link = obj["url"].toString(); } else { - QString error = jsonObj["error"].toString(); - emitFailed(tr("Error: %1 returned an error: %2").arg(m_uploadUrl, error)); - qCCritical(taskUploadLogC) << getUid().toString() << m_uploadUrl << " returned error: " << error; - qCCritical(taskUploadLogC) << getUid().toString() << "Response body: " << data; - return; + m_result->error = obj["error"].toString(); } } else { - emitFailed(tr("Error: %1 returned a malformed response body").arg(m_uploadUrl)); - qCCritical(taskUploadLogC) << getUid().toString() << m_uploadUrl << " returned malformed response body: " << data; - return; + qDebug() << "Log upload failed:" << doc.toJson(); + return Task::State::Failed; } break; } - case PasteGG: - QJsonDocument jsonDoc{ QJsonDocument::fromJson(data) }; - QJsonObject jsonObj{ jsonDoc.object() }; - if (jsonObj.contains("status") && jsonObj["status"].isString()) { - QString status = jsonObj["status"].toString(); + case PasteUpload::PasteGG: + QJsonParseError jsonError; + auto doc = QJsonDocument::fromJson(m_output, &jsonError); + if (jsonError.error != QJsonParseError::NoError) { + qDebug() << "pastegg server did not reply with JSON" << jsonError.errorString(); + return Task::State::Failed; + } + auto obj = doc.object(); + if (obj.contains("status") && obj["status"].isString()) { + QString status = obj["status"].toString(); if (status == "success") { - m_pasteLink = m_baseUrl + "/p/anonymous/" + jsonObj["result"].toObject()["id"].toString(); + m_result->link = m_base_url + "/p/anonymous/" + obj["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)); - qCCritical(taskUploadLogC) << getUid().toString() << m_uploadUrl << " returned error: " << error; - qCCritical(taskUploadLogC) << getUid().toString() << "Error message: " << message; - qCCritical(taskUploadLogC) << getUid().toString() << "Response body: " << data; - return; + m_result->error = obj["error"].toString(); + m_result->extra_message = (obj.contains("message") && obj["message"].isString()) ? obj["message"].toString() : "none"; } } else { - emitFailed(tr("Error: %1 returned a malformed response body").arg(m_uploadUrl)); - qCCritical(taskUploadLogC) << getUid().toString() << m_uploadUrl << " returned malformed response body: " << data; - return; + qDebug() << "Log upload failed:" << doc.toJson(); + return Task::State::Failed; } break; } - emitSucceeded(); + return Task::State::Succeeded; +} + +Net::NetRequest::Ptr PasteUpload::make(const QString& log, + const PasteUpload::PasteType pasteType, + const QString customBaseURL, + ResultPtr result) +{ + auto base = PasteUpload::PasteTypes.at(pasteType); + QString baseUrl = customBaseURL.isEmpty() ? base.defaultBase : customBaseURL; + auto up = makeShared(log, pasteType); + + // HACK: Paste's docs say the standard API path is at /api/ but the official instance paste.gg doesn't follow that?? + if (pasteType == PasteUpload::PasteGG && baseUrl == base.defaultBase) + up->m_url = "https://api.paste.gg/v1/pastes"; + else + up->m_url = baseUrl + base.endpointPath; + + up->m_sink.reset(new Sink(pasteType, baseUrl, result)); + return up; } diff --git a/launcher/net/PasteUpload.h b/launcher/net/PasteUpload.h index 2ba6067c3..b1247a16b 100644 --- a/launcher/net/PasteUpload.h +++ b/launcher/net/PasteUpload.h @@ -35,15 +35,16 @@ #pragma once -#include +#include "net/NetRequest.h" +#include "tasks/Task.h" + #include #include + #include #include -#include "tasks/Task.h" -class PasteUpload : public Task { - Q_OBJECT +class PasteUpload : public Net::NetRequest { public: enum PasteType : int { // 0x0.st @@ -58,32 +59,47 @@ class PasteUpload : public Task { First = NullPointer, Last = Mclogs }; - struct PasteTypeInfo { const QString name; const QString defaultBase; const QString endpointPath; }; - static std::array PasteTypes; + static const std::array PasteTypes; + struct Result { + QString link; + QString error; + QString extra_message; + }; + + using ResultPtr = std::shared_ptr; - PasteUpload(QWidget* window, QString text, QString url, PasteType pasteType); - virtual ~PasteUpload(); + class Sink : public Net::Sink { + public: + Sink(const PasteType pasteType, const QString base_url, ResultPtr result) + : m_paste_type(pasteType), m_base_url(base_url), m_result(result) {}; + virtual ~Sink() = default; - QString pasteLink() { return m_pasteLink; } + 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; + auto hasLocalData() -> bool override { return false; } + + private: + const PasteType m_paste_type; + const QString m_base_url; + ResultPtr m_result; + QByteArray m_output; + }; + PasteUpload(const QString& log, const PasteType pasteType) : m_log(log), m_paste_type(pasteType) {} + virtual ~PasteUpload() = default; - protected: - virtual void executeTask(); + static NetRequest::Ptr make(const QString& log, const PasteType pasteType, const QString baseURL, ResultPtr result); 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 slots: - void downloadError(QNetworkReply::NetworkError); - void downloadFinished(); -}; + virtual QNetworkReply* getReply(QNetworkRequest&) override; + QString m_log; + const PasteType m_paste_type; +}; \ No newline at end of file diff --git a/launcher/ui/GuiUtil.cpp b/launcher/ui/GuiUtil.cpp index d53ade86d..de11d66e0 100644 --- a/launcher/ui/GuiUtil.cpp +++ b/launcher/ui/GuiUtil.cpp @@ -38,10 +38,15 @@ #include "GuiUtil.h" #include +#include #include #include #include +#include + +#include "FileSystem.h" +#include "net/NetJob.h" #include "net/PasteUpload.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/ProgressDialog.h" @@ -74,52 +79,52 @@ QString truncateLogForMclogs(const QString& logContent) return logContent; } +std::optional GuiUtil::uploadPaste(const QString& name, const QFileInfo& filePath, QWidget* parentWidget) +{ + return uploadPaste(name, FS::read(filePath.absoluteFilePath()), parentWidget); +}; + std::optional GuiUtil::uploadPaste(const QString& name, const QString& text, QWidget* parentWidget) { ProgressDialog dialog(parentWidget); - auto pasteTypeSetting = static_cast(APPLICATION->settings()->get("PastebinType").toInt()); - auto pasteCustomAPIBaseSetting = APPLICATION->settings()->get("PastebinCustomAPIBase").toString(); + auto pasteType = static_cast(APPLICATION->settings()->get("PastebinType").toInt()); + auto baseURL = APPLICATION->settings()->get("PastebinCustomAPIBase").toString(); bool shouldTruncate = false; - { - QUrl baseUrl; - if (pasteCustomAPIBaseSetting.isEmpty()) - baseUrl = PasteUpload::PasteTypes[pasteTypeSetting].defaultBase; - else - baseUrl = pasteCustomAPIBaseSetting; - - if (baseUrl.isValid()) { - auto response = CustomMessageBox::selectable(parentWidget, QObject::tr("Confirm Upload"), - QObject::tr("You are about to upload \"%1\" to %2.\n" - "You should double-check for personal information.\n\n" - "Are you sure?") - .arg(name, baseUrl.host()), - QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) - ->exec(); - - if (response != QMessageBox::Yes) + if (baseURL.isEmpty()) + baseURL = PasteUpload::PasteTypes[pasteType].defaultBase; + + if (auto url = QUrl(baseURL); url.isValid()) { + auto response = CustomMessageBox::selectable(parentWidget, QObject::tr("Confirm Upload"), + QObject::tr("You are about to upload \"%1\" to %2.\n" + "You should double-check for personal information.\n\n" + "Are you sure?") + .arg(name, url.host()), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); + + if (response != QMessageBox::Yes) + return {}; + + if (baseURL == "https://api.mclo.gs" && text.count("\n") > MaxMclogsLines) { + auto truncateResponse = CustomMessageBox::selectable( + parentWidget, QObject::tr("Confirm Truncation"), + QObject::tr("The log has %1 lines, exceeding mclo.gs' limit of %2.\n" + "The launcher can keep the first %3 and last %4 lines, trimming the middle.\n\n" + "If you choose 'No', mclo.gs will only keep the first %2 lines, cutting off " + "potentially useful info like crashes at the end.\n\n" + "Proceed with truncation?") + .arg(text.count("\n")) + .arg(MaxMclogsLines) + .arg(InitialMclogsLines) + .arg(FinalMclogsLines), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel, QMessageBox::No) + ->exec(); + + if (truncateResponse == QMessageBox::Cancel) { return {}; - - if (baseUrl.toString() == "https://api.mclo.gs" && text.count("\n") > MaxMclogsLines) { - auto truncateResponse = CustomMessageBox::selectable( - parentWidget, QObject::tr("Confirm Truncation"), - QObject::tr("The log has %1 lines, exceeding mclo.gs' limit of %2.\n" - "The launcher can keep the first %3 and last %4 lines, trimming the middle.\n\n" - "If you choose 'No', mclo.gs will only keep the first %2 lines, cutting off " - "potentially useful info like crashes at the end.\n\n" - "Proceed with truncation?") - .arg(text.count("\n")) - .arg(MaxMclogsLines) - .arg(InitialMclogsLines) - .arg(FinalMclogsLines), - QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel, QMessageBox::No) - ->exec(); - - if (truncateResponse == QMessageBox::Cancel) { - return {}; - } - shouldTruncate = truncateResponse == QMessageBox::Yes; } + shouldTruncate = truncateResponse == QMessageBox::Yes; } } @@ -128,22 +133,43 @@ std::optional GuiUtil::uploadPaste(const QString& name, const QString& textToUpload = truncateLogForMclogs(text); } - std::unique_ptr paste(new PasteUpload(parentWidget, textToUpload, pasteCustomAPIBaseSetting, pasteTypeSetting)); - - dialog.execWithTask(paste.get()); - if (!paste->wasSuccessful()) { - CustomMessageBox::selectable(parentWidget, QObject::tr("Upload failed"), paste->failReason(), QMessageBox::Critical)->exec(); - return QString(); - } else { - const QString link = paste->pasteLink(); - setClipboardText(link); + auto result = std::make_shared(); + auto job = NetJob::Ptr(new NetJob("Log Upload", APPLICATION->network())); + + job->addNetAction(PasteUpload::make(textToUpload, pasteType, baseURL, result)); + QObject::connect(job.get(), &Task::failed, [parentWidget](QString reason) { + CustomMessageBox::selectable(parentWidget, QObject::tr("Failed to upload logs!"), reason, QMessageBox::Critical)->show(); + }); + QObject::connect(job.get(), &Task::aborted, [parentWidget] { + CustomMessageBox::selectable(parentWidget, QObject::tr("Logs upload aborted"), + QObject::tr("The task has been aborted by the user."), QMessageBox::Information) + ->show(); + }); + + if (dialog.execWithTask(job.get()) == QDialog::Accepted) { + if (!result->error.isEmpty() || !result->extra_message.isEmpty()) { + QString message = QObject::tr("Error: %1").arg(result->error); + if (!result->extra_message.isEmpty()) { + message += QObject::tr("\nError message: %1").arg(result->extra_message); + } + CustomMessageBox::selectable(parentWidget, QObject::tr("Failed to upload logs!"), message, QMessageBox::Critical)->show(); + return {}; + } + if (result->link.isEmpty()) { + CustomMessageBox::selectable(parentWidget, QObject::tr("Failed to upload logs!"), "The upload link is empty", + QMessageBox::Critical) + ->show(); + return {}; + } + setClipboardText(result->link); CustomMessageBox::selectable( parentWidget, QObject::tr("Upload finished"), - QObject::tr("The link to the uploaded log has been placed in your clipboard.").arg(link), + QObject::tr("The link to the uploaded log has been placed in your clipboard.").arg(result->link), QMessageBox::Information) ->exec(); - return link; + return result->link; } + return {}; } void GuiUtil::setClipboardText(const QString& text) diff --git a/launcher/ui/GuiUtil.h b/launcher/ui/GuiUtil.h index 8d384d3f6..d380ccfe7 100644 --- a/launcher/ui/GuiUtil.h +++ b/launcher/ui/GuiUtil.h @@ -1,10 +1,12 @@ #pragma once +#include #include #include namespace GuiUtil { -std::optional uploadPaste(const QString& name, const QString& text, QWidget* parentWidget); +std::optional uploadPaste(const QString& name, const QFileInfo& filePath, QWidget* parentWidget); +std::optional uploadPaste(const QString& name, const QString& data, QWidget* parentWidget); void setClipboardText(const QString& text); QStringList BrowseForFiles(QString context, QString caption, QString filter, QString defaultPath, QWidget* parentWidget); QString BrowseForFile(QString context, QString caption, QString filter, QString defaultPath, QWidget* parentWidget); From 63d40ecda429b64bd80df2b8f023d9429e0acf4e Mon Sep 17 00:00:00 2001 From: Trial97 Date: Sun, 19 Nov 2023 11:33:55 +0200 Subject: [PATCH 217/695] feat: add upload action for launcher logs Signed-off-by: Trial97 --- launcher/ui/MainWindow.cpp | 11 +++++++++++ launcher/ui/MainWindow.ui | 13 +++++++++++++ 2 files changed, 24 insertions(+) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index d9275a7ab..920db8ca7 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -90,6 +90,7 @@ #include #include "InstanceWindow.h" +#include "ui/GuiUtil.h" #include "ui/dialogs/AboutDialog.h" #include "ui/dialogs/CopyInstanceDialog.h" #include "ui/dialogs/CustomMessageBox.h" @@ -235,6 +236,16 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi ui->actionViewJavaFolder->setEnabled(BuildConfig.JAVA_DOWNLOADER_ENABLED); } + { // logs upload + + auto menu = new QMenu(this); + for (auto file : QDir("logs").entryInfoList(QDir::Files)) { + auto action = menu->addAction(file.fileName()); + connect(action, &QAction::triggered, this, [this, file] { GuiUtil::uploadPaste(file.fileName(), file, this); }); + } + ui->actionUploadLog->setMenu(menu); + } + // add the toolbar toggles to the view menu ui->viewMenu->addAction(ui->instanceToolBar->toggleViewAction()); ui->viewMenu->addAction(ui->newsToolBar->toggleViewAction()); diff --git a/launcher/ui/MainWindow.ui b/launcher/ui/MainWindow.ui index f20c34206..e01d0fa74 100644 --- a/launcher/ui/MainWindow.ui +++ b/launcher/ui/MainWindow.ui @@ -215,6 +215,7 @@
    + @@ -696,6 +697,18 @@ Clear cached metadata
    + + + + .. + + + Upload logs + + + Upload launcher logs to the selected log provider + + From e3258061737ba18bdf328d270f7dbdc8886a1ae4 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Sun, 4 May 2025 09:12:58 +0300 Subject: [PATCH 218/695] feat: add regex removal for log sesnitive data Signed-off-by: Trial97 --- launcher/CMakeLists.txt | 4 ++ launcher/logs/AnonymizeLog.cpp | 68 ++++++++++++++++++++++++++++++++++ launcher/logs/AnonymizeLog.h | 40 ++++++++++++++++++++ launcher/net/PasteUpload.cpp | 12 ++++-- launcher/net/PasteUpload.h | 5 ++- launcher/ui/GuiUtil.cpp | 4 +- launcher/ui/GuiUtil.h | 2 +- 7 files changed, 127 insertions(+), 8 deletions(-) create mode 100644 launcher/logs/AnonymizeLog.cpp create mode 100644 launcher/logs/AnonymizeLog.h diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 4f8b9018a..dbd9dc0bb 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -834,6 +834,10 @@ SET(LAUNCHER_SOURCES icons/IconList.h icons/IconList.cpp + # log utils + logs/AnonymizeLog.cpp + logs/AnonymizeLog.h + # GUI - windows ui/GuiUtil.h ui/GuiUtil.cpp diff --git a/launcher/logs/AnonymizeLog.cpp b/launcher/logs/AnonymizeLog.cpp new file mode 100644 index 000000000..e5021a616 --- /dev/null +++ b/launcher/logs/AnonymizeLog.cpp @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2025 Trial97 + * + * 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 "AnonymizeLog.h" + +#include + +struct RegReplace { + RegReplace(QRegularExpression r, QString w) : reg(r), with(w) { reg.optimize(); } + QRegularExpression reg; + QString with; +}; + +static const QVector anonymizeRules = { + RegReplace(QRegularExpression("C:\\\\Users\\\\([^\\\\]+)\\\\", QRegularExpression::CaseInsensitiveOption), + "C:\\Users\\********\\"), // windows + RegReplace(QRegularExpression("C:\\/Users\\/([^\\/]+)\\/", QRegularExpression::CaseInsensitiveOption), + "C:/Users/********/"), // windows with forward slashes + RegReplace(QRegularExpression("(?)"), // SESSION_TOKEN + RegReplace(QRegularExpression("new refresh token: \"[^\"]+\"", QRegularExpression::CaseInsensitiveOption), + "new refresh token: \"\""), // refresh token + RegReplace(QRegularExpression("\"device_code\" : \"[^\"]+\"", QRegularExpression::CaseInsensitiveOption), + "\"device_code\" : \"\""), // device code +}; + +void anonymizeLog(QString& log) +{ + for (auto rule : anonymizeRules) { + log.replace(rule.reg, rule.with); + } +} \ No newline at end of file diff --git a/launcher/logs/AnonymizeLog.h b/launcher/logs/AnonymizeLog.h new file mode 100644 index 000000000..2409ecee7 --- /dev/null +++ b/launcher/logs/AnonymizeLog.h @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2025 Trial97 + * + * 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 + +void anonymizeLog(QString& log); \ No newline at end of file diff --git a/launcher/net/PasteUpload.cpp b/launcher/net/PasteUpload.cpp index 8df47d006..2333a2256 100644 --- a/launcher/net/PasteUpload.cpp +++ b/launcher/net/PasteUpload.cpp @@ -41,7 +41,9 @@ #include #include #include +#include #include +#include "logs/AnonymizeLog.h" const std::array PasteUpload::PasteTypes = { { { "0x0.st", "https://0x0.st", "" }, { "hastebin", "https://hst.sh", "/documents" }, @@ -184,10 +186,7 @@ auto PasteUpload::Sink::finalize(QNetworkReply&) -> Task::State return Task::State::Succeeded; } -Net::NetRequest::Ptr PasteUpload::make(const QString& log, - const PasteUpload::PasteType pasteType, - const QString customBaseURL, - ResultPtr result) +Net::NetRequest::Ptr PasteUpload::make(const QString& log, PasteUpload::PasteType pasteType, QString customBaseURL, ResultPtr result) { auto base = PasteUpload::PasteTypes.at(pasteType); QString baseUrl = customBaseURL.isEmpty() ? base.defaultBase : customBaseURL; @@ -202,3 +201,8 @@ Net::NetRequest::Ptr PasteUpload::make(const QString& log, up->m_sink.reset(new Sink(pasteType, baseUrl, result)); return up; } + +PasteUpload::PasteUpload(const QString& log, PasteType pasteType) : m_log(log), m_paste_type(pasteType) +{ + anonymizeLog(m_log); +} diff --git a/launcher/net/PasteUpload.h b/launcher/net/PasteUpload.h index b1247a16b..55fb2231c 100644 --- a/launcher/net/PasteUpload.h +++ b/launcher/net/PasteUpload.h @@ -39,6 +39,7 @@ #include "tasks/Task.h" #include +#include #include #include @@ -93,10 +94,10 @@ class PasteUpload : public Net::NetRequest { ResultPtr m_result; QByteArray m_output; }; - PasteUpload(const QString& log, const PasteType pasteType) : m_log(log), m_paste_type(pasteType) {} + PasteUpload(const QString& log, PasteType pasteType); virtual ~PasteUpload() = default; - static NetRequest::Ptr make(const QString& log, const PasteType pasteType, const QString baseURL, ResultPtr result); + static NetRequest::Ptr make(const QString& log, PasteType pasteType, QString baseURL, ResultPtr result); private: virtual QNetworkReply* getReply(QNetworkRequest&) override; diff --git a/launcher/ui/GuiUtil.cpp b/launcher/ui/GuiUtil.cpp index de11d66e0..adb6e8bf2 100644 --- a/launcher/ui/GuiUtil.cpp +++ b/launcher/ui/GuiUtil.cpp @@ -46,6 +46,7 @@ #include #include "FileSystem.h" +#include "logs/AnonymizeLog.h" #include "net/NetJob.h" #include "net/PasteUpload.h" #include "ui/dialogs/CustomMessageBox.h" @@ -172,8 +173,9 @@ std::optional GuiUtil::uploadPaste(const QString& name, const QString& return {}; } -void GuiUtil::setClipboardText(const QString& text) +void GuiUtil::setClipboardText(QString text) { + anonymizeLog(text); QApplication::clipboard()->setText(text); } diff --git a/launcher/ui/GuiUtil.h b/launcher/ui/GuiUtil.h index d380ccfe7..c3ba01f5b 100644 --- a/launcher/ui/GuiUtil.h +++ b/launcher/ui/GuiUtil.h @@ -7,7 +7,7 @@ namespace GuiUtil { std::optional uploadPaste(const QString& name, const QFileInfo& filePath, QWidget* parentWidget); std::optional uploadPaste(const QString& name, const QString& data, QWidget* parentWidget); -void setClipboardText(const QString& text); +void setClipboardText(QString text); QStringList BrowseForFiles(QString context, QString caption, QString filter, QString defaultPath, QWidget* parentWidget); QString BrowseForFile(QString context, QString caption, QString filter, QString defaultPath, QWidget* parentWidget); } // namespace GuiUtil From fa189572dbd0d5ed1b4a09f315efc3d0d2f80bf7 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Wed, 7 May 2025 22:52:22 +0300 Subject: [PATCH 219/695] feat: search for pack icon in the actual file Signed-off-by: Trial97 --- launcher/InstanceImportTask.cpp | 47 +++++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index e2735385b..b0314743d 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -262,6 +262,25 @@ void InstanceImportTask::extractFinished() } } +bool installIcon(QString root, QString instIcon) +{ + auto importIconPath = IconUtils::findBestIconIn(root, instIcon); + if (importIconPath.isNull() || !QFile::exists(importIconPath)) + importIconPath = IconUtils::findBestIconIn(root, "icon.png"); + if (importIconPath.isNull() || !QFile::exists(importIconPath)) + importIconPath = IconUtils::findBestIconIn(FS::PathCombine(root, "overrides"), "icon.png"); + if (!importIconPath.isNull() && QFile::exists(importIconPath)) { + // import icon + auto iconList = APPLICATION->icons(); + if (iconList->iconFileExists(instIcon)) { + iconList->deleteIcon(instIcon); + } + iconList->installIcon(importIconPath, instIcon); + return true; + } + return false; +} + void InstanceImportTask::processFlame() { shared_qobject_ptr inst_creation_task = nullptr; @@ -287,6 +306,14 @@ void InstanceImportTask::processFlame() } inst_creation_task->setName(*this); + // if the icon was specified by user, use that. otherwise pull icon from the pack + if (m_instIcon == "default") { + auto iconKey = QString("Flame_%1_Icon").arg(name()); + + if (installIcon(m_stagingPath, iconKey)) { + m_instIcon = iconKey; + } + } inst_creation_task->setIcon(m_instIcon); inst_creation_task->setGroup(m_instGroup); inst_creation_task->setConfirmUpdate(shouldConfirmUpdate()); @@ -339,17 +366,7 @@ void InstanceImportTask::processMultiMC() } else { m_instIcon = instance.iconKey(); - auto importIconPath = IconUtils::findBestIconIn(instance.instanceRoot(), m_instIcon); - if (importIconPath.isNull() || !QFile::exists(importIconPath)) - importIconPath = IconUtils::findBestIconIn(instance.instanceRoot(), "icon.png"); - if (!importIconPath.isNull() && QFile::exists(importIconPath)) { - // import icon - auto iconList = APPLICATION->icons(); - if (iconList->iconFileExists(m_instIcon)) { - iconList->deleteIcon(m_instIcon); - } - iconList->installIcon(importIconPath, m_instIcon); - } + installIcon(instance.instanceRoot(), m_instIcon); } emitSucceeded(); } @@ -386,6 +403,14 @@ void InstanceImportTask::processModrinth() } inst_creation_task->setName(*this); + // if the icon was specified by user, use that. otherwise pull icon from the pack + if (m_instIcon == "default") { + auto iconKey = QString("Modrinth_%1_Icon").arg(name()); + + if (installIcon(m_stagingPath, iconKey)) { + m_instIcon = iconKey; + } + } inst_creation_task->setIcon(m_instIcon); inst_creation_task->setGroup(m_instGroup); inst_creation_task->setConfirmUpdate(shouldConfirmUpdate()); From 4bad7e48c30d3aa3b29967493ca5286270f1ce46 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Wed, 7 May 2025 23:21:35 +0300 Subject: [PATCH 220/695] feat: prevent deletion of running instances Signed-off-by: Trial97 --- launcher/ui/MainWindow.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index d9275a7ab..4043a4285 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -1383,6 +1383,14 @@ void MainWindow::on_actionDeleteInstance_triggered() return; } + if (m_selectedInstance->isRunning()) { + CustomMessageBox::selectable(this, tr("Cannot Delete Running Instance"), + tr("The selected instance is currently running and cannot be deleted. Please stop the instance before " + "attempting to delete it."), + QMessageBox::Warning, QMessageBox::Ok) + ->exec(); + return; + } auto id = m_selectedInstance->id(); auto response = CustomMessageBox::selectable(this, tr("Confirm Deletion"), From cf7061b9a82db766c67df402f9976e59368dd85b Mon Sep 17 00:00:00 2001 From: seth Date: Wed, 7 May 2025 17:42:30 -0400 Subject: [PATCH 221/695] fix(FileSystem): dont re-define structs for newer mingw versions Signed-off-by: seth --- launcher/FileSystem.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp index 08dc7d2cc..ecb1e42d2 100644 --- a/launcher/FileSystem.cpp +++ b/launcher/FileSystem.cpp @@ -107,6 +107,10 @@ namespace fs = std::filesystem; #if defined(__MINGW32__) +// Avoid re-defining structs retroactively added to MinGW +// https://github.com/mingw-w64/mingw-w64/issues/90#issuecomment-2829284729 +#if __MINGW64_VERSION_MAJOR < 13 + struct _DUPLICATE_EXTENTS_DATA { HANDLE FileHandle; LARGE_INTEGER SourceFileOffset; @@ -116,6 +120,7 @@ struct _DUPLICATE_EXTENTS_DATA { using DUPLICATE_EXTENTS_DATA = _DUPLICATE_EXTENTS_DATA; using PDUPLICATE_EXTENTS_DATA = _DUPLICATE_EXTENTS_DATA*; +#endif struct _FSCTL_GET_INTEGRITY_INFORMATION_BUFFER { WORD ChecksumAlgorithm; // Checksum algorithm. e.g. CHECKSUM_TYPE_UNCHANGED, CHECKSUM_TYPE_NONE, CHECKSUM_TYPE_CRC32 From 0711890d1870db899222ffc18189850af5402f61 Mon Sep 17 00:00:00 2001 From: seth Date: Wed, 7 May 2025 17:55:05 -0400 Subject: [PATCH 222/695] ci(mingw): use tomlplusplus from msys2 Signed-off-by: seth --- .github/actions/setup-dependencies/windows/action.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/actions/setup-dependencies/windows/action.yml b/.github/actions/setup-dependencies/windows/action.yml index 78717ddf4..b7ade26e0 100644 --- a/.github/actions/setup-dependencies/windows/action.yml +++ b/.github/actions/setup-dependencies/windows/action.yml @@ -45,6 +45,7 @@ runs: qt6-5compat:p qt6-networkauth:p cmark:p + tomlplusplus:p quazip-qt6:p - name: Retrieve ccache cache (MinGW) From 9d79695512acbfd487760400f11c68fd2dfa5a38 Mon Sep 17 00:00:00 2001 From: seth Date: Wed, 7 May 2025 17:56:33 -0400 Subject: [PATCH 223/695] ci(mingw): print msys2 packages Also lists theirs versions, which is useful for debugging issues like those fixed in https://github.com/PrismLauncher/PrismLauncher/pull/3756 Signed-off-by: seth --- .github/actions/setup-dependencies/windows/action.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/actions/setup-dependencies/windows/action.yml b/.github/actions/setup-dependencies/windows/action.yml index b7ade26e0..0a643f583 100644 --- a/.github/actions/setup-dependencies/windows/action.yml +++ b/.github/actions/setup-dependencies/windows/action.yml @@ -25,7 +25,7 @@ runs: arch: ${{ inputs.vcvars-arch }} vsversion: 2022 - - name: Setup MSYS2 (MinGW-64) + - name: Setup MSYS2 (MinGW) if: ${{ inputs.msystem != '' }} uses: msys2/setup-msys2@v2 with: @@ -48,6 +48,12 @@ runs: tomlplusplus:p quazip-qt6:p + - name: List pacman packages (MinGW) + if: ${{ inputs.msystem != '' }} + shell: msys2 {0} + run: | + pacman -Qe + - name: Retrieve ccache cache (MinGW) if: ${{ inputs.msystem != '' && inputs.build-type == 'Debug' }} uses: actions/cache@v4.2.3 From 3dcac0de502de5c7425bb8e66e0d0ad761418c88 Mon Sep 17 00:00:00 2001 From: seth Date: Wed, 7 May 2025 18:03:37 -0400 Subject: [PATCH 224/695] ci: run workflows on local action changes Helps ensure they still actually work when changes are made Signed-off-by: seth --- .github/workflows/build.yml | 4 ++++ .github/workflows/codeql.yml | 2 ++ 2 files changed, 6 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ac33aba27..f4cdae97c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,6 +24,8 @@ on: # Workflows - ".github/workflows/build.yml" + - ".github/actions/package/" + - ".github/actions/setup-dependencies/" pull_request: paths: # File types @@ -44,6 +46,8 @@ on: # Workflows - ".github/workflows/build.yml" + - ".github/actions/package/" + - ".github/actions/setup-dependencies/" workflow_call: inputs: build-type: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 5a2ecbd6d..f8fae8ecf 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -23,6 +23,7 @@ on: # Workflows - ".github/codeql" - ".github/workflows/codeql.yml" + - ".github/actions/setup-dependencies/" pull_request: paths: # File types @@ -44,6 +45,7 @@ on: # Workflows - ".github/codeql" - ".github/workflows/codeql.yml" + - ".github/actions/setup-dependencies/" workflow_dispatch: jobs: From b9a97c8647767d85c2a96545007f33fb557656cb Mon Sep 17 00:00:00 2001 From: seth Date: Wed, 7 May 2025 21:01:00 -0400 Subject: [PATCH 225/695] ci(release): upload mingw-arm64 artifacts Signed-off-by: seth --- .github/workflows/release.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a93233dab..6e879cfd7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -66,6 +66,17 @@ jobs: cd .. done + for d in PrismLauncher-Windows-MinGW-arm64*; do + cd "${d}" || continue + INST="$(echo -n ${d} | grep -o Setup || true)" + PORT="$(echo -n ${d} | grep -o Portable || true)" + NAME="PrismLauncher-Windows-MinGW-arm64" + test -z "${PORT}" || NAME="${NAME}-Portable" + test -z "${INST}" || mv PrismLauncher-*.exe ../${NAME}-Setup-${{ env.VERSION }}.exe + test -n "${INST}" || zip -r -9 "../${NAME}-${{ env.VERSION }}.zip" * + cd .. + done + - name: Create release id: create_release uses: softprops/action-gh-release@v2 From c3749c4fdc80a2863da7e273cb7062bd98922d4b Mon Sep 17 00:00:00 2001 From: Trial97 Date: Wed, 7 May 2025 10:11:45 +0300 Subject: [PATCH 226/695] feat: add sink fail reason and correctly propagate it through the NetRequest Signed-off-by: Trial97 --- launcher/net/ByteArraySink.h | 6 +++++- launcher/net/FileSink.cpp | 9 ++++++++- launcher/net/NetRequest.cpp | 12 ++++++++---- launcher/net/Sink.h | 3 +++ launcher/screenshots/ImgurAlbumCreation.cpp | 3 +++ launcher/screenshots/ImgurUpload.cpp | 3 +++ launcher/tasks/ConcurrentTask.cpp | 8 ++++++-- 7 files changed, 36 insertions(+), 8 deletions(-) diff --git a/launcher/net/ByteArraySink.h b/launcher/net/ByteArraySink.h index ac64052b9..f68230838 100644 --- a/launcher/net/ByteArraySink.h +++ b/launcher/net/ByteArraySink.h @@ -58,6 +58,7 @@ class ByteArraySink : public Sink { qWarning() << "ByteArraySink did not initialize the buffer because it's not addressable"; if (initAllValidators(request)) return Task::State::Running; + m_fail_reason = "Failed to initialize validators"; return Task::State::Failed; }; @@ -69,12 +70,14 @@ class ByteArraySink : public Sink { qWarning() << "ByteArraySink did not write the buffer because it's not addressable"; if (writeAllValidators(data)) return Task::State::Running; + m_fail_reason = "Failed to write validators"; return Task::State::Failed; } auto abort() -> Task::State override { failAllValidators(); + m_fail_reason = "Aborted"; return Task::State::Failed; } @@ -82,12 +85,13 @@ class ByteArraySink : public Sink { { if (finalizeAllValidators(reply)) return Task::State::Succeeded; + m_fail_reason = "Failed to finalize validators"; return Task::State::Failed; } auto hasLocalData() -> bool override { return false; } - private: + protected: std::shared_ptr m_output; }; } // namespace Net diff --git a/launcher/net/FileSink.cpp b/launcher/net/FileSink.cpp index 3a58a4667..1f519708f 100644 --- a/launcher/net/FileSink.cpp +++ b/launcher/net/FileSink.cpp @@ -51,6 +51,7 @@ Task::State FileSink::init(QNetworkRequest& request) // create a new save file and open it for writing if (!FS::ensureFilePathExists(m_filename)) { qCCritical(taskNetLogC) << "Could not create folder for " + m_filename; + m_fail_reason = "Could not create folder"; return Task::State::Failed; } @@ -58,11 +59,13 @@ Task::State FileSink::init(QNetworkRequest& request) m_output_file.reset(new PSaveFile(m_filename)); if (!m_output_file->open(QIODevice::WriteOnly)) { qCCritical(taskNetLogC) << "Could not open " + m_filename + " for writing"; + m_fail_reason = "Could not open file"; return Task::State::Failed; } if (initAllValidators(request)) return Task::State::Running; + m_fail_reason = "Failed to initialize validators"; return Task::State::Failed; } @@ -73,6 +76,7 @@ Task::State FileSink::write(QByteArray& data) m_output_file->cancelWriting(); m_output_file.reset(); m_wroteAnyData = false; + m_fail_reason = "Failed to write validators"; return Task::State::Failed; } @@ -105,13 +109,16 @@ Task::State FileSink::finalize(QNetworkReply& reply) if (gotFile || m_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)) { + m_fail_reason = "Failed to finalize validators"; return Task::State::Failed; + } // nothing went wrong... if (!m_output_file->commit()) { qCCritical(taskNetLogC) << "Failed to commit changes to " << m_filename; m_output_file->cancelWriting(); + m_fail_reason = "Failed to commit changes"; return Task::State::Failed; } } diff --git a/launcher/net/NetRequest.cpp b/launcher/net/NetRequest.cpp index ef533f599..7d8468a97 100644 --- a/launcher/net/NetRequest.cpp +++ b/launcher/net/NetRequest.cpp @@ -84,7 +84,8 @@ void NetRequest::executeTask() break; case State::Inactive: case State::Failed: - emit failed("Failed to initialize sink"); + m_failReason = m_sink->failReason(); + emit failed(m_sink->failReason()); emit finished(); return; case State::AbortedByUser: @@ -259,6 +260,7 @@ void NetRequest::downloadFinished() } else if (m_state == State::Failed) { qCDebug(logCat) << getUid().toString() << "Request failed in previous step:" << m_url.toString(); m_sink->abort(); + m_failReason = m_reply->errorString(); emit failed(m_reply->errorString()); emit finished(); return; @@ -278,7 +280,8 @@ void NetRequest::downloadFinished() if (m_state != State::Succeeded) { qCDebug(logCat) << getUid().toString() << "Request failed to write:" << m_url.toString(); m_sink->abort(); - emit failed("failed to write in sink"); + m_failReason = m_sink->failReason(); + emit failed(m_sink->failReason()); emit finished(); return; } @@ -289,7 +292,8 @@ void NetRequest::downloadFinished() if (m_state != State::Succeeded) { qCDebug(logCat) << getUid().toString() << "Request failed to finalize:" << m_url.toString(); m_sink->abort(); - emit failed("failed to finalize the request"); + m_failReason = m_sink->failReason(); + emit failed(m_sink->failReason()); emit finished(); return; } @@ -305,7 +309,7 @@ void NetRequest::downloadReadyRead() auto data = m_reply->readAll(); m_state = m_sink->write(data); if (m_state == State::Failed) { - qCCritical(logCat) << getUid().toString() << "Failed to process response chunk"; + qCCritical(logCat) << getUid().toString() << "Failed to process response chunk:" << m_sink->failReason(); } // qDebug() << "Request" << m_url.toString() << "gained" << data.size() << "bytes"; } else { diff --git a/launcher/net/Sink.h b/launcher/net/Sink.h index d1fd9de10..3f04cbd82 100644 --- a/launcher/net/Sink.h +++ b/launcher/net/Sink.h @@ -52,6 +52,8 @@ class Sink { virtual auto hasLocalData() -> bool = 0; + QString failReason() const { return m_fail_reason; } + void addValidator(Validator* validator) { if (validator) { @@ -95,5 +97,6 @@ class Sink { protected: std::vector> validators; + QString m_fail_reason; }; } // namespace Net diff --git a/launcher/screenshots/ImgurAlbumCreation.cpp b/launcher/screenshots/ImgurAlbumCreation.cpp index 7ee98760a..1355c74c0 100644 --- a/launcher/screenshots/ImgurAlbumCreation.cpp +++ b/launcher/screenshots/ImgurAlbumCreation.cpp @@ -86,6 +86,7 @@ auto ImgurAlbumCreation::Sink::write(QByteArray& data) -> Task::State auto ImgurAlbumCreation::Sink::abort() -> Task::State { m_output.clear(); + m_fail_reason = "Aborted"; return Task::State::Failed; } @@ -95,11 +96,13 @@ auto ImgurAlbumCreation::Sink::finalize(QNetworkReply&) -> Task::State QJsonDocument doc = QJsonDocument::fromJson(m_output, &jsonError); if (jsonError.error != QJsonParseError::NoError) { qDebug() << jsonError.errorString(); + m_fail_reason = "Invalid json reply"; return Task::State::Failed; } auto object = doc.object(); if (!object.value("success").toBool()) { qDebug() << doc.toJson(); + m_fail_reason = "Failed to create album"; return Task::State::Failed; } m_result->deleteHash = object.value("data").toObject().value("deletehash").toString(); diff --git a/launcher/screenshots/ImgurUpload.cpp b/launcher/screenshots/ImgurUpload.cpp index 8b4ef5327..835a1ab81 100644 --- a/launcher/screenshots/ImgurUpload.cpp +++ b/launcher/screenshots/ImgurUpload.cpp @@ -90,6 +90,7 @@ auto ImgurUpload::Sink::write(QByteArray& data) -> Task::State auto ImgurUpload::Sink::abort() -> Task::State { m_output.clear(); + m_fail_reason = "Aborted"; return Task::State::Failed; } @@ -99,11 +100,13 @@ auto ImgurUpload::Sink::finalize(QNetworkReply&) -> Task::State QJsonDocument doc = QJsonDocument::fromJson(m_output, &jsonError); if (jsonError.error != QJsonParseError::NoError) { qDebug() << "imgur server did not reply with JSON" << jsonError.errorString(); + m_fail_reason = "Invalid json reply"; return Task::State::Failed; } auto object = doc.object(); if (!object.value("success").toBool()) { qDebug() << "Screenshot upload not successful:" << doc.toJson(); + m_fail_reason = "Screenshot was not uploaded successfully"; return Task::State::Failed; } m_shot->m_imgurId = object.value("data").toObject().value("id").toString(); diff --git a/launcher/tasks/ConcurrentTask.cpp b/launcher/tasks/ConcurrentTask.cpp index ad2a14c42..5cff93017 100644 --- a/launcher/tasks/ConcurrentTask.cpp +++ b/launcher/tasks/ConcurrentTask.cpp @@ -118,10 +118,14 @@ void ConcurrentTask::executeNextSubTask() } if (m_queue.isEmpty()) { if (m_doing.isEmpty()) { - if (m_failed.isEmpty()) + if (m_failed.isEmpty()) { emitSucceeded(); - else + } else if (m_failed.count() == 1) { + auto task = m_failed.keys().first(); + emitFailed(task->failReason()); + } else { emitFailed(tr("One or more subtasks failed")); + } } return; } From de541bf39700d028e87caf3704d753eb87edb70f Mon Sep 17 00:00:00 2001 From: Trial97 Date: Wed, 7 May 2025 10:12:46 +0300 Subject: [PATCH 227/695] refactor: paste upload to report the error directly Signed-off-by: Trial97 --- launcher/net/PasteUpload.cpp | 95 ++++++++++++++++++++---------------- launcher/net/PasteUpload.h | 32 +++++------- launcher/ui/GuiUtil.cpp | 23 +++------ 3 files changed, 71 insertions(+), 79 deletions(-) diff --git a/launcher/net/PasteUpload.cpp b/launcher/net/PasteUpload.cpp index 2333a2256..0bbd077cf 100644 --- a/launcher/net/PasteUpload.cpp +++ b/launcher/net/PasteUpload.cpp @@ -36,6 +36,7 @@ */ #include "PasteUpload.h" +#include #include #include @@ -99,86 +100,101 @@ QNetworkReply* PasteUpload::getReply(QNetworkRequest& request) return nullptr; }; -auto PasteUpload::Sink::init(QNetworkRequest&) -> Task::State +auto PasteUpload::Sink::finalize(QNetworkReply& reply) -> Task::State { - m_output.clear(); - return Task::State::Running; -}; - -auto PasteUpload::Sink::write(QByteArray& data) -> Task::State -{ - m_output.append(data); - return Task::State::Running; -} - -auto PasteUpload::Sink::abort() -> Task::State -{ - m_output.clear(); - return Task::State::Failed; -} + if (!finalizeAllValidators(reply)) { + m_fail_reason = "Failed to finalize validators"; + return Task::State::Failed; + } + int statusCode = reply.attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + if (reply.error() != QNetworkReply::NetworkError::NoError) { + m_fail_reason = QObject::tr("Network error: %1").arg(reply.errorString()); + return Task::State::Failed; + } else if (statusCode != 200 && statusCode != 201) { + QString reasonPhrase = reply.attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString(); + m_fail_reason = + QObject::tr("Error: %1 returned unexpected status code %2 %3").arg(m_d->url().toString()).arg(statusCode).arg(reasonPhrase); + return Task::State::Failed; + } -auto PasteUpload::Sink::finalize(QNetworkReply&) -> Task::State -{ - switch (m_paste_type) { + switch (m_d->m_paste_type) { case PasteUpload::NullPointer: - m_result->link = QString::fromUtf8(m_output).trimmed(); + m_d->m_pasteLink = QString::fromUtf8(*m_output).trimmed(); break; case PasteUpload::Hastebin: { QJsonParseError jsonError; - auto doc = QJsonDocument::fromJson(m_output, &jsonError); + auto doc = QJsonDocument::fromJson(*m_output, &jsonError); if (jsonError.error != QJsonParseError::NoError) { qDebug() << "hastebin server did not reply with JSON" << jsonError.errorString(); + m_fail_reason = + QObject::tr("Failed to parse response from hastebin server: expected JSON but got an invalid response. Error: %1") + .arg(jsonError.errorString()); return Task::State::Failed; } auto obj = doc.object(); if (obj.contains("key") && obj["key"].isString()) { QString key = doc.object()["key"].toString(); - m_result->link = m_base_url + "/" + key; + m_d->m_pasteLink = m_d->m_baseUrl + "/" + key; } else { qDebug() << "Log upload failed:" << doc.toJson(); + m_fail_reason = QObject::tr("Error: %1 returned a malformed response body").arg(m_d->url().toString()); return Task::State::Failed; } break; } case PasteUpload::Mclogs: { QJsonParseError jsonError; - auto doc = QJsonDocument::fromJson(m_output, &jsonError); + auto doc = QJsonDocument::fromJson(*m_output, &jsonError); if (jsonError.error != QJsonParseError::NoError) { qDebug() << "mclogs server did not reply with JSON" << jsonError.errorString(); + m_fail_reason = + QObject::tr("Failed to parse response from mclogs server: expected JSON but got an invalid response. Error: %1") + .arg(jsonError.errorString()); return Task::State::Failed; } auto obj = doc.object(); if (obj.contains("success") && obj["success"].isBool()) { bool success = obj["success"].toBool(); if (success) { - m_result->link = obj["url"].toString(); + m_d->m_pasteLink = obj["url"].toString(); } else { - m_result->error = obj["error"].toString(); + QString error = obj["error"].toString(); + m_fail_reason = QObject::tr("Error: %1 returned an error: %2").arg(m_d->url().toString(), error); + return Task::State::Failed; } } else { qDebug() << "Log upload failed:" << doc.toJson(); + m_fail_reason = QObject::tr("Error: %1 returned a malformed response body").arg(m_d->url().toString()); return Task::State::Failed; } break; } case PasteUpload::PasteGG: QJsonParseError jsonError; - auto doc = QJsonDocument::fromJson(m_output, &jsonError); + auto doc = QJsonDocument::fromJson(*m_output, &jsonError); if (jsonError.error != QJsonParseError::NoError) { qDebug() << "pastegg server did not reply with JSON" << jsonError.errorString(); + m_fail_reason = + QObject::tr("Failed to parse response from pasteGG server: expected JSON but got an invalid response. Error: %1") + .arg(jsonError.errorString()); return Task::State::Failed; } auto obj = doc.object(); if (obj.contains("status") && obj["status"].isString()) { QString status = obj["status"].toString(); if (status == "success") { - m_result->link = m_base_url + "/p/anonymous/" + obj["result"].toObject()["id"].toString(); + m_d->m_pasteLink = m_d->m_baseUrl + "/p/anonymous/" + obj["result"].toObject()["id"].toString(); } else { - m_result->error = obj["error"].toString(); - m_result->extra_message = (obj.contains("message") && obj["message"].isString()) ? obj["message"].toString() : "none"; + QString error = obj["error"].toString(); + QString message = (obj.contains("message") && obj["message"].isString()) ? obj["message"].toString() : "none"; + m_fail_reason = + QObject::tr("Error: %1 returned an error code: %2\nError message: %3").arg(m_d->url().toString(), error, message); + return Task::State::Failed; } } else { qDebug() << "Log upload failed:" << doc.toJson(); + m_fail_reason = QObject::tr("Error: %1 returned a malformed response body").arg(m_d->url().toString()); return Task::State::Failed; } break; @@ -186,23 +202,18 @@ auto PasteUpload::Sink::finalize(QNetworkReply&) -> Task::State return Task::State::Succeeded; } -Net::NetRequest::Ptr PasteUpload::make(const QString& log, PasteUpload::PasteType pasteType, QString customBaseURL, ResultPtr result) +PasteUpload::PasteUpload(const QString& log, QString url, PasteType pasteType) : m_log(log), m_baseUrl(url), m_paste_type(pasteType) { + anonymizeLog(m_log); auto base = PasteUpload::PasteTypes.at(pasteType); - QString baseUrl = customBaseURL.isEmpty() ? base.defaultBase : customBaseURL; - auto up = makeShared(log, pasteType); + if (m_baseUrl.isEmpty()) + m_baseUrl = base.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 == PasteUpload::PasteGG && baseUrl == base.defaultBase) - up->m_url = "https://api.paste.gg/v1/pastes"; + if (pasteType == PasteUpload::PasteGG && m_baseUrl == base.defaultBase) + m_url = "https://api.paste.gg/v1/pastes"; else - up->m_url = baseUrl + base.endpointPath; + m_url = m_baseUrl + base.endpointPath; - up->m_sink.reset(new Sink(pasteType, baseUrl, result)); - return up; -} - -PasteUpload::PasteUpload(const QString& log, PasteType pasteType) : m_log(log), m_paste_type(pasteType) -{ - anonymizeLog(m_log); + m_sink.reset(new Sink(this)); } diff --git a/launcher/net/PasteUpload.h b/launcher/net/PasteUpload.h index 55fb2231c..7f43779c4 100644 --- a/launcher/net/PasteUpload.h +++ b/launcher/net/PasteUpload.h @@ -35,6 +35,7 @@ #pragma once +#include "net/ByteArraySink.h" #include "net/NetRequest.h" #include "tasks/Task.h" @@ -67,40 +68,29 @@ class PasteUpload : public Net::NetRequest { }; static const std::array PasteTypes; - struct Result { - QString link; - QString error; - QString extra_message; - }; - - using ResultPtr = std::shared_ptr; - class Sink : public Net::Sink { + class Sink : public Net::ByteArraySink { public: - Sink(const PasteType pasteType, const QString base_url, ResultPtr result) - : m_paste_type(pasteType), m_base_url(base_url), m_result(result) {}; + Sink(PasteUpload* p) : Net::ByteArraySink(std::make_shared()), m_d(p) {}; virtual ~Sink() = 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; - auto hasLocalData() -> bool override { return false; } private: - const PasteType m_paste_type; - const QString m_base_url; - ResultPtr m_result; - QByteArray m_output; + PasteUpload* m_d; }; - PasteUpload(const QString& log, PasteType pasteType); + friend Sink; + + PasteUpload(const QString& log, QString url, PasteType pasteType); virtual ~PasteUpload() = default; - static NetRequest::Ptr make(const QString& log, PasteType pasteType, QString baseURL, ResultPtr result); + QString pasteLink() { return m_pasteLink; } private: virtual QNetworkReply* getReply(QNetworkRequest&) override; QString m_log; + QString m_pasteLink; + QString m_baseUrl; const PasteType m_paste_type; -}; \ No newline at end of file +}; diff --git a/launcher/ui/GuiUtil.cpp b/launcher/ui/GuiUtil.cpp index adb6e8bf2..141153b92 100644 --- a/launcher/ui/GuiUtil.cpp +++ b/launcher/ui/GuiUtil.cpp @@ -43,11 +43,10 @@ #include #include -#include - #include "FileSystem.h" #include "logs/AnonymizeLog.h" #include "net/NetJob.h" +#include "net/NetRequest.h" #include "net/PasteUpload.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/ProgressDialog.h" @@ -134,10 +133,10 @@ std::optional GuiUtil::uploadPaste(const QString& name, const QString& textToUpload = truncateLogForMclogs(text); } - auto result = std::make_shared(); auto job = NetJob::Ptr(new NetJob("Log Upload", APPLICATION->network())); - job->addNetAction(PasteUpload::make(textToUpload, pasteType, baseURL, result)); + auto pasteJob = new PasteUpload(textToUpload, baseURL, pasteType); + job->addNetAction(Net::NetRequest::Ptr(pasteJob)); QObject::connect(job.get(), &Task::failed, [parentWidget](QString reason) { CustomMessageBox::selectable(parentWidget, QObject::tr("Failed to upload logs!"), reason, QMessageBox::Critical)->show(); }); @@ -148,27 +147,19 @@ std::optional GuiUtil::uploadPaste(const QString& name, const QString& }); if (dialog.execWithTask(job.get()) == QDialog::Accepted) { - if (!result->error.isEmpty() || !result->extra_message.isEmpty()) { - QString message = QObject::tr("Error: %1").arg(result->error); - if (!result->extra_message.isEmpty()) { - message += QObject::tr("\nError message: %1").arg(result->extra_message); - } - CustomMessageBox::selectable(parentWidget, QObject::tr("Failed to upload logs!"), message, QMessageBox::Critical)->show(); - return {}; - } - if (result->link.isEmpty()) { + if (pasteJob->pasteLink().isEmpty()) { CustomMessageBox::selectable(parentWidget, QObject::tr("Failed to upload logs!"), "The upload link is empty", QMessageBox::Critical) ->show(); return {}; } - setClipboardText(result->link); + setClipboardText(pasteJob->pasteLink()); CustomMessageBox::selectable( parentWidget, QObject::tr("Upload finished"), - QObject::tr("The link to the uploaded log has been placed in your clipboard.").arg(result->link), + QObject::tr("The link to the uploaded log has been placed in your clipboard.").arg(pasteJob->pasteLink()), QMessageBox::Information) ->exec(); - return result->link; + return pasteJob->pasteLink(); } return {}; } From 3d0bef92a1f318494711059707b7aad04cf9b7f0 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Wed, 7 May 2025 10:19:01 +0300 Subject: [PATCH 228/695] feat: compound the conncurent task error Signed-off-by: Trial97 --- launcher/tasks/ConcurrentTask.cpp | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/launcher/tasks/ConcurrentTask.cpp b/launcher/tasks/ConcurrentTask.cpp index 5cff93017..84530ec99 100644 --- a/launcher/tasks/ConcurrentTask.cpp +++ b/launcher/tasks/ConcurrentTask.cpp @@ -122,9 +122,24 @@ void ConcurrentTask::executeNextSubTask() emitSucceeded(); } else if (m_failed.count() == 1) { auto task = m_failed.keys().first(); - emitFailed(task->failReason()); + auto reason = task->failReason(); + if (reason.isEmpty()) { // clearly a bug somewhere + reason = tr("Task failed"); + } + emitFailed(reason); } else { - emitFailed(tr("One or more subtasks failed")); + QStringList failReason; + for (auto t : m_failed) { + auto reason = t->failReason(); + if (!reason.isEmpty()) { + failReason << reason; + } + } + if (failReason.isEmpty()) { + emitFailed(tr("Multiple subtasks failed")); + } else { + emitFailed(tr("Multiple subtasks failed\n%1").arg(failReason.join("\n"))); + } } } return; From a67a015e4956465550cee1a86b8a0a1c6de231cf Mon Sep 17 00:00:00 2001 From: Trial97 Date: Wed, 7 May 2025 23:10:07 +0300 Subject: [PATCH 229/695] fix: qr code overlaping with text when adding account Signed-off-by: Trial97 --- launcher/ui/dialogs/MSALoginDialog.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/launcher/ui/dialogs/MSALoginDialog.cpp b/launcher/ui/dialogs/MSALoginDialog.cpp index 83f46294d..4c22df8a0 100644 --- a/launcher/ui/dialogs/MSALoginDialog.cpp +++ b/launcher/ui/dialogs/MSALoginDialog.cpp @@ -135,6 +135,9 @@ void MSALoginDialog::onTaskFailed(QString reason) void MSALoginDialog::authorizeWithBrowser(const QUrl& url) { ui->stackedWidget2->setCurrentIndex(1); + ui->stackedWidget2->adjustSize(); + ui->stackedWidget2->updateGeometry(); + this->adjustSize(); ui->loginButton->setToolTip(QString("
    %1
    ").arg(url.toString())); m_url = url; } @@ -142,6 +145,9 @@ void MSALoginDialog::authorizeWithBrowser(const QUrl& url) void MSALoginDialog::authorizeWithBrowserWithExtra(QString url, QString code, [[maybe_unused]] int expiresIn) { ui->stackedWidget->setCurrentIndex(1); + ui->stackedWidget->adjustSize(); + ui->stackedWidget->updateGeometry(); + this->adjustSize(); const auto linkString = QString("%2").arg(url, url); if (url == "https://www.microsoft.com/link" && !code.isEmpty()) { @@ -165,12 +171,18 @@ void MSALoginDialog::authorizeWithBrowserWithExtra(QString url, QString code, [[ void MSALoginDialog::onDeviceFlowStatus(QString status) { ui->stackedWidget->setCurrentIndex(0); + ui->stackedWidget->adjustSize(); + ui->stackedWidget->updateGeometry(); + this->adjustSize(); ui->status->setText(status); } void MSALoginDialog::onAuthFlowStatus(QString status) { ui->stackedWidget2->setCurrentIndex(0); + ui->stackedWidget2->adjustSize(); + ui->stackedWidget2->updateGeometry(); + this->adjustSize(); ui->status2->setText(status); } From e1cfae5e063663d97ca9d1f5668432fe83a80180 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Wed, 7 May 2025 23:56:40 +0300 Subject: [PATCH 230/695] fix(skin manager): accept files with same name Signed-off-by: Trial97 --- launcher/minecraft/skins/SkinList.cpp | 25 +++++++++++++++++++++++-- launcher/minecraft/skins/SkinModel.cpp | 6 +++++- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/launcher/minecraft/skins/SkinList.cpp b/launcher/minecraft/skins/SkinList.cpp index 56379aaab..be3bc776b 100644 --- a/launcher/minecraft/skins/SkinList.cpp +++ b/launcher/minecraft/skins/SkinList.cpp @@ -268,6 +268,26 @@ void SkinList::installSkins(const QStringList& iconFiles) installSkin(file); } +QString getUniqueFile(const QString& root, const QString& file) +{ + auto result = FS::PathCombine(root, file); + if (!QFileInfo::exists(result)) { + return result; + } + + QString baseName = QFileInfo(file).completeBaseName(); + QString extension = QFileInfo(file).suffix(); + int tries = 0; + while (QFileInfo::exists(result)) { + if (++tries > 256) + return {}; + + QString key = QString("%1%2.%3").arg(baseName).arg(tries).arg(extension); + result = FS::PathCombine(root, key); + } + + return result; +} QString SkinList::installSkin(const QString& file, const QString& name) { if (file.isEmpty()) @@ -282,7 +302,7 @@ QString SkinList::installSkin(const QString& file, const QString& name) if (fileinfo.suffix() != "png" && !SkinModel(fileinfo.absoluteFilePath()).isValid()) return tr("Skin images must be 64x64 or 64x32 pixel PNG files."); - QString target = FS::PathCombine(m_dir.absolutePath(), name.isEmpty() ? fileinfo.fileName() : name); + QString target = getUniqueFile(m_dir.absolutePath(), name.isEmpty() ? fileinfo.fileName() : name); return QFile::copy(file, target) ? "" : tr("Unable to copy file"); } @@ -371,7 +391,8 @@ bool SkinList::setData(const QModelIndex& idx, const QVariant& value, int role) auto& skin = m_skinList[row]; auto newName = value.toString(); if (skin.name() != newName) { - skin.rename(newName); + if (!skin.rename(newName)) + return false; save(); } return true; diff --git a/launcher/minecraft/skins/SkinModel.cpp b/launcher/minecraft/skins/SkinModel.cpp index b609bc6c7..209207215 100644 --- a/launcher/minecraft/skins/SkinModel.cpp +++ b/launcher/minecraft/skins/SkinModel.cpp @@ -122,7 +122,11 @@ QString SkinModel::name() const bool SkinModel::rename(QString newName) { auto info = QFileInfo(m_path); - m_path = FS::PathCombine(info.absolutePath(), newName + ".png"); + auto new_path = FS::PathCombine(info.absolutePath(), newName + ".png"); + if (QFileInfo::exists(new_path)) { + return false; + } + m_path = new_path; return FS::move(info.absoluteFilePath(), m_path); } From 69469b4484b88459a2fa74358197ac1dba4896ff Mon Sep 17 00:00:00 2001 From: Yihe Li Date: Sun, 11 May 2025 03:33:03 +0800 Subject: [PATCH 231/695] Refactor shortcut creation logic into its own file Signed-off-by: Yihe Li --- launcher/CMakeLists.txt | 2 + launcher/minecraft/ShortcutUtils.cpp | 264 +++++++++++++++++++++++++++ launcher/minecraft/ShortcutUtils.h | 71 +++++++ launcher/ui/MainWindow.cpp | 191 +------------------ launcher/ui/MainWindow.h | 2 - launcher/ui/MainWindow.ui | 2 +- 6 files changed, 345 insertions(+), 187 deletions(-) create mode 100644 launcher/minecraft/ShortcutUtils.cpp create mode 100644 launcher/minecraft/ShortcutUtils.h diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index dbd9dc0bb..918e38df0 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -310,6 +310,8 @@ set(MINECRAFT_SOURCES minecraft/ParseUtils.h minecraft/ProfileUtils.cpp minecraft/ProfileUtils.h + minecraft/ShortcutUtils.cpp + minecraft/ShortcutUtils.h minecraft/Library.cpp minecraft/Library.h minecraft/MojangDownloadInfo.h diff --git a/launcher/minecraft/ShortcutUtils.cpp b/launcher/minecraft/ShortcutUtils.cpp new file mode 100644 index 000000000..7d4faf231 --- /dev/null +++ b/launcher/minecraft/ShortcutUtils.cpp @@ -0,0 +1,264 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * + * parent 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. + * + * parent 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 parent program. If not, see . + * + * parent 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 parent 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 "ShortcutUtils.h" + +#include "FileSystem.h" + +#include +#include + +#include +#include +#include + +namespace ShortcutUtils { + +void createInstanceShortcut(BaseInstance* instance, + QString shortcutName, + QString shortcutFilePath, + QString targetString, + QWidget* parent, + const QStringList& extraArgs) +{ + if (!instance) + return; + + QString appPath = QApplication::applicationFilePath(); + QString iconPath; + QStringList args; +#if defined(Q_OS_MACOS) + appPath = QApplication::applicationFilePath(); + if (appPath.startsWith("/private/var/")) { + QMessageBox::critical(parent, QObject::tr("Create Shortcut"), + QObject::tr("The launcher is in the folder it was extracted from, therefore it cannot create shortcuts.")); + return; + } + + auto pIcon = APPLICATION->icons()->icon(instance->iconKey()); + if (pIcon == nullptr) { + pIcon = APPLICATION->icons()->icon("grass"); + } + + iconPath = FS::PathCombine(instance->instanceRoot(), "Icon.icns"); + + QFile iconFile(iconPath); + if (!iconFile.open(QFile::WriteOnly)) { + QMessageBox::critical(parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for application.")); + return; + } + + QIcon icon = pIcon->icon(); + + bool success = icon.pixmap(1024, 1024).save(iconPath, "ICNS"); + iconFile.close(); + + if (!success) { + iconFile.remove(); + QMessageBox::critical(parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for application.")); + return; + } +#elif defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) + if (appPath.startsWith("/tmp/.mount_")) { + // AppImage! + appPath = QProcessEnvironment::systemEnvironment().value(QStringLiteral("APPIMAGE")); + if (appPath.isEmpty()) { + QMessageBox::critical( + parent, QObject::tr("Create Shortcut"), + QObject::tr("Launcher is running as misconfigured AppImage? ($APPIMAGE environment variable is missing)")); + } else if (appPath.endsWith("/")) { + appPath.chop(1); + } + } + + auto icon = APPLICATION->icons()->icon(instance->iconKey()); + if (icon == nullptr) { + icon = APPLICATION->icons()->icon("grass"); + } + + iconPath = FS::PathCombine(instance->instanceRoot(), "icon.png"); + + QFile iconFile(iconPath); + if (!iconFile.open(QFile::WriteOnly)) { + QMessageBox::critical(parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for shortcut.")); + return; + } + bool success = icon->icon().pixmap(64, 64).save(&iconFile, "PNG"); + iconFile.close(); + + if (!success) { + iconFile.remove(); + QMessageBox::critical(parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for shortcut.")); + return; + } + + if (DesktopServices::isFlatpak()) { + appPath = "flatpak"; + args.append({ "run", BuildConfig.LAUNCHER_APPID }); + } + +#elif defined(Q_OS_WIN) + auto icon = APPLICATION->icons()->icon(instance->iconKey()); + if (icon == nullptr) { + icon = APPLICATION->icons()->icon("grass"); + } + + iconPath = FS::PathCombine(instance->instanceRoot(), "icon.ico"); + + // part of fix for weird bug involving the window icon being replaced + // dunno why it happens, but parent 2-line fix seems to be enough, so w/e + auto appIcon = APPLICATION->getThemedIcon("logo"); + + QFile iconFile(iconPath); + if (!iconFile.open(QFile::WriteOnly)) { + QMessageBox::critical(parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for shortcut.")); + return; + } + bool success = icon->icon().pixmap(64, 64).save(&iconFile, "ICO"); + iconFile.close(); + + // restore original window icon + QGuiApplication::setWindowIcon(appIcon); + + if (!success) { + iconFile.remove(); + QMessageBox::critical(parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for shortcut.")); + return; + } + +#else + QMessageBox::critical(parent, QObject::tr("Create Shortcut"), QObject::tr("Not supported on your platform!")); + return; +#endif + args.append({ "--launch", instance->id() }); + args.append(extraArgs); + + if (!FS::createShortcut(std::move(shortcutFilePath), appPath, args, shortcutName, iconPath)) { +#if not defined(Q_OS_MACOS) + iconFile.remove(); +#endif + QMessageBox::critical(parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create %1 shortcut!").arg(targetString)); + return; + } +} + +void createInstanceShortcutOnDesktop(BaseInstance* instance, + QString shortcutName, + QString targetString, + QWidget* parent, + const QStringList& extraArgs) +{ + if (!instance) + return; + + QString desktopDir = FS::getDesktopDir(); + if (desktopDir.isEmpty()) { + QMessageBox::critical(parent, QObject::tr("Create Shortcut"), QObject::tr("Couldn't find desktop?!")); + return; + } + + QString shortcutFilePath = FS::PathCombine(FS::getDesktopDir(), FS::RemoveInvalidFilenameChars(shortcutName)); + createInstanceShortcut(instance, shortcutName, shortcutFilePath, targetString, parent, extraArgs); + QMessageBox::information(parent, QObject::tr("Create Shortcut"), + QObject::tr("Created a shortcut to this %1 on your desktop!").arg(targetString)); +} + +void createInstanceShortcutInApplications(BaseInstance* instance, + QString shortcutName, + QString targetString, + QWidget* parent, + const QStringList& extraArgs) +{ + if (!instance) + return; + + QString applicationsDir = FS::getApplicationsDir(); + if (applicationsDir.isEmpty()) { + QMessageBox::critical(parent, QObject::tr("Create Shortcut"), QObject::tr("Couldn't find applications folder?!")); + return; + } + +#if defined(Q_OS_MACOS) || defined(Q_OS_WIN) + applicationsDir = FS::PathCombine(applicationsDir, BuildConfig.LAUNCHER_DISPLAYNAME + " Instances"); + + QDir applicationsDirQ(applicationsDir); + if (!applicationsDirQ.mkpath(".")) { + QMessageBox::critical(parent, QObject::tr("Create Shortcut"), + QObject::tr("Failed to create instances folder in applications folder!")); + return; + } +#endif + + QString shortcutFilePath = FS::PathCombine(applicationsDir, FS::RemoveInvalidFilenameChars(shortcutName)); + createInstanceShortcut(instance, shortcutName, shortcutFilePath, targetString, parent, extraArgs); + QMessageBox::information(parent, QObject::tr("Create Shortcut"), + QObject::tr("Created a shortcut to this %1 in your applications folder!").arg(targetString)); +} + +void createInstanceShortcutInOther(BaseInstance* instance, + QString shortcutName, + QString targetString, + QWidget* parent, + const QStringList& extraArgs) +{ + if (!instance) + return; + + QString defaultedDir = FS::getDesktopDir(); +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) + QString extension = ".desktop"; +#elif defined(Q_OS_WINDOWS) + QString extension = ".lnk"; +#else + QString extension = ""; +#endif + + QString shortcutFilePath = FS::PathCombine(defaultedDir, FS::RemoveInvalidFilenameChars(shortcutName) + extension); + QFileDialog fileDialog; + // workaround to make sure the portal file dialog opens in the desktop directory + fileDialog.setDirectoryUrl(defaultedDir); + + shortcutFilePath = fileDialog.getSaveFileName(parent, QObject::tr("Create Shortcut"), shortcutFilePath, + QObject::tr("Desktop Entries") + " (*" + extension + ")"); + if (shortcutFilePath.isEmpty()) + return; // file dialog canceled by user + + if (shortcutFilePath.endsWith(extension)) + shortcutFilePath = shortcutFilePath.mid(0, shortcutFilePath.length() - extension.length()); + createInstanceShortcut(instance, shortcutName, shortcutFilePath, targetString, parent, extraArgs); + QMessageBox::information(parent, QObject::tr("Create Shortcut"), QObject::tr("Created a shortcut to this %1!").arg(targetString)); +} + +} // namespace ShortcutUtils diff --git a/launcher/minecraft/ShortcutUtils.h b/launcher/minecraft/ShortcutUtils.h new file mode 100644 index 000000000..88800f5e6 --- /dev/null +++ b/launcher/minecraft/ShortcutUtils.h @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - 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 . + * + * 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 "Application.h" + +#include + +namespace ShortcutUtils { +/// Create an instance shortcut on the specified file path +void createInstanceShortcut(BaseInstance* instance, + QString shortcutName, + QString shortcutFilePath, + QString targetString, + QWidget* parent = nullptr, + const QStringList& extraArgs = {}); + +/// Create an instance shortcut on the desktop +void createInstanceShortcutOnDesktop(BaseInstance* instance, + QString shortcutName, + QString targetString, + QWidget* parent = nullptr, + const QStringList& extraArgs = {}); + +/// Create an instance shortcut in the Applications directory +void createInstanceShortcutInApplications(BaseInstance* instance, + QString shortcutName, + QString targetString, + QWidget* parent = nullptr, + const QStringList& extraArgs = {}); + +/// Create an instance shortcut in other directories +void createInstanceShortcutInOther(BaseInstance* instance, + QString shortcutName, + QString targetString, + QWidget* parent = nullptr, + const QStringList& extraArgs = {}); + +} // namespace ShortcutUtils diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 232ba45cd..ed9961975 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -109,6 +109,7 @@ #include "ui/widgets/LabeledToolButton.h" #include "minecraft/PackProfile.h" +#include "minecraft/ShortcutUtils.h" #include "minecraft/VersionFile.h" #include "minecraft/WorldList.h" #include "minecraft/mod/ModFolderModel.h" @@ -1545,157 +1546,6 @@ void MainWindow::on_actionKillInstance_triggered() } } -void MainWindow::createInstanceShortcut(QString shortcutFilePath) -{ - if (!m_selectedInstance) - return; - - QString appPath = QApplication::applicationFilePath(); - QString iconPath; - QStringList args; -#if defined(Q_OS_MACOS) - appPath = QApplication::applicationFilePath(); - if (appPath.startsWith("/private/var/")) { - QMessageBox::critical(this, tr("Create instance shortcut"), - tr("The launcher is in the folder it was extracted from, therefore it cannot create shortcuts.")); - return; - } - - auto pIcon = APPLICATION->icons()->icon(m_selectedInstance->iconKey()); - if (pIcon == nullptr) { - pIcon = APPLICATION->icons()->icon("grass"); - } - - iconPath = FS::PathCombine(m_selectedInstance->instanceRoot(), "Icon.icns"); - - QFile iconFile(iconPath); - if (!iconFile.open(QFile::WriteOnly)) { - QMessageBox::critical(this, tr("Create instance Application"), tr("Failed to create icon for Application.")); - return; - } - - QIcon icon = pIcon->icon(); - - bool success = icon.pixmap(1024, 1024).save(iconPath, "ICNS"); - iconFile.close(); - - if (!success) { - iconFile.remove(); - QMessageBox::critical(this, tr("Create instance Application"), tr("Failed to create icon for Application.")); - return; - } -#elif defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) - if (appPath.startsWith("/tmp/.mount_")) { - // AppImage! - appPath = QProcessEnvironment::systemEnvironment().value(QStringLiteral("APPIMAGE")); - if (appPath.isEmpty()) { - QMessageBox::critical(this, tr("Create instance shortcut"), - tr("Launcher is running as misconfigured AppImage? ($APPIMAGE environment variable is missing)")); - } else if (appPath.endsWith("/")) { - appPath.chop(1); - } - } - - auto icon = APPLICATION->icons()->icon(m_selectedInstance->iconKey()); - if (icon == nullptr) { - icon = APPLICATION->icons()->icon("grass"); - } - - iconPath = FS::PathCombine(m_selectedInstance->instanceRoot(), "icon.png"); - - QFile iconFile(iconPath); - if (!iconFile.open(QFile::WriteOnly)) { - QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut.")); - return; - } - bool success = icon->icon().pixmap(64, 64).save(&iconFile, "PNG"); - iconFile.close(); - - if (!success) { - iconFile.remove(); - QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut.")); - return; - } - - if (DesktopServices::isFlatpak()) { - appPath = "flatpak"; - args.append({ "run", BuildConfig.LAUNCHER_APPID }); - } - -#elif defined(Q_OS_WIN) - auto icon = APPLICATION->icons()->icon(m_selectedInstance->iconKey()); - if (icon == nullptr) { - icon = APPLICATION->icons()->icon("grass"); - } - - iconPath = FS::PathCombine(m_selectedInstance->instanceRoot(), "icon.ico"); - - // part of fix for weird bug involving the window icon being replaced - // dunno why it happens, but this 2-line fix seems to be enough, so w/e - auto appIcon = APPLICATION->getThemedIcon("logo"); - - QFile iconFile(iconPath); - if (!iconFile.open(QFile::WriteOnly)) { - QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut.")); - return; - } - bool success = icon->icon().pixmap(64, 64).save(&iconFile, "ICO"); - iconFile.close(); - - // restore original window icon - QGuiApplication::setWindowIcon(appIcon); - - if (!success) { - iconFile.remove(); - QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create icon for shortcut.")); - return; - } - -#else - QMessageBox::critical(this, tr("Create instance shortcut"), tr("Not supported on your platform!")); - return; -#endif - args.append({ "--launch", m_selectedInstance->id() }); - - if (!FS::createShortcut(std::move(shortcutFilePath), appPath, args, m_selectedInstance->name(), iconPath)) { -#if not defined(Q_OS_MACOS) - iconFile.remove(); -#endif - QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create instance shortcut!")); - return; - } -} - -void MainWindow::on_actionCreateInstanceShortcutOther_triggered() -{ - if (!m_selectedInstance) - return; - - QString defaultedDir = FS::getDesktopDir(); -#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) - QString extension = ".desktop"; -#elif defined(Q_OS_WINDOWS) - QString extension = ".lnk"; -#else - QString extension = ""; -#endif - - QString shortcutFilePath = FS::PathCombine(defaultedDir, FS::RemoveInvalidFilenameChars(m_selectedInstance->name()) + extension); - QFileDialog fileDialog; - // workaround to make sure the portal file dialog opens in the desktop directory - fileDialog.setDirectoryUrl(defaultedDir); - - shortcutFilePath = - fileDialog.getSaveFileName(this, tr("Create Shortcut"), shortcutFilePath, tr("Desktop Entries") + " (*" + extension + ")"); - if (shortcutFilePath.isEmpty()) - return; // file dialog canceled by user - - if(shortcutFilePath.endsWith(extension)) - shortcutFilePath = shortcutFilePath.mid(0, shortcutFilePath.length() - extension.length()); - createInstanceShortcut(shortcutFilePath); - QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance!")); -} - void MainWindow::on_actionCreateInstanceShortcut_triggered() { if (!m_selectedInstance) @@ -1709,44 +1559,17 @@ void MainWindow::on_actionCreateInstanceShortcut_triggered() void MainWindow::on_actionCreateInstanceShortcutDesktop_triggered() { - if (!m_selectedInstance) - return; - - QString desktopDir = FS::getDesktopDir(); - if (desktopDir.isEmpty()) { - QMessageBox::critical(this, tr("Create instance shortcut"), tr("Couldn't find desktop?!")); - return; - } - - QString shortcutFilePath = FS::PathCombine(FS::getDesktopDir(), FS::RemoveInvalidFilenameChars(m_selectedInstance->name())); - createInstanceShortcut(shortcutFilePath); - QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance on your desktop!")); + ShortcutUtils::createInstanceShortcutOnDesktop(m_selectedInstance.get(), m_selectedInstance->name(), tr("instance"), this); } void MainWindow::on_actionCreateInstanceShortcutApplications_triggered() { - if (!m_selectedInstance) - return; - - QString applicationsDir = FS::getApplicationsDir(); - if (applicationsDir.isEmpty()) { - QMessageBox::critical(this, tr("Create instance shortcut"), tr("Couldn't find applications folder?!")); - return; - } - -#if defined(Q_OS_MACOS) || defined(Q_OS_WIN) - applicationsDir = FS::PathCombine(applicationsDir, BuildConfig.LAUNCHER_DISPLAYNAME + " Instances"); - - QDir applicationsDirQ(applicationsDir); - if (!applicationsDirQ.mkpath(".")) { - QMessageBox::critical(this, tr("Create instance shortcut"), tr("Failed to create instances folder in applications folder!")); - return; - } -#endif + ShortcutUtils::createInstanceShortcutInApplications(m_selectedInstance.get(), m_selectedInstance->name(), tr("instance"), this); +} - QString shortcutFilePath = FS::PathCombine(applicationsDir, FS::RemoveInvalidFilenameChars(m_selectedInstance->name())); - createInstanceShortcut(shortcutFilePath); - QMessageBox::information(this, tr("Create instance shortcut"), tr("Created a shortcut to this instance in your applications folder!")); +void MainWindow::on_actionCreateInstanceShortcutOther_triggered() +{ + ShortcutUtils::createInstanceShortcutInOther(m_selectedInstance.get(), m_selectedInstance->name(), tr("instance"), this); } void MainWindow::taskEnd() diff --git a/launcher/ui/MainWindow.h b/launcher/ui/MainWindow.h index f3f2de730..20ab21e67 100644 --- a/launcher/ui/MainWindow.h +++ b/launcher/ui/MainWindow.h @@ -166,7 +166,6 @@ class MainWindow : public QMainWindow { void on_actionEditInstance_triggered(); void on_actionCreateInstanceShortcut_triggered(); - void on_actionCreateInstanceShortcutDesktop_triggered(); void on_actionCreateInstanceShortcutApplications_triggered(); void on_actionCreateInstanceShortcutOther_triggered(); @@ -230,7 +229,6 @@ class MainWindow : public QMainWindow { void setSelectedInstanceById(const QString& id); void updateStatusCenter(); void setInstanceActionsEnabled(bool enabled); - void createInstanceShortcut(QString shortcutDirPath); void runModalTask(Task* task); void instanceFromInstanceTask(InstanceTask* task); diff --git a/launcher/ui/MainWindow.ui b/launcher/ui/MainWindow.ui index 69d31589b..6530e2c5a 100644 --- a/launcher/ui/MainWindow.ui +++ b/launcher/ui/MainWindow.ui @@ -776,7 +776,7 @@ Desktop
    - Creates an shortcut to this instance on your desktop + Creates a shortcut to this instance on your desktop QAction::TextHeuristicRole From 9f1ee3594e557e830fa689f1e86a8ec815bf39df Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 11 May 2025 00:28:34 +0000 Subject: [PATCH 232/695] chore(nix): update lockfile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'nixpkgs': 'github:NixOS/nixpkgs/f771eb401a46846c1aebd20552521b233dd7e18b?narHash=sha256-ITSpPDwvLBZBnPRS2bUcHY3gZSwis/uTe255QgMtTLA%3D' (2025-04-24) → 'github:NixOS/nixpkgs/dda3dcd3fe03e991015e9a74b22d35950f264a54?narHash=sha256-Ua0drDHawlzNqJnclTJGf87dBmaO/tn7iZ%2BTCkTRpRc%3D' (2025-05-08) --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 5418557a3..d0cc6f54c 100644 --- a/flake.lock +++ b/flake.lock @@ -18,11 +18,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1745526057, - "narHash": "sha256-ITSpPDwvLBZBnPRS2bUcHY3gZSwis/uTe255QgMtTLA=", + "lastModified": 1746663147, + "narHash": "sha256-Ua0drDHawlzNqJnclTJGf87dBmaO/tn7iZ+TCkTRpRc=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "f771eb401a46846c1aebd20552521b233dd7e18b", + "rev": "dda3dcd3fe03e991015e9a74b22d35950f264a54", "type": "github" }, "original": { From 37213ecc34797dd4388e1c8e97f22e407d8dfef5 Mon Sep 17 00:00:00 2001 From: Yihe Li Date: Sun, 11 May 2025 19:01:37 +0800 Subject: [PATCH 233/695] Add create shortcut button for worlds Signed-off-by: Yihe Li --- launcher/minecraft/WorldList.cpp | 38 +++++++++++++ launcher/minecraft/WorldList.h | 5 ++ launcher/ui/MainWindow.cpp | 8 +-- launcher/ui/pages/instance/WorldListPage.cpp | 58 ++++++++++++++++++++ launcher/ui/pages/instance/WorldListPage.h | 4 ++ launcher/ui/pages/instance/WorldListPage.ui | 44 ++++++++++++++- 6 files changed, 152 insertions(+), 5 deletions(-) diff --git a/launcher/minecraft/WorldList.cpp b/launcher/minecraft/WorldList.cpp index 6a821ba60..9a5cf042c 100644 --- a/launcher/minecraft/WorldList.cpp +++ b/launcher/minecraft/WorldList.cpp @@ -46,6 +46,9 @@ #include #include +#include +#include "minecraft/ShortcutUtils.h" + WorldList::WorldList(const QString& dir, BaseInstance* instance) : QAbstractListModel(), m_instance(instance), m_dir(dir) { FS::ensureFolderPathExists(m_dir.absolutePath()); @@ -454,4 +457,39 @@ void WorldList::loadWorldsAsync() } } +void WorldList::createWorldShortcut(const QModelIndex& index, QWidget* parent) const +{ + if (!m_instance) + return; + + if (DesktopServices::isFlatpak()) + createWorldShortcutInOther(index, parent); + else + createWorldShortcutOnDesktop(index, parent); +} + +void WorldList::createWorldShortcutOnDesktop(const QModelIndex& index, QWidget* parent) const +{ + auto world = static_cast(data(index, ObjectRole).value()); + QString name = QString(tr("%1 - %2")).arg(m_instance->name(), world->name()); + QStringList extraArgs{ "--world", world->name() }; + ShortcutUtils::createInstanceShortcutOnDesktop(m_instance, name, tr("world"), parent, extraArgs); +} + +void WorldList::createWorldShortcutInApplications(const QModelIndex& index, QWidget* parent) const +{ + auto world = static_cast(data(index, ObjectRole).value()); + QString name = QString(tr("%1 - %2")).arg(m_instance->name(), world->name()); + QStringList extraArgs{ "--world", world->name() }; + ShortcutUtils::createInstanceShortcutInApplications(m_instance, name, tr("world"), parent, extraArgs); +} + +void WorldList::createWorldShortcutInOther(const QModelIndex& index, QWidget* parent) const +{ + auto world = static_cast(data(index, ObjectRole).value()); + QString name = QString(tr("%1 - %2")).arg(m_instance->name(), world->name()); + QStringList extraArgs{ "--world", world->name() }; + ShortcutUtils::createInstanceShortcutInOther(m_instance, name, tr("world"), parent, extraArgs); +} + #include "WorldList.moc" diff --git a/launcher/minecraft/WorldList.h b/launcher/minecraft/WorldList.h index 93fecf1f5..4f54e0737 100644 --- a/launcher/minecraft/WorldList.h +++ b/launcher/minecraft/WorldList.h @@ -84,6 +84,11 @@ class WorldList : public QAbstractListModel { const QList& allWorlds() const { return m_worlds; } + void createWorldShortcut(const QModelIndex& index, QWidget* parent = nullptr) const; + void createWorldShortcutOnDesktop(const QModelIndex& index, QWidget* parent = nullptr) const; + void createWorldShortcutInApplications(const QModelIndex& index, QWidget* parent = nullptr) const; + void createWorldShortcutInOther(const QModelIndex& index, QWidget* parent = nullptr) const; + private slots: void directoryChanged(QString path); void loadWorldsAsync(); diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index ed9961975..4f03d14da 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -214,17 +214,17 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi QString desktopDir = FS::getDesktopDir(); QString applicationDir = FS::getApplicationsDir(); - if(!applicationDir.isEmpty()) + if (!applicationDir.isEmpty()) shortcutActions.push_front(ui->actionCreateInstanceShortcutApplications); - if(!desktopDir.isEmpty()) + if (!desktopDir.isEmpty()) shortcutActions.push_front(ui->actionCreateInstanceShortcutDesktop); } - if(shortcutActions.length() > 1) { + if (shortcutActions.length() > 1) { auto shortcutInstanceMenu = new QMenu(this); - for(auto action : shortcutActions) + for (auto action : shortcutActions) shortcutInstanceMenu->addAction(action); ui->actionCreateInstanceShortcut->setMenu(shortcutInstanceMenu); } diff --git a/launcher/ui/pages/instance/WorldListPage.cpp b/launcher/ui/pages/instance/WorldListPage.cpp index 9e1a0fb55..f6a1e0e5f 100644 --- a/launcher/ui/pages/instance/WorldListPage.cpp +++ b/launcher/ui/pages/instance/WorldListPage.cpp @@ -345,6 +345,28 @@ void WorldListPage::worldChanged([[maybe_unused]] const QModelIndex& current, [[ if (!supportsJoin) { ui->toolBar->removeAction(ui->actionJoin); + ui->toolBar->removeAction(ui->actionCreateWorldShortcut); + } else { + QList shortcutActions = { ui->actionCreateWorldShortcutOther }; + if (!DesktopServices::isFlatpak()) { + QString desktopDir = FS::getDesktopDir(); + QString applicationDir = FS::getApplicationsDir(); + + if (!applicationDir.isEmpty()) + shortcutActions.push_front(ui->actionCreateWorldShortcutApplications); + + if (!desktopDir.isEmpty()) + shortcutActions.push_front(ui->actionCreateWorldShortcutDesktop); + } + + if (shortcutActions.length() > 1) { + auto shortcutInstanceMenu = new QMenu(this); + + for (auto action : shortcutActions) + shortcutInstanceMenu->addAction(action); + ui->actionCreateWorldShortcut->setMenu(shortcutInstanceMenu); + } + ui->actionCreateWorldShortcut->setEnabled(enable); } } @@ -420,6 +442,42 @@ void WorldListPage::on_actionRename_triggered() } } +void WorldListPage::on_actionCreateWorldShortcut_triggered() +{ + QModelIndex index = getSelectedWorld(); + if (!index.isValid()) { + return; + } + m_worlds->createWorldShortcut(index, this); +} + +void WorldListPage::on_actionCreateWorldShortcutDesktop_triggered() +{ + QModelIndex index = getSelectedWorld(); + if (!index.isValid()) { + return; + } + m_worlds->createWorldShortcutOnDesktop(index, this); +} + +void WorldListPage::on_actionCreateWorldShortcutApplications_triggered() +{ + QModelIndex index = getSelectedWorld(); + if (!index.isValid()) { + return; + } + m_worlds->createWorldShortcutInApplications(index, this); +} + +void WorldListPage::on_actionCreateWorldShortcutOther_triggered() +{ + QModelIndex index = getSelectedWorld(); + if (!index.isValid()) { + return; + } + m_worlds->createWorldShortcutInOther(index, this); +} + void WorldListPage::on_actionRefresh_triggered() { m_worlds->update(); diff --git a/launcher/ui/pages/instance/WorldListPage.h b/launcher/ui/pages/instance/WorldListPage.h index 84d9cd075..f2c081bc5 100644 --- a/launcher/ui/pages/instance/WorldListPage.h +++ b/launcher/ui/pages/instance/WorldListPage.h @@ -95,6 +95,10 @@ class WorldListPage : public QMainWindow, public BasePage { void on_actionAdd_triggered(); void on_actionCopy_triggered(); void on_actionRename_triggered(); + void on_actionCreateWorldShortcut_triggered(); + void on_actionCreateWorldShortcutDesktop_triggered(); + void on_actionCreateWorldShortcutApplications_triggered(); + void on_actionCreateWorldShortcutOther_triggered(); void on_actionRefresh_triggered(); void on_actionView_Folder_triggered(); void on_actionDatapacks_triggered(); diff --git a/launcher/ui/pages/instance/WorldListPage.ui b/launcher/ui/pages/instance/WorldListPage.ui index 04344b453..f4664d503 100644 --- a/launcher/ui/pages/instance/WorldListPage.ui +++ b/launcher/ui/pages/instance/WorldListPage.ui @@ -85,10 +85,11 @@ + + - @@ -118,6 +119,14 @@ Delete
    + + + Create Shortcut + + + Creates a shortcut on a selected folder to join the selected world. + + MCEdit @@ -154,6 +163,39 @@ Manage datapacks inside the world. + + + Desktop + + + Creates a shortcut to this world on your desktop + + + QAction::TextHeuristicRole + + + + + Applications + + + Create a shortcut of this world on your start menu + + + QAction::TextHeuristicRole + + + + + Other... + + + Creates a shortcut in a folder selected by you + + + QAction::TextHeuristicRole + + From dbdc9bea7a53ac0e26e69476d7a016f92afb8dff Mon Sep 17 00:00:00 2001 From: Yihe Li Date: Sun, 11 May 2025 19:16:37 +0800 Subject: [PATCH 234/695] Add create shortcut button for servers Signed-off-by: Yihe Li --- launcher/minecraft/ShortcutUtils.cpp | 1 - launcher/ui/pages/instance/ServersPage.cpp | 75 ++++++++++++++++++++++ launcher/ui/pages/instance/ServersPage.h | 4 ++ launcher/ui/pages/instance/ServersPage.ui | 45 ++++++++++++- 4 files changed, 123 insertions(+), 2 deletions(-) diff --git a/launcher/minecraft/ShortcutUtils.cpp b/launcher/minecraft/ShortcutUtils.cpp index 7d4faf231..c579368c2 100644 --- a/launcher/minecraft/ShortcutUtils.cpp +++ b/launcher/minecraft/ShortcutUtils.cpp @@ -170,7 +170,6 @@ void createInstanceShortcut(BaseInstance* instance, iconFile.remove(); #endif QMessageBox::critical(parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create %1 shortcut!").arg(targetString)); - return; } } diff --git a/launcher/ui/pages/instance/ServersPage.cpp b/launcher/ui/pages/instance/ServersPage.cpp index 245bbffe2..88b21a787 100644 --- a/launcher/ui/pages/instance/ServersPage.cpp +++ b/launcher/ui/pages/instance/ServersPage.cpp @@ -40,8 +40,10 @@ #include "ui/dialogs/CustomMessageBox.h" #include "ui_ServersPage.h" +#include #include #include +#include #include #include #include @@ -102,6 +104,38 @@ struct Server { } } + void createServerShortcut(BaseInstance* instance, QWidget* parent = nullptr) const + { + if (!instance) + return; + + if (DesktopServices::isFlatpak()) + createServerShortcutInOther(instance, parent); + else + createServerShortcutOnDesktop(instance, parent); + } + + void createServerShortcutOnDesktop(BaseInstance* instance, QWidget* parent = nullptr) const + { + QString name = QString(QObject::tr("%1 - Server %2")).arg(instance->name(), m_name); + QStringList extraArgs{ "--server", m_address }; + ShortcutUtils::createInstanceShortcutOnDesktop(instance, name, QObject::tr("server"), parent, extraArgs); + } + + void createServerShortcutInApplications(BaseInstance* instance, QWidget* parent = nullptr) const + { + QString name = QString(QObject::tr("%1 - Server %2")).arg(instance->name(), m_name); + QStringList extraArgs{ "--server", m_address }; + ShortcutUtils::createInstanceShortcutInApplications(instance, name, QObject::tr("server"), parent, extraArgs); + } + + void createServerShortcutInOther(BaseInstance* instance, QWidget* parent = nullptr) const + { + QString name = QString(QObject::tr("%1 - Server %2")).arg(instance->name(), m_name); + QStringList extraArgs{ "--server", m_address }; + ShortcutUtils::createInstanceShortcutInOther(instance, name, QObject::tr("server"), parent, extraArgs); + } + // Data - persistent and user changeable QString m_name; QString m_address; @@ -696,6 +730,27 @@ void ServersPage::updateState() } ui->actionAdd->setDisabled(m_locked); + + QList shortcutActions = { ui->actionCreateServerShortcutOther }; + if (!DesktopServices::isFlatpak()) { + QString desktopDir = FS::getDesktopDir(); + QString applicationDir = FS::getApplicationsDir(); + + if (!applicationDir.isEmpty()) + shortcutActions.push_front(ui->actionCreateServerShortcutApplications); + + if (!desktopDir.isEmpty()) + shortcutActions.push_front(ui->actionCreateServerShortcutDesktop); + } + + if (shortcutActions.length() > 1) { + auto shortcutInstanceMenu = new QMenu(this); + + for (auto action : shortcutActions) + shortcutInstanceMenu->addAction(action); + ui->actionCreateServerShortcut->setMenu(shortcutInstanceMenu); + } + ui->actionCreateServerShortcut->setEnabled(serverEditEnabled); } void ServersPage::openedImpl() @@ -767,6 +822,26 @@ void ServersPage::on_actionJoin_triggered() APPLICATION->launch(m_inst, true, false, std::make_shared(MinecraftTarget::parse(address, false))); } +void ServersPage::on_actionCreateServerShortcut_triggered() +{ + m_model->at(currentServer)->createServerShortcut(m_inst.get(), this); +} + +void ServersPage::on_actionCreateServerShortcutDesktop_triggered() +{ + m_model->at(currentServer)->createServerShortcutOnDesktop(m_inst.get(), this); +} + +void ServersPage::on_actionCreateServerShortcutApplications_triggered() +{ + m_model->at(currentServer)->createServerShortcutInApplications(m_inst.get(), this); +} + +void ServersPage::on_actionCreateServerShortcutOther_triggered() +{ + m_model->at(currentServer)->createServerShortcutInOther(m_inst.get(), this); +} + void ServersPage::on_actionRefresh_triggered() { m_model->queryServersStatus(); diff --git a/launcher/ui/pages/instance/ServersPage.h b/launcher/ui/pages/instance/ServersPage.h index 77710d6cc..94baaa004 100644 --- a/launcher/ui/pages/instance/ServersPage.h +++ b/launcher/ui/pages/instance/ServersPage.h @@ -85,6 +85,10 @@ class ServersPage : public QMainWindow, public BasePage { void on_actionMove_Up_triggered(); void on_actionMove_Down_triggered(); void on_actionJoin_triggered(); + void on_actionCreateServerShortcut_triggered(); + void on_actionCreateServerShortcutDesktop_triggered(); + void on_actionCreateServerShortcutApplications_triggered(); + void on_actionCreateServerShortcutOther_triggered(); void on_actionRefresh_triggered(); void runningStateChanged(bool running); diff --git a/launcher/ui/pages/instance/ServersPage.ui b/launcher/ui/pages/instance/ServersPage.ui index d330835c8..e26152242 100644 --- a/launcher/ui/pages/instance/ServersPage.ui +++ b/launcher/ui/pages/instance/ServersPage.ui @@ -145,10 +145,12 @@ false + + - + @@ -177,6 +179,47 @@ Join + + + Create Shortcut + + + Creates a shortcut on a selected folder to join the selected server. + + + + + Desktop + + + Creates a shortcut to this server on your desktop + + + QAction::TextHeuristicRole + + + + + Applications + + + Create a shortcut of this server on your start menu + + + QAction::TextHeuristicRole + + + + + Other... + + + Creates a shortcut in a folder selected by you + + + QAction::TextHeuristicRole + + Refresh From 039682b7dc70b7d5d9c6d5b7694444010e1b0574 Mon Sep 17 00:00:00 2001 From: Yihe Li Date: Tue, 13 May 2025 03:09:49 +0800 Subject: [PATCH 235/695] Remove inappropriate comments Signed-off-by: Yihe Li --- launcher/minecraft/ShortcutUtils.cpp | 1 - launcher/minecraft/ShortcutUtils.h | 1 - 2 files changed, 2 deletions(-) diff --git a/launcher/minecraft/ShortcutUtils.cpp b/launcher/minecraft/ShortcutUtils.cpp index c579368c2..de8bd3cb3 100644 --- a/launcher/minecraft/ShortcutUtils.cpp +++ b/launcher/minecraft/ShortcutUtils.cpp @@ -1,7 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher - * Copyright (C) 2022 Sefa Eyeoglu * * parent 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/minecraft/ShortcutUtils.h b/launcher/minecraft/ShortcutUtils.h index 88800f5e6..0bc18b61e 100644 --- a/launcher/minecraft/ShortcutUtils.h +++ b/launcher/minecraft/ShortcutUtils.h @@ -1,7 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - 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 From bae0ac7ad6bdcd1299c8fdacd91796176308e0ac Mon Sep 17 00:00:00 2001 From: Yihe Li Date: Tue, 13 May 2025 03:12:20 +0800 Subject: [PATCH 236/695] Use index.row() directly Signed-off-by: Yihe Li --- launcher/minecraft/WorldList.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/launcher/minecraft/WorldList.cpp b/launcher/minecraft/WorldList.cpp index 9a5cf042c..ec6328018 100644 --- a/launcher/minecraft/WorldList.cpp +++ b/launcher/minecraft/WorldList.cpp @@ -470,25 +470,25 @@ void WorldList::createWorldShortcut(const QModelIndex& index, QWidget* parent) c void WorldList::createWorldShortcutOnDesktop(const QModelIndex& index, QWidget* parent) const { - auto world = static_cast(data(index, ObjectRole).value()); - QString name = QString(tr("%1 - %2")).arg(m_instance->name(), world->name()); - QStringList extraArgs{ "--world", world->name() }; + const auto& world = allWorlds().at(index.row()); + QString name = QString(tr("%1 - %2")).arg(m_instance->name(), world.name()); + QStringList extraArgs{ "--world", world.name() }; ShortcutUtils::createInstanceShortcutOnDesktop(m_instance, name, tr("world"), parent, extraArgs); } void WorldList::createWorldShortcutInApplications(const QModelIndex& index, QWidget* parent) const { - auto world = static_cast(data(index, ObjectRole).value()); - QString name = QString(tr("%1 - %2")).arg(m_instance->name(), world->name()); - QStringList extraArgs{ "--world", world->name() }; + const auto& world = allWorlds().at(index.row()); + QString name = QString(tr("%1 - %2")).arg(m_instance->name(), world.name()); + QStringList extraArgs{ "--world", world.name() }; ShortcutUtils::createInstanceShortcutInApplications(m_instance, name, tr("world"), parent, extraArgs); } void WorldList::createWorldShortcutInOther(const QModelIndex& index, QWidget* parent) const { - auto world = static_cast(data(index, ObjectRole).value()); - QString name = QString(tr("%1 - %2")).arg(m_instance->name(), world->name()); - QStringList extraArgs{ "--world", world->name() }; + const auto& world = allWorlds().at(index.row()); + QString name = QString(tr("%1 - %2")).arg(m_instance->name(), world.name()); + QStringList extraArgs{ "--world", world.name() }; ShortcutUtils::createInstanceShortcutInOther(m_instance, name, tr("world"), parent, extraArgs); } From db82988943a0f98dda5adbd2463239623226a5b0 Mon Sep 17 00:00:00 2001 From: Yihe Li Date: Tue, 13 May 2025 03:23:28 +0800 Subject: [PATCH 237/695] Re-add an appropriate copyright comment Signed-off-by: Yihe Li --- launcher/minecraft/ShortcutUtils.cpp | 3 +++ launcher/minecraft/ShortcutUtils.h | 3 +++ 2 files changed, 6 insertions(+) diff --git a/launcher/minecraft/ShortcutUtils.cpp b/launcher/minecraft/ShortcutUtils.cpp index de8bd3cb3..ac3a60614 100644 --- a/launcher/minecraft/ShortcutUtils.cpp +++ b/launcher/minecraft/ShortcutUtils.cpp @@ -1,6 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2023 TheKodeToad + * Copyright (C) 2025 Yihe Li * * parent 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/minecraft/ShortcutUtils.h b/launcher/minecraft/ShortcutUtils.h index 0bc18b61e..7c0eeea5d 100644 --- a/launcher/minecraft/ShortcutUtils.h +++ b/launcher/minecraft/ShortcutUtils.h @@ -1,6 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-only /* * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2023 TheKodeToad + * Copyright (C) 2025 Yihe Li * * 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 31dc84653d454e8b913d7bf06609096e335c8318 Mon Sep 17 00:00:00 2001 From: Yihe Li Date: Tue, 13 May 2025 05:14:45 +0800 Subject: [PATCH 238/695] Refactor shortcut parameter into its own struct Signed-off-by: Yihe Li --- launcher/minecraft/ShortcutUtils.cpp | 103 +++++++++------------ launcher/minecraft/ShortcutUtils.h | 34 +++---- launcher/minecraft/WorldList.cpp | 6 +- launcher/ui/MainWindow.cpp | 6 +- launcher/ui/pages/instance/ServersPage.cpp | 6 +- 5 files changed, 66 insertions(+), 89 deletions(-) diff --git a/launcher/minecraft/ShortcutUtils.cpp b/launcher/minecraft/ShortcutUtils.cpp index ac3a60614..2bbeacb08 100644 --- a/launcher/minecraft/ShortcutUtils.cpp +++ b/launcher/minecraft/ShortcutUtils.cpp @@ -48,14 +48,9 @@ namespace ShortcutUtils { -void createInstanceShortcut(BaseInstance* instance, - QString shortcutName, - QString shortcutFilePath, - QString targetString, - QWidget* parent, - const QStringList& extraArgs) +void createInstanceShortcut(const Shortcut& shortcut, const QString& filePath) { - if (!instance) + if (!shortcut.instance) return; QString appPath = QApplication::applicationFilePath(); @@ -64,21 +59,21 @@ void createInstanceShortcut(BaseInstance* instance, #if defined(Q_OS_MACOS) appPath = QApplication::applicationFilePath(); if (appPath.startsWith("/private/var/")) { - QMessageBox::critical(parent, QObject::tr("Create Shortcut"), + QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("The launcher is in the folder it was extracted from, therefore it cannot create shortcuts.")); return; } - auto pIcon = APPLICATION->icons()->icon(instance->iconKey()); + auto pIcon = APPLICATION->icons()->icon(shortcut.instance->iconKey()); if (pIcon == nullptr) { pIcon = APPLICATION->icons()->icon("grass"); } - iconPath = FS::PathCombine(instance->instanceRoot(), "Icon.icns"); + iconPath = FS::PathCombine(shortcut.instance->instanceRoot(), "Icon.icns"); QFile iconFile(iconPath); if (!iconFile.open(QFile::WriteOnly)) { - QMessageBox::critical(parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for application.")); + QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for application.")); return; } @@ -89,7 +84,7 @@ void createInstanceShortcut(BaseInstance* instance, if (!success) { iconFile.remove(); - QMessageBox::critical(parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for application.")); + QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for application.")); return; } #elif defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) @@ -98,23 +93,23 @@ void createInstanceShortcut(BaseInstance* instance, appPath = QProcessEnvironment::systemEnvironment().value(QStringLiteral("APPIMAGE")); if (appPath.isEmpty()) { QMessageBox::critical( - parent, QObject::tr("Create Shortcut"), + shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Launcher is running as misconfigured AppImage? ($APPIMAGE environment variable is missing)")); } else if (appPath.endsWith("/")) { appPath.chop(1); } } - auto icon = APPLICATION->icons()->icon(instance->iconKey()); + auto icon = APPLICATION->icons()->icon(shortcut.instance->iconKey()); if (icon == nullptr) { icon = APPLICATION->icons()->icon("grass"); } - iconPath = FS::PathCombine(instance->instanceRoot(), "icon.png"); + iconPath = FS::PathCombine(shortcut.instance->instanceRoot(), "icon.png"); QFile iconFile(iconPath); if (!iconFile.open(QFile::WriteOnly)) { - QMessageBox::critical(parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for shortcut.")); + QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for shortcut.")); return; } bool success = icon->icon().pixmap(64, 64).save(&iconFile, "PNG"); @@ -122,7 +117,7 @@ void createInstanceShortcut(BaseInstance* instance, if (!success) { iconFile.remove(); - QMessageBox::critical(parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for shortcut.")); + QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for shortcut.")); return; } @@ -132,12 +127,12 @@ void createInstanceShortcut(BaseInstance* instance, } #elif defined(Q_OS_WIN) - auto icon = APPLICATION->icons()->icon(instance->iconKey()); + auto icon = APPLICATION->icons()->icon(shortcut.instance->iconKey()); if (icon == nullptr) { icon = APPLICATION->icons()->icon("grass"); } - iconPath = FS::PathCombine(instance->instanceRoot(), "icon.ico"); + iconPath = FS::PathCombine(shortcut.instance->instanceRoot(), "icon.ico"); // part of fix for weird bug involving the window icon being replaced // dunno why it happens, but parent 2-line fix seems to be enough, so w/e @@ -145,7 +140,7 @@ void createInstanceShortcut(BaseInstance* instance, QFile iconFile(iconPath); if (!iconFile.open(QFile::WriteOnly)) { - QMessageBox::critical(parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for shortcut.")); + QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for shortcut.")); return; } bool success = icon->icon().pixmap(64, 64).save(&iconFile, "ICO"); @@ -156,58 +151,51 @@ void createInstanceShortcut(BaseInstance* instance, if (!success) { iconFile.remove(); - QMessageBox::critical(parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for shortcut.")); + QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for shortcut.")); return; } #else - QMessageBox::critical(parent, QObject::tr("Create Shortcut"), QObject::tr("Not supported on your platform!")); + QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Not supported on your platform!")); return; #endif - args.append({ "--launch", instance->id() }); - args.append(extraArgs); + args.append({ "--launch", shortcut.instance->id() }); + args.append(shortcut.extraArgs); - if (!FS::createShortcut(std::move(shortcutFilePath), appPath, args, shortcutName, iconPath)) { + if (!FS::createShortcut(std::move(filePath), appPath, args, shortcut.name, iconPath)) { #if not defined(Q_OS_MACOS) iconFile.remove(); #endif - QMessageBox::critical(parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create %1 shortcut!").arg(targetString)); + QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), + QObject::tr("Failed to create %1 shortcut!").arg(shortcut.targetString)); } } -void createInstanceShortcutOnDesktop(BaseInstance* instance, - QString shortcutName, - QString targetString, - QWidget* parent, - const QStringList& extraArgs) +void createInstanceShortcutOnDesktop(const Shortcut& shortcut) { - if (!instance) + if (!shortcut.instance) return; QString desktopDir = FS::getDesktopDir(); if (desktopDir.isEmpty()) { - QMessageBox::critical(parent, QObject::tr("Create Shortcut"), QObject::tr("Couldn't find desktop?!")); + QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Couldn't find desktop?!")); return; } - QString shortcutFilePath = FS::PathCombine(FS::getDesktopDir(), FS::RemoveInvalidFilenameChars(shortcutName)); - createInstanceShortcut(instance, shortcutName, shortcutFilePath, targetString, parent, extraArgs); - QMessageBox::information(parent, QObject::tr("Create Shortcut"), - QObject::tr("Created a shortcut to this %1 on your desktop!").arg(targetString)); + QString shortcutFilePath = FS::PathCombine(desktopDir, FS::RemoveInvalidFilenameChars(shortcut.name)); + createInstanceShortcut(shortcut, shortcutFilePath); + QMessageBox::information(shortcut.parent, QObject::tr("Create Shortcut"), + QObject::tr("Created a shortcut to this %1 on your desktop!").arg(shortcut.targetString)); } -void createInstanceShortcutInApplications(BaseInstance* instance, - QString shortcutName, - QString targetString, - QWidget* parent, - const QStringList& extraArgs) +void createInstanceShortcutInApplications(const Shortcut& shortcut) { - if (!instance) + if (!shortcut.instance) return; QString applicationsDir = FS::getApplicationsDir(); if (applicationsDir.isEmpty()) { - QMessageBox::critical(parent, QObject::tr("Create Shortcut"), QObject::tr("Couldn't find applications folder?!")); + QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Couldn't find applications folder?!")); return; } @@ -216,25 +204,21 @@ void createInstanceShortcutInApplications(BaseInstance* instance, QDir applicationsDirQ(applicationsDir); if (!applicationsDirQ.mkpath(".")) { - QMessageBox::critical(parent, QObject::tr("Create Shortcut"), + QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create instances folder in applications folder!")); return; } #endif - QString shortcutFilePath = FS::PathCombine(applicationsDir, FS::RemoveInvalidFilenameChars(shortcutName)); - createInstanceShortcut(instance, shortcutName, shortcutFilePath, targetString, parent, extraArgs); - QMessageBox::information(parent, QObject::tr("Create Shortcut"), - QObject::tr("Created a shortcut to this %1 in your applications folder!").arg(targetString)); + QString shortcutFilePath = FS::PathCombine(applicationsDir, FS::RemoveInvalidFilenameChars(shortcut.name)); + createInstanceShortcut(shortcut, shortcutFilePath); + QMessageBox::information(shortcut.parent, QObject::tr("Create Shortcut"), + QObject::tr("Created a shortcut to this %1 in your applications folder!").arg(shortcut.targetString)); } -void createInstanceShortcutInOther(BaseInstance* instance, - QString shortcutName, - QString targetString, - QWidget* parent, - const QStringList& extraArgs) +void createInstanceShortcutInOther(const Shortcut& shortcut) { - if (!instance) + if (!shortcut.instance) return; QString defaultedDir = FS::getDesktopDir(); @@ -246,20 +230,21 @@ void createInstanceShortcutInOther(BaseInstance* instance, QString extension = ""; #endif - QString shortcutFilePath = FS::PathCombine(defaultedDir, FS::RemoveInvalidFilenameChars(shortcutName) + extension); + QString shortcutFilePath = FS::PathCombine(defaultedDir, FS::RemoveInvalidFilenameChars(shortcut.name) + extension); QFileDialog fileDialog; // workaround to make sure the portal file dialog opens in the desktop directory fileDialog.setDirectoryUrl(defaultedDir); - shortcutFilePath = fileDialog.getSaveFileName(parent, QObject::tr("Create Shortcut"), shortcutFilePath, + shortcutFilePath = fileDialog.getSaveFileName(shortcut.parent, QObject::tr("Create Shortcut"), shortcutFilePath, QObject::tr("Desktop Entries") + " (*" + extension + ")"); if (shortcutFilePath.isEmpty()) return; // file dialog canceled by user if (shortcutFilePath.endsWith(extension)) shortcutFilePath = shortcutFilePath.mid(0, shortcutFilePath.length() - extension.length()); - createInstanceShortcut(instance, shortcutName, shortcutFilePath, targetString, parent, extraArgs); - QMessageBox::information(parent, QObject::tr("Create Shortcut"), QObject::tr("Created a shortcut to this %1!").arg(targetString)); + createInstanceShortcut(shortcut, shortcutFilePath); + QMessageBox::information(shortcut.parent, QObject::tr("Create Shortcut"), + QObject::tr("Created a shortcut to this %1!").arg(shortcut.targetString)); } } // namespace ShortcutUtils diff --git a/launcher/minecraft/ShortcutUtils.h b/launcher/minecraft/ShortcutUtils.h index 7c0eeea5d..a21ccf06a 100644 --- a/launcher/minecraft/ShortcutUtils.h +++ b/launcher/minecraft/ShortcutUtils.h @@ -41,33 +41,25 @@ #include namespace ShortcutUtils { +/// A struct to hold parameters for creating a shortcut +struct Shortcut { + BaseInstance* instance; + QString name; + QString targetString; + QWidget* parent = nullptr; + QStringList extraArgs = {}; +}; + /// Create an instance shortcut on the specified file path -void createInstanceShortcut(BaseInstance* instance, - QString shortcutName, - QString shortcutFilePath, - QString targetString, - QWidget* parent = nullptr, - const QStringList& extraArgs = {}); +void createInstanceShortcut(const Shortcut& shortcut, const QString& filePath); /// Create an instance shortcut on the desktop -void createInstanceShortcutOnDesktop(BaseInstance* instance, - QString shortcutName, - QString targetString, - QWidget* parent = nullptr, - const QStringList& extraArgs = {}); +void createInstanceShortcutOnDesktop(const Shortcut& shortcut); /// Create an instance shortcut in the Applications directory -void createInstanceShortcutInApplications(BaseInstance* instance, - QString shortcutName, - QString targetString, - QWidget* parent = nullptr, - const QStringList& extraArgs = {}); +void createInstanceShortcutInApplications(const Shortcut& shortcut); /// Create an instance shortcut in other directories -void createInstanceShortcutInOther(BaseInstance* instance, - QString shortcutName, - QString targetString, - QWidget* parent = nullptr, - const QStringList& extraArgs = {}); +void createInstanceShortcutInOther(const Shortcut& shortcut); } // namespace ShortcutUtils diff --git a/launcher/minecraft/WorldList.cpp b/launcher/minecraft/WorldList.cpp index ec6328018..582531577 100644 --- a/launcher/minecraft/WorldList.cpp +++ b/launcher/minecraft/WorldList.cpp @@ -473,7 +473,7 @@ void WorldList::createWorldShortcutOnDesktop(const QModelIndex& index, QWidget* const auto& world = allWorlds().at(index.row()); QString name = QString(tr("%1 - %2")).arg(m_instance->name(), world.name()); QStringList extraArgs{ "--world", world.name() }; - ShortcutUtils::createInstanceShortcutOnDesktop(m_instance, name, tr("world"), parent, extraArgs); + ShortcutUtils::createInstanceShortcutOnDesktop({ m_instance, name, tr("world"), parent, extraArgs }); } void WorldList::createWorldShortcutInApplications(const QModelIndex& index, QWidget* parent) const @@ -481,7 +481,7 @@ void WorldList::createWorldShortcutInApplications(const QModelIndex& index, QWid const auto& world = allWorlds().at(index.row()); QString name = QString(tr("%1 - %2")).arg(m_instance->name(), world.name()); QStringList extraArgs{ "--world", world.name() }; - ShortcutUtils::createInstanceShortcutInApplications(m_instance, name, tr("world"), parent, extraArgs); + ShortcutUtils::createInstanceShortcutInApplications({ m_instance, name, tr("world"), parent, extraArgs }); } void WorldList::createWorldShortcutInOther(const QModelIndex& index, QWidget* parent) const @@ -489,7 +489,7 @@ void WorldList::createWorldShortcutInOther(const QModelIndex& index, QWidget* pa const auto& world = allWorlds().at(index.row()); QString name = QString(tr("%1 - %2")).arg(m_instance->name(), world.name()); QStringList extraArgs{ "--world", world.name() }; - ShortcutUtils::createInstanceShortcutInOther(m_instance, name, tr("world"), parent, extraArgs); + ShortcutUtils::createInstanceShortcutInOther({ m_instance, name, tr("world"), parent, extraArgs }); } #include "WorldList.moc" diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 4f03d14da..d64e92124 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -1559,17 +1559,17 @@ void MainWindow::on_actionCreateInstanceShortcut_triggered() void MainWindow::on_actionCreateInstanceShortcutDesktop_triggered() { - ShortcutUtils::createInstanceShortcutOnDesktop(m_selectedInstance.get(), m_selectedInstance->name(), tr("instance"), this); + ShortcutUtils::createInstanceShortcutOnDesktop({ m_selectedInstance.get(), m_selectedInstance->name(), tr("instance"), this }); } void MainWindow::on_actionCreateInstanceShortcutApplications_triggered() { - ShortcutUtils::createInstanceShortcutInApplications(m_selectedInstance.get(), m_selectedInstance->name(), tr("instance"), this); + ShortcutUtils::createInstanceShortcutInApplications({ m_selectedInstance.get(), m_selectedInstance->name(), tr("instance"), this }); } void MainWindow::on_actionCreateInstanceShortcutOther_triggered() { - ShortcutUtils::createInstanceShortcutInOther(m_selectedInstance.get(), m_selectedInstance->name(), tr("instance"), this); + ShortcutUtils::createInstanceShortcutInOther({ m_selectedInstance.get(), m_selectedInstance->name(), tr("instance"), this }); } void MainWindow::taskEnd() diff --git a/launcher/ui/pages/instance/ServersPage.cpp b/launcher/ui/pages/instance/ServersPage.cpp index 88b21a787..b29cc1137 100644 --- a/launcher/ui/pages/instance/ServersPage.cpp +++ b/launcher/ui/pages/instance/ServersPage.cpp @@ -119,21 +119,21 @@ struct Server { { QString name = QString(QObject::tr("%1 - Server %2")).arg(instance->name(), m_name); QStringList extraArgs{ "--server", m_address }; - ShortcutUtils::createInstanceShortcutOnDesktop(instance, name, QObject::tr("server"), parent, extraArgs); + ShortcutUtils::createInstanceShortcutOnDesktop({ instance, name, QObject::tr("server"), parent, extraArgs }); } void createServerShortcutInApplications(BaseInstance* instance, QWidget* parent = nullptr) const { QString name = QString(QObject::tr("%1 - Server %2")).arg(instance->name(), m_name); QStringList extraArgs{ "--server", m_address }; - ShortcutUtils::createInstanceShortcutInApplications(instance, name, QObject::tr("server"), parent, extraArgs); + ShortcutUtils::createInstanceShortcutInApplications({ instance, name, QObject::tr("server"), parent, extraArgs }); } void createServerShortcutInOther(BaseInstance* instance, QWidget* parent = nullptr) const { QString name = QString(QObject::tr("%1 - Server %2")).arg(instance->name(), m_name); QStringList extraArgs{ "--server", m_address }; - ShortcutUtils::createInstanceShortcutInOther(instance, name, QObject::tr("server"), parent, extraArgs); + ShortcutUtils::createInstanceShortcutInOther({ instance, name, QObject::tr("server"), parent, extraArgs }); } // Data - persistent and user changeable From 003d4226262d32c1369910f1f2d63eff33b0a6a6 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Tue, 13 May 2025 15:51:52 +0200 Subject: [PATCH 239/695] chore(readme): update Jetbrains logo Signed-off-by: Sefa Eyeoglu --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9c4909509..361864dfe 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,13 @@ We thank all the wonderful backers over at Open Collective! Support Prism Launch Thanks to JetBrains for providing us a few licenses for all their products, as part of their [Open Source program](https://www.jetbrains.com/opensource/). -[![JetBrains](https://resources.jetbrains.com/storage/products/company/brand/logos/jb_beam.svg)](https://www.jetbrains.com/opensource/) + + + + + JetBrains logo + + Thanks to Weblate for hosting our translation efforts. From bbfaaef31d991643ffb43a1173b383db06bea000 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Wed, 14 May 2025 09:55:55 +0300 Subject: [PATCH 240/695] chore: replace foreach macro Signed-off-by: Trial97 --- launcher/java/JavaUtils.cpp | 8 ++++---- launcher/ui/widgets/CheckComboBox.cpp | 2 +- launcher/ui/widgets/InfoFrame.cpp | 4 ++-- launcher/updater/MacSparkleUpdater.mm | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/launcher/java/JavaUtils.cpp b/launcher/java/JavaUtils.cpp index 072cb1d16..2d0560049 100644 --- a/launcher/java/JavaUtils.cpp +++ b/launcher/java/JavaUtils.cpp @@ -365,13 +365,13 @@ QList JavaUtils::FindJavaPaths() javas.append("/System/Library/Frameworks/JavaVM.framework/Versions/Current/Commands/java"); QDir libraryJVMDir("/Library/Java/JavaVirtualMachines/"); QStringList libraryJVMJavas = libraryJVMDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); - foreach (const QString& java, libraryJVMJavas) { + for (const QString& java : libraryJVMJavas) { javas.append(libraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/bin/java"); javas.append(libraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/jre/bin/java"); } QDir systemLibraryJVMDir("/System/Library/Java/JavaVirtualMachines/"); QStringList systemLibraryJVMJavas = systemLibraryJVMDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); - foreach (const QString& java, systemLibraryJVMJavas) { + for (const QString& java : systemLibraryJVMJavas) { javas.append(systemLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/bin/java"); javas.append(systemLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Commands/java"); } @@ -381,14 +381,14 @@ QList JavaUtils::FindJavaPaths() // javas downloaded by sdkman QDir sdkmanDir(FS::PathCombine(home, ".sdkman/candidates/java")); QStringList sdkmanJavas = sdkmanDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); - foreach (const QString& java, sdkmanJavas) { + for (const QString& java : sdkmanJavas) { javas.append(sdkmanDir.absolutePath() + "/" + java + "/bin/java"); } // java in user library folder (like from intellij downloads) QDir userLibraryJVMDir(FS::PathCombine(home, "Library/Java/JavaVirtualMachines/")); QStringList userLibraryJVMJavas = userLibraryJVMDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); - foreach (const QString& java, userLibraryJVMJavas) { + for (const QString& java : userLibraryJVMJavas) { javas.append(userLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/bin/java"); javas.append(userLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Commands/java"); } diff --git a/launcher/ui/widgets/CheckComboBox.cpp b/launcher/ui/widgets/CheckComboBox.cpp index 02b629162..57d98ea7f 100644 --- a/launcher/ui/widgets/CheckComboBox.cpp +++ b/launcher/ui/widgets/CheckComboBox.cpp @@ -178,7 +178,7 @@ QStringList CheckComboBox::checkedItems() const void CheckComboBox::setCheckedItems(const QStringList& items) { - foreach (auto text, items) { + for (auto text : items) { auto index = findText(text); setItemCheckState(index, index != -1 ? Qt::Checked : Qt::Unchecked); } diff --git a/launcher/ui/widgets/InfoFrame.cpp b/launcher/ui/widgets/InfoFrame.cpp index 3ef5dcb88..93520f611 100644 --- a/launcher/ui/widgets/InfoFrame.cpp +++ b/launcher/ui/widgets/InfoFrame.cpp @@ -287,7 +287,7 @@ void InfoFrame::setDescription(QString text) QChar rem('\n'); QString finaltext; finaltext.reserve(intermediatetext.size()); - foreach (const QChar& c, intermediatetext) { + for (const QChar& c : intermediatetext) { if (c == rem && prev) { continue; } @@ -341,7 +341,7 @@ void InfoFrame::setLicense(QString text) QChar rem('\n'); QString finaltext; finaltext.reserve(intermediatetext.size()); - foreach (const QChar& c, intermediatetext) { + for (const QChar& c : intermediatetext) { if (c == rem && prev) { continue; } diff --git a/launcher/updater/MacSparkleUpdater.mm b/launcher/updater/MacSparkleUpdater.mm index b2b631593..07862c9a3 100644 --- a/launcher/updater/MacSparkleUpdater.mm +++ b/launcher/updater/MacSparkleUpdater.mm @@ -166,7 +166,7 @@ @implementation UpdaterDelegate QString channelsConfig = ""; // Convert QSet -> NSSet NSMutableSet* nsChannels = [NSMutableSet setWithCapacity:channels.count()]; - foreach (const QString channel, channels) { + for (const QString channel : channels) { [nsChannels addObject:channel.toNSString()]; channelsConfig += channel + " "; } From f3c253d7086e669aa7d56488cf0ad3227cc28368 Mon Sep 17 00:00:00 2001 From: Yihe Li Date: Thu, 15 May 2025 19:05:40 +0800 Subject: [PATCH 241/695] Fix menu issues Signed-off-by: Yihe Li --- launcher/ui/pages/instance/ServersPage.ui | 3 +++ launcher/ui/pages/instance/WorldListPage.ui | 3 +++ 2 files changed, 6 insertions(+) diff --git a/launcher/ui/pages/instance/ServersPage.ui b/launcher/ui/pages/instance/ServersPage.ui index e26152242..bb8bff5aa 100644 --- a/launcher/ui/pages/instance/ServersPage.ui +++ b/launcher/ui/pages/instance/ServersPage.ui @@ -135,6 +135,9 @@ Qt::ToolButtonTextOnly + + true + false diff --git a/launcher/ui/pages/instance/WorldListPage.ui b/launcher/ui/pages/instance/WorldListPage.ui index f4664d503..6d951cbdd 100644 --- a/launcher/ui/pages/instance/WorldListPage.ui +++ b/launcher/ui/pages/instance/WorldListPage.ui @@ -70,6 +70,9 @@ Qt::ToolButtonTextOnly + + true + false From bc1d1b41c0c8c8718fb175790d458744a5c9fe42 Mon Sep 17 00:00:00 2001 From: Yihe Li Date: Thu, 15 May 2025 19:12:15 +0800 Subject: [PATCH 242/695] Move menu creation to constructors to avoid performance issues Signed-off-by: Yihe Li --- launcher/ui/pages/instance/ServersPage.cpp | 42 ++++++++++---------- launcher/ui/pages/instance/WorldListPage.cpp | 39 +++++++++--------- 2 files changed, 41 insertions(+), 40 deletions(-) diff --git a/launcher/ui/pages/instance/ServersPage.cpp b/launcher/ui/pages/instance/ServersPage.cpp index b29cc1137..36844d92a 100644 --- a/launcher/ui/pages/instance/ServersPage.cpp +++ b/launcher/ui/pages/instance/ServersPage.cpp @@ -615,6 +615,26 @@ ServersPage::ServersPage(InstancePtr inst, QWidget* parent) : QMainWindow(parent connect(ui->resourceComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(resourceIndexChanged(int))); connect(m_model, &QAbstractItemModel::rowsRemoved, this, &ServersPage::rowsRemoved); + QList shortcutActions = { ui->actionCreateServerShortcutOther }; + if (!DesktopServices::isFlatpak()) { + QString desktopDir = FS::getDesktopDir(); + QString applicationDir = FS::getApplicationsDir(); + + if (!applicationDir.isEmpty()) + shortcutActions.push_front(ui->actionCreateServerShortcutApplications); + + if (!desktopDir.isEmpty()) + shortcutActions.push_front(ui->actionCreateServerShortcutDesktop); + } + + if (shortcutActions.length() > 1) { + auto shortcutInstanceMenu = new QMenu(this); + + for (auto action : shortcutActions) + shortcutInstanceMenu->addAction(action); + ui->actionCreateServerShortcut->setMenu(shortcutInstanceMenu); + } + m_locked = m_inst->isRunning(); if (m_locked) { m_model->lock(); @@ -718,6 +738,7 @@ void ServersPage::updateState() ui->actionMove_Up->setEnabled(serverEditEnabled); ui->actionRemove->setEnabled(serverEditEnabled); ui->actionJoin->setEnabled(serverEditEnabled); + ui->actionCreateServerShortcut->setEnabled(serverEditEnabled); if (server) { ui->addressLine->setText(server->m_address); @@ -730,27 +751,6 @@ void ServersPage::updateState() } ui->actionAdd->setDisabled(m_locked); - - QList shortcutActions = { ui->actionCreateServerShortcutOther }; - if (!DesktopServices::isFlatpak()) { - QString desktopDir = FS::getDesktopDir(); - QString applicationDir = FS::getApplicationsDir(); - - if (!applicationDir.isEmpty()) - shortcutActions.push_front(ui->actionCreateServerShortcutApplications); - - if (!desktopDir.isEmpty()) - shortcutActions.push_front(ui->actionCreateServerShortcutDesktop); - } - - if (shortcutActions.length() > 1) { - auto shortcutInstanceMenu = new QMenu(this); - - for (auto action : shortcutActions) - shortcutInstanceMenu->addAction(action); - ui->actionCreateServerShortcut->setMenu(shortcutInstanceMenu); - } - ui->actionCreateServerShortcut->setEnabled(serverEditEnabled); } void ServersPage::openedImpl() diff --git a/launcher/ui/pages/instance/WorldListPage.cpp b/launcher/ui/pages/instance/WorldListPage.cpp index f6a1e0e5f..c770f9f23 100644 --- a/launcher/ui/pages/instance/WorldListPage.cpp +++ b/launcher/ui/pages/instance/WorldListPage.cpp @@ -89,6 +89,26 @@ WorldListPage::WorldListPage(InstancePtr inst, std::shared_ptr worlds ui->toolBar->insertSpacer(ui->actionRefresh); + QList shortcutActions = { ui->actionCreateWorldShortcutOther }; + if (!DesktopServices::isFlatpak()) { + QString desktopDir = FS::getDesktopDir(); + QString applicationDir = FS::getApplicationsDir(); + + if (!applicationDir.isEmpty()) + shortcutActions.push_front(ui->actionCreateWorldShortcutApplications); + + if (!desktopDir.isEmpty()) + shortcutActions.push_front(ui->actionCreateWorldShortcutDesktop); + } + + if (shortcutActions.length() > 1) { + auto shortcutInstanceMenu = new QMenu(this); + + for (auto action : shortcutActions) + shortcutInstanceMenu->addAction(action); + ui->actionCreateWorldShortcut->setMenu(shortcutInstanceMenu); + } + WorldListProxyModel* proxy = new WorldListProxyModel(this); proxy->setSortCaseSensitivity(Qt::CaseInsensitive); proxy->setSourceModel(m_worlds.get()); @@ -347,25 +367,6 @@ void WorldListPage::worldChanged([[maybe_unused]] const QModelIndex& current, [[ ui->toolBar->removeAction(ui->actionJoin); ui->toolBar->removeAction(ui->actionCreateWorldShortcut); } else { - QList shortcutActions = { ui->actionCreateWorldShortcutOther }; - if (!DesktopServices::isFlatpak()) { - QString desktopDir = FS::getDesktopDir(); - QString applicationDir = FS::getApplicationsDir(); - - if (!applicationDir.isEmpty()) - shortcutActions.push_front(ui->actionCreateWorldShortcutApplications); - - if (!desktopDir.isEmpty()) - shortcutActions.push_front(ui->actionCreateWorldShortcutDesktop); - } - - if (shortcutActions.length() > 1) { - auto shortcutInstanceMenu = new QMenu(this); - - for (auto action : shortcutActions) - shortcutInstanceMenu->addAction(action); - ui->actionCreateWorldShortcut->setMenu(shortcutInstanceMenu); - } ui->actionCreateWorldShortcut->setEnabled(enable); } } From 776b15d587b32af0f64a4b60c92043a8b10ed507 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 16 May 2025 16:08:06 +0000 Subject: [PATCH 243/695] chore(deps): update determinatesystems/update-flake-lock action to v25 --- .github/workflows/update-flake.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/update-flake.yml b/.github/workflows/update-flake.yml index 62852171b..da3fb144e 100644 --- a/.github/workflows/update-flake.yml +++ b/.github/workflows/update-flake.yml @@ -19,7 +19,7 @@ jobs: - uses: actions/checkout@v4 - uses: cachix/install-nix-action@526118121621777ccd86f79b04685a9319637641 # v31 - - uses: DeterminateSystems/update-flake-lock@v24 + - uses: DeterminateSystems/update-flake-lock@v25 with: commit-msg: "chore(nix): update lockfile" pr-title: "chore(nix): update lockfile" From d9c9eb6521fceb8a9555758633a4b4a5633ebc07 Mon Sep 17 00:00:00 2001 From: Yihe Li Date: Sun, 18 May 2025 20:41:11 +0800 Subject: [PATCH 244/695] Remove redundant assignment Signed-off-by: Yihe Li --- launcher/minecraft/ShortcutUtils.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/launcher/minecraft/ShortcutUtils.cpp b/launcher/minecraft/ShortcutUtils.cpp index 2bbeacb08..cbf4f00e0 100644 --- a/launcher/minecraft/ShortcutUtils.cpp +++ b/launcher/minecraft/ShortcutUtils.cpp @@ -57,7 +57,6 @@ void createInstanceShortcut(const Shortcut& shortcut, const QString& filePath) QString iconPath; QStringList args; #if defined(Q_OS_MACOS) - appPath = QApplication::applicationFilePath(); if (appPath.startsWith("/private/var/")) { QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("The launcher is in the folder it was extracted from, therefore it cannot create shortcuts.")); From 183f3d4e9a21ca0491dc0aa474020376dcb297cb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 11 May 2025 00:28:34 +0000 Subject: [PATCH 245/695] chore(nix): update lockfile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'nixpkgs': 'github:NixOS/nixpkgs/f771eb401a46846c1aebd20552521b233dd7e18b?narHash=sha256-ITSpPDwvLBZBnPRS2bUcHY3gZSwis/uTe255QgMtTLA%3D' (2025-04-24) → 'github:NixOS/nixpkgs/dda3dcd3fe03e991015e9a74b22d35950f264a54?narHash=sha256-Ua0drDHawlzNqJnclTJGf87dBmaO/tn7iZ%2BTCkTRpRc%3D' (2025-05-08) --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 5418557a3..d0cc6f54c 100644 --- a/flake.lock +++ b/flake.lock @@ -18,11 +18,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1745526057, - "narHash": "sha256-ITSpPDwvLBZBnPRS2bUcHY3gZSwis/uTe255QgMtTLA=", + "lastModified": 1746663147, + "narHash": "sha256-Ua0drDHawlzNqJnclTJGf87dBmaO/tn7iZ+TCkTRpRc=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "f771eb401a46846c1aebd20552521b233dd7e18b", + "rev": "dda3dcd3fe03e991015e9a74b22d35950f264a54", "type": "github" }, "original": { From 91e9e49d2ce637f8bca4f4cfd9570cff6d3d4b07 Mon Sep 17 00:00:00 2001 From: Sefa Eyeoglu Date: Tue, 13 May 2025 15:51:52 +0200 Subject: [PATCH 246/695] chore(readme): update Jetbrains logo Signed-off-by: Sefa Eyeoglu --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9c4909509..361864dfe 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,13 @@ We thank all the wonderful backers over at Open Collective! Support Prism Launch Thanks to JetBrains for providing us a few licenses for all their products, as part of their [Open Source program](https://www.jetbrains.com/opensource/). -[![JetBrains](https://resources.jetbrains.com/storage/products/company/brand/logos/jb_beam.svg)](https://www.jetbrains.com/opensource/) + + + + + JetBrains logo + + Thanks to Weblate for hosting our translation efforts. From 8a60ec1c4a7779a78521a438b0e607959bba8fcb Mon Sep 17 00:00:00 2001 From: Trial97 Date: Wed, 14 May 2025 09:55:55 +0300 Subject: [PATCH 247/695] chore: replace foreach macro Signed-off-by: Trial97 --- launcher/java/JavaUtils.cpp | 8 ++++---- launcher/ui/widgets/CheckComboBox.cpp | 2 +- launcher/ui/widgets/InfoFrame.cpp | 4 ++-- launcher/updater/MacSparkleUpdater.mm | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/launcher/java/JavaUtils.cpp b/launcher/java/JavaUtils.cpp index 072cb1d16..2d0560049 100644 --- a/launcher/java/JavaUtils.cpp +++ b/launcher/java/JavaUtils.cpp @@ -365,13 +365,13 @@ QList JavaUtils::FindJavaPaths() javas.append("/System/Library/Frameworks/JavaVM.framework/Versions/Current/Commands/java"); QDir libraryJVMDir("/Library/Java/JavaVirtualMachines/"); QStringList libraryJVMJavas = libraryJVMDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); - foreach (const QString& java, libraryJVMJavas) { + for (const QString& java : libraryJVMJavas) { javas.append(libraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/bin/java"); javas.append(libraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/jre/bin/java"); } QDir systemLibraryJVMDir("/System/Library/Java/JavaVirtualMachines/"); QStringList systemLibraryJVMJavas = systemLibraryJVMDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); - foreach (const QString& java, systemLibraryJVMJavas) { + for (const QString& java : systemLibraryJVMJavas) { javas.append(systemLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/bin/java"); javas.append(systemLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Commands/java"); } @@ -381,14 +381,14 @@ QList JavaUtils::FindJavaPaths() // javas downloaded by sdkman QDir sdkmanDir(FS::PathCombine(home, ".sdkman/candidates/java")); QStringList sdkmanJavas = sdkmanDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); - foreach (const QString& java, sdkmanJavas) { + for (const QString& java : sdkmanJavas) { javas.append(sdkmanDir.absolutePath() + "/" + java + "/bin/java"); } // java in user library folder (like from intellij downloads) QDir userLibraryJVMDir(FS::PathCombine(home, "Library/Java/JavaVirtualMachines/")); QStringList userLibraryJVMJavas = userLibraryJVMDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); - foreach (const QString& java, userLibraryJVMJavas) { + for (const QString& java : userLibraryJVMJavas) { javas.append(userLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/bin/java"); javas.append(userLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Commands/java"); } diff --git a/launcher/ui/widgets/CheckComboBox.cpp b/launcher/ui/widgets/CheckComboBox.cpp index 02b629162..57d98ea7f 100644 --- a/launcher/ui/widgets/CheckComboBox.cpp +++ b/launcher/ui/widgets/CheckComboBox.cpp @@ -178,7 +178,7 @@ QStringList CheckComboBox::checkedItems() const void CheckComboBox::setCheckedItems(const QStringList& items) { - foreach (auto text, items) { + for (auto text : items) { auto index = findText(text); setItemCheckState(index, index != -1 ? Qt::Checked : Qt::Unchecked); } diff --git a/launcher/ui/widgets/InfoFrame.cpp b/launcher/ui/widgets/InfoFrame.cpp index 3ef5dcb88..93520f611 100644 --- a/launcher/ui/widgets/InfoFrame.cpp +++ b/launcher/ui/widgets/InfoFrame.cpp @@ -287,7 +287,7 @@ void InfoFrame::setDescription(QString text) QChar rem('\n'); QString finaltext; finaltext.reserve(intermediatetext.size()); - foreach (const QChar& c, intermediatetext) { + for (const QChar& c : intermediatetext) { if (c == rem && prev) { continue; } @@ -341,7 +341,7 @@ void InfoFrame::setLicense(QString text) QChar rem('\n'); QString finaltext; finaltext.reserve(intermediatetext.size()); - foreach (const QChar& c, intermediatetext) { + for (const QChar& c : intermediatetext) { if (c == rem && prev) { continue; } diff --git a/launcher/updater/MacSparkleUpdater.mm b/launcher/updater/MacSparkleUpdater.mm index b2b631593..07862c9a3 100644 --- a/launcher/updater/MacSparkleUpdater.mm +++ b/launcher/updater/MacSparkleUpdater.mm @@ -166,7 +166,7 @@ @implementation UpdaterDelegate QString channelsConfig = ""; // Convert QSet -> NSSet NSMutableSet* nsChannels = [NSMutableSet setWithCapacity:channels.count()]; - foreach (const QString channel, channels) { + for (const QString channel : channels) { [nsChannels addObject:channel.toNSString()]; channelsConfig += channel + " "; } From 1c288543f2c05f0a9969da2e17c957566bf28584 Mon Sep 17 00:00:00 2001 From: Yihe Li Date: Sun, 18 May 2025 22:32:30 +0800 Subject: [PATCH 248/695] Initial UI for shortcut dialog Signed-off-by: Yihe Li --- launcher/ui/dialogs/CreateShortcutDialog.ui | 217 ++++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 launcher/ui/dialogs/CreateShortcutDialog.ui diff --git a/launcher/ui/dialogs/CreateShortcutDialog.ui b/launcher/ui/dialogs/CreateShortcutDialog.ui new file mode 100644 index 000000000..26e34ecde --- /dev/null +++ b/launcher/ui/dialogs/CreateShortcutDialog.ui @@ -0,0 +1,217 @@ + + + CreateShortcutDialog + + + Qt::WindowModality::ApplicationModal + + + + 0 + 0 + 450 + 365 + + + + Create Instance Shortcut + + + true + + + + + + + + + :/icons/instances/grass:/icons/instances/grass + + + + 80 + 80 + + + + + + + + + + Save To: + + + + + + + + 0 + 0 + + + + + + + + Name: + + + + + + + Name + + + + + + + + + + + Use a different account than the default specified. + + + Override the default account + + + + + + + false + + + + 0 + 0 + + + + + + + + 0 + 0 + + + + + + + + + + + Specify a world or server to automatically join on launch. + + + Select a target to join on launch + + + + + + + false + + + + 0 + 0 + + + + + + + World: + + + + + + + + 0 + 0 + + + + + + + + Server Address: + + + + + + + Server Address + + + + + + + + + + Qt::Orientation::Horizontal + + + QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok + + + + + + + iconButton + + + + + buttonBox + accepted() + CreateShortcutDialog + accept() + + + 20 + 20 + + + 20 + 20 + + + + + buttonBox + rejected() + CreateShortcutDialog + reject() + + + 20 + 20 + + + 20 + 20 + + + + + From 0a5013ff9f795c74f293d43c092fceb88bb378aa Mon Sep 17 00:00:00 2001 From: Yihe Li Date: Sun, 18 May 2025 22:54:57 +0800 Subject: [PATCH 249/695] Add source files for UI Signed-off-by: Yihe Li --- launcher/CMakeLists.txt | 3 + launcher/ui/MainWindow.cpp | 1 + launcher/ui/dialogs/CreateShortcutDialog.cpp | 70 ++++++++++++++++++++ launcher/ui/dialogs/CreateShortcutDialog.h | 56 ++++++++++++++++ launcher/ui/dialogs/CreateShortcutDialog.ui | 9 +++ 5 files changed, 139 insertions(+) create mode 100644 launcher/ui/dialogs/CreateShortcutDialog.cpp create mode 100644 launcher/ui/dialogs/CreateShortcutDialog.h diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 918e38df0..90a2093b6 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -1050,6 +1050,8 @@ SET(LAUNCHER_SOURCES ui/dialogs/ProfileSetupDialog.h ui/dialogs/CopyInstanceDialog.cpp ui/dialogs/CopyInstanceDialog.h + ui/dialogs/CreateShortcutDialog.cpp + ui/dialogs/CreateShortcutDialog.h ui/dialogs/CustomMessageBox.cpp ui/dialogs/CustomMessageBox.h ui/dialogs/ExportInstanceDialog.cpp @@ -1232,6 +1234,7 @@ qt_wrap_ui(LAUNCHER_UI ui/widgets/MinecraftSettingsWidget.ui ui/widgets/JavaSettingsWidget.ui ui/dialogs/CopyInstanceDialog.ui + ui/dialogs/CreateShortcutDialog.ui ui/dialogs/ProfileSetupDialog.ui ui/dialogs/ProgressDialog.ui ui/dialogs/NewInstanceDialog.ui diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index d64e92124..7cc28917a 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -93,6 +93,7 @@ #include "ui/GuiUtil.h" #include "ui/dialogs/AboutDialog.h" #include "ui/dialogs/CopyInstanceDialog.h" +#include "ui/dialogs/CreateShortcutDialog.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/ExportInstanceDialog.h" #include "ui/dialogs/ExportPackDialog.h" diff --git a/launcher/ui/dialogs/CreateShortcutDialog.cpp b/launcher/ui/dialogs/CreateShortcutDialog.cpp new file mode 100644 index 000000000..eda8eb214 --- /dev/null +++ b/launcher/ui/dialogs/CreateShortcutDialog.cpp @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2023 TheKodeToad + * Copyright (C) 2025 Yihe Li + * + * 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 +#include + +#include "Application.h" +#include "BuildConfig.h" +#include "CreateShortcutDialog.h" +#include "ui_CreateShortcutDialog.h" + +#include "ui/dialogs/IconPickerDialog.h" + +#include "BaseInstance.h" +#include "DesktopServices.h" +#include "FileSystem.h" +#include "InstanceList.h" +#include "icons/IconList.h" + +CreateShortcutDialog::CreateShortcutDialog(InstancePtr instance, QWidget* parent) + : QDialog(parent), ui(new Ui::CreateShortcutDialog), m_instance(instance) +{ + ui->setupUi(this); + resize(minimumSizeHint()); + layout()->setSizeConstraint(QLayout::SetFixedSize); + + InstIconKey = instance->iconKey(); + ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey)); + ui->instNameTextBox->setText(instance->name()); + ui->instNameTextBox->setFocus(); +} + +CreateShortcutDialog::~CreateShortcutDialog() +{ + delete ui; +} diff --git a/launcher/ui/dialogs/CreateShortcutDialog.h b/launcher/ui/dialogs/CreateShortcutDialog.h new file mode 100644 index 000000000..5fba78931 --- /dev/null +++ b/launcher/ui/dialogs/CreateShortcutDialog.h @@ -0,0 +1,56 @@ +/* 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 "BaseInstance.h" + +class BaseInstance; + +namespace Ui { +class CreateShortcutDialog; +} + +class CreateShortcutDialog : public QDialog { + Q_OBJECT + + public: + explicit CreateShortcutDialog(InstancePtr instance, QWidget* parent = nullptr); + ~CreateShortcutDialog(); + + private slots: + // Icon, target and name + void on_iconButton_clicked(); + void on_saveTargetSelectionBox_currentIndexChanged(int index); + void on_instNameTextBox_textChanged(const QString& arg1); + + // Override account + void on_overrideAccountCheckbox_stateChanged(int state); + void on_accountSelectionBox_currentIndexChanged(int index); + + // Override target (world, server) + void on_targetCheckbox_stateChanged(int state); + void on_worldSelectionBox_currentIndexChanged(int index); + void on_serverAddressTextBox_textChanged(const QString& arg1); + void targetChanged(); + + private: + /* data */ + Ui::CreateShortcutDialog* ui; + QString InstIconKey; + InstancePtr m_instance; + bool m_QuickJoinSupported = false; +}; diff --git a/launcher/ui/dialogs/CreateShortcutDialog.ui b/launcher/ui/dialogs/CreateShortcutDialog.ui index 26e34ecde..364c42b15 100644 --- a/launcher/ui/dialogs/CreateShortcutDialog.ui +++ b/launcher/ui/dialogs/CreateShortcutDialog.ui @@ -135,6 +135,9 @@ World: + + targetBtnGroup + @@ -152,6 +155,9 @@ Server Address: + + targetBtnGroup + @@ -214,4 +220,7 @@ + + + From ea8f105292058289ec7350d61ab5fec1c3e94306 Mon Sep 17 00:00:00 2001 From: Yihe Li Date: Sun, 18 May 2025 23:03:14 +0800 Subject: [PATCH 250/695] Add stubs and b asic integration with MainWindow Signed-off-by: Yihe Li --- launcher/ui/MainWindow.cpp | 44 ++------------------ launcher/ui/MainWindow.h | 3 -- launcher/ui/MainWindow.ui | 33 --------------- launcher/ui/dialogs/CreateShortcutDialog.cpp | 44 ++++++++++++++++++-- launcher/ui/dialogs/CreateShortcutDialog.h | 2 + 5 files changed, 47 insertions(+), 79 deletions(-) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 7cc28917a..062175ece 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -110,7 +110,6 @@ #include "ui/widgets/LabeledToolButton.h" #include "minecraft/PackProfile.h" -#include "minecraft/ShortcutUtils.h" #include "minecraft/VersionFile.h" #include "minecraft/WorldList.h" #include "minecraft/mod/ModFolderModel.h" @@ -209,26 +208,6 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi exportInstanceMenu->addAction(ui->actionExportInstanceMrPack); exportInstanceMenu->addAction(ui->actionExportInstanceFlamePack); ui->actionExportInstance->setMenu(exportInstanceMenu); - - QList shortcutActions = { ui->actionCreateInstanceShortcutOther }; - if (!DesktopServices::isFlatpak()) { - QString desktopDir = FS::getDesktopDir(); - QString applicationDir = FS::getApplicationsDir(); - - if (!applicationDir.isEmpty()) - shortcutActions.push_front(ui->actionCreateInstanceShortcutApplications); - - if (!desktopDir.isEmpty()) - shortcutActions.push_front(ui->actionCreateInstanceShortcutDesktop); - } - - if (shortcutActions.length() > 1) { - auto shortcutInstanceMenu = new QMenu(this); - - for (auto action : shortcutActions) - shortcutInstanceMenu->addAction(action); - ui->actionCreateInstanceShortcut->setMenu(shortcutInstanceMenu); - } } // hide, disable and show stuff @@ -1552,25 +1531,10 @@ void MainWindow::on_actionCreateInstanceShortcut_triggered() if (!m_selectedInstance) return; - if (DesktopServices::isFlatpak()) - on_actionCreateInstanceShortcutOther_triggered(); - else - on_actionCreateInstanceShortcutDesktop_triggered(); -} - -void MainWindow::on_actionCreateInstanceShortcutDesktop_triggered() -{ - ShortcutUtils::createInstanceShortcutOnDesktop({ m_selectedInstance.get(), m_selectedInstance->name(), tr("instance"), this }); -} - -void MainWindow::on_actionCreateInstanceShortcutApplications_triggered() -{ - ShortcutUtils::createInstanceShortcutInApplications({ m_selectedInstance.get(), m_selectedInstance->name(), tr("instance"), this }); -} - -void MainWindow::on_actionCreateInstanceShortcutOther_triggered() -{ - ShortcutUtils::createInstanceShortcutInOther({ m_selectedInstance.get(), m_selectedInstance->name(), tr("instance"), this }); + CreateShortcutDialog shortcutDlg(m_selectedInstance, this); + if (!shortcutDlg.exec()) + return; + shortcutDlg.createShortcut(); } void MainWindow::taskEnd() diff --git a/launcher/ui/MainWindow.h b/launcher/ui/MainWindow.h index 20ab21e67..0e692eda7 100644 --- a/launcher/ui/MainWindow.h +++ b/launcher/ui/MainWindow.h @@ -166,9 +166,6 @@ class MainWindow : public QMainWindow { void on_actionEditInstance_triggered(); void on_actionCreateInstanceShortcut_triggered(); - void on_actionCreateInstanceShortcutDesktop_triggered(); - void on_actionCreateInstanceShortcutApplications_triggered(); - void on_actionCreateInstanceShortcutOther_triggered(); void taskEnd(); diff --git a/launcher/ui/MainWindow.ui b/launcher/ui/MainWindow.ui index 6530e2c5a..1499ec872 100644 --- a/launcher/ui/MainWindow.ui +++ b/launcher/ui/MainWindow.ui @@ -771,39 +771,6 @@ Open the Java folder in a file browser. Only available if the built-in Java downloader is used. - - - Desktop - - - Creates a shortcut to this instance on your desktop - - - QAction::TextHeuristicRole - - - - - Applications - - - Create a shortcut of this instance on your start menu - - - QAction::TextHeuristicRole - - - - - Other... - - - Creates a shortcut in a folder selected by you - - - QAction::TextHeuristicRole - - diff --git a/launcher/ui/dialogs/CreateShortcutDialog.cpp b/launcher/ui/dialogs/CreateShortcutDialog.cpp index eda8eb214..8b6ce8a04 100644 --- a/launcher/ui/dialogs/CreateShortcutDialog.cpp +++ b/launcher/ui/dialogs/CreateShortcutDialog.cpp @@ -50,21 +50,59 @@ #include "FileSystem.h" #include "InstanceList.h" #include "icons/IconList.h" +#include "minecraft/ShortcutUtils.h" CreateShortcutDialog::CreateShortcutDialog(InstancePtr instance, QWidget* parent) : QDialog(parent), ui(new Ui::CreateShortcutDialog), m_instance(instance) { ui->setupUi(this); - resize(minimumSizeHint()); - layout()->setSizeConstraint(QLayout::SetFixedSize); InstIconKey = instance->iconKey(); ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey)); ui->instNameTextBox->setText(instance->name()); - ui->instNameTextBox->setFocus(); } CreateShortcutDialog::~CreateShortcutDialog() { delete ui; } + +void CreateShortcutDialog::on_iconButton_clicked() +{ + IconPickerDialog dlg(this); + dlg.execWithSelection(InstIconKey); + + if (dlg.result() == QDialog::Accepted) { + InstIconKey = dlg.selectedIconKey; + ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey)); + } +} + +void CreateShortcutDialog::on_saveTargetSelectionBox_currentIndexChanged(int index) +{} + +void CreateShortcutDialog::on_instNameTextBox_textChanged(const QString& arg1) +{} + +void CreateShortcutDialog::on_overrideAccountCheckbox_stateChanged(int state) +{} + +void CreateShortcutDialog::on_accountSelectionBox_currentIndexChanged(int index) +{} + +void CreateShortcutDialog::on_targetCheckbox_stateChanged(int state) +{} + +void CreateShortcutDialog::on_worldSelectionBox_currentIndexChanged(int index) +{} + +void CreateShortcutDialog::on_serverAddressTextBox_textChanged(const QString& arg1) +{} + +void CreateShortcutDialog::targetChanged() +{} + +// Real work +void CreateShortcutDialog::createShortcut() const +{} + diff --git a/launcher/ui/dialogs/CreateShortcutDialog.h b/launcher/ui/dialogs/CreateShortcutDialog.h index 5fba78931..be0a5e792 100644 --- a/launcher/ui/dialogs/CreateShortcutDialog.h +++ b/launcher/ui/dialogs/CreateShortcutDialog.h @@ -31,6 +31,8 @@ class CreateShortcutDialog : public QDialog { explicit CreateShortcutDialog(InstancePtr instance, QWidget* parent = nullptr); ~CreateShortcutDialog(); + void createShortcut() const; + private slots: // Icon, target and name void on_iconButton_clicked(); From b296085ea064f22cc21c32ff9d90e3c850dc56de Mon Sep 17 00:00:00 2001 From: Yihe Li Date: Sun, 18 May 2025 23:32:23 +0800 Subject: [PATCH 251/695] Small adjustments Signed-off-by: Yihe Li --- launcher/ui/dialogs/CreateShortcutDialog.cpp | 47 ++++++++++++-------- launcher/ui/dialogs/CreateShortcutDialog.h | 8 ++-- launcher/ui/dialogs/CreateShortcutDialog.ui | 14 ++++-- 3 files changed, 43 insertions(+), 26 deletions(-) diff --git a/launcher/ui/dialogs/CreateShortcutDialog.cpp b/launcher/ui/dialogs/CreateShortcutDialog.cpp index 8b6ce8a04..38c22d861 100644 --- a/launcher/ui/dialogs/CreateShortcutDialog.cpp +++ b/launcher/ui/dialogs/CreateShortcutDialog.cpp @@ -60,6 +60,11 @@ CreateShortcutDialog::CreateShortcutDialog(InstancePtr instance, QWidget* parent InstIconKey = instance->iconKey(); ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey)); ui->instNameTextBox->setText(instance->name()); + + m_QuickJoinSupported = instance->traits().contains("feature:is_quick_play_singleplayer"); + if (!m_QuickJoinSupported) { + // TODO: Remove radio box and add a single server address textbox instead + } } CreateShortcutDialog::~CreateShortcutDialog() @@ -78,31 +83,35 @@ void CreateShortcutDialog::on_iconButton_clicked() } } -void CreateShortcutDialog::on_saveTargetSelectionBox_currentIndexChanged(int index) -{} +void CreateShortcutDialog::on_saveTargetSelectionBox_currentIndexChanged(int index) {} -void CreateShortcutDialog::on_instNameTextBox_textChanged(const QString& arg1) -{} +void CreateShortcutDialog::on_instNameTextBox_textChanged(const QString& arg1) {} -void CreateShortcutDialog::on_overrideAccountCheckbox_stateChanged(int state) -{} +void CreateShortcutDialog::on_overrideAccountCheckbox_stateChanged(int state) {} -void CreateShortcutDialog::on_accountSelectionBox_currentIndexChanged(int index) -{} +void CreateShortcutDialog::on_accountSelectionBox_currentIndexChanged(int index) {} -void CreateShortcutDialog::on_targetCheckbox_stateChanged(int state) -{} +void CreateShortcutDialog::on_targetCheckbox_stateChanged(int state) {} -void CreateShortcutDialog::on_worldSelectionBox_currentIndexChanged(int index) -{} +void CreateShortcutDialog::on_worldSelectionBox_currentIndexChanged(int index) {} -void CreateShortcutDialog::on_serverAddressTextBox_textChanged(const QString& arg1) -{} +void CreateShortcutDialog::on_serverAddressBox_textChanged(const QString& arg1) {} -void CreateShortcutDialog::targetChanged() -{} +void CreateShortcutDialog::targetChanged() {} // Real work -void CreateShortcutDialog::createShortcut() const -{} - +void CreateShortcutDialog::createShortcut() +{ + QString targetString = tr("instance"); + QStringList extraArgs; + if (ui->targetCheckbox->isChecked()) { + if (ui->worldTarget->isChecked()) { + targetString = tr("world"); + extraArgs = { "--world", /* world ID */ }; + } else if (ui->serverTarget->isChecked()) { + targetString = tr("server"); + extraArgs = { "--server", /* server address */ }; + } + } + ShortcutUtils::createInstanceShortcutOnDesktop({ m_instance.get(), m_instance->name(), targetString, this, extraArgs }); +} diff --git a/launcher/ui/dialogs/CreateShortcutDialog.h b/launcher/ui/dialogs/CreateShortcutDialog.h index be0a5e792..7be6b339c 100644 --- a/launcher/ui/dialogs/CreateShortcutDialog.h +++ b/launcher/ui/dialogs/CreateShortcutDialog.h @@ -31,7 +31,7 @@ class CreateShortcutDialog : public QDialog { explicit CreateShortcutDialog(InstancePtr instance, QWidget* parent = nullptr); ~CreateShortcutDialog(); - void createShortcut() const; + void createShortcut(); private slots: // Icon, target and name @@ -46,13 +46,15 @@ class CreateShortcutDialog : public QDialog { // Override target (world, server) void on_targetCheckbox_stateChanged(int state); void on_worldSelectionBox_currentIndexChanged(int index); - void on_serverAddressTextBox_textChanged(const QString& arg1); + void on_serverAddressBox_textChanged(const QString& arg1); void targetChanged(); private: - /* data */ + // Data Ui::CreateShortcutDialog* ui; QString InstIconKey; InstancePtr m_instance; bool m_QuickJoinSupported = false; + + // Index representations }; diff --git a/launcher/ui/dialogs/CreateShortcutDialog.ui b/launcher/ui/dialogs/CreateShortcutDialog.ui index 364c42b15..e45a8428a 100644 --- a/launcher/ui/dialogs/CreateShortcutDialog.ui +++ b/launcher/ui/dialogs/CreateShortcutDialog.ui @@ -10,7 +10,7 @@ 0 0 450 - 365 + 370 @@ -161,9 +161,15 @@ - - - Server Address + + + + 0 + 0 + + + + true From 2e6981977b3b457b1e25b1d6d55fb8ad89d72632 Mon Sep 17 00:00:00 2001 From: Yihe Li Date: Mon, 19 May 2025 00:00:39 +0800 Subject: [PATCH 252/695] Add basic shortcut creation integration Signed-off-by: Yihe Li --- launcher/minecraft/ShortcutUtils.cpp | 24 +++--------- launcher/minecraft/ShortcutUtils.h | 1 + launcher/ui/dialogs/CreateShortcutDialog.cpp | 40 ++++++++++++++++---- launcher/ui/dialogs/CreateShortcutDialog.h | 3 +- 4 files changed, 40 insertions(+), 28 deletions(-) diff --git a/launcher/minecraft/ShortcutUtils.cpp b/launcher/minecraft/ShortcutUtils.cpp index cbf4f00e0..ea2a988be 100644 --- a/launcher/minecraft/ShortcutUtils.cpp +++ b/launcher/minecraft/ShortcutUtils.cpp @@ -54,6 +54,10 @@ void createInstanceShortcut(const Shortcut& shortcut, const QString& filePath) return; QString appPath = QApplication::applicationFilePath(); + auto icon = APPLICATION->icons()->icon(shortcut.iconKey.isEmpty() ? shortcut.instance->iconKey() : shortcut.iconKey); + if (icon == nullptr) { + icon = APPLICATION->icons()->icon("grass"); + } QString iconPath; QStringList args; #if defined(Q_OS_MACOS) @@ -63,11 +67,6 @@ void createInstanceShortcut(const Shortcut& shortcut, const QString& filePath) return; } - auto pIcon = APPLICATION->icons()->icon(shortcut.instance->iconKey()); - if (pIcon == nullptr) { - pIcon = APPLICATION->icons()->icon("grass"); - } - iconPath = FS::PathCombine(shortcut.instance->instanceRoot(), "Icon.icns"); QFile iconFile(iconPath); @@ -76,9 +75,8 @@ void createInstanceShortcut(const Shortcut& shortcut, const QString& filePath) return; } - QIcon icon = pIcon->icon(); - - bool success = icon.pixmap(1024, 1024).save(iconPath, "ICNS"); + QIcon iconObj = icon->icon(); + bool success = iconObj.pixmap(1024, 1024).save(iconPath, "ICNS"); iconFile.close(); if (!success) { @@ -99,11 +97,6 @@ void createInstanceShortcut(const Shortcut& shortcut, const QString& filePath) } } - auto icon = APPLICATION->icons()->icon(shortcut.instance->iconKey()); - if (icon == nullptr) { - icon = APPLICATION->icons()->icon("grass"); - } - iconPath = FS::PathCombine(shortcut.instance->instanceRoot(), "icon.png"); QFile iconFile(iconPath); @@ -126,11 +119,6 @@ void createInstanceShortcut(const Shortcut& shortcut, const QString& filePath) } #elif defined(Q_OS_WIN) - auto icon = APPLICATION->icons()->icon(shortcut.instance->iconKey()); - if (icon == nullptr) { - icon = APPLICATION->icons()->icon("grass"); - } - iconPath = FS::PathCombine(shortcut.instance->instanceRoot(), "icon.ico"); // part of fix for weird bug involving the window icon being replaced diff --git a/launcher/minecraft/ShortcutUtils.h b/launcher/minecraft/ShortcutUtils.h index a21ccf06a..e3d2e283a 100644 --- a/launcher/minecraft/ShortcutUtils.h +++ b/launcher/minecraft/ShortcutUtils.h @@ -48,6 +48,7 @@ struct Shortcut { QString targetString; QWidget* parent = nullptr; QStringList extraArgs = {}; + QString iconKey = ""; }; /// Create an instance shortcut on the specified file path diff --git a/launcher/ui/dialogs/CreateShortcutDialog.cpp b/launcher/ui/dialogs/CreateShortcutDialog.cpp index 38c22d861..83057619e 100644 --- a/launcher/ui/dialogs/CreateShortcutDialog.cpp +++ b/launcher/ui/dialogs/CreateShortcutDialog.cpp @@ -63,8 +63,22 @@ CreateShortcutDialog::CreateShortcutDialog(InstancePtr instance, QWidget* parent m_QuickJoinSupported = instance->traits().contains("feature:is_quick_play_singleplayer"); if (!m_QuickJoinSupported) { - // TODO: Remove radio box and add a single server address textbox instead + ui->worldTarget->hide(); + ui->worldSelectionBox->hide(); } + + // Populate save targets + if (!DesktopServices::isFlatpak()) { + QString desktopDir = FS::getDesktopDir(); + QString applicationDir = FS::getApplicationsDir(); + + if (!desktopDir.isEmpty()) + ui->saveTargetSelectionBox->addItem("Desktop", QVariant::fromValue(SaveTarget::Desktop)); + + if (!applicationDir.isEmpty()) + ui->saveTargetSelectionBox->addItem("Applications", QVariant::fromValue(SaveTarget::Applications)); + } + ui->saveTargetSelectionBox->addItem("Other...", QVariant::fromValue(SaveTarget::Other)); } CreateShortcutDialog::~CreateShortcutDialog() @@ -83,15 +97,17 @@ void CreateShortcutDialog::on_iconButton_clicked() } } -void CreateShortcutDialog::on_saveTargetSelectionBox_currentIndexChanged(int index) {} - -void CreateShortcutDialog::on_instNameTextBox_textChanged(const QString& arg1) {} - -void CreateShortcutDialog::on_overrideAccountCheckbox_stateChanged(int state) {} +void CreateShortcutDialog::on_overrideAccountCheckbox_stateChanged(int state) +{ + ui->accountOptionsGroup->setEnabled(state == Qt::Checked); +} void CreateShortcutDialog::on_accountSelectionBox_currentIndexChanged(int index) {} -void CreateShortcutDialog::on_targetCheckbox_stateChanged(int state) {} +void CreateShortcutDialog::on_targetCheckbox_stateChanged(int state) +{ + ui->targetOptionsGroup->setEnabled(state == Qt::Checked); +} void CreateShortcutDialog::on_worldSelectionBox_currentIndexChanged(int index) {} @@ -113,5 +129,13 @@ void CreateShortcutDialog::createShortcut() extraArgs = { "--server", /* server address */ }; } } - ShortcutUtils::createInstanceShortcutOnDesktop({ m_instance.get(), m_instance->name(), targetString, this, extraArgs }); + + auto target = ui->saveTargetSelectionBox->currentData().value(); + auto name = ui->instNameTextBox->text(); + if (target == SaveTarget::Desktop) + ShortcutUtils::createInstanceShortcutOnDesktop({ m_instance.get(), name, targetString, this, extraArgs, InstIconKey }); + else if (target == SaveTarget::Applications) + ShortcutUtils::createInstanceShortcutInApplications({ m_instance.get(), name, targetString, this, extraArgs, InstIconKey }); + else + ShortcutUtils::createInstanceShortcutInOther({ m_instance.get(), m_instance->name(), targetString, this, extraArgs, InstIconKey }); } diff --git a/launcher/ui/dialogs/CreateShortcutDialog.h b/launcher/ui/dialogs/CreateShortcutDialog.h index 7be6b339c..04849ebfa 100644 --- a/launcher/ui/dialogs/CreateShortcutDialog.h +++ b/launcher/ui/dialogs/CreateShortcutDialog.h @@ -36,8 +36,6 @@ class CreateShortcutDialog : public QDialog { private slots: // Icon, target and name void on_iconButton_clicked(); - void on_saveTargetSelectionBox_currentIndexChanged(int index); - void on_instNameTextBox_textChanged(const QString& arg1); // Override account void on_overrideAccountCheckbox_stateChanged(int state); @@ -57,4 +55,5 @@ class CreateShortcutDialog : public QDialog { bool m_QuickJoinSupported = false; // Index representations + enum class SaveTarget { Desktop, Applications, Other }; }; From 3529d295843812796d191c79bbbc3d3e08df4ed9 Mon Sep 17 00:00:00 2001 From: Yihe Li Date: Mon, 19 May 2025 01:05:08 +0800 Subject: [PATCH 253/695] Implement world and server selection Signed-off-by: Yihe Li --- launcher/ui/dialogs/CreateShortcutDialog.cpp | 70 +++++++++++++++++--- launcher/ui/dialogs/CreateShortcutDialog.h | 8 ++- launcher/ui/dialogs/CreateShortcutDialog.ui | 12 +--- 3 files changed, 71 insertions(+), 19 deletions(-) diff --git a/launcher/ui/dialogs/CreateShortcutDialog.cpp b/launcher/ui/dialogs/CreateShortcutDialog.cpp index 83057619e..9a41a7961 100644 --- a/launcher/ui/dialogs/CreateShortcutDialog.cpp +++ b/launcher/ui/dialogs/CreateShortcutDialog.cpp @@ -50,7 +50,9 @@ #include "FileSystem.h" #include "InstanceList.h" #include "icons/IconList.h" +#include "minecraft/MinecraftInstance.h" #include "minecraft/ShortcutUtils.h" +#include "minecraft/WorldList.h" CreateShortcutDialog::CreateShortcutDialog(InstancePtr instance, QWidget* parent) : QDialog(parent), ui(new Ui::CreateShortcutDialog), m_instance(instance) @@ -59,12 +61,14 @@ CreateShortcutDialog::CreateShortcutDialog(InstancePtr instance, QWidget* parent InstIconKey = instance->iconKey(); ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey)); - ui->instNameTextBox->setText(instance->name()); + ui->instNameTextBox->setPlaceholderText(instance->name()); - m_QuickJoinSupported = instance->traits().contains("feature:is_quick_play_singleplayer"); + auto mInst = std::dynamic_pointer_cast(instance); + m_QuickJoinSupported = mInst && mInst->traits().contains("feature:is_quick_play_singleplayer"); if (!m_QuickJoinSupported) { ui->worldTarget->hide(); ui->worldSelectionBox->hide(); + ui->serverTarget->setChecked(true); } // Populate save targets @@ -79,6 +83,18 @@ CreateShortcutDialog::CreateShortcutDialog(InstancePtr instance, QWidget* parent ui->saveTargetSelectionBox->addItem("Applications", QVariant::fromValue(SaveTarget::Applications)); } ui->saveTargetSelectionBox->addItem("Other...", QVariant::fromValue(SaveTarget::Other)); + + // Populate worlds + if (m_QuickJoinSupported) { + auto worldList = mInst->worldList(); + worldList->update(); + for (const auto& world : worldList->allWorlds()) { + // Entry name: World Name [Game Mode] - Last Played: DateTime + QString entry_name = tr("%1 [%2] - Last Played: %3") + .arg(world.name(), world.gameType().toTranslatedString(), world.lastPlayed().toString(Qt::ISODate)); + ui->worldSelectionBox->addItem(entry_name, world.name()); + } + } } CreateShortcutDialog::~CreateShortcutDialog() @@ -107,13 +123,49 @@ void CreateShortcutDialog::on_accountSelectionBox_currentIndexChanged(int index) void CreateShortcutDialog::on_targetCheckbox_stateChanged(int state) { ui->targetOptionsGroup->setEnabled(state == Qt::Checked); + ui->worldSelectionBox->setEnabled(ui->worldTarget->isChecked()); + ui->serverAddressBox->setEnabled(ui->serverTarget->isChecked()); + stateChanged(); +} + +void CreateShortcutDialog::on_worldTarget_toggled(bool checked) +{ + ui->worldSelectionBox->setEnabled(checked); + stateChanged(); +} + +void CreateShortcutDialog::on_serverTarget_toggled(bool checked) +{ + ui->serverAddressBox->setEnabled(checked); + stateChanged(); } -void CreateShortcutDialog::on_worldSelectionBox_currentIndexChanged(int index) {} +void CreateShortcutDialog::on_worldSelectionBox_currentIndexChanged(int index) +{ + stateChanged(); +} -void CreateShortcutDialog::on_serverAddressBox_textChanged(const QString& arg1) {} +void CreateShortcutDialog::on_serverAddressBox_textChanged(const QString& text) +{ + stateChanged(); +} -void CreateShortcutDialog::targetChanged() {} +void CreateShortcutDialog::stateChanged() +{ + QString result = m_instance->name(); + if (ui->targetCheckbox->isChecked()) { + if (ui->worldTarget->isChecked()) + result = tr("%1 - %2").arg(result, ui->worldSelectionBox->currentData().toString()); + else if (ui->serverTarget->isChecked()) + result = tr("%1 - Server %2").arg(result, ui->serverAddressBox->text()); + } + ui->instNameTextBox->setPlaceholderText(result); + if (!ui->targetCheckbox->isChecked()) + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); + else + ui->buttonBox->button(QDialogButtonBox::Ok) + ->setEnabled(ui->worldTarget->isChecked() || (ui->serverTarget->isChecked() && !ui->serverAddressBox->text().isEmpty())); +} // Real work void CreateShortcutDialog::createShortcut() @@ -123,19 +175,21 @@ void CreateShortcutDialog::createShortcut() if (ui->targetCheckbox->isChecked()) { if (ui->worldTarget->isChecked()) { targetString = tr("world"); - extraArgs = { "--world", /* world ID */ }; + extraArgs = { "--world", ui->worldSelectionBox->currentData().toString() }; } else if (ui->serverTarget->isChecked()) { targetString = tr("server"); - extraArgs = { "--server", /* server address */ }; + extraArgs = { "--server", ui->serverAddressBox->text() }; } } auto target = ui->saveTargetSelectionBox->currentData().value(); auto name = ui->instNameTextBox->text(); + if (name.isEmpty()) + name = ui->instNameTextBox->placeholderText(); if (target == SaveTarget::Desktop) ShortcutUtils::createInstanceShortcutOnDesktop({ m_instance.get(), name, targetString, this, extraArgs, InstIconKey }); else if (target == SaveTarget::Applications) ShortcutUtils::createInstanceShortcutInApplications({ m_instance.get(), name, targetString, this, extraArgs, InstIconKey }); else - ShortcutUtils::createInstanceShortcutInOther({ m_instance.get(), m_instance->name(), targetString, this, extraArgs, InstIconKey }); + ShortcutUtils::createInstanceShortcutInOther({ m_instance.get(), name, targetString, this, extraArgs, InstIconKey }); } diff --git a/launcher/ui/dialogs/CreateShortcutDialog.h b/launcher/ui/dialogs/CreateShortcutDialog.h index 04849ebfa..c26005304 100644 --- a/launcher/ui/dialogs/CreateShortcutDialog.h +++ b/launcher/ui/dialogs/CreateShortcutDialog.h @@ -43,9 +43,10 @@ class CreateShortcutDialog : public QDialog { // Override target (world, server) void on_targetCheckbox_stateChanged(int state); + void on_worldTarget_toggled(bool checked); + void on_serverTarget_toggled(bool checked); void on_worldSelectionBox_currentIndexChanged(int index); - void on_serverAddressBox_textChanged(const QString& arg1); - void targetChanged(); + void on_serverAddressBox_textChanged(const QString& text); private: // Data @@ -56,4 +57,7 @@ class CreateShortcutDialog : public QDialog { // Index representations enum class SaveTarget { Desktop, Applications, Other }; + + // Functions + void stateChanged(); }; diff --git a/launcher/ui/dialogs/CreateShortcutDialog.ui b/launcher/ui/dialogs/CreateShortcutDialog.ui index e45a8428a..2a90f43ab 100644 --- a/launcher/ui/dialogs/CreateShortcutDialog.ui +++ b/launcher/ui/dialogs/CreateShortcutDialog.ui @@ -161,15 +161,9 @@ - - - - 0 - 0 - - - - true + + + Server Address From 4839595a117ee65e952f7c18ffb4231136b7a084 Mon Sep 17 00:00:00 2001 From: Yihe Li Date: Mon, 19 May 2025 01:29:23 +0800 Subject: [PATCH 254/695] Implement account override Signed-off-by: Yihe Li --- launcher/ui/dialogs/CreateShortcutDialog.cpp | 33 +++++++++++++++++--- launcher/ui/dialogs/CreateShortcutDialog.h | 1 - 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/launcher/ui/dialogs/CreateShortcutDialog.cpp b/launcher/ui/dialogs/CreateShortcutDialog.cpp index 9a41a7961..6bc2b80cf 100644 --- a/launcher/ui/dialogs/CreateShortcutDialog.cpp +++ b/launcher/ui/dialogs/CreateShortcutDialog.cpp @@ -50,9 +50,11 @@ #include "FileSystem.h" #include "InstanceList.h" #include "icons/IconList.h" + #include "minecraft/MinecraftInstance.h" #include "minecraft/ShortcutUtils.h" #include "minecraft/WorldList.h" +#include "minecraft/auth/AccountList.h" CreateShortcutDialog::CreateShortcutDialog(InstancePtr instance, QWidget* parent) : QDialog(parent), ui(new Ui::CreateShortcutDialog), m_instance(instance) @@ -95,6 +97,25 @@ CreateShortcutDialog::CreateShortcutDialog(InstancePtr instance, QWidget* parent ui->worldSelectionBox->addItem(entry_name, world.name()); } } + + // Populate accounts + auto accounts = APPLICATION->accounts(); + MinecraftAccountPtr defaultAccount = accounts->defaultAccount(); + if (accounts->count() <= 0) { + ui->overrideAccountCheckbox->setEnabled(false); + } else + for (int i = 0; i < accounts->count(); i++) { + MinecraftAccountPtr account = accounts->at(i); + auto profileLabel = account->profileName(); + if (account->isInUse()) + profileLabel = tr("%1 (in use)").arg(profileLabel); + auto face = account->getFace(); + QIcon icon = face.isNull() ? APPLICATION->getThemedIcon("noaccount") : face; + ui->accountSelectionBox->addItem(profileLabel, account->profileName()); + ui->accountSelectionBox->setItemIcon(i, icon); + if (defaultAccount == account) + ui->accountSelectionBox->setCurrentIndex(i); + } } CreateShortcutDialog::~CreateShortcutDialog() @@ -118,8 +139,6 @@ void CreateShortcutDialog::on_overrideAccountCheckbox_stateChanged(int state) ui->accountOptionsGroup->setEnabled(state == Qt::Checked); } -void CreateShortcutDialog::on_accountSelectionBox_currentIndexChanged(int index) {} - void CreateShortcutDialog::on_targetCheckbox_stateChanged(int state) { ui->targetOptionsGroup->setEnabled(state == Qt::Checked); @@ -186,10 +205,14 @@ void CreateShortcutDialog::createShortcut() auto name = ui->instNameTextBox->text(); if (name.isEmpty()) name = ui->instNameTextBox->placeholderText(); + if (ui->overrideAccountCheckbox->isChecked()) + extraArgs.append({ "--profile", ui->accountSelectionBox->currentData().toString() }); + + ShortcutUtils::Shortcut args{ m_instance.get(), name, targetString, this, extraArgs, InstIconKey }; if (target == SaveTarget::Desktop) - ShortcutUtils::createInstanceShortcutOnDesktop({ m_instance.get(), name, targetString, this, extraArgs, InstIconKey }); + ShortcutUtils::createInstanceShortcutOnDesktop(args); else if (target == SaveTarget::Applications) - ShortcutUtils::createInstanceShortcutInApplications({ m_instance.get(), name, targetString, this, extraArgs, InstIconKey }); + ShortcutUtils::createInstanceShortcutInApplications(args); else - ShortcutUtils::createInstanceShortcutInOther({ m_instance.get(), name, targetString, this, extraArgs, InstIconKey }); + ShortcutUtils::createInstanceShortcutInOther(args); } diff --git a/launcher/ui/dialogs/CreateShortcutDialog.h b/launcher/ui/dialogs/CreateShortcutDialog.h index c26005304..cfedbf017 100644 --- a/launcher/ui/dialogs/CreateShortcutDialog.h +++ b/launcher/ui/dialogs/CreateShortcutDialog.h @@ -39,7 +39,6 @@ class CreateShortcutDialog : public QDialog { // Override account void on_overrideAccountCheckbox_stateChanged(int state); - void on_accountSelectionBox_currentIndexChanged(int index); // Override target (world, server) void on_targetCheckbox_stateChanged(int state); From 46c9eb1d5fb2df8ebe10f4a57d41e9172b3aca84 Mon Sep 17 00:00:00 2001 From: Yihe Li Date: Mon, 19 May 2025 01:33:15 +0800 Subject: [PATCH 255/695] Remove button additions Signed-off-by: Yihe Li --- launcher/minecraft/WorldList.cpp | 38 ---------- launcher/minecraft/WorldList.h | 5 -- launcher/ui/pages/instance/ServersPage.cpp | 75 -------------------- launcher/ui/pages/instance/ServersPage.h | 4 -- launcher/ui/pages/instance/ServersPage.ui | 48 +------------ launcher/ui/pages/instance/WorldListPage.cpp | 59 --------------- launcher/ui/pages/instance/WorldListPage.h | 4 -- launcher/ui/pages/instance/WorldListPage.ui | 47 +----------- 8 files changed, 2 insertions(+), 278 deletions(-) diff --git a/launcher/minecraft/WorldList.cpp b/launcher/minecraft/WorldList.cpp index 582531577..6a821ba60 100644 --- a/launcher/minecraft/WorldList.cpp +++ b/launcher/minecraft/WorldList.cpp @@ -46,9 +46,6 @@ #include #include -#include -#include "minecraft/ShortcutUtils.h" - WorldList::WorldList(const QString& dir, BaseInstance* instance) : QAbstractListModel(), m_instance(instance), m_dir(dir) { FS::ensureFolderPathExists(m_dir.absolutePath()); @@ -457,39 +454,4 @@ void WorldList::loadWorldsAsync() } } -void WorldList::createWorldShortcut(const QModelIndex& index, QWidget* parent) const -{ - if (!m_instance) - return; - - if (DesktopServices::isFlatpak()) - createWorldShortcutInOther(index, parent); - else - createWorldShortcutOnDesktop(index, parent); -} - -void WorldList::createWorldShortcutOnDesktop(const QModelIndex& index, QWidget* parent) const -{ - const auto& world = allWorlds().at(index.row()); - QString name = QString(tr("%1 - %2")).arg(m_instance->name(), world.name()); - QStringList extraArgs{ "--world", world.name() }; - ShortcutUtils::createInstanceShortcutOnDesktop({ m_instance, name, tr("world"), parent, extraArgs }); -} - -void WorldList::createWorldShortcutInApplications(const QModelIndex& index, QWidget* parent) const -{ - const auto& world = allWorlds().at(index.row()); - QString name = QString(tr("%1 - %2")).arg(m_instance->name(), world.name()); - QStringList extraArgs{ "--world", world.name() }; - ShortcutUtils::createInstanceShortcutInApplications({ m_instance, name, tr("world"), parent, extraArgs }); -} - -void WorldList::createWorldShortcutInOther(const QModelIndex& index, QWidget* parent) const -{ - const auto& world = allWorlds().at(index.row()); - QString name = QString(tr("%1 - %2")).arg(m_instance->name(), world.name()); - QStringList extraArgs{ "--world", world.name() }; - ShortcutUtils::createInstanceShortcutInOther({ m_instance, name, tr("world"), parent, extraArgs }); -} - #include "WorldList.moc" diff --git a/launcher/minecraft/WorldList.h b/launcher/minecraft/WorldList.h index 4f54e0737..93fecf1f5 100644 --- a/launcher/minecraft/WorldList.h +++ b/launcher/minecraft/WorldList.h @@ -84,11 +84,6 @@ class WorldList : public QAbstractListModel { const QList& allWorlds() const { return m_worlds; } - void createWorldShortcut(const QModelIndex& index, QWidget* parent = nullptr) const; - void createWorldShortcutOnDesktop(const QModelIndex& index, QWidget* parent = nullptr) const; - void createWorldShortcutInApplications(const QModelIndex& index, QWidget* parent = nullptr) const; - void createWorldShortcutInOther(const QModelIndex& index, QWidget* parent = nullptr) const; - private slots: void directoryChanged(QString path); void loadWorldsAsync(); diff --git a/launcher/ui/pages/instance/ServersPage.cpp b/launcher/ui/pages/instance/ServersPage.cpp index 36844d92a..245bbffe2 100644 --- a/launcher/ui/pages/instance/ServersPage.cpp +++ b/launcher/ui/pages/instance/ServersPage.cpp @@ -40,10 +40,8 @@ #include "ui/dialogs/CustomMessageBox.h" #include "ui_ServersPage.h" -#include #include #include -#include #include #include #include @@ -104,38 +102,6 @@ struct Server { } } - void createServerShortcut(BaseInstance* instance, QWidget* parent = nullptr) const - { - if (!instance) - return; - - if (DesktopServices::isFlatpak()) - createServerShortcutInOther(instance, parent); - else - createServerShortcutOnDesktop(instance, parent); - } - - void createServerShortcutOnDesktop(BaseInstance* instance, QWidget* parent = nullptr) const - { - QString name = QString(QObject::tr("%1 - Server %2")).arg(instance->name(), m_name); - QStringList extraArgs{ "--server", m_address }; - ShortcutUtils::createInstanceShortcutOnDesktop({ instance, name, QObject::tr("server"), parent, extraArgs }); - } - - void createServerShortcutInApplications(BaseInstance* instance, QWidget* parent = nullptr) const - { - QString name = QString(QObject::tr("%1 - Server %2")).arg(instance->name(), m_name); - QStringList extraArgs{ "--server", m_address }; - ShortcutUtils::createInstanceShortcutInApplications({ instance, name, QObject::tr("server"), parent, extraArgs }); - } - - void createServerShortcutInOther(BaseInstance* instance, QWidget* parent = nullptr) const - { - QString name = QString(QObject::tr("%1 - Server %2")).arg(instance->name(), m_name); - QStringList extraArgs{ "--server", m_address }; - ShortcutUtils::createInstanceShortcutInOther({ instance, name, QObject::tr("server"), parent, extraArgs }); - } - // Data - persistent and user changeable QString m_name; QString m_address; @@ -615,26 +581,6 @@ ServersPage::ServersPage(InstancePtr inst, QWidget* parent) : QMainWindow(parent connect(ui->resourceComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(resourceIndexChanged(int))); connect(m_model, &QAbstractItemModel::rowsRemoved, this, &ServersPage::rowsRemoved); - QList shortcutActions = { ui->actionCreateServerShortcutOther }; - if (!DesktopServices::isFlatpak()) { - QString desktopDir = FS::getDesktopDir(); - QString applicationDir = FS::getApplicationsDir(); - - if (!applicationDir.isEmpty()) - shortcutActions.push_front(ui->actionCreateServerShortcutApplications); - - if (!desktopDir.isEmpty()) - shortcutActions.push_front(ui->actionCreateServerShortcutDesktop); - } - - if (shortcutActions.length() > 1) { - auto shortcutInstanceMenu = new QMenu(this); - - for (auto action : shortcutActions) - shortcutInstanceMenu->addAction(action); - ui->actionCreateServerShortcut->setMenu(shortcutInstanceMenu); - } - m_locked = m_inst->isRunning(); if (m_locked) { m_model->lock(); @@ -738,7 +684,6 @@ void ServersPage::updateState() ui->actionMove_Up->setEnabled(serverEditEnabled); ui->actionRemove->setEnabled(serverEditEnabled); ui->actionJoin->setEnabled(serverEditEnabled); - ui->actionCreateServerShortcut->setEnabled(serverEditEnabled); if (server) { ui->addressLine->setText(server->m_address); @@ -822,26 +767,6 @@ void ServersPage::on_actionJoin_triggered() APPLICATION->launch(m_inst, true, false, std::make_shared(MinecraftTarget::parse(address, false))); } -void ServersPage::on_actionCreateServerShortcut_triggered() -{ - m_model->at(currentServer)->createServerShortcut(m_inst.get(), this); -} - -void ServersPage::on_actionCreateServerShortcutDesktop_triggered() -{ - m_model->at(currentServer)->createServerShortcutOnDesktop(m_inst.get(), this); -} - -void ServersPage::on_actionCreateServerShortcutApplications_triggered() -{ - m_model->at(currentServer)->createServerShortcutInApplications(m_inst.get(), this); -} - -void ServersPage::on_actionCreateServerShortcutOther_triggered() -{ - m_model->at(currentServer)->createServerShortcutInOther(m_inst.get(), this); -} - void ServersPage::on_actionRefresh_triggered() { m_model->queryServersStatus(); diff --git a/launcher/ui/pages/instance/ServersPage.h b/launcher/ui/pages/instance/ServersPage.h index 94baaa004..77710d6cc 100644 --- a/launcher/ui/pages/instance/ServersPage.h +++ b/launcher/ui/pages/instance/ServersPage.h @@ -85,10 +85,6 @@ class ServersPage : public QMainWindow, public BasePage { void on_actionMove_Up_triggered(); void on_actionMove_Down_triggered(); void on_actionJoin_triggered(); - void on_actionCreateServerShortcut_triggered(); - void on_actionCreateServerShortcutDesktop_triggered(); - void on_actionCreateServerShortcutApplications_triggered(); - void on_actionCreateServerShortcutOther_triggered(); void on_actionRefresh_triggered(); void runningStateChanged(bool running); diff --git a/launcher/ui/pages/instance/ServersPage.ui b/launcher/ui/pages/instance/ServersPage.ui index bb8bff5aa..d330835c8 100644 --- a/launcher/ui/pages/instance/ServersPage.ui +++ b/launcher/ui/pages/instance/ServersPage.ui @@ -135,9 +135,6 @@ Qt::ToolButtonTextOnly - - true - false @@ -148,12 +145,10 @@ false - - - + @@ -182,47 +177,6 @@ Join - - - Create Shortcut - - - Creates a shortcut on a selected folder to join the selected server. - - - - - Desktop - - - Creates a shortcut to this server on your desktop - - - QAction::TextHeuristicRole - - - - - Applications - - - Create a shortcut of this server on your start menu - - - QAction::TextHeuristicRole - - - - - Other... - - - Creates a shortcut in a folder selected by you - - - QAction::TextHeuristicRole - - Refresh diff --git a/launcher/ui/pages/instance/WorldListPage.cpp b/launcher/ui/pages/instance/WorldListPage.cpp index c770f9f23..9e1a0fb55 100644 --- a/launcher/ui/pages/instance/WorldListPage.cpp +++ b/launcher/ui/pages/instance/WorldListPage.cpp @@ -89,26 +89,6 @@ WorldListPage::WorldListPage(InstancePtr inst, std::shared_ptr worlds ui->toolBar->insertSpacer(ui->actionRefresh); - QList shortcutActions = { ui->actionCreateWorldShortcutOther }; - if (!DesktopServices::isFlatpak()) { - QString desktopDir = FS::getDesktopDir(); - QString applicationDir = FS::getApplicationsDir(); - - if (!applicationDir.isEmpty()) - shortcutActions.push_front(ui->actionCreateWorldShortcutApplications); - - if (!desktopDir.isEmpty()) - shortcutActions.push_front(ui->actionCreateWorldShortcutDesktop); - } - - if (shortcutActions.length() > 1) { - auto shortcutInstanceMenu = new QMenu(this); - - for (auto action : shortcutActions) - shortcutInstanceMenu->addAction(action); - ui->actionCreateWorldShortcut->setMenu(shortcutInstanceMenu); - } - WorldListProxyModel* proxy = new WorldListProxyModel(this); proxy->setSortCaseSensitivity(Qt::CaseInsensitive); proxy->setSourceModel(m_worlds.get()); @@ -365,9 +345,6 @@ void WorldListPage::worldChanged([[maybe_unused]] const QModelIndex& current, [[ if (!supportsJoin) { ui->toolBar->removeAction(ui->actionJoin); - ui->toolBar->removeAction(ui->actionCreateWorldShortcut); - } else { - ui->actionCreateWorldShortcut->setEnabled(enable); } } @@ -443,42 +420,6 @@ void WorldListPage::on_actionRename_triggered() } } -void WorldListPage::on_actionCreateWorldShortcut_triggered() -{ - QModelIndex index = getSelectedWorld(); - if (!index.isValid()) { - return; - } - m_worlds->createWorldShortcut(index, this); -} - -void WorldListPage::on_actionCreateWorldShortcutDesktop_triggered() -{ - QModelIndex index = getSelectedWorld(); - if (!index.isValid()) { - return; - } - m_worlds->createWorldShortcutOnDesktop(index, this); -} - -void WorldListPage::on_actionCreateWorldShortcutApplications_triggered() -{ - QModelIndex index = getSelectedWorld(); - if (!index.isValid()) { - return; - } - m_worlds->createWorldShortcutInApplications(index, this); -} - -void WorldListPage::on_actionCreateWorldShortcutOther_triggered() -{ - QModelIndex index = getSelectedWorld(); - if (!index.isValid()) { - return; - } - m_worlds->createWorldShortcutInOther(index, this); -} - void WorldListPage::on_actionRefresh_triggered() { m_worlds->update(); diff --git a/launcher/ui/pages/instance/WorldListPage.h b/launcher/ui/pages/instance/WorldListPage.h index f2c081bc5..84d9cd075 100644 --- a/launcher/ui/pages/instance/WorldListPage.h +++ b/launcher/ui/pages/instance/WorldListPage.h @@ -95,10 +95,6 @@ class WorldListPage : public QMainWindow, public BasePage { void on_actionAdd_triggered(); void on_actionCopy_triggered(); void on_actionRename_triggered(); - void on_actionCreateWorldShortcut_triggered(); - void on_actionCreateWorldShortcutDesktop_triggered(); - void on_actionCreateWorldShortcutApplications_triggered(); - void on_actionCreateWorldShortcutOther_triggered(); void on_actionRefresh_triggered(); void on_actionView_Folder_triggered(); void on_actionDatapacks_triggered(); diff --git a/launcher/ui/pages/instance/WorldListPage.ui b/launcher/ui/pages/instance/WorldListPage.ui index 6d951cbdd..04344b453 100644 --- a/launcher/ui/pages/instance/WorldListPage.ui +++ b/launcher/ui/pages/instance/WorldListPage.ui @@ -70,9 +70,6 @@ Qt::ToolButtonTextOnly - - true - false @@ -88,11 +85,10 @@ - - + @@ -122,14 +118,6 @@ Delete - - - Create Shortcut - - - Creates a shortcut on a selected folder to join the selected world. - - MCEdit @@ -166,39 +154,6 @@ Manage datapacks inside the world. - - - Desktop - - - Creates a shortcut to this world on your desktop - - - QAction::TextHeuristicRole - - - - - Applications - - - Create a shortcut of this world on your start menu - - - QAction::TextHeuristicRole - - - - - Other... - - - Creates a shortcut in a folder selected by you - - - QAction::TextHeuristicRole - - From 3745bdb6f2725d51e24692f25fc4965a325ecde9 Mon Sep 17 00:00:00 2001 From: Yihe Li Date: Mon, 19 May 2025 01:46:23 +0800 Subject: [PATCH 256/695] Special treatment of non-Quick Join worlds Signed-off-by: Yihe Li --- launcher/ui/dialogs/CreateShortcutDialog.cpp | 2 + launcher/ui/dialogs/CreateShortcutDialog.ui | 52 ++++++++++++++------ 2 files changed, 40 insertions(+), 14 deletions(-) diff --git a/launcher/ui/dialogs/CreateShortcutDialog.cpp b/launcher/ui/dialogs/CreateShortcutDialog.cpp index 6bc2b80cf..6fb6e7050 100644 --- a/launcher/ui/dialogs/CreateShortcutDialog.cpp +++ b/launcher/ui/dialogs/CreateShortcutDialog.cpp @@ -71,6 +71,8 @@ CreateShortcutDialog::CreateShortcutDialog(InstancePtr instance, QWidget* parent ui->worldTarget->hide(); ui->worldSelectionBox->hide(); ui->serverTarget->setChecked(true); + ui->serverTarget->hide(); + ui->serverLabel->show(); } // Populate save targets diff --git a/launcher/ui/dialogs/CreateShortcutDialog.ui b/launcher/ui/dialogs/CreateShortcutDialog.ui index 2a90f43ab..9e2bdd747 100644 --- a/launcher/ui/dialogs/CreateShortcutDialog.ui +++ b/launcher/ui/dialogs/CreateShortcutDialog.ui @@ -131,14 +131,21 @@ - - - World: + + + 0 - - targetBtnGroup - - + + + + World: + + + targetBtnGroup + + + + @@ -151,14 +158,31 @@ - - - Server Address: + + + 0 - - targetBtnGroup - - + + + + Server Address: + + + targetBtnGroup + + + + + + + false + + + Server Address: + + + + From 64ef14100d88c4ba44e4716540c85b5180ec3d65 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Thu, 8 May 2025 13:10:37 +0300 Subject: [PATCH 257/695] feat(skin manager): add elytra preview Signed-off-by: Trial97 --- .../ui/dialogs/skins/SkinManageDialog.cpp | 31 ++++++++++++--- launcher/ui/dialogs/skins/SkinManageDialog.ui | 7 ++++ .../ui/dialogs/skins/draw/BoxGeometry.cpp | 8 +++- launcher/ui/dialogs/skins/draw/BoxGeometry.h | 1 + launcher/ui/dialogs/skins/draw/Scene.cpp | 38 +++++++++++++++++-- launcher/ui/dialogs/skins/draw/Scene.h | 5 ++- .../dialogs/skins/draw/SkinOpenGLWindow.cpp | 5 +++ .../ui/dialogs/skins/draw/SkinOpenGLWindow.h | 1 + 8 files changed, 85 insertions(+), 11 deletions(-) diff --git a/launcher/ui/dialogs/skins/SkinManageDialog.cpp b/launcher/ui/dialogs/skins/SkinManageDialog.cpp index 3bc0bc2d9..8e661d37c 100644 --- a/launcher/ui/dialogs/skins/SkinManageDialog.cpp +++ b/launcher/ui/dialogs/skins/SkinManageDialog.cpp @@ -92,6 +92,10 @@ SkinManageDialog::SkinManageDialog(QWidget* parent, MinecraftAccountPtr acct) connect(contentsWidget->selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)), SLOT(selectionChanged(QItemSelection, QItemSelection))); connect(m_ui->listView, &QListView::customContextMenuRequested, this, &SkinManageDialog::show_context_menu); + connect(m_ui->elytraCB, &QCheckBox::stateChanged, this, [this]() { + m_skinPreview->setElytraVisible(m_ui->elytraCB->isChecked()); + on_capeCombo_currentIndexChanged(0); + }); setupCapes(); @@ -159,10 +163,24 @@ void SkinManageDialog::on_fileBtn_clicked() } } -QPixmap previewCape(QImage capeImage) +QPixmap previewCape(QImage capeImage, bool elytra = false) { + if (elytra) { + auto wing = capeImage.copy(34, 0, 12, 22); + QImage mirrored = wing.mirrored(true, false); + + QImage combined(wing.width() * 2 - 2, wing.height(), capeImage.format()); + combined.fill(Qt::transparent); + + QPainter painter(&combined); + painter.drawImage(0, 0, wing); + painter.drawImage(wing.width() - 2, 0, mirrored); + painter.end(); + return QPixmap::fromImage(combined.scaled(96, 176, Qt::IgnoreAspectRatio, Qt::FastTransformation)); + } return QPixmap::fromImage(capeImage.copy(1, 1, 10, 16).scaled(80, 128, Qt::IgnoreAspectRatio, Qt::FastTransformation)); } + void SkinManageDialog::setupCapes() { // FIXME: add a model for this, download/refresh the capes on demand @@ -208,7 +226,7 @@ void SkinManageDialog::setupCapes() } } if (!capeImage.isNull()) { - m_ui->capeCombo->addItem(previewCape(capeImage), cape.alias, cape.id); + m_ui->capeCombo->addItem(previewCape(capeImage, m_ui->elytraCB->isChecked()), cape.alias, cape.id); } else { m_ui->capeCombo->addItem(cape.alias, cape.id); } @@ -222,7 +240,8 @@ void SkinManageDialog::on_capeCombo_currentIndexChanged(int index) auto id = m_ui->capeCombo->currentData(); auto cape = m_capes.value(id.toString(), {}); if (!cape.isNull()) { - m_ui->capeImage->setPixmap(previewCape(cape).scaled(size() * (1. / 3), Qt::KeepAspectRatio, Qt::FastTransformation)); + m_ui->capeImage->setPixmap( + previewCape(cape, m_ui->elytraCB->isChecked()).scaled(size() * (1. / 3), Qt::KeepAspectRatio, Qt::FastTransformation)); } else { m_ui->capeImage->clear(); } @@ -319,14 +338,14 @@ bool SkinManageDialog::eventFilter(QObject* obj, QEvent* ev) return QDialog::eventFilter(obj, ev); } -void SkinManageDialog::on_action_Rename_Skin_triggered(bool checked) +void SkinManageDialog::on_action_Rename_Skin_triggered(bool) { if (!m_selectedSkinKey.isEmpty()) { m_ui->listView->edit(m_ui->listView->currentIndex()); } } -void SkinManageDialog::on_action_Delete_Skin_triggered(bool checked) +void SkinManageDialog::on_action_Delete_Skin_triggered(bool) { if (m_selectedSkinKey.isEmpty()) return; @@ -523,7 +542,7 @@ void SkinManageDialog::resizeEvent(QResizeEvent* event) auto id = m_ui->capeCombo->currentData(); auto cape = m_capes.value(id.toString(), {}); if (!cape.isNull()) { - m_ui->capeImage->setPixmap(previewCape(cape).scaled(s, Qt::KeepAspectRatio, Qt::FastTransformation)); + m_ui->capeImage->setPixmap(previewCape(cape, m_ui->elytraCB->isChecked()).scaled(s, Qt::KeepAspectRatio, Qt::FastTransformation)); } else { m_ui->capeImage->clear(); } diff --git a/launcher/ui/dialogs/skins/SkinManageDialog.ui b/launcher/ui/dialogs/skins/SkinManageDialog.ui index 7e8b4bc46..065c5cafc 100644 --- a/launcher/ui/dialogs/skins/SkinManageDialog.ui +++ b/launcher/ui/dialogs/skins/SkinManageDialog.ui @@ -59,6 +59,13 @@ Cape + + + + Elytra + + + diff --git a/launcher/ui/dialogs/skins/draw/BoxGeometry.cpp b/launcher/ui/dialogs/skins/draw/BoxGeometry.cpp index b4ab8d4cc..f91fe2f1f 100644 --- a/launcher/ui/dialogs/skins/draw/BoxGeometry.cpp +++ b/launcher/ui/dialogs/skins/draw/BoxGeometry.cpp @@ -180,7 +180,8 @@ QList getCubeUVs(float u, float v, float width, float height, float d } namespace opengl { -BoxGeometry::BoxGeometry(QVector3D size, QVector3D position) : m_indexBuf(QOpenGLBuffer::IndexBuffer), m_size(size), m_position(position) +BoxGeometry::BoxGeometry(QVector3D size, QVector3D position) + : QOpenGLFunctions(), m_indexBuf(QOpenGLBuffer::IndexBuffer), m_size(size), m_position(position) { initializeOpenGLFunctions(); @@ -274,4 +275,9 @@ BoxGeometry* BoxGeometry::Plane() return b; } + +void BoxGeometry::scale(const QVector3D& vector) +{ + m_matrix.scale(vector); +} } // namespace opengl \ No newline at end of file diff --git a/launcher/ui/dialogs/skins/draw/BoxGeometry.h b/launcher/ui/dialogs/skins/draw/BoxGeometry.h index 1a245bc14..fa1a4c622 100644 --- a/launcher/ui/dialogs/skins/draw/BoxGeometry.h +++ b/launcher/ui/dialogs/skins/draw/BoxGeometry.h @@ -36,6 +36,7 @@ class BoxGeometry : protected QOpenGLFunctions { void initGeometry(float u, float v, float width, float height, float depth, float textureWidth = 64, float textureHeight = 64); void rotate(float angle, const QVector3D& vector); + void scale(const QVector3D& vector); private: QOpenGLBuffer m_vertexBuf; diff --git a/launcher/ui/dialogs/skins/draw/Scene.cpp b/launcher/ui/dialogs/skins/draw/Scene.cpp index 45d0ba191..89a783622 100644 --- a/launcher/ui/dialogs/skins/draw/Scene.cpp +++ b/launcher/ui/dialogs/skins/draw/Scene.cpp @@ -18,9 +18,16 @@ */ #include "ui/dialogs/skins/draw/Scene.h" + +#include +#include +#include +#include + namespace opengl { -Scene::Scene(const QImage& skin, bool slim, const QImage& cape) : m_slim(slim), m_capeVisible(!cape.isNull()) +Scene::Scene(const QImage& skin, bool slim, const QImage& cape) : QOpenGLFunctions(), m_slim(slim), m_capeVisible(!cape.isNull()) { + initializeOpenGLFunctions(); m_staticComponents = { // head new opengl::BoxGeometry(QVector3D(8, 8, 8), QVector3D(0, 4, 0), QPoint(0, 0), QVector3D(8, 8, 8)), @@ -57,6 +64,19 @@ Scene::Scene(const QImage& skin, bool slim, const QImage& cape) : m_slim(slim), m_cape->rotate(10.8, QVector3D(1, 0, 0)); m_cape->rotate(180, QVector3D(0, 1, 0)); + auto leftWing = + new opengl::BoxGeometry(QVector3D(12, 22, 4), QVector3D(0, -13, -2), QPoint(22, 0), QVector3D(10, 20, 2), QSize(64, 32)); + leftWing->rotate(15, QVector3D(1, 0, 0)); + leftWing->rotate(15, QVector3D(0, 0, 1)); + leftWing->rotate(1, QVector3D(1, 0, 0)); + auto rightWing = + new opengl::BoxGeometry(QVector3D(12, 22, 4), QVector3D(0, -13, -2), QPoint(22, 0), QVector3D(10, 20, 2), QSize(64, 32)); + rightWing->scale(QVector3D(-1, 1, 1)); + rightWing->rotate(15, QVector3D(1, 0, 0)); + rightWing->rotate(15, QVector3D(0, 0, 1)); + rightWing->rotate(1, QVector3D(1, 0, 0)); + m_elytra << leftWing << rightWing; + // texture init m_skinTexture = new QOpenGLTexture(skin.mirrored()); m_skinTexture->setMinificationFilter(QOpenGLTexture::Nearest); @@ -68,7 +88,7 @@ Scene::Scene(const QImage& skin, bool slim, const QImage& cape) : m_slim(slim), } Scene::~Scene() { - for (auto array : { m_staticComponents, m_normalArms, m_slimArms }) { + for (auto array : { m_staticComponents, m_normalArms, m_slimArms, m_elytra }) { for (auto g : array) { delete g; } @@ -95,7 +115,15 @@ void Scene::draw(QOpenGLShaderProgram* program) if (m_capeVisible) { m_capeTexture->bind(); program->setUniformValue("texture", 0); - m_cape->draw(program); + if (!m_elytraVisible) { + m_cape->draw(program); + } else { + glDisable(GL_CULL_FACE); + for (auto e : m_elytra) { + e->draw(program); + } + glEnable(GL_CULL_FACE); + } m_capeTexture->release(); } } @@ -131,4 +159,8 @@ void Scene::setCapeVisible(bool visible) { m_capeVisible = visible; } +void Scene::setElytraVisible(bool elytraVisible) +{ + m_elytraVisible = elytraVisible; +} } // namespace opengl \ No newline at end of file diff --git a/launcher/ui/dialogs/skins/draw/Scene.h b/launcher/ui/dialogs/skins/draw/Scene.h index 3560d1d74..c9bba1f20 100644 --- a/launcher/ui/dialogs/skins/draw/Scene.h +++ b/launcher/ui/dialogs/skins/draw/Scene.h @@ -22,7 +22,7 @@ #include namespace opengl { -class Scene { +class Scene : protected QOpenGLFunctions { public: Scene(const QImage& skin, bool slim, const QImage& cape); virtual ~Scene(); @@ -32,15 +32,18 @@ class Scene { void setCape(const QImage& cape); void setMode(bool slim); void setCapeVisible(bool visible); + void setElytraVisible(bool elytraVisible); private: QList m_staticComponents; QList m_normalArms; QList m_slimArms; BoxGeometry* m_cape = nullptr; + QList m_elytra; QOpenGLTexture* m_skinTexture = nullptr; QOpenGLTexture* m_capeTexture = nullptr; bool m_slim = false; bool m_capeVisible = false; + bool m_elytraVisible = false; }; } // namespace opengl \ No newline at end of file diff --git a/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.cpp b/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.cpp index e1e539050..f035e6b91 100644 --- a/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.cpp +++ b/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.cpp @@ -263,3 +263,8 @@ void SkinOpenGLWindow::wheelEvent(QWheelEvent* event) m_distance = qMax(16.f, m_distance); // Clamp distance update(); // Trigger a repaint } +void SkinOpenGLWindow::setElytraVisible(bool visible) +{ + if (m_scene) + m_scene->setElytraVisible(visible); +} diff --git a/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.h b/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.h index e2c32da0f..2a06c23e5 100644 --- a/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.h +++ b/launcher/ui/dialogs/skins/draw/SkinOpenGLWindow.h @@ -43,6 +43,7 @@ class SkinOpenGLWindow : public QOpenGLWindow, protected QOpenGLFunctions { void updateScene(SkinModel* skin); void updateCape(const QImage& cape); + void setElytraVisible(bool visible); protected: void mousePressEvent(QMouseEvent* e) override; From a89caf7362d595a66c37d06a77f56270042784f4 Mon Sep 17 00:00:00 2001 From: Yihe Li Date: Thu, 22 May 2025 23:09:29 +0800 Subject: [PATCH 258/695] Apply suggestions from review Signed-off-by: Yihe Li --- launcher/minecraft/ShortcutUtils.cpp | 2 +- launcher/ui/dialogs/CreateShortcutDialog.cpp | 15 +++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/launcher/minecraft/ShortcutUtils.cpp b/launcher/minecraft/ShortcutUtils.cpp index ea2a988be..43954aa6a 100644 --- a/launcher/minecraft/ShortcutUtils.cpp +++ b/launcher/minecraft/ShortcutUtils.cpp @@ -149,7 +149,7 @@ void createInstanceShortcut(const Shortcut& shortcut, const QString& filePath) args.append({ "--launch", shortcut.instance->id() }); args.append(shortcut.extraArgs); - if (!FS::createShortcut(std::move(filePath), appPath, args, shortcut.name, iconPath)) { + if (!FS::createShortcut(filePath, appPath, args, shortcut.name, iconPath)) { #if not defined(Q_OS_MACOS) iconFile.remove(); #endif diff --git a/launcher/ui/dialogs/CreateShortcutDialog.cpp b/launcher/ui/dialogs/CreateShortcutDialog.cpp index 6fb6e7050..581ac29e3 100644 --- a/launcher/ui/dialogs/CreateShortcutDialog.cpp +++ b/launcher/ui/dialogs/CreateShortcutDialog.cpp @@ -81,12 +81,12 @@ CreateShortcutDialog::CreateShortcutDialog(InstancePtr instance, QWidget* parent QString applicationDir = FS::getApplicationsDir(); if (!desktopDir.isEmpty()) - ui->saveTargetSelectionBox->addItem("Desktop", QVariant::fromValue(SaveTarget::Desktop)); + ui->saveTargetSelectionBox->addItem(tr("Desktop"), QVariant::fromValue(SaveTarget::Desktop)); if (!applicationDir.isEmpty()) - ui->saveTargetSelectionBox->addItem("Applications", QVariant::fromValue(SaveTarget::Applications)); + ui->saveTargetSelectionBox->addItem(tr("Applications"), QVariant::fromValue(SaveTarget::Applications)); } - ui->saveTargetSelectionBox->addItem("Other...", QVariant::fromValue(SaveTarget::Other)); + ui->saveTargetSelectionBox->addItem(tr("Other..."), QVariant::fromValue(SaveTarget::Other)); // Populate worlds if (m_QuickJoinSupported) { @@ -105,7 +105,7 @@ CreateShortcutDialog::CreateShortcutDialog(InstancePtr instance, QWidget* parent MinecraftAccountPtr defaultAccount = accounts->defaultAccount(); if (accounts->count() <= 0) { ui->overrideAccountCheckbox->setEnabled(false); - } else + } else { for (int i = 0; i < accounts->count(); i++) { MinecraftAccountPtr account = accounts->at(i); auto profileLabel = account->profileName(); @@ -118,6 +118,7 @@ CreateShortcutDialog::CreateShortcutDialog(InstancePtr instance, QWidget* parent if (defaultAccount == account) ui->accountSelectionBox->setCurrentIndex(i); } + } } CreateShortcutDialog::~CreateShortcutDialog() @@ -183,9 +184,11 @@ void CreateShortcutDialog::stateChanged() ui->instNameTextBox->setPlaceholderText(result); if (!ui->targetCheckbox->isChecked()) ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); - else + else { ui->buttonBox->button(QDialogButtonBox::Ok) - ->setEnabled(ui->worldTarget->isChecked() || (ui->serverTarget->isChecked() && !ui->serverAddressBox->text().isEmpty())); + ->setEnabled((ui->worldTarget->isChecked() && ui->worldSelectionBox->currentIndex() != -1) || + (ui->serverTarget->isChecked() && !ui->serverAddressBox->text().isEmpty())); + } } // Real work From 8425861fb14f1c47ea60a9d99d79ee9c857c3f26 Mon Sep 17 00:00:00 2001 From: Yihe Li Date: Fri, 23 May 2025 00:53:03 +0800 Subject: [PATCH 259/695] Just disable world selection when there is no world Signed-off-by: Yihe Li --- launcher/ui/dialogs/CreateShortcutDialog.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/launcher/ui/dialogs/CreateShortcutDialog.cpp b/launcher/ui/dialogs/CreateShortcutDialog.cpp index 581ac29e3..278573a22 100644 --- a/launcher/ui/dialogs/CreateShortcutDialog.cpp +++ b/launcher/ui/dialogs/CreateShortcutDialog.cpp @@ -67,7 +67,9 @@ CreateShortcutDialog::CreateShortcutDialog(InstancePtr instance, QWidget* parent auto mInst = std::dynamic_pointer_cast(instance); m_QuickJoinSupported = mInst && mInst->traits().contains("feature:is_quick_play_singleplayer"); - if (!m_QuickJoinSupported) { + auto worldList = mInst->worldList(); + worldList->update(); + if (!m_QuickJoinSupported || worldList->empty()) { ui->worldTarget->hide(); ui->worldSelectionBox->hide(); ui->serverTarget->setChecked(true); @@ -90,8 +92,6 @@ CreateShortcutDialog::CreateShortcutDialog(InstancePtr instance, QWidget* parent // Populate worlds if (m_QuickJoinSupported) { - auto worldList = mInst->worldList(); - worldList->update(); for (const auto& world : worldList->allWorlds()) { // Entry name: World Name [Game Mode] - Last Played: DateTime QString entry_name = tr("%1 [%2] - Last Played: %3") From ff1fb8755a7a2e2cb25dcc7a3ed271de88ff37dc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 25 May 2025 00:30:05 +0000 Subject: [PATCH 260/695] chore(nix): update lockfile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'nixpkgs': 'github:NixOS/nixpkgs/dda3dcd3fe03e991015e9a74b22d35950f264a54?narHash=sha256-Ua0drDHawlzNqJnclTJGf87dBmaO/tn7iZ%2BTCkTRpRc%3D' (2025-05-08) → 'github:NixOS/nixpkgs/063f43f2dbdef86376cc29ad646c45c46e93234c?narHash=sha256-6m1Y3/4pVw1RWTsrkAK2VMYSzG4MMIj7sqUy7o8th1o%3D' (2025-05-23) --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index d0cc6f54c..7479be1b7 100644 --- a/flake.lock +++ b/flake.lock @@ -18,11 +18,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1746663147, - "narHash": "sha256-Ua0drDHawlzNqJnclTJGf87dBmaO/tn7iZ+TCkTRpRc=", + "lastModified": 1748026106, + "narHash": "sha256-6m1Y3/4pVw1RWTsrkAK2VMYSzG4MMIj7sqUy7o8th1o=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "dda3dcd3fe03e991015e9a74b22d35950f264a54", + "rev": "063f43f2dbdef86376cc29ad646c45c46e93234c", "type": "github" }, "original": { From 3690d935919271cc59a1ba1a33f543638c16c2f1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 27 May 2025 12:24:27 +0000 Subject: [PATCH 261/695] chore(deps): update cachix/install-nix-action digest to 17fe5fb --- .github/workflows/update-flake.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/update-flake.yml b/.github/workflows/update-flake.yml index 62852171b..8957e4a98 100644 --- a/.github/workflows/update-flake.yml +++ b/.github/workflows/update-flake.yml @@ -17,7 +17,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: cachix/install-nix-action@526118121621777ccd86f79b04685a9319637641 # v31 + - uses: cachix/install-nix-action@17fe5fb4a23ad6cbbe47d6b3f359611ad276644c # v31 - uses: DeterminateSystems/update-flake-lock@v24 with: From 03e1b7b4d591ffacabbe3a6cc5b13afad4a8c8ca Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 29 May 2025 22:07:33 +0000 Subject: [PATCH 262/695] chore(deps): update determinatesystems/flakehub-cache-action action to v2 --- .github/workflows/nix.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index 5a40ebb1f..80b41161a 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -111,7 +111,7 @@ jobs: # For PRs - name: Setup Nix Magic Cache if: ${{ env.USE_DETERMINATE == 'true' }} - uses: DeterminateSystems/flakehub-cache-action@v1 + uses: DeterminateSystems/flakehub-cache-action@v2 # For in-tree builds - name: Setup Cachix From 75779a841ebbf9a3774887038ffed7b7412ae0d8 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Fri, 30 May 2025 18:55:58 +0000 Subject: [PATCH 263/695] Re-apply my suggestion Signed-off-by: TheKodeToad --- launcher/ui/dialogs/skins/SkinManageDialog.ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/dialogs/skins/SkinManageDialog.ui b/launcher/ui/dialogs/skins/SkinManageDialog.ui index 065c5cafc..aeb516854 100644 --- a/launcher/ui/dialogs/skins/SkinManageDialog.ui +++ b/launcher/ui/dialogs/skins/SkinManageDialog.ui @@ -62,7 +62,7 @@ - Elytra + Preview Elytra From d3f337d6ef3f6e824d13a7ab8d160b42c59228f8 Mon Sep 17 00:00:00 2001 From: Yihe Li Date: Sun, 1 Jun 2025 08:13:10 +0800 Subject: [PATCH 264/695] Delete shortcut when deleting instances Signed-off-by: Yihe Li --- launcher/BaseInstance.cpp | 11 ++++ launcher/BaseInstance.h | 17 +++++ launcher/FileSystem.cpp | 32 ++++----- launcher/FileSystem.h | 3 +- launcher/InstanceList.cpp | 69 +++++++++++++++++--- launcher/InstanceList.h | 10 ++- launcher/minecraft/ShortcutUtils.cpp | 59 ++++++++++------- launcher/minecraft/ShortcutUtils.h | 10 +-- launcher/ui/MainWindow.cpp | 7 +- launcher/ui/dialogs/CreateShortcutDialog.cpp | 14 ++-- launcher/ui/dialogs/CreateShortcutDialog.h | 3 - 11 files changed, 168 insertions(+), 67 deletions(-) diff --git a/launcher/BaseInstance.cpp b/launcher/BaseInstance.cpp index 70e0f9dc1..1aa01568c 100644 --- a/launcher/BaseInstance.cpp +++ b/launcher/BaseInstance.cpp @@ -398,6 +398,17 @@ bool BaseInstance::syncInstanceDirName(const QString& newRoot) const return oldRoot == newRoot || QFile::rename(oldRoot, newRoot); } +void BaseInstance::registerShortcut(const ShortcutData& data) +{ + m_shortcuts.append(data); + qDebug() << "Registering shortcut for instance" << id() << "with name" << data.name << "and path" << data.filePath; +} + +QList& BaseInstance::getShortcuts() +{ + return m_shortcuts; +} + QString BaseInstance::name() const { return m_settings->get("name").toString(); diff --git a/launcher/BaseInstance.h b/launcher/BaseInstance.h index 1acf1afe0..d2ff64e7e 100644 --- a/launcher/BaseInstance.h +++ b/launcher/BaseInstance.h @@ -39,6 +39,7 @@ #include #include +#include #include #include #include @@ -66,6 +67,16 @@ class BaseInstance; // pointer for lazy people using InstancePtr = std::shared_ptr; +/// Shortcut saving target representations +enum class ShortcutTarget { Desktop, Applications, Other }; + +/// Shortcut data representation +struct ShortcutData { + QString name; + QString filePath; + ShortcutTarget target; +}; + /*! * \brief Base class for instances. * This class implements many functions that are common between instances and @@ -129,6 +140,10 @@ class BaseInstance : public QObject, public std::enable_shared_from_this& getShortcuts(); + /// Value used for instance window titles QString windowTitle() const; @@ -308,6 +323,8 @@ class BaseInstance : public QObject, public std::enable_shared_from_this m_shortcuts; }; Q_DECLARE_METATYPE(shared_qobject_ptr) diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp index 9d45f4af3..c5386a43b 100644 --- a/launcher/FileSystem.cpp +++ b/launcher/FileSystem.cpp @@ -898,26 +898,26 @@ QString getApplicationsDir() } // Cross-platform Shortcut creation -bool createShortcut(QString destination, QString target, QStringList args, QString name, QString icon) +QString createShortcut(QString destination, QString target, QStringList args, QString name, QString icon) { if (destination.isEmpty()) { destination = PathCombine(getDesktopDir(), RemoveInvalidFilenameChars(name)); } if (!ensureFilePathExists(destination)) { qWarning() << "Destination path can't be created!"; - return false; + return ""; } #if defined(Q_OS_MACOS) QDir application = destination + ".app/"; if (application.exists()) { qWarning() << "Application already exists!"; - return false; + return ""; } if (!application.mkpath(".")) { qWarning() << "Couldn't create application"; - return false; + return ""; } QDir content = application.path() + "/Contents/"; @@ -927,7 +927,7 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri if (!(content.mkpath(".") && resources.mkpath(".") && binaryDir.mkpath("."))) { qWarning() << "Couldn't create directories within application"; - return false; + return ""; } info.open(QIODevice::WriteOnly | QIODevice::Text); @@ -976,7 +976,7 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri "\n" ""; - return true; + return application.path(); #elif defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) if (!destination.endsWith(".desktop")) // in case of isFlatpak destination is already populated destination += ".desktop"; @@ -1002,32 +1002,32 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri f.setPermissions(f.permissions() | QFileDevice::ExeOwner | QFileDevice::ExeGroup | QFileDevice::ExeOther); - return true; + return destination; #elif defined(Q_OS_WIN) QFileInfo targetInfo(target); if (!targetInfo.exists()) { qWarning() << "Target file does not exist!"; - return false; + return ""; } target = targetInfo.absoluteFilePath(); if (target.length() >= MAX_PATH) { qWarning() << "Target file path is too long!"; - return false; + return ""; } if (!icon.isEmpty() && icon.length() >= MAX_PATH) { qWarning() << "Icon path is too long!"; - return false; + return ""; } destination += ".lnk"; if (destination.length() >= MAX_PATH) { qWarning() << "Destination path is too long!"; - return false; + return ""; } QString argStr; @@ -1046,7 +1046,7 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri if (argStr.length() >= MAX_PATH) { qWarning() << "Arguments string is too long!"; - return false; + return ""; } HRESULT hres; @@ -1055,7 +1055,7 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri hres = CoInitialize(nullptr); if (FAILED(hres)) { qWarning() << "Failed to initialize COM!"; - return false; + return ""; } WCHAR wsz[MAX_PATH]; @@ -1109,10 +1109,12 @@ bool createShortcut(QString destination, QString target, QStringList args, QStri // go away COM, nobody likes you CoUninitialize(); - return SUCCEEDED(hres); + if (SUCCEEDED(hres)) + return destination; + return ""; #else qWarning("Desktop Shortcuts not supported on your platform!"); - return false; + return ""; #endif } diff --git a/launcher/FileSystem.h b/launcher/FileSystem.h index b1108eded..83cf41d7f 100644 --- a/launcher/FileSystem.h +++ b/launcher/FileSystem.h @@ -362,8 +362,9 @@ bool overrideFolder(QString overwritten_path, QString override_path); /** * Creates a shortcut to the specified target file at the specified destination path. + * Returns empty string if creation failed; otherwise returns the path to the created shortcut. */ -bool createShortcut(QString destination, QString target, QStringList args, QString name, QString icon); +QString createShortcut(QString destination, QString target, QStringList args, QString name, QString icon); enum class FilesystemType { FAT, diff --git a/launcher/InstanceList.cpp b/launcher/InstanceList.cpp index 89e7dc04d..ef8be4919 100644 --- a/launcher/InstanceList.cpp +++ b/launcher/InstanceList.cpp @@ -60,6 +60,7 @@ #include "NullInstance.h" #include "WatchLock.h" #include "minecraft/MinecraftInstance.h" +#include "minecraft/ShortcutUtils.h" #include "settings/INISettingsObject.h" #ifdef Q_OS_WIN32 @@ -333,7 +334,7 @@ bool InstanceList::trashInstance(const InstanceId& id) { auto inst = getInstanceById(id); if (!inst) { - qDebug() << "Cannot trash instance" << id << ". No such instance is present (deleted externally?)."; + qWarning() << "Cannot trash instance" << id << ". No such instance is present (deleted externally?)."; return false; } @@ -348,26 +349,48 @@ bool InstanceList::trashInstance(const InstanceId& id) } if (!FS::trash(inst->instanceRoot(), &trashedLoc)) { - qDebug() << "Trash of instance" << id << "has not been completely successfully..."; + qWarning() << "Trash of instance" << id << "has not been completely successful..."; return false; } qDebug() << "Instance" << id << "has been trashed by the launcher."; m_trashHistory.push({ id, inst->instanceRoot(), trashedLoc, cachedGroupId }); + // Also trash all of its shortcuts; we remove the shortcuts if trash fails since it is invalid anyway + auto& shortcuts = inst->getShortcuts(); + for (auto it = shortcuts.begin(); it != shortcuts.end();) { + const auto& [name, filePath, target] = *it; + if (!FS::trash(filePath, &trashedLoc)) { + qWarning() << "Trash of shortcut" << name << "at path" << filePath << "for instance" << id + << "has not been successful, trying to delete it instead..."; + if (!FS::deletePath(filePath)) { + qWarning() << "Deletion of shortcut" << name << "at path" << filePath << "for instance" << id + << "has not been successful, given up..."; + ++it; + } else { + qDebug() << "Shortcut" << name << "at path" << filePath << "for instance" << id << "has been deleted by the launcher."; + it = shortcuts.erase(it); + } + continue; + } + qDebug() << "Shortcut" << name << "at path" << filePath << "for instance" << id << "has been trashed by the launcher."; + m_trashHistory.top().shortcuts.append({ *it, trashedLoc }); + it = shortcuts.erase(it); + } + return true; } -bool InstanceList::trashedSomething() +bool InstanceList::trashedSomething() const { return !m_trashHistory.empty(); } -void InstanceList::undoTrashInstance() +bool InstanceList::undoTrashInstance() { if (m_trashHistory.empty()) { qWarning() << "Nothing to recover from trash."; - return; + return true; } auto top = m_trashHistory.pop(); @@ -377,21 +400,41 @@ void InstanceList::undoTrashInstance() top.path += "1"; } + if (!QFile(top.trashPath).rename(top.path)) { + qWarning() << "Moving" << top.trashPath << "back to" << top.path << "failed!"; + return false; + } qDebug() << "Moving" << top.trashPath << "back to" << top.path; - QFile(top.trashPath).rename(top.path); + + bool ok = true; + for (const auto& [data, trashPath] : top.shortcuts) { + if (QDir(data.filePath).exists()) { + // Don't try to append 1 here as the shortcut may have suffixes like .app, just warn and skip it + qWarning() << "Shortcut" << trashPath << "original directory" << data.filePath << "already exists!"; + ok = false; + continue; + } + if (!QFile(trashPath).rename(data.filePath)) { + qWarning() << "Moving shortcut from" << trashPath << "back to" << data.filePath << "failed!"; + ok = false; + continue; + } + qDebug() << "Moving shortcut from" << trashPath << "back to" << data.filePath; + } m_instanceGroupIndex[top.id] = top.groupName; increaseGroupCount(top.groupName); saveGroupList(); emit instancesChanged(); + return ok; } void InstanceList::deleteInstance(const InstanceId& id) { auto inst = getInstanceById(id); if (!inst) { - qDebug() << "Cannot delete instance" << id << ". No such instance is present (deleted externally?)."; + qWarning() << "Cannot delete instance" << id << ". No such instance is present (deleted externally?)."; return; } @@ -404,11 +447,19 @@ void InstanceList::deleteInstance(const InstanceId& id) qDebug() << "Will delete instance" << id; if (!FS::deletePath(inst->instanceRoot())) { - qWarning() << "Deletion of instance" << id << "has not been completely successful ..."; + qWarning() << "Deletion of instance" << id << "has not been completely successful..."; return; } qDebug() << "Instance" << id << "has been deleted by the launcher."; + + for (const auto& [name, filePath, target] : inst->getShortcuts()) { + if (!FS::deletePath(filePath)) { + qWarning() << "Deletion of shortcut" << name << "at path" << filePath << "for instance" << id << "has not been successful..."; + continue; + } + qDebug() << "Shortcut" << name << "at path" << filePath << "for instance" << id << "has been deleted by the launcher."; + } } static QMap getIdMapping(const QList& list) @@ -638,7 +689,7 @@ InstancePtr InstanceList::loadInstance(const InstanceId& id) } else { inst.reset(new NullInstance(m_globalSettings, instanceSettings, instanceRoot)); } - qDebug() << "Loaded instance " << inst->name() << " from " << inst->instanceRoot(); + qDebug() << "Loaded instance" << inst->name() << "from" << inst->instanceRoot(); return inst; } diff --git a/launcher/InstanceList.h b/launcher/InstanceList.h index c85fe55c7..fc4fa9a39 100644 --- a/launcher/InstanceList.h +++ b/launcher/InstanceList.h @@ -56,11 +56,17 @@ enum class InstCreateError { NoCreateError = 0, NoSuchVersion, UnknownCreateErro enum class GroupsState { NotLoaded, Steady, Dirty }; +struct TrashShortcutItem { + ShortcutData data; + QString trashPath; +}; + struct TrashHistoryItem { QString id; QString path; QString trashPath; QString groupName; + QList shortcuts; }; class InstanceList : public QAbstractListModel { @@ -111,8 +117,8 @@ class InstanceList : public QAbstractListModel { void deleteGroup(const GroupId& name); void renameGroup(const GroupId& src, const GroupId& dst); bool trashInstance(const InstanceId& id); - bool trashedSomething(); - void undoTrashInstance(); + bool trashedSomething() const; + bool undoTrashInstance(); void deleteInstance(const InstanceId& id); // Wrap an instance creation task in some more task machinery and make it ready to be used diff --git a/launcher/minecraft/ShortcutUtils.cpp b/launcher/minecraft/ShortcutUtils.cpp index 43954aa6a..0336a9512 100644 --- a/launcher/minecraft/ShortcutUtils.cpp +++ b/launcher/minecraft/ShortcutUtils.cpp @@ -48,10 +48,10 @@ namespace ShortcutUtils { -void createInstanceShortcut(const Shortcut& shortcut, const QString& filePath) +bool createInstanceShortcut(const Shortcut& shortcut, const QString& filePath) { if (!shortcut.instance) - return; + return false; QString appPath = QApplication::applicationFilePath(); auto icon = APPLICATION->icons()->icon(shortcut.iconKey.isEmpty() ? shortcut.instance->iconKey() : shortcut.iconKey); @@ -64,7 +64,7 @@ void createInstanceShortcut(const Shortcut& shortcut, const QString& filePath) if (appPath.startsWith("/private/var/")) { QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("The launcher is in the folder it was extracted from, therefore it cannot create shortcuts.")); - return; + return false; } iconPath = FS::PathCombine(shortcut.instance->instanceRoot(), "Icon.icns"); @@ -72,7 +72,7 @@ void createInstanceShortcut(const Shortcut& shortcut, const QString& filePath) QFile iconFile(iconPath); if (!iconFile.open(QFile::WriteOnly)) { QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for application.")); - return; + return false; } QIcon iconObj = icon->icon(); @@ -82,7 +82,7 @@ void createInstanceShortcut(const Shortcut& shortcut, const QString& filePath) if (!success) { iconFile.remove(); QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for application.")); - return; + return false; } #elif defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) if (appPath.startsWith("/tmp/.mount_")) { @@ -102,7 +102,7 @@ void createInstanceShortcut(const Shortcut& shortcut, const QString& filePath) QFile iconFile(iconPath); if (!iconFile.open(QFile::WriteOnly)) { QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for shortcut.")); - return; + return false; } bool success = icon->icon().pixmap(64, 64).save(&iconFile, "PNG"); iconFile.close(); @@ -110,7 +110,7 @@ void createInstanceShortcut(const Shortcut& shortcut, const QString& filePath) if (!success) { iconFile.remove(); QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for shortcut.")); - return; + return false; } if (DesktopServices::isFlatpak()) { @@ -128,7 +128,7 @@ void createInstanceShortcut(const Shortcut& shortcut, const QString& filePath) QFile iconFile(iconPath); if (!iconFile.open(QFile::WriteOnly)) { QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for shortcut.")); - return; + return false; } bool success = icon->icon().pixmap(64, 64).save(&iconFile, "ICO"); iconFile.close(); @@ -139,51 +139,58 @@ void createInstanceShortcut(const Shortcut& shortcut, const QString& filePath) if (!success) { iconFile.remove(); QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create icon for shortcut.")); - return; + return false; } #else QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Not supported on your platform!")); - return; + return false; #endif args.append({ "--launch", shortcut.instance->id() }); args.append(shortcut.extraArgs); - if (!FS::createShortcut(filePath, appPath, args, shortcut.name, iconPath)) { + QString shortcutPath = FS::createShortcut(filePath, appPath, args, shortcut.name, iconPath); + if (shortcutPath.isEmpty()) { #if not defined(Q_OS_MACOS) iconFile.remove(); #endif QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create %1 shortcut!").arg(shortcut.targetString)); + return false; } + + shortcut.instance->registerShortcut({ shortcut.name, shortcutPath, shortcut.target }); + return true; } -void createInstanceShortcutOnDesktop(const Shortcut& shortcut) +bool createInstanceShortcutOnDesktop(const Shortcut& shortcut) { if (!shortcut.instance) - return; + return false; QString desktopDir = FS::getDesktopDir(); if (desktopDir.isEmpty()) { QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Couldn't find desktop?!")); - return; + return false; } QString shortcutFilePath = FS::PathCombine(desktopDir, FS::RemoveInvalidFilenameChars(shortcut.name)); - createInstanceShortcut(shortcut, shortcutFilePath); + if (!createInstanceShortcut(shortcut, shortcutFilePath)) + return false; QMessageBox::information(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Created a shortcut to this %1 on your desktop!").arg(shortcut.targetString)); + return true; } -void createInstanceShortcutInApplications(const Shortcut& shortcut) +bool createInstanceShortcutInApplications(const Shortcut& shortcut) { if (!shortcut.instance) - return; + return false; QString applicationsDir = FS::getApplicationsDir(); if (applicationsDir.isEmpty()) { QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Couldn't find applications folder?!")); - return; + return false; } #if defined(Q_OS_MACOS) || defined(Q_OS_WIN) @@ -193,20 +200,22 @@ void createInstanceShortcutInApplications(const Shortcut& shortcut) if (!applicationsDirQ.mkpath(".")) { QMessageBox::critical(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Failed to create instances folder in applications folder!")); - return; + return false; } #endif QString shortcutFilePath = FS::PathCombine(applicationsDir, FS::RemoveInvalidFilenameChars(shortcut.name)); - createInstanceShortcut(shortcut, shortcutFilePath); + if (!createInstanceShortcut(shortcut, shortcutFilePath)) + return false; QMessageBox::information(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Created a shortcut to this %1 in your applications folder!").arg(shortcut.targetString)); + return true; } -void createInstanceShortcutInOther(const Shortcut& shortcut) +bool createInstanceShortcutInOther(const Shortcut& shortcut) { if (!shortcut.instance) - return; + return false; QString defaultedDir = FS::getDesktopDir(); #if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) @@ -225,13 +234,15 @@ void createInstanceShortcutInOther(const Shortcut& shortcut) shortcutFilePath = fileDialog.getSaveFileName(shortcut.parent, QObject::tr("Create Shortcut"), shortcutFilePath, QObject::tr("Desktop Entries") + " (*" + extension + ")"); if (shortcutFilePath.isEmpty()) - return; // file dialog canceled by user + return false; // file dialog canceled by user if (shortcutFilePath.endsWith(extension)) shortcutFilePath = shortcutFilePath.mid(0, shortcutFilePath.length() - extension.length()); - createInstanceShortcut(shortcut, shortcutFilePath); + if (!createInstanceShortcut(shortcut, shortcutFilePath)) + return false; QMessageBox::information(shortcut.parent, QObject::tr("Create Shortcut"), QObject::tr("Created a shortcut to this %1!").arg(shortcut.targetString)); + return true; } } // namespace ShortcutUtils diff --git a/launcher/minecraft/ShortcutUtils.h b/launcher/minecraft/ShortcutUtils.h index e3d2e283a..b995c36bd 100644 --- a/launcher/minecraft/ShortcutUtils.h +++ b/launcher/minecraft/ShortcutUtils.h @@ -38,6 +38,7 @@ #pragma once #include "Application.h" +#include #include namespace ShortcutUtils { @@ -49,18 +50,19 @@ struct Shortcut { QWidget* parent = nullptr; QStringList extraArgs = {}; QString iconKey = ""; + ShortcutTarget target; }; /// Create an instance shortcut on the specified file path -void createInstanceShortcut(const Shortcut& shortcut, const QString& filePath); +bool createInstanceShortcut(const Shortcut& shortcut, const QString& filePath); /// Create an instance shortcut on the desktop -void createInstanceShortcutOnDesktop(const Shortcut& shortcut); +bool createInstanceShortcutOnDesktop(const Shortcut& shortcut); /// Create an instance shortcut in the Applications directory -void createInstanceShortcutInApplications(const Shortcut& shortcut); +bool createInstanceShortcutInApplications(const Shortcut& shortcut); /// Create an instance shortcut in other directories -void createInstanceShortcutInOther(const Shortcut& shortcut); +bool createInstanceShortcutInOther(const Shortcut& shortcut); } // namespace ShortcutUtils diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 63fcc3c9b..4f38a53b3 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -1207,7 +1207,10 @@ void MainWindow::renameGroup(QString group) void MainWindow::undoTrashInstance() { - APPLICATION->instances()->undoTrashInstance(); + if (!APPLICATION->instances()->undoTrashInstance()) + QMessageBox::warning( + this, tr("Failed to undo trashing instance"), + tr("Some instances and shortcuts could not be restored.\nPlease check your trashbin to manually restore them.")); ui->actionUndoTrashInstance->setEnabled(APPLICATION->instances()->trashedSomething()); } @@ -1407,7 +1410,7 @@ void MainWindow::on_actionDeleteInstance_triggered() auto id = m_selectedInstance->id(); auto response = CustomMessageBox::selectable(this, tr("Confirm Deletion"), - tr("You are about to delete \"%1\".\n" + tr("You are about to delete \"%1\" and all of its shortcut(s).\n" "This may be permanent and will completely delete the instance.\n\n" "Are you sure?") .arg(m_selectedInstance->name()), diff --git a/launcher/ui/dialogs/CreateShortcutDialog.cpp b/launcher/ui/dialogs/CreateShortcutDialog.cpp index 278573a22..5cfe33c7f 100644 --- a/launcher/ui/dialogs/CreateShortcutDialog.cpp +++ b/launcher/ui/dialogs/CreateShortcutDialog.cpp @@ -83,12 +83,12 @@ CreateShortcutDialog::CreateShortcutDialog(InstancePtr instance, QWidget* parent QString applicationDir = FS::getApplicationsDir(); if (!desktopDir.isEmpty()) - ui->saveTargetSelectionBox->addItem(tr("Desktop"), QVariant::fromValue(SaveTarget::Desktop)); + ui->saveTargetSelectionBox->addItem(tr("Desktop"), QVariant::fromValue(ShortcutTarget::Desktop)); if (!applicationDir.isEmpty()) - ui->saveTargetSelectionBox->addItem(tr("Applications"), QVariant::fromValue(SaveTarget::Applications)); + ui->saveTargetSelectionBox->addItem(tr("Applications"), QVariant::fromValue(ShortcutTarget::Applications)); } - ui->saveTargetSelectionBox->addItem(tr("Other..."), QVariant::fromValue(SaveTarget::Other)); + ui->saveTargetSelectionBox->addItem(tr("Other..."), QVariant::fromValue(ShortcutTarget::Other)); // Populate worlds if (m_QuickJoinSupported) { @@ -206,17 +206,17 @@ void CreateShortcutDialog::createShortcut() } } - auto target = ui->saveTargetSelectionBox->currentData().value(); + auto target = ui->saveTargetSelectionBox->currentData().value(); auto name = ui->instNameTextBox->text(); if (name.isEmpty()) name = ui->instNameTextBox->placeholderText(); if (ui->overrideAccountCheckbox->isChecked()) extraArgs.append({ "--profile", ui->accountSelectionBox->currentData().toString() }); - ShortcutUtils::Shortcut args{ m_instance.get(), name, targetString, this, extraArgs, InstIconKey }; - if (target == SaveTarget::Desktop) + ShortcutUtils::Shortcut args{ m_instance.get(), name, targetString, this, extraArgs, InstIconKey, target }; + if (target == ShortcutTarget::Desktop) ShortcutUtils::createInstanceShortcutOnDesktop(args); - else if (target == SaveTarget::Applications) + else if (target == ShortcutTarget::Applications) ShortcutUtils::createInstanceShortcutInApplications(args); else ShortcutUtils::createInstanceShortcutInOther(args); diff --git a/launcher/ui/dialogs/CreateShortcutDialog.h b/launcher/ui/dialogs/CreateShortcutDialog.h index cfedbf017..29e68a787 100644 --- a/launcher/ui/dialogs/CreateShortcutDialog.h +++ b/launcher/ui/dialogs/CreateShortcutDialog.h @@ -54,9 +54,6 @@ class CreateShortcutDialog : public QDialog { InstancePtr m_instance; bool m_QuickJoinSupported = false; - // Index representations - enum class SaveTarget { Desktop, Applications, Other }; - // Functions void stateChanged(); }; From 48bc6ebcc2887c8694a3519e762c777763d82682 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 1 Jun 2025 00:33:52 +0000 Subject: [PATCH 265/695] chore(nix): update lockfile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'nixpkgs': 'github:NixOS/nixpkgs/063f43f2dbdef86376cc29ad646c45c46e93234c?narHash=sha256-6m1Y3/4pVw1RWTsrkAK2VMYSzG4MMIj7sqUy7o8th1o%3D' (2025-05-23) → 'github:NixOS/nixpkgs/96ec055edbe5ee227f28cdbc3f1ddf1df5965102?narHash=sha256-7doLyJBzCllvqX4gszYtmZUToxKvMUrg45EUWaUYmBg%3D' (2025-05-28) --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index a4061e8e3..2d2f820f4 100644 --- a/flake.lock +++ b/flake.lock @@ -18,11 +18,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1748026106, - "narHash": "sha256-6m1Y3/4pVw1RWTsrkAK2VMYSzG4MMIj7sqUy7o8th1o=", + "lastModified": 1748460289, + "narHash": "sha256-7doLyJBzCllvqX4gszYtmZUToxKvMUrg45EUWaUYmBg=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "063f43f2dbdef86376cc29ad646c45c46e93234c", + "rev": "96ec055edbe5ee227f28cdbc3f1ddf1df5965102", "type": "github" }, "original": { From 7c3a810d3d6bda55f002bf9224e4df945b9d86e4 Mon Sep 17 00:00:00 2001 From: Yihe Li Date: Sun, 1 Jun 2025 14:16:40 +0800 Subject: [PATCH 266/695] Implement persistence by storing shortcut in settings Signed-off-by: Yihe Li --- launcher/BaseInstance.cpp | 15 ++++++++++--- launcher/BaseInstance.h | 20 +++++++++++++---- launcher/InstanceList.cpp | 28 +++++++++++++++++------- launcher/minecraft/MinecraftInstance.cpp | 4 ++++ 4 files changed, 52 insertions(+), 15 deletions(-) diff --git a/launcher/BaseInstance.cpp b/launcher/BaseInstance.cpp index 1aa01568c..2187bc09a 100644 --- a/launcher/BaseInstance.cpp +++ b/launcher/BaseInstance.cpp @@ -69,6 +69,7 @@ BaseInstance::BaseInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr s m_settings->registerSetting("lastTimePlayed", 0); m_settings->registerSetting("linkedInstances", "[]"); + m_settings->registerSetting("shortcuts", QVariant::fromValue(QList{})); // Game time override auto gameTimeOverride = m_settings->registerSetting("OverrideGameTime", false); @@ -400,13 +401,21 @@ bool BaseInstance::syncInstanceDirName(const QString& newRoot) const void BaseInstance::registerShortcut(const ShortcutData& data) { - m_shortcuts.append(data); + auto currentShortcuts = shortcuts(); + currentShortcuts.append(data); qDebug() << "Registering shortcut for instance" << id() << "with name" << data.name << "and path" << data.filePath; + setShortcuts(currentShortcuts); } -QList& BaseInstance::getShortcuts() +void BaseInstance::setShortcuts(const QList& shortcuts) { - return m_shortcuts; + // FIXME: if no change, do not set. setting involves saving a file. + m_settings->set("shortcuts", QVariant::fromValue(shortcuts)); +} + +QList BaseInstance::shortcuts() const +{ + return m_settings->get("shortcuts").value>(); } QString BaseInstance::name() const diff --git a/launcher/BaseInstance.h b/launcher/BaseInstance.h index d2ff64e7e..52aa39067 100644 --- a/launcher/BaseInstance.h +++ b/launcher/BaseInstance.h @@ -38,6 +38,7 @@ #pragma once #include +#include #include #include #include @@ -74,7 +75,18 @@ enum class ShortcutTarget { Desktop, Applications, Other }; struct ShortcutData { QString name; QString filePath; - ShortcutTarget target; + ShortcutTarget target = ShortcutTarget::Other; + + friend QDataStream& operator<<(QDataStream& out, const ShortcutData& data) + { + out << data.name << data.filePath << data.target; + return out; + } + friend QDataStream& operator>>(QDataStream& in, ShortcutData& data) + { + in >> data.name >> data.filePath >> data.target; + return in; + } }; /*! @@ -142,7 +154,8 @@ class BaseInstance : public QObject, public std::enable_shared_from_this& getShortcuts(); + QList shortcuts() const; + void setShortcuts(const QList& shortcuts); /// Value used for instance window titles QString windowTitle() const; @@ -323,10 +336,9 @@ class BaseInstance : public QObject, public std::enable_shared_from_this m_shortcuts; }; Q_DECLARE_METATYPE(shared_qobject_ptr) +Q_DECLARE_METATYPE(ShortcutData) // Q_DECLARE_METATYPE(BaseInstance::InstanceFlag) // Q_DECLARE_OPERATORS_FOR_FLAGS(BaseInstance::InstanceFlags) diff --git a/launcher/InstanceList.cpp b/launcher/InstanceList.cpp index ef8be4919..b98f51d82 100644 --- a/launcher/InstanceList.cpp +++ b/launcher/InstanceList.cpp @@ -357,25 +357,20 @@ bool InstanceList::trashInstance(const InstanceId& id) m_trashHistory.push({ id, inst->instanceRoot(), trashedLoc, cachedGroupId }); // Also trash all of its shortcuts; we remove the shortcuts if trash fails since it is invalid anyway - auto& shortcuts = inst->getShortcuts(); - for (auto it = shortcuts.begin(); it != shortcuts.end();) { - const auto& [name, filePath, target] = *it; + for (const auto& [name, filePath, target] : inst->shortcuts()) { if (!FS::trash(filePath, &trashedLoc)) { qWarning() << "Trash of shortcut" << name << "at path" << filePath << "for instance" << id << "has not been successful, trying to delete it instead..."; if (!FS::deletePath(filePath)) { qWarning() << "Deletion of shortcut" << name << "at path" << filePath << "for instance" << id << "has not been successful, given up..."; - ++it; } else { qDebug() << "Shortcut" << name << "at path" << filePath << "for instance" << id << "has been deleted by the launcher."; - it = shortcuts.erase(it); } continue; } qDebug() << "Shortcut" << name << "at path" << filePath << "for instance" << id << "has been trashed by the launcher."; - m_trashHistory.top().shortcuts.append({ *it, trashedLoc }); - it = shortcuts.erase(it); + m_trashHistory.top().shortcuts.append({ { name, filePath, target }, trashedLoc }); } return true; @@ -453,7 +448,7 @@ void InstanceList::deleteInstance(const InstanceId& id) qDebug() << "Instance" << id << "has been deleted by the launcher."; - for (const auto& [name, filePath, target] : inst->getShortcuts()) { + for (const auto& [name, filePath, target] : inst->shortcuts()) { if (!FS::deletePath(filePath)) { qWarning() << "Deletion of shortcut" << name << "at path" << filePath << "for instance" << id << "has not been successful..."; continue; @@ -675,6 +670,7 @@ InstancePtr InstanceList::loadInstance(const InstanceId& id) } auto instanceRoot = FS::PathCombine(m_instDir, id); + qRegisterMetaType>("QList"); auto instanceSettings = std::make_shared(FS::PathCombine(instanceRoot, "instance.cfg")); InstancePtr inst; @@ -690,6 +686,22 @@ InstancePtr InstanceList::loadInstance(const InstanceId& id) inst.reset(new NullInstance(m_globalSettings, instanceSettings, instanceRoot)); } qDebug() << "Loaded instance" << inst->name() << "from" << inst->instanceRoot(); + + // Fixup the shortcuts by pruning all non-existing links + auto shortcut = inst->shortcuts(); + for (auto it = shortcut.begin(); it != shortcut.end();) { + const auto& [name, filePath, target] = *it; + if (!QDir(filePath).exists()) { + qWarning() << "Shortcut" << name << "have non-existent path" << filePath; + it = shortcut.erase(it); + continue; + } + ++it; + } + inst->setShortcuts(shortcut); + if (!shortcut.isEmpty()) + qDebug() << "Loaded" << shortcut.size() << "shortcut(s) for instance" << inst->name(); + return inst; } diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index 635cecfac..72c863bdb 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -1040,6 +1040,10 @@ QString MinecraftInstance::getStatusbarDescription() .arg(Time::prettifyDuration(totalTimePlayed(), APPLICATION->settings()->get("ShowGameTimeWithoutDays").toBool()))); } } + auto currentShortcuts = shortcuts(); + if (!currentShortcuts.isEmpty()) { + description.append(tr(", %n shortcut(s) registered", "", currentShortcuts.size())); + } if (hasCrashed()) { description.append(tr(", has crashed.")); } From d2ee023788341237dc6c195ab5facdf1b031315b Mon Sep 17 00:00:00 2001 From: Yihe Li Date: Sun, 1 Jun 2025 16:13:16 +0800 Subject: [PATCH 267/695] Switch to JSON-encoded store Signed-off-by: Yihe Li --- launcher/BaseInstance.cpp | 20 +++++++++++++++++--- launcher/BaseInstance.h | 12 ------------ launcher/InstanceList.cpp | 1 - 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/launcher/BaseInstance.cpp b/launcher/BaseInstance.cpp index 2187bc09a..8a6060948 100644 --- a/launcher/BaseInstance.cpp +++ b/launcher/BaseInstance.cpp @@ -69,7 +69,7 @@ BaseInstance::BaseInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr s m_settings->registerSetting("lastTimePlayed", 0); m_settings->registerSetting("linkedInstances", "[]"); - m_settings->registerSetting("shortcuts", QVariant::fromValue(QList{})); + m_settings->registerSetting("shortcuts", QVariant::fromValue(QByteArray{})); // Game time override auto gameTimeOverride = m_settings->registerSetting("OverrideGameTime", false); @@ -410,12 +410,26 @@ void BaseInstance::registerShortcut(const ShortcutData& data) void BaseInstance::setShortcuts(const QList& shortcuts) { // FIXME: if no change, do not set. setting involves saving a file. - m_settings->set("shortcuts", QVariant::fromValue(shortcuts)); + QJsonArray array; + for (const auto& elem : shortcuts) { + array.append(QJsonObject{ { "name", elem.name }, { "filePath", elem.filePath }, { "target", static_cast(elem.target) } }); + } + + QJsonDocument document; + document.setArray(array); + m_settings->set("shortcuts", QVariant::fromValue(document.toJson(QJsonDocument::Compact))); } QList BaseInstance::shortcuts() const { - return m_settings->get("shortcuts").value>(); + auto data = m_settings->get("shortcuts").value(); + auto document = QJsonDocument::fromJson(data); + QList results; + for (const auto& elem : document.array()) { + auto dict = elem.toObject(); + results.append({ dict["name"].toString(), dict["filePath"].toString(), static_cast(dict["target"].toInt()) }); + } + return results; } QString BaseInstance::name() const diff --git a/launcher/BaseInstance.h b/launcher/BaseInstance.h index 52aa39067..3509c0155 100644 --- a/launcher/BaseInstance.h +++ b/launcher/BaseInstance.h @@ -76,17 +76,6 @@ struct ShortcutData { QString name; QString filePath; ShortcutTarget target = ShortcutTarget::Other; - - friend QDataStream& operator<<(QDataStream& out, const ShortcutData& data) - { - out << data.name << data.filePath << data.target; - return out; - } - friend QDataStream& operator>>(QDataStream& in, ShortcutData& data) - { - in >> data.name >> data.filePath >> data.target; - return in; - } }; /*! @@ -339,6 +328,5 @@ class BaseInstance : public QObject, public std::enable_shared_from_this) -Q_DECLARE_METATYPE(ShortcutData) // Q_DECLARE_METATYPE(BaseInstance::InstanceFlag) // Q_DECLARE_OPERATORS_FOR_FLAGS(BaseInstance::InstanceFlags) diff --git a/launcher/InstanceList.cpp b/launcher/InstanceList.cpp index b98f51d82..f6512b25d 100644 --- a/launcher/InstanceList.cpp +++ b/launcher/InstanceList.cpp @@ -670,7 +670,6 @@ InstancePtr InstanceList::loadInstance(const InstanceId& id) } auto instanceRoot = FS::PathCombine(m_instDir, id); - qRegisterMetaType>("QList"); auto instanceSettings = std::make_shared(FS::PathCombine(instanceRoot, "instance.cfg")); InstancePtr inst; From 1c69f6335747ebec171245c62340eaa4002b1e66 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Sun, 1 Jun 2025 09:15:21 +0100 Subject: [PATCH 268/695] Hopefully fix segfault with HintOverrideProxyStyle Signed-off-by: TheKodeToad --- launcher/ui/themes/HintOverrideProxyStyle.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/themes/HintOverrideProxyStyle.cpp b/launcher/ui/themes/HintOverrideProxyStyle.cpp index f5b8232a8..1a044ae70 100644 --- a/launcher/ui/themes/HintOverrideProxyStyle.cpp +++ b/launcher/ui/themes/HintOverrideProxyStyle.cpp @@ -20,7 +20,7 @@ HintOverrideProxyStyle::HintOverrideProxyStyle(QStyle* style) : QProxyStyle(style) { - setObjectName(style->objectName()); + setObjectName(baseStyle()->objectName()); } int HintOverrideProxyStyle::styleHint(QStyle::StyleHint hint, From 4214571cffaf0754a90b208c196a549e9bef7f95 Mon Sep 17 00:00:00 2001 From: Yihe Li Date: Sun, 1 Jun 2025 16:18:35 +0800 Subject: [PATCH 269/695] Add # of shortcuts to deletion confirmation screen instead Signed-off-by: Yihe Li --- launcher/minecraft/MinecraftInstance.cpp | 4 ---- launcher/ui/MainWindow.cpp | 8 ++++++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index 72c863bdb..635cecfac 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -1040,10 +1040,6 @@ QString MinecraftInstance::getStatusbarDescription() .arg(Time::prettifyDuration(totalTimePlayed(), APPLICATION->settings()->get("ShowGameTimeWithoutDays").toBool()))); } } - auto currentShortcuts = shortcuts(); - if (!currentShortcuts.isEmpty()) { - description.append(tr(", %n shortcut(s) registered", "", currentShortcuts.size())); - } if (hasCrashed()) { description.append(tr(", has crashed.")); } diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 4f38a53b3..88724d788 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -1409,11 +1409,15 @@ void MainWindow::on_actionDeleteInstance_triggered() } auto id = m_selectedInstance->id(); + QString shortcutStr; + auto shortcuts = m_selectedInstance->shortcuts(); + if (!shortcuts.isEmpty()) + shortcutStr = tr(" and its %n registered shortcut(s)", "", shortcuts.size()); auto response = CustomMessageBox::selectable(this, tr("Confirm Deletion"), - tr("You are about to delete \"%1\" and all of its shortcut(s).\n" + tr("You are about to delete \"%1\"%2.\n" "This may be permanent and will completely delete the instance.\n\n" "Are you sure?") - .arg(m_selectedInstance->name()), + .arg(m_selectedInstance->name(), shortcutStr), QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) ->exec(); From d9884f0d0365a19f7d41792aed6d57540f7fa853 Mon Sep 17 00:00:00 2001 From: Yihe Li Date: Sun, 1 Jun 2025 16:33:43 +0800 Subject: [PATCH 270/695] Add notes in shortcut creation screen Signed-off-by: Yihe Li --- launcher/ui/dialogs/CreateShortcutDialog.ui | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/launcher/ui/dialogs/CreateShortcutDialog.ui b/launcher/ui/dialogs/CreateShortcutDialog.ui index 9e2bdd747..24d4dc2dc 100644 --- a/launcher/ui/dialogs/CreateShortcutDialog.ui +++ b/launcher/ui/dialogs/CreateShortcutDialog.ui @@ -194,6 +194,20 @@ + + + + Note: If a shortcut is moved after creation, it won't be deleted when deleting the instance. + + + + + + + You'll need to delete them manually if that is the case. + + + From caff4b884f7037f646bedac59da01fcd4340525e Mon Sep 17 00:00:00 2001 From: Yihe Li Date: Sun, 1 Jun 2025 18:31:27 +0800 Subject: [PATCH 271/695] Use null QString instead of empty Signed-off-by: Yihe Li --- launcher/FileSystem.cpp | 24 ++++++++++++------------ launcher/FileSystem.h | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp index c5386a43b..5136e7954 100644 --- a/launcher/FileSystem.cpp +++ b/launcher/FileSystem.cpp @@ -905,19 +905,19 @@ QString createShortcut(QString destination, QString target, QStringList args, QS } if (!ensureFilePathExists(destination)) { qWarning() << "Destination path can't be created!"; - return ""; + return QString(); } #if defined(Q_OS_MACOS) QDir application = destination + ".app/"; if (application.exists()) { qWarning() << "Application already exists!"; - return ""; + return QString(); } if (!application.mkpath(".")) { qWarning() << "Couldn't create application"; - return ""; + return QString(); } QDir content = application.path() + "/Contents/"; @@ -927,7 +927,7 @@ QString createShortcut(QString destination, QString target, QStringList args, QS if (!(content.mkpath(".") && resources.mkpath(".") && binaryDir.mkpath("."))) { qWarning() << "Couldn't create directories within application"; - return ""; + return QString(); } info.open(QIODevice::WriteOnly | QIODevice::Text); @@ -1008,26 +1008,26 @@ QString createShortcut(QString destination, QString target, QStringList args, QS if (!targetInfo.exists()) { qWarning() << "Target file does not exist!"; - return ""; + return QString(); } target = targetInfo.absoluteFilePath(); if (target.length() >= MAX_PATH) { qWarning() << "Target file path is too long!"; - return ""; + return QString(); } if (!icon.isEmpty() && icon.length() >= MAX_PATH) { qWarning() << "Icon path is too long!"; - return ""; + return QString(); } destination += ".lnk"; if (destination.length() >= MAX_PATH) { qWarning() << "Destination path is too long!"; - return ""; + return QString(); } QString argStr; @@ -1046,7 +1046,7 @@ QString createShortcut(QString destination, QString target, QStringList args, QS if (argStr.length() >= MAX_PATH) { qWarning() << "Arguments string is too long!"; - return ""; + return QString(); } HRESULT hres; @@ -1055,7 +1055,7 @@ QString createShortcut(QString destination, QString target, QStringList args, QS hres = CoInitialize(nullptr); if (FAILED(hres)) { qWarning() << "Failed to initialize COM!"; - return ""; + return QString(); } WCHAR wsz[MAX_PATH]; @@ -1111,10 +1111,10 @@ QString createShortcut(QString destination, QString target, QStringList args, QS if (SUCCEEDED(hres)) return destination; - return ""; + return QString(); #else qWarning("Desktop Shortcuts not supported on your platform!"); - return ""; + return QString(); #endif } diff --git a/launcher/FileSystem.h b/launcher/FileSystem.h index 83cf41d7f..0e573a09e 100644 --- a/launcher/FileSystem.h +++ b/launcher/FileSystem.h @@ -362,7 +362,7 @@ bool overrideFolder(QString overwritten_path, QString override_path); /** * Creates a shortcut to the specified target file at the specified destination path. - * Returns empty string if creation failed; otherwise returns the path to the created shortcut. + * Returns null QString if creation failed; otherwise returns the path to the created shortcut. */ QString createShortcut(QString destination, QString target, QStringList args, QString name, QString icon); From 49dc9a5d3f76ce36e779805cb38c89b6b5efc3d5 Mon Sep 17 00:00:00 2001 From: Puqns67 Date: Mon, 2 Jun 2025 00:56:07 +0800 Subject: [PATCH 272/695] chore(deps): try find system wide qrcodegencpp-cmake and use it Signed-off-by: Puqns67 --- CMakeLists.txt | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ce3d433fb..e3d60a102 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -338,6 +338,9 @@ if(NOT Launcher_FORCE_BUNDLED_LIBS) # Find cmark find_package(cmark QUIET) + + # Find qrcodegencpp-cmake + find_package(qrcodegencpp QUIET) endif() include(ECMQtDeclareLoggingCategory) @@ -528,19 +531,22 @@ if(NOT cmark_FOUND) else() message(STATUS "Using system cmark") endif() +if(NOT qrcodegencpp_FOUND) + set(QRCODE_SOURCES + libraries/qrcodegenerator/cpp/qrcodegen.cpp + libraries/qrcodegenerator/cpp/qrcodegen.hpp + ) + add_library(qrcodegenerator STATIC ${QRCODE_SOURCES}) + target_include_directories(qrcodegenerator PUBLIC "libraries/qrcodegenerator/cpp/" ) + generate_export_header(qrcodegenerator) +else() + add_library(qrcodegenerator ALIAS qrcodegencpp::qrcodegencpp) + message(STATUS "Using system qrcodegencpp-cmake") +endif() add_subdirectory(libraries/gamemode) add_subdirectory(libraries/murmur2) # Hash for usage with the CurseForge API add_subdirectory(libraries/qdcss) # css parser -# qr code generator -set(QRCODE_SOURCES - libraries/qrcodegenerator/cpp/qrcodegen.cpp - libraries/qrcodegenerator/cpp/qrcodegen.hpp -) -add_library(qrcodegenerator STATIC ${QRCODE_SOURCES}) -target_include_directories(qrcodegenerator PUBLIC "libraries/qrcodegenerator/cpp/" ) -generate_export_header(qrcodegenerator) - ############################### Built Artifacts ############################### add_subdirectory(buildconfig) From 0a1001ee84db35e0d58c5caac7a0c9bbea738932 Mon Sep 17 00:00:00 2001 From: Yihe Li Date: Mon, 2 Jun 2025 01:34:24 +0800 Subject: [PATCH 273/695] Use QString instead of QByteArray Signed-off-by: Yihe Li --- launcher/BaseInstance.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/launcher/BaseInstance.cpp b/launcher/BaseInstance.cpp index 8a6060948..3b4612c73 100644 --- a/launcher/BaseInstance.cpp +++ b/launcher/BaseInstance.cpp @@ -69,7 +69,7 @@ BaseInstance::BaseInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr s m_settings->registerSetting("lastTimePlayed", 0); m_settings->registerSetting("linkedInstances", "[]"); - m_settings->registerSetting("shortcuts", QVariant::fromValue(QByteArray{})); + m_settings->registerSetting("shortcuts", QString()); // Game time override auto gameTimeOverride = m_settings->registerSetting("OverrideGameTime", false); @@ -417,12 +417,12 @@ void BaseInstance::setShortcuts(const QList& shortcuts) QJsonDocument document; document.setArray(array); - m_settings->set("shortcuts", QVariant::fromValue(document.toJson(QJsonDocument::Compact))); + m_settings->set("shortcuts", QString::fromUtf8(document.toJson(QJsonDocument::Compact))); } QList BaseInstance::shortcuts() const { - auto data = m_settings->get("shortcuts").value(); + auto data = m_settings->get("shortcuts").toString().toUtf8(); auto document = QJsonDocument::fromJson(data); QList results; for (const auto& elem : document.array()) { From b54dd051fbbd2f06106420a28c132ab26a16b774 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Sun, 1 Jun 2025 19:24:21 +0000 Subject: [PATCH 274/695] Fix close button on world datapacks dialog Signed-off-by: TheKodeToad --- launcher/ui/pages/instance/WorldListPage.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/pages/instance/WorldListPage.cpp b/launcher/ui/pages/instance/WorldListPage.cpp index de71eb8fb..6c10413e4 100644 --- a/launcher/ui/pages/instance/WorldListPage.cpp +++ b/launcher/ui/pages/instance/WorldListPage.cpp @@ -255,7 +255,7 @@ void WorldListPage::on_actionData_Packs_triggered() layout->addWidget(pageContainer); auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Close | QDialogButtonBox::Help); - connect(buttonBox, &QDialogButtonBox::accepted, dialog, &QDialog::accept); + connect(buttonBox, &QDialogButtonBox::rejected, dialog, &QDialog::reject); connect(buttonBox, &QDialogButtonBox::helpRequested, pageContainer, &PageContainer::help); layout->addWidget(buttonBox); From 50fb2db7184344b87ea0d0070d374e90361f6721 Mon Sep 17 00:00:00 2001 From: Yihe Li Date: Mon, 2 Jun 2025 07:36:53 +0800 Subject: [PATCH 275/695] Validate JSON parsing results Signed-off-by: Yihe Li --- launcher/BaseInstance.cpp | 14 +++++++++++++- launcher/InstanceList.cpp | 11 ----------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/launcher/BaseInstance.cpp b/launcher/BaseInstance.cpp index 3b4612c73..aa7812649 100644 --- a/launcher/BaseInstance.cpp +++ b/launcher/BaseInstance.cpp @@ -427,7 +427,19 @@ QList BaseInstance::shortcuts() const QList results; for (const auto& elem : document.array()) { auto dict = elem.toObject(); - results.append({ dict["name"].toString(), dict["filePath"].toString(), static_cast(dict["target"].toInt()) }); + if (!dict.contains("name") || !dict.contains("filePath") || !dict.contains("target")) + return {}; + int value = dict["target"].toInt(-1); + if (!dict["name"].isString() || !dict["filePath"].isString() || value < 0 || value >= 3) + return {}; + + QString shortcutName = dict["name"].toString(); + QString filePath = dict["filePath"].toString(); + if (!QDir(filePath).exists()) { + qWarning() << "Shortcut" << shortcutName << "for instance" << name() << "have non-existent path" << filePath; + continue; + } + results.append({ shortcutName, filePath, static_cast(value) }); } return results; } diff --git a/launcher/InstanceList.cpp b/launcher/InstanceList.cpp index f6512b25d..de94db7c3 100644 --- a/launcher/InstanceList.cpp +++ b/launcher/InstanceList.cpp @@ -686,18 +686,7 @@ InstancePtr InstanceList::loadInstance(const InstanceId& id) } qDebug() << "Loaded instance" << inst->name() << "from" << inst->instanceRoot(); - // Fixup the shortcuts by pruning all non-existing links auto shortcut = inst->shortcuts(); - for (auto it = shortcut.begin(); it != shortcut.end();) { - const auto& [name, filePath, target] = *it; - if (!QDir(filePath).exists()) { - qWarning() << "Shortcut" << name << "have non-existent path" << filePath; - it = shortcut.erase(it); - continue; - } - ++it; - } - inst->setShortcuts(shortcut); if (!shortcut.isEmpty()) qDebug() << "Loaded" << shortcut.size() << "shortcut(s) for instance" << inst->name(); From 8965200384936ffb2cc5f5d4fe3c7705bf286413 Mon Sep 17 00:00:00 2001 From: Yihe Li Date: Mon, 2 Jun 2025 14:15:30 +0800 Subject: [PATCH 276/695] Handle JSON parse error Signed-off-by: Yihe Li --- launcher/BaseInstance.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/launcher/BaseInstance.cpp b/launcher/BaseInstance.cpp index aa7812649..096052a45 100644 --- a/launcher/BaseInstance.cpp +++ b/launcher/BaseInstance.cpp @@ -423,15 +423,21 @@ void BaseInstance::setShortcuts(const QList& shortcuts) QList BaseInstance::shortcuts() const { auto data = m_settings->get("shortcuts").toString().toUtf8(); - auto document = QJsonDocument::fromJson(data); + QJsonParseError parseError; + auto document = QJsonDocument::fromJson(data, &parseError); + if (parseError.error != QJsonParseError::NoError || !document.isArray()) + return {}; + QList results; for (const auto& elem : document.array()) { + if (!elem.isObject()) + continue; auto dict = elem.toObject(); if (!dict.contains("name") || !dict.contains("filePath") || !dict.contains("target")) - return {}; + continue; int value = dict["target"].toInt(-1); if (!dict["name"].isString() || !dict["filePath"].isString() || value < 0 || value >= 3) - return {}; + continue; QString shortcutName = dict["name"].toString(); QString filePath = dict["filePath"].toString(); From 492769aea6e78087e5e3f2524884ba3c9f2b59e1 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Fri, 18 Apr 2025 19:59:35 +0300 Subject: [PATCH 277/695] feat: add setting to control the loaders for mod search Signed-off-by: Trial97 --- launcher/minecraft/MinecraftInstance.cpp | 4 ++ .../ui/widgets/MinecraftSettingsWidget.cpp | 61 +++++++++++++++- launcher/ui/widgets/MinecraftSettingsWidget.h | 2 + .../ui/widgets/MinecraftSettingsWidget.ui | 70 +++++++++++++++++-- launcher/ui/widgets/ModFilterWidget.cpp | 9 ++- 5 files changed, 136 insertions(+), 10 deletions(-) diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index 635cecfac..90c066fe3 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -250,6 +250,10 @@ void MinecraftInstance::loadSpecificSettings() m_settings->registerSetting("ExportOptionalFiles", true); m_settings->registerSetting("ExportRecommendedRAM"); + // Join server on launch, this does not have a global override + m_settings->registerSetting("OverrideModDownloadLoaders", false); + m_settings->registerSetting("ModDownloadLoaders", QStringList()); + qDebug() << "Instance-type specific settings were loaded!"; setSpecificSettingsLoaded(true); diff --git a/launcher/ui/widgets/MinecraftSettingsWidget.cpp b/launcher/ui/widgets/MinecraftSettingsWidget.cpp index c3d342d42..637e34db7 100644 --- a/launcher/ui/widgets/MinecraftSettingsWidget.cpp +++ b/launcher/ui/widgets/MinecraftSettingsWidget.cpp @@ -39,6 +39,7 @@ #include "Application.h" #include "BuildConfig.h" +#include "minecraft/PackProfile.h" #include "minecraft/WorldList.h" #include "minecraft/auth/AccountList.h" #include "settings/Setting.h" @@ -55,6 +56,7 @@ MinecraftSettingsWidget::MinecraftSettingsWidget(MinecraftInstancePtr instance, m_ui->openGlobalSettingsButton->setVisible(false); m_ui->instanceAccountGroupBox->hide(); m_ui->serverJoinGroupBox->hide(); + m_ui->loaderGroup->hide(); } else { m_javaSettings = new JavaSettingsWidget(m_instance, this); m_ui->javaScrollArea->setWidget(m_javaSettings); @@ -93,6 +95,17 @@ MinecraftSettingsWidget::MinecraftSettingsWidget(MinecraftInstancePtr instance, connect(m_ui->openGlobalSettingsButton, &QCommandLinkButton::clicked, this, &MinecraftSettingsWidget::openGlobalSettings); connect(m_ui->serverJoinAddressButton, &QAbstractButton::toggled, m_ui->serverJoinAddress, &QWidget::setEnabled); connect(m_ui->worldJoinButton, &QAbstractButton::toggled, m_ui->worldsCb, &QWidget::setEnabled); + + connect(m_ui->loaderGroup, &QGroupBox::toggled, this, [this](bool value) { + m_instance->settings()->set("OverrideModDownloadLoaders", value); + if (!value) + m_instance->settings()->reset("ModDownloadLoaders"); + }); + connect(m_ui->neoForge, &QCheckBox::stateChanged, this, &MinecraftSettingsWidget::selectedLoadersChanged); + connect(m_ui->forge, &QCheckBox::stateChanged, this, &MinecraftSettingsWidget::selectedLoadersChanged); + connect(m_ui->fabric, &QCheckBox::stateChanged, this, &MinecraftSettingsWidget::selectedLoadersChanged); + connect(m_ui->quilt, &QCheckBox::stateChanged, this, &MinecraftSettingsWidget::selectedLoadersChanged); + connect(m_ui->liteLoader, &QCheckBox::stateChanged, this, &MinecraftSettingsWidget::selectedLoadersChanged); } m_ui->maximizedWarning->hide(); @@ -220,6 +233,35 @@ void MinecraftSettingsWidget::loadSettings() m_ui->instanceAccountGroupBox->setChecked(settings->get("UseAccountForInstance").toBool()); updateAccountsMenu(*settings); + + m_ui->loaderGroup->blockSignals(true); + m_ui->neoForge->blockSignals(true); + m_ui->forge->blockSignals(true); + m_ui->fabric->blockSignals(true); + m_ui->quilt->blockSignals(true); + m_ui->liteLoader->blockSignals(true); + auto instLoaders = m_instance->getPackProfile()->getSupportedModLoaders().value(); + m_ui->loaderGroup->setChecked(settings->get("OverrideModDownloadLoaders").toBool()); + auto loaders = settings->get("ModDownloadLoaders").toStringList(); + if (loaders.isEmpty()) { + m_ui->neoForge->setChecked(instLoaders & ModPlatform::NeoForge); + m_ui->forge->setChecked(instLoaders & ModPlatform::Forge); + m_ui->fabric->setChecked(instLoaders & ModPlatform::Fabric); + m_ui->quilt->setChecked(instLoaders & ModPlatform::Quilt); + m_ui->liteLoader->setChecked(instLoaders & ModPlatform::LiteLoader); + } else { + m_ui->neoForge->setChecked(loaders.contains(getModLoaderAsString(ModPlatform::NeoForge))); + m_ui->forge->setChecked(loaders.contains(getModLoaderAsString(ModPlatform::Forge))); + m_ui->fabric->setChecked(loaders.contains(getModLoaderAsString(ModPlatform::Fabric))); + m_ui->quilt->setChecked(loaders.contains(getModLoaderAsString(ModPlatform::Quilt))); + m_ui->liteLoader->setChecked(loaders.contains(getModLoaderAsString(ModPlatform::LiteLoader))); + } + m_ui->loaderGroup->blockSignals(false); + m_ui->neoForge->blockSignals(false); + m_ui->forge->blockSignals(false); + m_ui->fabric->blockSignals(false); + m_ui->quilt->blockSignals(false); + m_ui->liteLoader->blockSignals(false); } m_ui->legacySettingsGroupBox->setChecked(settings->get("OverrideLegacySettings").toBool()); @@ -238,7 +280,6 @@ void MinecraftSettingsWidget::saveSettings() { SettingsObject::Lock lock(settings); - // Console bool console = m_instance == nullptr || m_ui->consoleSettingsBox->isChecked(); @@ -267,7 +308,7 @@ void MinecraftSettingsWidget::saveSettings() settings->set("LaunchMaximized", m_ui->maximizedCheckBox->isChecked()); settings->set("MinecraftWinWidth", m_ui->windowWidthSpinBox->value()); settings->set("MinecraftWinHeight", m_ui->windowHeightSpinBox->value()); - settings->set("CloseAfterLaunch", m_ui->closeAfterLaunchCheck->isChecked()); + settings->set("CloseAfterLaunch", m_ui->closeAfterLaunchCheck->isChecked()); settings->set("QuitAfterGameStop", m_ui->quitAfterGameStopCheck->isChecked()); } else { settings->reset("LaunchMaximized"); @@ -444,3 +485,19 @@ bool MinecraftSettingsWidget::isQuickPlaySupported() { return m_instance->traits().contains("feature:is_quick_play_singleplayer"); } + +void MinecraftSettingsWidget::selectedLoadersChanged() +{ + QStringList loaders; + if (m_ui->neoForge->isChecked()) + loaders << getModLoaderAsString(ModPlatform::NeoForge); + if (m_ui->forge->isChecked()) + loaders << getModLoaderAsString(ModPlatform::Forge); + if (m_ui->fabric->isChecked()) + loaders << getModLoaderAsString(ModPlatform::Fabric); + if (m_ui->quilt->isChecked()) + loaders << getModLoaderAsString(ModPlatform::Quilt); + if (m_ui->liteLoader->isChecked()) + loaders << getModLoaderAsString(ModPlatform::LiteLoader); + m_instance->settings()->set("ModDownloadLoaders", loaders); +} \ No newline at end of file diff --git a/launcher/ui/widgets/MinecraftSettingsWidget.h b/launcher/ui/widgets/MinecraftSettingsWidget.h index 86effb337..6be73375e 100644 --- a/launcher/ui/widgets/MinecraftSettingsWidget.h +++ b/launcher/ui/widgets/MinecraftSettingsWidget.h @@ -56,6 +56,8 @@ class MinecraftSettingsWidget : public QWidget { void openGlobalSettings(); void updateAccountsMenu(const SettingsObject& settings); bool isQuickPlaySupported(); + private slots: + void selectedLoadersChanged(); MinecraftInstancePtr m_instance; Ui::MinecraftSettingsWidget* m_ui; diff --git a/launcher/ui/widgets/MinecraftSettingsWidget.ui b/launcher/ui/widgets/MinecraftSettingsWidget.ui index ed12604fd..15406873a 100644 --- a/launcher/ui/widgets/MinecraftSettingsWidget.ui +++ b/launcher/ui/widgets/MinecraftSettingsWidget.ui @@ -58,9 +58,9 @@ 0 - -207 - 603 - 694 + -537 + 623 + 1007 @@ -394,11 +394,67 @@ Qt::Horizontal + + + 0 + 0 + + + + + + Override Mod Download &Loaders + + + true + + + false + + + + + + NeoForge + + + + + + + Forge + + + + + + + Fabric + + + + + + + Quilt + + + + + + + LiteLoader + + + + + + @@ -433,8 +489,8 @@ 0 0 - 624 - 487 + 98 + 28 @@ -457,8 +513,8 @@ 0 0 - 624 - 487 + 299 + 499 diff --git a/launcher/ui/widgets/ModFilterWidget.cpp b/launcher/ui/widgets/ModFilterWidget.cpp index da41b990a..699f5f7d6 100644 --- a/launcher/ui/widgets/ModFilterWidget.cpp +++ b/launcher/ui/widgets/ModFilterWidget.cpp @@ -218,7 +218,14 @@ void ModFilterWidget::prepareBasicFilter() if (m_instance) { m_filter->hideInstalled = false; m_filter->side = ""; // or "both" - auto loaders = m_instance->getPackProfile()->getSupportedModLoaders().value(); + ModPlatform::ModLoaderTypes loaders; + if (m_instance->settings()->get("OverrideModDownloadLoaders").toBool()) { + for (auto loader : m_instance->settings()->get("ModDownloadLoaders").toStringList()) { + loaders |= ModPlatform::getModLoaderFromString(loader); + } + } else { + loaders = m_instance->getPackProfile()->getSupportedModLoaders().value(); + } ui->neoForge->setChecked(loaders & ModPlatform::NeoForge); ui->forge->setChecked(loaders & ModPlatform::Forge); ui->fabric->setChecked(loaders & ModPlatform::Fabric); From ca549714991be1888ed00a26dfdf19d49aede977 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Mon, 2 Jun 2025 09:29:37 +0300 Subject: [PATCH 278/695] chore: ensure the setting is saved as string Signed-off-by: Trial97 --- launcher/Json.cpp | 25 +++++++++++++++++++ launcher/Json.h | 6 ++++- launcher/minecraft/MinecraftInstance.cpp | 2 +- .../ui/widgets/MinecraftSettingsWidget.cpp | 5 ++-- launcher/ui/widgets/ModFilterWidget.cpp | 3 ++- 5 files changed, 36 insertions(+), 5 deletions(-) diff --git a/launcher/Json.cpp b/launcher/Json.cpp index f397f89c5..8623eb2a8 100644 --- a/launcher/Json.cpp +++ b/launcher/Json.cpp @@ -279,4 +279,29 @@ QJsonValue requireIsType(const QJsonValue& value, const QString& wha return value; } +QStringList toStringList(const QString& jsonString) +{ + QJsonParseError parseError; + QJsonDocument doc = QJsonDocument::fromJson(jsonString.toUtf8(), &parseError); + + if (parseError.error != QJsonParseError::NoError || !doc.isArray()) + return {}; + try { + return ensureIsArrayOf(doc.array(), ""); + } catch (Json::JsonException& e) { + return {}; + } +} + +QString fromStringList(const QStringList& list) +{ + QJsonArray array; + for (const QString& str : list) { + array.append(str); + } + + QJsonDocument doc(toJsonArray(list)); + return QString::fromUtf8(doc.toJson(QJsonDocument::Compact)); +} + } // namespace Json diff --git a/launcher/Json.h b/launcher/Json.h index c13be6470..509a41fd5 100644 --- a/launcher/Json.h +++ b/launcher/Json.h @@ -99,7 +99,7 @@ template QJsonArray toJsonArray(const QList& container) { QJsonArray array; - for (const T item : container) { + for (const T& item : container) { array.append(toJson(item)); } return array; @@ -278,5 +278,9 @@ JSON_HELPERFUNCTIONS(Variant, QVariant) #undef JSON_HELPERFUNCTIONS +// helper functions for settings +QStringList toStringList(const QString& jsonString); +QString fromStringList(const QStringList& list); + } // namespace Json using JSONValidationError = Json::JsonException; diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index 90c066fe3..fafe7bd37 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -252,7 +252,7 @@ void MinecraftInstance::loadSpecificSettings() // Join server on launch, this does not have a global override m_settings->registerSetting("OverrideModDownloadLoaders", false); - m_settings->registerSetting("ModDownloadLoaders", QStringList()); + m_settings->registerSetting("ModDownloadLoaders", "[]"); qDebug() << "Instance-type specific settings were loaded!"; diff --git a/launcher/ui/widgets/MinecraftSettingsWidget.cpp b/launcher/ui/widgets/MinecraftSettingsWidget.cpp index 637e34db7..610dc143b 100644 --- a/launcher/ui/widgets/MinecraftSettingsWidget.cpp +++ b/launcher/ui/widgets/MinecraftSettingsWidget.cpp @@ -39,6 +39,7 @@ #include "Application.h" #include "BuildConfig.h" +#include "Json.h" #include "minecraft/PackProfile.h" #include "minecraft/WorldList.h" #include "minecraft/auth/AccountList.h" @@ -242,7 +243,7 @@ void MinecraftSettingsWidget::loadSettings() m_ui->liteLoader->blockSignals(true); auto instLoaders = m_instance->getPackProfile()->getSupportedModLoaders().value(); m_ui->loaderGroup->setChecked(settings->get("OverrideModDownloadLoaders").toBool()); - auto loaders = settings->get("ModDownloadLoaders").toStringList(); + auto loaders = Json::toStringList(settings->get("ModDownloadLoaders").toString()); if (loaders.isEmpty()) { m_ui->neoForge->setChecked(instLoaders & ModPlatform::NeoForge); m_ui->forge->setChecked(instLoaders & ModPlatform::Forge); @@ -499,5 +500,5 @@ void MinecraftSettingsWidget::selectedLoadersChanged() loaders << getModLoaderAsString(ModPlatform::Quilt); if (m_ui->liteLoader->isChecked()) loaders << getModLoaderAsString(ModPlatform::LiteLoader); - m_instance->settings()->set("ModDownloadLoaders", loaders); + m_instance->settings()->set("ModDownloadLoaders", Json::fromStringList(loaders)); } \ No newline at end of file diff --git a/launcher/ui/widgets/ModFilterWidget.cpp b/launcher/ui/widgets/ModFilterWidget.cpp index 699f5f7d6..031ff0f94 100644 --- a/launcher/ui/widgets/ModFilterWidget.cpp +++ b/launcher/ui/widgets/ModFilterWidget.cpp @@ -40,6 +40,7 @@ #include #include #include "BaseVersionList.h" +#include "Json.h" #include "Version.h" #include "meta/Index.h" #include "modplatform/ModIndex.h" @@ -220,7 +221,7 @@ void ModFilterWidget::prepareBasicFilter() m_filter->side = ""; // or "both" ModPlatform::ModLoaderTypes loaders; if (m_instance->settings()->get("OverrideModDownloadLoaders").toBool()) { - for (auto loader : m_instance->settings()->get("ModDownloadLoaders").toStringList()) { + for (auto loader : Json::toStringList(m_instance->settings()->get("ModDownloadLoaders").toString())) { loaders |= ModPlatform::getModLoaderFromString(loader); } } else { From af0176b12a53209d45351aa0a6237d34f2e5cd77 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Mon, 2 Jun 2025 09:34:23 +0100 Subject: [PATCH 279/695] Allow data packs on vanilla instances Signed-off-by: TheKodeToad --- launcher/ui/pages/instance/DataPackPage.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/launcher/ui/pages/instance/DataPackPage.cpp b/launcher/ui/pages/instance/DataPackPage.cpp index 2fc4ec31d..a56cc9b79 100644 --- a/launcher/ui/pages/instance/DataPackPage.cpp +++ b/launcher/ui/pages/instance/DataPackPage.cpp @@ -68,10 +68,6 @@ void DataPackPage::downloadDataPacks() return; // this is a null instance or a legacy instance auto profile = static_cast(m_instance)->getPackProfile(); - if (!profile->getModLoaders().has_value()) { - QMessageBox::critical(this, tr("Error"), tr("Please install a mod loader first!")); - return; - } m_downloadDialog = new ResourceDownload::DataPackDownloadDialog(this, m_model, m_instance); connect(this, &QObject::destroyed, m_downloadDialog, &QDialog::close); From 6177fa014885515bccc38a9c9f917fa60c1fd4ab Mon Sep 17 00:00:00 2001 From: Trial97 Date: Wed, 7 May 2025 20:22:41 +0300 Subject: [PATCH 280/695] feat: make resource header config global Signed-off-by: Trial97 --- launcher/Application.cpp | 2 + launcher/minecraft/MinecraftInstance.cpp | 3 + launcher/minecraft/mod/ModFolderModel.cpp | 1 - .../minecraft/mod/ResourceFolderModel.cpp | 68 ++++++++++++++++--- launcher/minecraft/mod/ResourceFolderModel.h | 1 - .../minecraft/mod/TexturePackFolderModel.cpp | 1 - launcher/ui/MainWindow.cpp | 4 +- .../pages/instance/ExternalResourcesPage.cpp | 4 +- .../ui/pages/instance/ScreenshotsPage.cpp | 4 +- launcher/ui/pages/instance/ServersPage.cpp | 4 +- launcher/ui/pages/instance/VersionPage.cpp | 4 +- launcher/ui/pages/instance/WorldListPage.cpp | 4 +- 12 files changed, 76 insertions(+), 24 deletions(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index a098eab14..c8d2849c9 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -816,6 +816,8 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_settings->registerSetting("TPDownloadGeometry", ""); m_settings->registerSetting("ShaderDownloadGeometry", ""); + m_settings->registerSetting("UI/FolderResourceColumnVisibility", QVariantMap{}); + // HACK: This code feels so stupid is there a less stupid way of doing this? { m_settings->registerSetting("PastebinURL", ""); diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index fafe7bd37..ed8ed398d 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -231,6 +231,9 @@ void MinecraftInstance::loadSpecificSettings() auto envSetting = m_settings->registerSetting("OverrideEnv", false); m_settings->registerOverride(global_settings->getSetting("Env"), envSetting); + m_settings->registerSetting("UI/ColumnsOverride", false); + m_settings->registerSetting("UI/FolderResourceColumnVisibility", QVariantMap{}); + m_settings->set("InstanceType", "OneSix"); } diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index 43888ae27..b613e0af1 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -67,7 +67,6 @@ ModFolderModel::ModFolderModel(const QDir& dir, BaseInstance* instance, bool is_ QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive }; m_columnsHideable = { false, true, false, true, true, true, true, true, true, true, true }; - m_columnsHiddenByDefault = { false, false, false, false, false, false, false, true, true, true, true }; } QVariant ModFolderModel::data(const QModelIndex& index, int role) const diff --git a/launcher/minecraft/mod/ResourceFolderModel.cpp b/launcher/minecraft/mod/ResourceFolderModel.cpp index bf40c81d7..21093066d 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.cpp +++ b/launcher/minecraft/mod/ResourceFolderModel.cpp @@ -1,4 +1,5 @@ #include "ResourceFolderModel.h" +#include #include #include @@ -587,28 +588,77 @@ void ResourceFolderModel::setupHeaderAction(QAction* act, int column) void ResourceFolderModel::saveColumns(QTreeView* tree) { - auto const setting_name = QString("UI/%1_Page/Columns").arg(id()); - auto setting = m_instance->settings()->getOrRegisterSetting(setting_name); + auto const settingName = QString("UI/%1_Page/Columns").arg(id()); + auto setting = m_instance->settings()->getSetting(settingName); - setting->set(tree->header()->saveState()); + setting->set(tree->header()->saveState().toBase64()); + + // neither passthrough nor override settings works for this usecase as I need to only set the global when the gate is false + auto settings = m_instance->settings(); + if (!settings->get("UI/ColumnsOverride").toBool()) { + settings = APPLICATION->settings(); + } + auto visibility = settings->get("UI/FolderResourceColumnVisibility").toMap(); + for (auto i = 0; i < m_column_names.size(); ++i) { + if (m_columnsHideable[i]) { + auto name = m_column_names[i]; + visibility[name] = !tree->isColumnHidden(i); + } + } + settings->set("UI/FolderResourceColumnVisibility", visibility); } void ResourceFolderModel::loadColumns(QTreeView* tree) { - for (auto i = 0; i < m_columnsHiddenByDefault.size(); ++i) { - tree->setColumnHidden(i, m_columnsHiddenByDefault[i]); - } + auto const settingName = QString("UI/%1_Page/Columns").arg(id()); - auto const setting_name = QString("UI/%1_Page/Columns").arg(id()); - auto setting = m_instance->settings()->getOrRegisterSetting(setting_name); + auto setting = m_instance->settings()->getOrRegisterSetting(settingName, QByteArray{}); + tree->header()->restoreState(QByteArray::fromBase64(setting->get().toByteArray())); - tree->header()->restoreState(setting->get().toByteArray()); + auto setVisible = [this, tree](QVariant value) { + auto visibility = value.toMap(); + for (auto i = 0; i < m_column_names.size(); ++i) { + if (m_columnsHideable[i]) { + auto name = m_column_names[i]; + tree->setColumnHidden(i, !visibility.value(name, false).toBool()); + } + } + }; + + // neither passthrough nor override settings works for this usecase as I need to only set the global when the gate is false + auto settings = m_instance->settings(); + if (!settings->get("UI/ColumnsOverride").toBool()) { + settings = APPLICATION->settings(); + } + auto visibility = settings->getSetting("UI/FolderResourceColumnVisibility"); + setVisible(visibility->get().toMap()); + + // allways connect the signal in case the setting is toggled on and off + auto gSetting = APPLICATION->settings()->getOrRegisterSetting("UI/FolderResourceColumnVisibility"); + connect(gSetting.get(), &Setting::SettingChanged, tree, [this, setVisible](const Setting&, QVariant value) { + if (!m_instance->settings()->get("UI/ColumnsOverride").toBool()) { + setVisible(value.toMap()); + } + }); } QMenu* ResourceFolderModel::createHeaderContextMenu(QTreeView* tree) { auto menu = new QMenu(tree); + { // action to decide if the visibility is per instance or not + auto act = new QAction(tr("Overide Columns Visibility"), menu); + + act->setCheckable(true); + act->setChecked(m_instance->settings()->get("UI/ColumnsOverride").toBool()); + + connect(act, &QAction::toggled, tree, [this, tree](bool toggled) { + m_instance->settings()->set("UI/ColumnsOverride", toggled); + saveColumns(tree); + }); + + menu->addAction(act); + } menu->addSeparator()->setText(tr("Show / Hide Columns")); for (int col = 0; col < columnCount(); ++col) { diff --git a/launcher/minecraft/mod/ResourceFolderModel.h b/launcher/minecraft/mod/ResourceFolderModel.h index f6173b0d9..759861e14 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.h +++ b/launcher/minecraft/mod/ResourceFolderModel.h @@ -243,7 +243,6 @@ class ResourceFolderModel : public QAbstractListModel { QList m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive }; QList m_columnsHideable = { false, false, true, true, true }; - QList m_columnsHiddenByDefault = { false, false, false, false, true }; QDir m_dir; BaseInstance* m_instance; diff --git a/launcher/minecraft/mod/TexturePackFolderModel.cpp b/launcher/minecraft/mod/TexturePackFolderModel.cpp index 4d7c71359..8b89b45cd 100644 --- a/launcher/minecraft/mod/TexturePackFolderModel.cpp +++ b/launcher/minecraft/mod/TexturePackFolderModel.cpp @@ -51,7 +51,6 @@ TexturePackFolderModel::TexturePackFolderModel(const QDir& dir, BaseInstance* in m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive }; m_columnsHideable = { false, true, false, true, true, true }; - m_columnsHiddenByDefault = { false, false, false, false, false, true }; } Task* TexturePackFolderModel::createParseTask(Resource& resource) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index e0f37968f..5af108331 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -181,7 +181,7 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi auto const setting_name = QString("WideBarVisibility_%1").arg(ui->instanceToolBar->objectName()); instanceToolbarSetting = APPLICATION->settings()->getOrRegisterSetting(setting_name); - ui->instanceToolBar->setVisibilityState(instanceToolbarSetting->get().toByteArray()); + ui->instanceToolBar->setVisibilityState(QByteArray::fromBase64(instanceToolbarSetting->get().toByteArray())); ui->instanceToolBar->addContextMenuAction(ui->newsToolBar->toggleViewAction()); ui->instanceToolBar->addContextMenuAction(ui->instanceToolBar->toggleViewAction()); @@ -1493,7 +1493,7 @@ void MainWindow::closeEvent(QCloseEvent* event) // Save the window state and geometry. APPLICATION->settings()->set("MainWindowState", saveState().toBase64()); APPLICATION->settings()->set("MainWindowGeometry", saveGeometry().toBase64()); - instanceToolbarSetting->set(ui->instanceToolBar->getVisibilityState()); + instanceToolbarSetting->set(ui->instanceToolBar->getVisibilityState().toBase64()); event->accept(); emit isClosing(); } diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.cpp b/launcher/ui/pages/instance/ExternalResourcesPage.cpp index be65e6948..b11b4970a 100644 --- a/launcher/ui/pages/instance/ExternalResourcesPage.cpp +++ b/launcher/ui/pages/instance/ExternalResourcesPage.cpp @@ -148,14 +148,14 @@ void ExternalResourcesPage::openedImpl() auto const setting_name = QString("WideBarVisibility_%1").arg(id()); m_wide_bar_setting = APPLICATION->settings()->getOrRegisterSetting(setting_name); - ui->actionsToolbar->setVisibilityState(m_wide_bar_setting->get().toByteArray()); + ui->actionsToolbar->setVisibilityState(QByteArray::fromBase64(m_wide_bar_setting->get().toByteArray())); } void ExternalResourcesPage::closedImpl() { m_model->stopWatching(); - m_wide_bar_setting->set(ui->actionsToolbar->getVisibilityState()); + m_wide_bar_setting->set(ui->actionsToolbar->getVisibilityState().toBase64()); } void ExternalResourcesPage::retranslate() diff --git a/launcher/ui/pages/instance/ScreenshotsPage.cpp b/launcher/ui/pages/instance/ScreenshotsPage.cpp index e59002a15..850d85288 100644 --- a/launcher/ui/pages/instance/ScreenshotsPage.cpp +++ b/launcher/ui/pages/instance/ScreenshotsPage.cpp @@ -562,12 +562,12 @@ void ScreenshotsPage::openedImpl() auto const setting_name = QString("WideBarVisibility_%1").arg(id()); m_wide_bar_setting = APPLICATION->settings()->getOrRegisterSetting(setting_name); - ui->toolBar->setVisibilityState(m_wide_bar_setting->get().toByteArray()); + ui->toolBar->setVisibilityState(QByteArray::fromBase64(m_wide_bar_setting->get().toByteArray())); } void ScreenshotsPage::closedImpl() { - m_wide_bar_setting->set(ui->toolBar->getVisibilityState()); + m_wide_bar_setting->set(ui->toolBar->getVisibilityState().toBase64()); } #include "ScreenshotsPage.moc" diff --git a/launcher/ui/pages/instance/ServersPage.cpp b/launcher/ui/pages/instance/ServersPage.cpp index 245bbffe2..ecf4b58dd 100644 --- a/launcher/ui/pages/instance/ServersPage.cpp +++ b/launcher/ui/pages/instance/ServersPage.cpp @@ -705,7 +705,7 @@ void ServersPage::openedImpl() auto const setting_name = QString("WideBarVisibility_%1").arg(id()); m_wide_bar_setting = APPLICATION->settings()->getOrRegisterSetting(setting_name); - ui->toolBar->setVisibilityState(m_wide_bar_setting->get().toByteArray()); + ui->toolBar->setVisibilityState(QByteArray::fromBase64(m_wide_bar_setting->get().toByteArray())); // ping servers m_model->queryServersStatus(); @@ -715,7 +715,7 @@ void ServersPage::closedImpl() { m_model->unobserve(); - m_wide_bar_setting->set(ui->toolBar->getVisibilityState()); + m_wide_bar_setting->set(ui->toolBar->getVisibilityState().toBase64()); } void ServersPage::on_actionAdd_triggered() diff --git a/launcher/ui/pages/instance/VersionPage.cpp b/launcher/ui/pages/instance/VersionPage.cpp index a1eeb3d25..16f5918b8 100644 --- a/launcher/ui/pages/instance/VersionPage.cpp +++ b/launcher/ui/pages/instance/VersionPage.cpp @@ -126,11 +126,11 @@ void VersionPage::openedImpl() auto const setting_name = QString("WideBarVisibility_%1").arg(id()); m_wide_bar_setting = APPLICATION->settings()->getOrRegisterSetting(setting_name); - ui->toolBar->setVisibilityState(m_wide_bar_setting->get().toByteArray()); + ui->toolBar->setVisibilityState(QByteArray::fromBase64(m_wide_bar_setting->get().toByteArray())); } void VersionPage::closedImpl() { - m_wide_bar_setting->set(ui->toolBar->getVisibilityState()); + m_wide_bar_setting->set(ui->toolBar->getVisibilityState().toBase64()); } QMenu* VersionPage::createPopupMenu() diff --git a/launcher/ui/pages/instance/WorldListPage.cpp b/launcher/ui/pages/instance/WorldListPage.cpp index 9e1a0fb55..e044c1083 100644 --- a/launcher/ui/pages/instance/WorldListPage.cpp +++ b/launcher/ui/pages/instance/WorldListPage.cpp @@ -121,14 +121,14 @@ void WorldListPage::openedImpl() auto const setting_name = QString("WideBarVisibility_%1").arg(id()); m_wide_bar_setting = APPLICATION->settings()->getOrRegisterSetting(setting_name); - ui->toolBar->setVisibilityState(m_wide_bar_setting->get().toByteArray()); + ui->toolBar->setVisibilityState(QByteArray::fromBase64(m_wide_bar_setting->get().toByteArray())); } void WorldListPage::closedImpl() { m_worlds->stopWatching(); - m_wide_bar_setting->set(ui->toolBar->getVisibilityState()); + m_wide_bar_setting->set(ui->toolBar->getVisibilityState().toBase64()); } WorldListPage::~WorldListPage() From 7e174f53af5959c04109a79e2d6e3b6e3bbd5feb Mon Sep 17 00:00:00 2001 From: Trial97 Date: Tue, 20 May 2025 22:59:18 +0300 Subject: [PATCH 281/695] chore: add migration for old QByteArray to base64 Signed-off-by: Trial97 --- .../minecraft/mod/ResourceFolderModel.cpp | 1 - launcher/settings/INIFile.cpp | 40 ++++++++++++++----- tests/INIFile_test.cpp | 6 +-- 3 files changed, 33 insertions(+), 14 deletions(-) diff --git a/launcher/minecraft/mod/ResourceFolderModel.cpp b/launcher/minecraft/mod/ResourceFolderModel.cpp index 21093066d..2956570af 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.cpp +++ b/launcher/minecraft/mod/ResourceFolderModel.cpp @@ -1,5 +1,4 @@ #include "ResourceFolderModel.h" -#include #include #include diff --git a/launcher/settings/INIFile.cpp b/launcher/settings/INIFile.cpp index 2c7620e65..530b4590a 100644 --- a/launcher/settings/INIFile.cpp +++ b/launcher/settings/INIFile.cpp @@ -50,7 +50,7 @@ INIFile::INIFile() {} bool INIFile::saveFile(QString fileName) { if (!contains("ConfigVersion")) - insert("ConfigVersion", "1.2"); + insert("ConfigVersion", "1.3"); QSettings _settings_obj{ fileName, QSettings::Format::IniFormat }; _settings_obj.setFallbacksEnabled(false); _settings_obj.clear(); @@ -149,6 +149,14 @@ bool parseOldFileFormat(QIODevice& device, QSettings::SettingsMap& map) return true; } +QVariant migrateQByteArrayToBase64(QString key, QVariant value) +{ + if (key.startsWith("WideBarVisibility_") || (key.startsWith("UI/") && key.endsWith("_Page/Columns"))) { + return value.toByteArray().toBase64(); + } + return value; +} + bool INIFile::loadFile(QString fileName) { QSettings _settings_obj{ fileName, QSettings::Format::IniFormat }; @@ -168,22 +176,34 @@ bool INIFile::loadFile(QString fileName) QSettings::SettingsMap map; parseOldFileFormat(file, map); file.close(); - for (auto&& key : map.keys()) - insert(key, map.value(key)); - insert("ConfigVersion", "1.2"); + for (auto&& key : map.keys()) { + auto value = migrateQByteArrayToBase64(key, map.value(key)); + insert(key, value); + } + insert("ConfigVersion", "1.3"); } else if (_settings_obj.value("ConfigVersion").toString() == "1.1") { for (auto&& key : _settings_obj.allKeys()) { - if (auto valueStr = _settings_obj.value(key).toString(); + auto value = migrateQByteArrayToBase64(key, _settings_obj.value(key)); + if (auto valueStr = value.toString(); (valueStr.contains(QChar(';')) || valueStr.contains(QChar('=')) || valueStr.contains(QChar(','))) && valueStr.endsWith("\"") && valueStr.startsWith("\"")) { insert(key, unquote(valueStr)); - } else - insert(key, _settings_obj.value(key)); + } else { + insert(key, value); + } } - insert("ConfigVersion", "1.2"); - } else - for (auto&& key : _settings_obj.allKeys()) + insert("ConfigVersion", "1.3"); + } else if (_settings_obj.value("ConfigVersion").toString() == "1.2") { + for (auto&& key : _settings_obj.allKeys()) { + auto value = migrateQByteArrayToBase64(key, _settings_obj.value(key)); + insert(key, value); + } + insert("ConfigVersion", "1.3"); + } else { + for (auto&& key : _settings_obj.allKeys()) { insert(key, _settings_obj.value(key)); + } + } return true; } diff --git a/tests/INIFile_test.cpp b/tests/INIFile_test.cpp index 95730e244..559600212 100644 --- a/tests/INIFile_test.cpp +++ b/tests/INIFile_test.cpp @@ -110,7 +110,7 @@ Wrapperommand=)"; f2.loadFile(fileName); QCOMPARE(f2.get("PreLaunchCommand", "NOT SET").toString(), "\"$INST_JAVA\" -jar packwiz-installer-bootstrap.jar link"); QCOMPARE(f2.get("Wrapperommand", "NOT SET").toString(), "\"$INST_JAVA\" -jar packwiz-installer-bootstrap.jar link ="); - QCOMPARE(f2.get("ConfigVersion", "NOT SET").toString(), "1.2"); + QCOMPARE(f2.get("ConfigVersion", "NOT SET").toString(), "1.3"); #if defined(Q_OS_WIN) FS::deletePath(fileName); #endif @@ -151,7 +151,7 @@ Wrapperommand=)"; f2.loadFile(fileName); for (auto key : settings.allKeys()) QCOMPARE(f2.get(key, "NOT SET").toString(), settings.value(key).toString()); - QCOMPARE(f2.get("ConfigVersion", "NOT SET").toString(), "1.2"); + QCOMPARE(f2.get("ConfigVersion", "NOT SET").toString(), "1.3"); #if defined(Q_OS_WIN) FS::deletePath(fileName); #endif @@ -185,7 +185,7 @@ PreLaunchCommand=)"; INIFile f1; f1.loadFile(fileName); QCOMPARE(f1.get("PreLaunchCommand", "NOT SET").toString(), "env mesa=true"); - QCOMPARE(f1.get("ConfigVersion", "NOT SET").toString(), "1.2"); + QCOMPARE(f1.get("ConfigVersion", "NOT SET").toString(), "1.3"); #if defined(Q_OS_WIN) FS::deletePath(fileName); #endif From 89be378ef6e152dd3c68b4462c44b0a1d3f886e8 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Wed, 21 May 2025 00:31:41 +0300 Subject: [PATCH 282/695] chore: add miggrate qbytearray settings to qstring Signed-off-by: Trial97 --- launcher/Application.cpp | 4 ++-- launcher/minecraft/mod/ResourceFolderModel.cpp | 6 +++--- launcher/settings/INIFile.cpp | 9 ++++++++- launcher/ui/InstanceWindow.cpp | 8 ++++---- launcher/ui/MainWindow.cpp | 10 +++++----- launcher/ui/dialogs/NewInstanceDialog.cpp | 8 ++++---- launcher/ui/dialogs/ResourceDownloadDialog.cpp | 12 ++++++------ launcher/ui/pagedialog/PageDialog.cpp | 4 ++-- launcher/ui/pages/instance/ExternalResourcesPage.cpp | 4 ++-- launcher/ui/pages/instance/ScreenshotsPage.cpp | 4 ++-- launcher/ui/pages/instance/ServersPage.cpp | 4 ++-- launcher/ui/pages/instance/VersionPage.cpp | 4 ++-- launcher/ui/pages/instance/WorldListPage.cpp | 4 ++-- 13 files changed, 44 insertions(+), 37 deletions(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index c8d2849c9..3084caca8 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -1662,8 +1662,8 @@ MainWindow* Application::showMainWindow(bool minimized) m_mainWindow->activateWindow(); } else { m_mainWindow = new MainWindow(); - m_mainWindow->restoreState(QByteArray::fromBase64(APPLICATION->settings()->get("MainWindowState").toByteArray())); - m_mainWindow->restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("MainWindowGeometry").toByteArray())); + m_mainWindow->restoreState(QByteArray::fromBase64(APPLICATION->settings()->get("MainWindowState").toString().toUtf8())); + m_mainWindow->restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("MainWindowGeometry").toString().toUtf8())); if (minimized) { m_mainWindow->showMinimized(); diff --git a/launcher/minecraft/mod/ResourceFolderModel.cpp b/launcher/minecraft/mod/ResourceFolderModel.cpp index 2956570af..183367b8b 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.cpp +++ b/launcher/minecraft/mod/ResourceFolderModel.cpp @@ -590,7 +590,7 @@ void ResourceFolderModel::saveColumns(QTreeView* tree) auto const settingName = QString("UI/%1_Page/Columns").arg(id()); auto setting = m_instance->settings()->getSetting(settingName); - setting->set(tree->header()->saveState().toBase64()); + setting->set(QString::fromUtf8(tree->header()->saveState().toBase64())); // neither passthrough nor override settings works for this usecase as I need to only set the global when the gate is false auto settings = m_instance->settings(); @@ -611,8 +611,8 @@ void ResourceFolderModel::loadColumns(QTreeView* tree) { auto const settingName = QString("UI/%1_Page/Columns").arg(id()); - auto setting = m_instance->settings()->getOrRegisterSetting(settingName, QByteArray{}); - tree->header()->restoreState(QByteArray::fromBase64(setting->get().toByteArray())); + auto setting = m_instance->settings()->getOrRegisterSetting(settingName, ""); + tree->header()->restoreState(QByteArray::fromBase64(setting->get().toString().toUtf8())); auto setVisible = [this, tree](QVariant value) { auto visibility = value.toMap(); diff --git a/launcher/settings/INIFile.cpp b/launcher/settings/INIFile.cpp index 530b4590a..8403642fa 100644 --- a/launcher/settings/INIFile.cpp +++ b/launcher/settings/INIFile.cpp @@ -152,7 +152,14 @@ bool parseOldFileFormat(QIODevice& device, QSettings::SettingsMap& map) QVariant migrateQByteArrayToBase64(QString key, QVariant value) { if (key.startsWith("WideBarVisibility_") || (key.startsWith("UI/") && key.endsWith("_Page/Columns"))) { - return value.toByteArray().toBase64(); + return QString::fromUtf8(value.toByteArray().toBase64()); + } + static const QStringList otherByteArrays = { "MainWindowState", "MainWindowGeometry", "ConsoleWindowState", + "ConsoleWindowGeometry", "PagedGeometry", "NewInstanceGeometry", + "ModDownloadGeometry", "RPDownloadGeometry", "TPDownloadGeometry", + "ShaderDownloadGeometry" }; + if (otherByteArrays.contains(key)) { + return QString::fromUtf8(value.toByteArray()); } return value; } diff --git a/launcher/ui/InstanceWindow.cpp b/launcher/ui/InstanceWindow.cpp index 2f156e125..0742fa51d 100644 --- a/launcher/ui/InstanceWindow.cpp +++ b/launcher/ui/InstanceWindow.cpp @@ -116,9 +116,9 @@ InstanceWindow::InstanceWindow(InstancePtr instance, QWidget* parent) : QMainWin // restore window state { - auto base64State = APPLICATION->settings()->get("ConsoleWindowState").toByteArray(); + auto base64State = APPLICATION->settings()->get("ConsoleWindowState").toString().toUtf8(); restoreState(QByteArray::fromBase64(base64State)); - auto base64Geometry = APPLICATION->settings()->get("ConsoleWindowGeometry").toByteArray(); + auto base64Geometry = APPLICATION->settings()->get("ConsoleWindowGeometry").toString().toUtf8(); restoreGeometry(QByteArray::fromBase64(base64Geometry)); } @@ -190,8 +190,8 @@ void InstanceWindow::closeEvent(QCloseEvent* event) return; } - APPLICATION->settings()->set("ConsoleWindowState", saveState().toBase64()); - APPLICATION->settings()->set("ConsoleWindowGeometry", saveGeometry().toBase64()); + APPLICATION->settings()->set("ConsoleWindowState", QString::fromUtf8(saveState().toBase64())); + APPLICATION->settings()->set("ConsoleWindowGeometry", QString::fromUtf8(saveGeometry().toBase64())); emit isClosing(); event->accept(); } diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 5af108331..455a95837 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -181,7 +181,7 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi auto const setting_name = QString("WideBarVisibility_%1").arg(ui->instanceToolBar->objectName()); instanceToolbarSetting = APPLICATION->settings()->getOrRegisterSetting(setting_name); - ui->instanceToolBar->setVisibilityState(QByteArray::fromBase64(instanceToolbarSetting->get().toByteArray())); + ui->instanceToolBar->setVisibilityState(QByteArray::fromBase64(instanceToolbarSetting->get().toString().toUtf8())); ui->instanceToolBar->addContextMenuAction(ui->newsToolBar->toggleViewAction()); ui->instanceToolBar->addContextMenuAction(ui->instanceToolBar->toggleViewAction()); @@ -1296,7 +1296,7 @@ void MainWindow::globalSettingsClosed() updateStatusCenter(); // This needs to be done to prevent UI elements disappearing in the event the config is changed // but Prism Launcher exits abnormally, causing the window state to never be saved: - APPLICATION->settings()->set("MainWindowState", saveState().toBase64()); + APPLICATION->settings()->set("MainWindowState", QString::fromUtf8(saveState().toBase64())); update(); } @@ -1491,9 +1491,9 @@ void MainWindow::on_actionViewSelectedInstFolder_triggered() void MainWindow::closeEvent(QCloseEvent* event) { // Save the window state and geometry. - APPLICATION->settings()->set("MainWindowState", saveState().toBase64()); - APPLICATION->settings()->set("MainWindowGeometry", saveGeometry().toBase64()); - instanceToolbarSetting->set(ui->instanceToolBar->getVisibilityState().toBase64()); + APPLICATION->settings()->set("MainWindowState", QString::fromUtf8(saveState().toBase64())); + APPLICATION->settings()->set("MainWindowGeometry", QString::fromUtf8(saveGeometry().toBase64())); + instanceToolbarSetting->set(QString::fromUtf8(ui->instanceToolBar->getVisibilityState().toBase64())); event->accept(); emit isClosing(); } diff --git a/launcher/ui/dialogs/NewInstanceDialog.cpp b/launcher/ui/dialogs/NewInstanceDialog.cpp index 6036663ba..9e74cd7ac 100644 --- a/launcher/ui/dialogs/NewInstanceDialog.cpp +++ b/launcher/ui/dialogs/NewInstanceDialog.cpp @@ -134,7 +134,7 @@ NewInstanceDialog::NewInstanceDialog(const QString& initialGroup, updateDialogState(); if (APPLICATION->settings()->get("NewInstanceGeometry").isValid()) { - restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("NewInstanceGeometry").toByteArray())); + restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("NewInstanceGeometry").toString().toUtf8())); } else { auto screen = parent->screen(); auto geometry = screen->availableSize(); @@ -146,7 +146,7 @@ NewInstanceDialog::NewInstanceDialog(const QString& initialGroup, void NewInstanceDialog::reject() { - APPLICATION->settings()->set("NewInstanceGeometry", saveGeometry().toBase64()); + APPLICATION->settings()->set("NewInstanceGeometry", QString::fromUtf8(saveGeometry().toBase64())); // This is just so that the pages get the close() call and can react to it, if needed. m_container->prepareToClose(); @@ -156,7 +156,7 @@ void NewInstanceDialog::reject() void NewInstanceDialog::accept() { - APPLICATION->settings()->set("NewInstanceGeometry", saveGeometry().toBase64()); + APPLICATION->settings()->set("NewInstanceGeometry", QString::fromUtf8(saveGeometry().toBase64())); importIconNow(); // This is just so that the pages get the close() call and can react to it, if needed. @@ -316,7 +316,7 @@ void NewInstanceDialog::importIconNow() InstIconKey = importIconName.mid(0, importIconName.lastIndexOf('.')); importIcon = false; } - APPLICATION->settings()->set("NewInstanceGeometry", saveGeometry().toBase64()); + APPLICATION->settings()->set("NewInstanceGeometry", QString::fromUtf8(saveGeometry().toBase64())); } void NewInstanceDialog::selectedPageChanged(BasePage* previous, BasePage* selected) diff --git a/launcher/ui/dialogs/ResourceDownloadDialog.cpp b/launcher/ui/dialogs/ResourceDownloadDialog.cpp index 718c0fe2c..1163e3bc1 100644 --- a/launcher/ui/dialogs/ResourceDownloadDialog.cpp +++ b/launcher/ui/dialogs/ResourceDownloadDialog.cpp @@ -84,7 +84,7 @@ ResourceDownloadDialog::ResourceDownloadDialog(QWidget* parent, const std::share void ResourceDownloadDialog::accept() { if (!geometrySaveKey().isEmpty()) - APPLICATION->settings()->set(geometrySaveKey(), saveGeometry().toBase64()); + APPLICATION->settings()->set(geometrySaveKey(), QString::fromUtf8(saveGeometry().toBase64())); QDialog::accept(); } @@ -105,7 +105,7 @@ void ResourceDownloadDialog::reject() } if (!geometrySaveKey().isEmpty()) - APPLICATION->settings()->set(geometrySaveKey(), saveGeometry().toBase64()); + APPLICATION->settings()->set(geometrySaveKey(), QString::fromUtf8(saveGeometry().toBase64())); QDialog::reject(); } @@ -275,7 +275,7 @@ ModDownloadDialog::ModDownloadDialog(QWidget* parent, const std::shared_ptrsettings()->get(geometrySaveKey()).toByteArray())); + restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get(geometrySaveKey()).toString().toUtf8())); } QList ModDownloadDialog::getPages() @@ -318,7 +318,7 @@ ResourcePackDownloadDialog::ResourcePackDownloadDialog(QWidget* parent, connectButtons(); if (!geometrySaveKey().isEmpty()) - restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get(geometrySaveKey()).toByteArray())); + restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get(geometrySaveKey()).toString().toUtf8())); } QList ResourcePackDownloadDialog::getPages() @@ -343,7 +343,7 @@ TexturePackDownloadDialog::TexturePackDownloadDialog(QWidget* parent, connectButtons(); if (!geometrySaveKey().isEmpty()) - restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get(geometrySaveKey()).toByteArray())); + restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get(geometrySaveKey()).toString().toUtf8())); } QList TexturePackDownloadDialog::getPages() @@ -368,7 +368,7 @@ ShaderPackDownloadDialog::ShaderPackDownloadDialog(QWidget* parent, connectButtons(); if (!geometrySaveKey().isEmpty()) - restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get(geometrySaveKey()).toByteArray())); + restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get(geometrySaveKey()).toString().toUtf8())); } QList ShaderPackDownloadDialog::getPages() diff --git a/launcher/ui/pagedialog/PageDialog.cpp b/launcher/ui/pagedialog/PageDialog.cpp index 8ce53448a..c333b3dd7 100644 --- a/launcher/ui/pagedialog/PageDialog.cpp +++ b/launcher/ui/pagedialog/PageDialog.cpp @@ -53,7 +53,7 @@ PageDialog::PageDialog(BasePageProvider* pageProvider, QString defaultId, QWidge connect(buttons->button(QDialogButtonBox::Cancel), &QPushButton::clicked, this, &PageDialog::reject); connect(buttons->button(QDialogButtonBox::Help), &QPushButton::clicked, m_container, &PageContainer::help); - restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("PagedGeometry").toByteArray())); + restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("PagedGeometry").toString().toUtf8())); } void PageDialog::accept() @@ -75,7 +75,7 @@ bool PageDialog::handleClose() return false; qDebug() << "Paged dialog close approved"; - APPLICATION->settings()->set("PagedGeometry", saveGeometry().toBase64()); + APPLICATION->settings()->set("PagedGeometry", QString::fromUtf8(saveGeometry().toBase64())); qDebug() << "Paged dialog geometry saved"; emit applied(); diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.cpp b/launcher/ui/pages/instance/ExternalResourcesPage.cpp index b11b4970a..d38d16284 100644 --- a/launcher/ui/pages/instance/ExternalResourcesPage.cpp +++ b/launcher/ui/pages/instance/ExternalResourcesPage.cpp @@ -148,14 +148,14 @@ void ExternalResourcesPage::openedImpl() auto const setting_name = QString("WideBarVisibility_%1").arg(id()); m_wide_bar_setting = APPLICATION->settings()->getOrRegisterSetting(setting_name); - ui->actionsToolbar->setVisibilityState(QByteArray::fromBase64(m_wide_bar_setting->get().toByteArray())); + ui->actionsToolbar->setVisibilityState(QByteArray::fromBase64(m_wide_bar_setting->get().toString().toUtf8())); } void ExternalResourcesPage::closedImpl() { m_model->stopWatching(); - m_wide_bar_setting->set(ui->actionsToolbar->getVisibilityState().toBase64()); + m_wide_bar_setting->set(QString::fromUtf8(ui->actionsToolbar->getVisibilityState().toBase64())); } void ExternalResourcesPage::retranslate() diff --git a/launcher/ui/pages/instance/ScreenshotsPage.cpp b/launcher/ui/pages/instance/ScreenshotsPage.cpp index 850d85288..c9a1b406a 100644 --- a/launcher/ui/pages/instance/ScreenshotsPage.cpp +++ b/launcher/ui/pages/instance/ScreenshotsPage.cpp @@ -562,12 +562,12 @@ void ScreenshotsPage::openedImpl() auto const setting_name = QString("WideBarVisibility_%1").arg(id()); m_wide_bar_setting = APPLICATION->settings()->getOrRegisterSetting(setting_name); - ui->toolBar->setVisibilityState(QByteArray::fromBase64(m_wide_bar_setting->get().toByteArray())); + ui->toolBar->setVisibilityState(QByteArray::fromBase64(m_wide_bar_setting->get().toString().toUtf8())); } void ScreenshotsPage::closedImpl() { - m_wide_bar_setting->set(ui->toolBar->getVisibilityState().toBase64()); + m_wide_bar_setting->set(QString::fromUtf8(ui->toolBar->getVisibilityState().toBase64())); } #include "ScreenshotsPage.moc" diff --git a/launcher/ui/pages/instance/ServersPage.cpp b/launcher/ui/pages/instance/ServersPage.cpp index ecf4b58dd..2f12c3523 100644 --- a/launcher/ui/pages/instance/ServersPage.cpp +++ b/launcher/ui/pages/instance/ServersPage.cpp @@ -705,7 +705,7 @@ void ServersPage::openedImpl() auto const setting_name = QString("WideBarVisibility_%1").arg(id()); m_wide_bar_setting = APPLICATION->settings()->getOrRegisterSetting(setting_name); - ui->toolBar->setVisibilityState(QByteArray::fromBase64(m_wide_bar_setting->get().toByteArray())); + ui->toolBar->setVisibilityState(QByteArray::fromBase64(m_wide_bar_setting->get().toString().toUtf8())); // ping servers m_model->queryServersStatus(); @@ -715,7 +715,7 @@ void ServersPage::closedImpl() { m_model->unobserve(); - m_wide_bar_setting->set(ui->toolBar->getVisibilityState().toBase64()); + m_wide_bar_setting->set(QString::fromUtf8(ui->toolBar->getVisibilityState().toBase64())); } void ServersPage::on_actionAdd_triggered() diff --git a/launcher/ui/pages/instance/VersionPage.cpp b/launcher/ui/pages/instance/VersionPage.cpp index 16f5918b8..d355f38fb 100644 --- a/launcher/ui/pages/instance/VersionPage.cpp +++ b/launcher/ui/pages/instance/VersionPage.cpp @@ -126,11 +126,11 @@ void VersionPage::openedImpl() auto const setting_name = QString("WideBarVisibility_%1").arg(id()); m_wide_bar_setting = APPLICATION->settings()->getOrRegisterSetting(setting_name); - ui->toolBar->setVisibilityState(QByteArray::fromBase64(m_wide_bar_setting->get().toByteArray())); + ui->toolBar->setVisibilityState(QByteArray::fromBase64(m_wide_bar_setting->get().toString().toUtf8())); } void VersionPage::closedImpl() { - m_wide_bar_setting->set(ui->toolBar->getVisibilityState().toBase64()); + m_wide_bar_setting->set(QString::fromUtf8(ui->toolBar->getVisibilityState().toBase64())); } QMenu* VersionPage::createPopupMenu() diff --git a/launcher/ui/pages/instance/WorldListPage.cpp b/launcher/ui/pages/instance/WorldListPage.cpp index e044c1083..e9584f744 100644 --- a/launcher/ui/pages/instance/WorldListPage.cpp +++ b/launcher/ui/pages/instance/WorldListPage.cpp @@ -121,14 +121,14 @@ void WorldListPage::openedImpl() auto const setting_name = QString("WideBarVisibility_%1").arg(id()); m_wide_bar_setting = APPLICATION->settings()->getOrRegisterSetting(setting_name); - ui->toolBar->setVisibilityState(QByteArray::fromBase64(m_wide_bar_setting->get().toByteArray())); + ui->toolBar->setVisibilityState(QByteArray::fromBase64(m_wide_bar_setting->get().toString().toUtf8())); } void WorldListPage::closedImpl() { m_worlds->stopWatching(); - m_wide_bar_setting->set(ui->toolBar->getVisibilityState().toBase64()); + m_wide_bar_setting->set(QString::fromUtf8(ui->toolBar->getVisibilityState().toBase64())); } WorldListPage::~WorldListPage() From 8c710fb8de3e44b237cb1e6b67de42f122ee0783 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Wed, 21 May 2025 18:17:08 +0300 Subject: [PATCH 283/695] chore: migrate map settings to json string Signed-off-by: Trial97 --- launcher/Application.cpp | 4 ++-- launcher/BaseInstance.cpp | 13 +++++++------ launcher/Json.cpp | 19 +++++++++++++++++++ launcher/Json.h | 3 +++ launcher/minecraft/MinecraftInstance.cpp | 10 ++++++---- .../minecraft/mod/ResourceFolderModel.cpp | 10 +++++----- launcher/settings/INIFile.cpp | 13 ++++++++++--- .../ui/widgets/MinecraftSettingsWidget.cpp | 4 ++-- 8 files changed, 54 insertions(+), 22 deletions(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 3084caca8..1a7f2d9d3 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -816,7 +816,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_settings->registerSetting("TPDownloadGeometry", ""); m_settings->registerSetting("ShaderDownloadGeometry", ""); - m_settings->registerSetting("UI/FolderResourceColumnVisibility", QVariantMap{}); + m_settings->registerSetting("UI/FolderResourceColumnVisibility", "{}"); // HACK: This code feels so stupid is there a less stupid way of doing this? { @@ -855,7 +855,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_settings->registerSetting("CloseAfterLaunch", false); m_settings->registerSetting("QuitAfterGameStop", false); - m_settings->registerSetting("Env", QVariant(QMap())); + m_settings->registerSetting("Env", "{}"); // Custom Microsoft Authentication Client ID m_settings->registerSetting("MSAClientIDOverride", ""); diff --git a/launcher/BaseInstance.cpp b/launcher/BaseInstance.cpp index 096052a45..f4bc7e30b 100644 --- a/launcher/BaseInstance.cpp +++ b/launcher/BaseInstance.cpp @@ -44,6 +44,7 @@ #include #include "Application.h" +#include "Json.h" #include "settings/INISettingsObject.h" #include "settings/OverrideSetting.h" #include "settings/Setting.h" @@ -202,25 +203,25 @@ bool BaseInstance::shouldStopOnConsoleOverflow() const QStringList BaseInstance::getLinkedInstances() const { - return m_settings->get("linkedInstances").toStringList(); + auto setting = m_settings->get("linkedInstances").toString(); + return Json::toStringList(setting); } void BaseInstance::setLinkedInstances(const QStringList& list) { - auto linkedInstances = m_settings->get("linkedInstances").toStringList(); - m_settings->set("linkedInstances", list); + m_settings->set("linkedInstances", Json::fromStringList(list)); } void BaseInstance::addLinkedInstanceId(const QString& id) { - auto linkedInstances = m_settings->get("linkedInstances").toStringList(); + auto linkedInstances = getLinkedInstances(); linkedInstances.append(id); setLinkedInstances(linkedInstances); } bool BaseInstance::removeLinkedInstanceId(const QString& id) { - auto linkedInstances = m_settings->get("linkedInstances").toStringList(); + auto linkedInstances = getLinkedInstances(); int numRemoved = linkedInstances.removeAll(id); setLinkedInstances(linkedInstances); return numRemoved > 0; @@ -228,7 +229,7 @@ bool BaseInstance::removeLinkedInstanceId(const QString& id) bool BaseInstance::isLinkedToInstanceId(const QString& id) const { - auto linkedInstances = m_settings->get("linkedInstances").toStringList(); + auto linkedInstances = getLinkedInstances(); return linkedInstances.contains(id); } diff --git a/launcher/Json.cpp b/launcher/Json.cpp index 8623eb2a8..8eedb9b05 100644 --- a/launcher/Json.cpp +++ b/launcher/Json.cpp @@ -304,4 +304,23 @@ QString fromStringList(const QStringList& list) return QString::fromUtf8(doc.toJson(QJsonDocument::Compact)); } +QVariantMap toMap(const QString& jsonString) +{ + QJsonParseError parseError; + QJsonDocument doc = QJsonDocument::fromJson(jsonString.toUtf8(), &parseError); + + if (parseError.error != QJsonParseError::NoError || !doc.isObject()) + return {}; + + QJsonObject obj = doc.object(); + return obj.toVariantMap(); +} + +QString fromMap(const QVariantMap& map) +{ + QJsonObject obj = QJsonObject::fromVariantMap(map); + QJsonDocument doc(obj); + return QString::fromUtf8(doc.toJson(QJsonDocument::Compact)); +} + } // namespace Json diff --git a/launcher/Json.h b/launcher/Json.h index 509a41fd5..e51c737c2 100644 --- a/launcher/Json.h +++ b/launcher/Json.h @@ -282,5 +282,8 @@ JSON_HELPERFUNCTIONS(Variant, QVariant) QStringList toStringList(const QString& jsonString); QString fromStringList(const QStringList& list); +QVariantMap toMap(const QString& jsonString); +QString fromMap(const QVariantMap& map); + } // namespace Json using JSONValidationError = Json::JsonException; diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index ed8ed398d..6df86bbed 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -38,6 +38,7 @@ #include "MinecraftInstance.h" #include "Application.h" #include "BuildConfig.h" +#include "Json.h" #include "QObjectPtr.h" #include "minecraft/launch/AutoInstallJava.h" #include "minecraft/launch/CreateGameFolders.h" @@ -232,7 +233,7 @@ void MinecraftInstance::loadSpecificSettings() m_settings->registerOverride(global_settings->getSetting("Env"), envSetting); m_settings->registerSetting("UI/ColumnsOverride", false); - m_settings->registerSetting("UI/FolderResourceColumnVisibility", QVariantMap{}); + m_settings->registerSetting("UI/FolderResourceColumnVisibility", "{}"); m_settings->set("InstanceType", "OneSix"); } @@ -623,7 +624,8 @@ QProcessEnvironment MinecraftInstance::createEnvironment() } // custom env - auto insertEnv = [&env](QMap envMap) { + auto insertEnv = [&env](QString value) { + auto envMap = Json::toMap(value); if (envMap.isEmpty()) return; @@ -634,9 +636,9 @@ QProcessEnvironment MinecraftInstance::createEnvironment() bool overrideEnv = settings()->get("OverrideEnv").toBool(); if (!overrideEnv) - insertEnv(APPLICATION->settings()->get("Env").toMap()); + insertEnv(APPLICATION->settings()->get("Env").toString()); else - insertEnv(settings()->get("Env").toMap()); + insertEnv(settings()->get("Env").toString()); return env; } diff --git a/launcher/minecraft/mod/ResourceFolderModel.cpp b/launcher/minecraft/mod/ResourceFolderModel.cpp index 183367b8b..7b745b46f 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.cpp +++ b/launcher/minecraft/mod/ResourceFolderModel.cpp @@ -597,14 +597,14 @@ void ResourceFolderModel::saveColumns(QTreeView* tree) if (!settings->get("UI/ColumnsOverride").toBool()) { settings = APPLICATION->settings(); } - auto visibility = settings->get("UI/FolderResourceColumnVisibility").toMap(); + auto visibility = Json::toMap(settings->get("UI/FolderResourceColumnVisibility").toString()); for (auto i = 0; i < m_column_names.size(); ++i) { if (m_columnsHideable[i]) { auto name = m_column_names[i]; visibility[name] = !tree->isColumnHidden(i); } } - settings->set("UI/FolderResourceColumnVisibility", visibility); + settings->set("UI/FolderResourceColumnVisibility", Json::fromMap(visibility)); } void ResourceFolderModel::loadColumns(QTreeView* tree) @@ -615,7 +615,7 @@ void ResourceFolderModel::loadColumns(QTreeView* tree) tree->header()->restoreState(QByteArray::fromBase64(setting->get().toString().toUtf8())); auto setVisible = [this, tree](QVariant value) { - auto visibility = value.toMap(); + auto visibility = Json::toMap(value.toString()); for (auto i = 0; i < m_column_names.size(); ++i) { if (m_columnsHideable[i]) { auto name = m_column_names[i]; @@ -630,13 +630,13 @@ void ResourceFolderModel::loadColumns(QTreeView* tree) settings = APPLICATION->settings(); } auto visibility = settings->getSetting("UI/FolderResourceColumnVisibility"); - setVisible(visibility->get().toMap()); + setVisible(visibility->get()); // allways connect the signal in case the setting is toggled on and off auto gSetting = APPLICATION->settings()->getOrRegisterSetting("UI/FolderResourceColumnVisibility"); connect(gSetting.get(), &Setting::SettingChanged, tree, [this, setVisible](const Setting&, QVariant value) { if (!m_instance->settings()->get("UI/ColumnsOverride").toBool()) { - setVisible(value.toMap()); + setVisible(value); } }); } diff --git a/launcher/settings/INIFile.cpp b/launcher/settings/INIFile.cpp index 8403642fa..75e888938 100644 --- a/launcher/settings/INIFile.cpp +++ b/launcher/settings/INIFile.cpp @@ -44,6 +44,7 @@ #include #include +#include "Json.h" INIFile::INIFile() {} @@ -151,16 +152,22 @@ bool parseOldFileFormat(QIODevice& device, QSettings::SettingsMap& map) QVariant migrateQByteArrayToBase64(QString key, QVariant value) { - if (key.startsWith("WideBarVisibility_") || (key.startsWith("UI/") && key.endsWith("_Page/Columns"))) { - return QString::fromUtf8(value.toByteArray().toBase64()); - } static const QStringList otherByteArrays = { "MainWindowState", "MainWindowGeometry", "ConsoleWindowState", "ConsoleWindowGeometry", "PagedGeometry", "NewInstanceGeometry", "ModDownloadGeometry", "RPDownloadGeometry", "TPDownloadGeometry", "ShaderDownloadGeometry" }; + if (key.startsWith("WideBarVisibility_") || (key.startsWith("UI/") && key.endsWith("_Page/Columns"))) { + return QString::fromUtf8(value.toByteArray().toBase64()); + } if (otherByteArrays.contains(key)) { return QString::fromUtf8(value.toByteArray()); } + if (key == "linkedInstances") { + return Json::fromStringList(value.toStringList()); + } + if (key == "Env") { + return Json::fromMap(value.toMap()); + } return value; } diff --git a/launcher/ui/widgets/MinecraftSettingsWidget.cpp b/launcher/ui/widgets/MinecraftSettingsWidget.cpp index 610dc143b..8ec5640c3 100644 --- a/launcher/ui/widgets/MinecraftSettingsWidget.cpp +++ b/launcher/ui/widgets/MinecraftSettingsWidget.cpp @@ -180,7 +180,7 @@ void MinecraftSettingsWidget::loadSettings() // Environment variables m_ui->environmentVariables->initialize(m_instance != nullptr, m_instance == nullptr || settings->get("OverrideEnv").toBool(), - settings->get("Env").toMap()); + Json::toMap(settings->get("Env").toString())); // Legacy Tweaks m_ui->legacySettingsGroupBox->setChecked(m_instance == nullptr || settings->get("OverrideLegacySettings").toBool()); @@ -342,7 +342,7 @@ void MinecraftSettingsWidget::saveSettings() settings->set("OverrideEnv", env); if (env) - settings->set("Env", m_ui->environmentVariables->value()); + settings->set("Env", Json::fromMap(m_ui->environmentVariables->value())); else settings->reset("Env"); From a29b189056c8742151e35ad6b4ff4070c48c46f5 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Thu, 22 May 2025 23:55:57 +0300 Subject: [PATCH 284/695] feat: make the visibility per resource Signed-off-by: Trial97 --- launcher/Application.cpp | 2 - launcher/minecraft/MinecraftInstance.cpp | 3 -- .../minecraft/mod/ResourceFolderModel.cpp | 46 ++++++++++++------- 3 files changed, 29 insertions(+), 22 deletions(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 1a7f2d9d3..f69520907 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -816,8 +816,6 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_settings->registerSetting("TPDownloadGeometry", ""); m_settings->registerSetting("ShaderDownloadGeometry", ""); - m_settings->registerSetting("UI/FolderResourceColumnVisibility", "{}"); - // HACK: This code feels so stupid is there a less stupid way of doing this? { m_settings->registerSetting("PastebinURL", ""); diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index 6df86bbed..5cf7047e0 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -232,9 +232,6 @@ void MinecraftInstance::loadSpecificSettings() auto envSetting = m_settings->registerSetting("OverrideEnv", false); m_settings->registerOverride(global_settings->getSetting("Env"), envSetting); - m_settings->registerSetting("UI/ColumnsOverride", false); - m_settings->registerSetting("UI/FolderResourceColumnVisibility", "{}"); - m_settings->set("InstanceType", "OneSix"); } diff --git a/launcher/minecraft/mod/ResourceFolderModel.cpp b/launcher/minecraft/mod/ResourceFolderModel.cpp index 7b745b46f..cbf6f96a0 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.cpp +++ b/launcher/minecraft/mod/ResourceFolderModel.cpp @@ -587,32 +587,36 @@ void ResourceFolderModel::setupHeaderAction(QAction* act, int column) void ResourceFolderModel::saveColumns(QTreeView* tree) { - auto const settingName = QString("UI/%1_Page/Columns").arg(id()); - auto setting = m_instance->settings()->getSetting(settingName); + auto const stateSettingName = QString("UI/%1_Page/Columns").arg(id()); + auto const overrideSettingName = QString("UI/%1_Page/ColumnsOverride").arg(id()); + auto const visibilitySettingName = QString("UI/%1_Page/ColumnsVisibility").arg(id()); - setting->set(QString::fromUtf8(tree->header()->saveState().toBase64())); + auto stateSetting = m_instance->settings()->getSetting(stateSettingName); + stateSetting->set(QString::fromUtf8(tree->header()->saveState().toBase64())); // neither passthrough nor override settings works for this usecase as I need to only set the global when the gate is false auto settings = m_instance->settings(); - if (!settings->get("UI/ColumnsOverride").toBool()) { + if (!settings->get(overrideSettingName).toBool()) { settings = APPLICATION->settings(); } - auto visibility = Json::toMap(settings->get("UI/FolderResourceColumnVisibility").toString()); + auto visibility = Json::toMap(settings->get(visibilitySettingName).toString()); for (auto i = 0; i < m_column_names.size(); ++i) { if (m_columnsHideable[i]) { auto name = m_column_names[i]; visibility[name] = !tree->isColumnHidden(i); } } - settings->set("UI/FolderResourceColumnVisibility", Json::fromMap(visibility)); + settings->set(visibilitySettingName, Json::fromMap(visibility)); } void ResourceFolderModel::loadColumns(QTreeView* tree) { - auto const settingName = QString("UI/%1_Page/Columns").arg(id()); + auto const stateSettingName = QString("UI/%1_Page/Columns").arg(id()); + auto const overrideSettingName = QString("UI/%1_Page/ColumnsOverride").arg(id()); + auto const visibilitySettingName = QString("UI/%1_Page/ColumnsVisibility").arg(id()); - auto setting = m_instance->settings()->getOrRegisterSetting(settingName, ""); - tree->header()->restoreState(QByteArray::fromBase64(setting->get().toString().toUtf8())); + auto stateSetting = m_instance->settings()->getOrRegisterSetting(stateSettingName, ""); + tree->header()->restoreState(QByteArray::fromBase64(stateSetting->get().toString().toUtf8())); auto setVisible = [this, tree](QVariant value) { auto visibility = Json::toMap(value.toString()); @@ -624,18 +628,25 @@ void ResourceFolderModel::loadColumns(QTreeView* tree) } }; + auto const defaultValue = Json::fromMap({ + { "Image", true }, + { "Version", true }, + { "Last Modified", true }, + { "Provider", true }, + { "Pack Format", true }, + }); // neither passthrough nor override settings works for this usecase as I need to only set the global when the gate is false auto settings = m_instance->settings(); - if (!settings->get("UI/ColumnsOverride").toBool()) { + if (!settings->getOrRegisterSetting(overrideSettingName, false)->get().toBool()) { settings = APPLICATION->settings(); } - auto visibility = settings->getSetting("UI/FolderResourceColumnVisibility"); + auto visibility = settings->getOrRegisterSetting(visibilitySettingName, defaultValue); setVisible(visibility->get()); // allways connect the signal in case the setting is toggled on and off - auto gSetting = APPLICATION->settings()->getOrRegisterSetting("UI/FolderResourceColumnVisibility"); - connect(gSetting.get(), &Setting::SettingChanged, tree, [this, setVisible](const Setting&, QVariant value) { - if (!m_instance->settings()->get("UI/ColumnsOverride").toBool()) { + auto gSetting = APPLICATION->settings()->getOrRegisterSetting(visibilitySettingName, defaultValue); + connect(gSetting.get(), &Setting::SettingChanged, tree, [this, setVisible, overrideSettingName](const Setting&, QVariant value) { + if (!m_instance->settings()->get(overrideSettingName).toBool()) { setVisible(value); } }); @@ -647,12 +658,13 @@ QMenu* ResourceFolderModel::createHeaderContextMenu(QTreeView* tree) { // action to decide if the visibility is per instance or not auto act = new QAction(tr("Overide Columns Visibility"), menu); + auto const overrideSettingName = QString("UI/%1_Page/ColumnsOverride").arg(id()); act->setCheckable(true); - act->setChecked(m_instance->settings()->get("UI/ColumnsOverride").toBool()); + act->setChecked(m_instance->settings()->getOrRegisterSetting(overrideSettingName, false)->get().toBool()); - connect(act, &QAction::toggled, tree, [this, tree](bool toggled) { - m_instance->settings()->set("UI/ColumnsOverride", toggled); + connect(act, &QAction::toggled, tree, [this, tree, overrideSettingName](bool toggled) { + m_instance->settings()->set(overrideSettingName, toggled); saveColumns(tree); }); From 609a4f71606edbd16ff3708091406aeb53219462 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Sat, 24 May 2025 10:47:16 +0100 Subject: [PATCH 285/695] Fix spelling of override Signed-off-by: TheKodeToad --- launcher/minecraft/mod/ResourceFolderModel.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/minecraft/mod/ResourceFolderModel.cpp b/launcher/minecraft/mod/ResourceFolderModel.cpp index cbf6f96a0..ed33ed5cd 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.cpp +++ b/launcher/minecraft/mod/ResourceFolderModel.cpp @@ -657,7 +657,7 @@ QMenu* ResourceFolderModel::createHeaderContextMenu(QTreeView* tree) auto menu = new QMenu(tree); { // action to decide if the visibility is per instance or not - auto act = new QAction(tr("Overide Columns Visibility"), menu); + auto act = new QAction(tr("Override Columns Visibility"), menu); auto const overrideSettingName = QString("UI/%1_Page/ColumnsOverride").arg(id()); act->setCheckable(true); From 5ccdb0a477482963d3a30a22694a44e76bb6caad Mon Sep 17 00:00:00 2001 From: Yihe Li Date: Mon, 2 Jun 2025 17:00:13 +0800 Subject: [PATCH 286/695] Remove several warnings when building the project Signed-off-by: Yihe Li --- launcher/NullInstance.h | 2 +- launcher/java/JavaUtils.cpp | 2 +- launcher/minecraft/MinecraftInstance.cpp | 2 +- launcher/tools/MCEditTool.cpp | 2 +- launcher/ui/pages/global/ExternalToolsPage.cpp | 2 +- launcher/ui/pages/instance/ServerPingTask.cpp | 4 ++-- launcher/ui/pages/modplatform/import_ftb/ListModel.cpp | 2 +- launcher/updater/MacSparkleUpdater.mm | 2 +- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/launcher/NullInstance.h b/launcher/NullInstance.h index e603b1634..13ad0b2c5 100644 --- a/launcher/NullInstance.h +++ b/launcher/NullInstance.h @@ -70,7 +70,7 @@ class NullInstance : public BaseInstance { return out; } QString modsRoot() const override { return QString(); } - void updateRuntimeContext() + void updateRuntimeContext() override { // NOOP } diff --git a/launcher/java/JavaUtils.cpp b/launcher/java/JavaUtils.cpp index 2d0560049..4f2ee3629 100644 --- a/launcher/java/JavaUtils.cpp +++ b/launcher/java/JavaUtils.cpp @@ -501,7 +501,7 @@ QString JavaUtils::getJavaCheckPath() QStringList getMinecraftJavaBundle() { QStringList processpaths; -#if defined(Q_OS_OSX) +#if defined(Q_OS_MACOS) processpaths << FS::PathCombine(QDir::homePath(), FS::PathCombine("Library", "Application Support", "minecraft", "runtime")); #elif defined(Q_OS_WIN32) diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index fafe7bd37..487f9c997 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -104,7 +104,7 @@ #define IBUS "@im=ibus" -static bool switcherooSetupGPU(QProcessEnvironment& env) +[[maybe_unused]] static bool switcherooSetupGPU(QProcessEnvironment& env) { #ifdef WITH_QTDBUS if (!QDBusConnection::systemBus().isConnected()) diff --git a/launcher/tools/MCEditTool.cpp b/launcher/tools/MCEditTool.cpp index 19bd5a062..e006a1411 100644 --- a/launcher/tools/MCEditTool.cpp +++ b/launcher/tools/MCEditTool.cpp @@ -45,7 +45,7 @@ bool MCEditTool::check(const QString& toolPath, QString& error) QString MCEditTool::getProgramPath() { -#ifdef Q_OS_OSX +#ifdef Q_OS_MACOS return path(); #else const QString mceditPath = path(); diff --git a/launcher/ui/pages/global/ExternalToolsPage.cpp b/launcher/ui/pages/global/ExternalToolsPage.cpp index 4110bd5ad..470704d29 100644 --- a/launcher/ui/pages/global/ExternalToolsPage.cpp +++ b/launcher/ui/pages/global/ExternalToolsPage.cpp @@ -156,7 +156,7 @@ void ExternalToolsPage::on_mceditPathBtn_clicked() QString raw_dir = ui->mceditPathEdit->text(); QString error; do { -#ifdef Q_OS_OSX +#ifdef Q_OS_MACOS raw_dir = QFileDialog::getOpenFileName(this, tr("MCEdit Application"), raw_dir); #else raw_dir = QFileDialog::getExistingDirectory(this, tr("MCEdit Folder"), raw_dir); diff --git a/launcher/ui/pages/instance/ServerPingTask.cpp b/launcher/ui/pages/instance/ServerPingTask.cpp index b39f3d117..4a9215ce5 100644 --- a/launcher/ui/pages/instance/ServerPingTask.cpp +++ b/launcher/ui/pages/instance/ServerPingTask.cpp @@ -16,7 +16,7 @@ void ServerPingTask::executeTask() // Resolve the actual IP and port for the server McResolver* resolver = new McResolver(nullptr, m_domain, m_port); - QObject::connect(resolver, &McResolver::succeeded, this, [this, resolver](QString ip, int port) { + QObject::connect(resolver, &McResolver::succeeded, this, [this](QString ip, int port) { qDebug() << "Resolved Address for" << m_domain << ": " << ip << ":" << port; // Now that we have the IP and port, query the server @@ -30,7 +30,7 @@ void ServerPingTask::executeTask() QObject::connect(client, &McClient::failed, this, [this](QString error) { emitFailed(error); }); // Delete McClient object when done - QObject::connect(client, &McClient::finished, this, [this, client]() { client->deleteLater(); }); + QObject::connect(client, &McClient::finished, this, [client]() { client->deleteLater(); }); client->getStatusData(); }); QObject::connect(resolver, &McResolver::failed, this, [this](QString error) { emitFailed(error); }); diff --git a/launcher/ui/pages/modplatform/import_ftb/ListModel.cpp b/launcher/ui/pages/modplatform/import_ftb/ListModel.cpp index 06836c3c5..f99ac8d65 100644 --- a/launcher/ui/pages/modplatform/import_ftb/ListModel.cpp +++ b/launcher/ui/pages/modplatform/import_ftb/ListModel.cpp @@ -36,7 +36,7 @@ namespace FTBImportAPP { QString getFTBRoot() { QString partialPath = QDir::homePath(); -#if defined(Q_OS_OSX) +#if defined(Q_OS_MACOS) partialPath = FS::PathCombine(partialPath, "Library/Application Support"); #endif return FS::PathCombine(partialPath, ".ftba"); diff --git a/launcher/updater/MacSparkleUpdater.mm b/launcher/updater/MacSparkleUpdater.mm index 07862c9a3..c54708ee1 100644 --- a/launcher/updater/MacSparkleUpdater.mm +++ b/launcher/updater/MacSparkleUpdater.mm @@ -166,7 +166,7 @@ @implementation UpdaterDelegate QString channelsConfig = ""; // Convert QSet -> NSSet NSMutableSet* nsChannels = [NSMutableSet setWithCapacity:channels.count()]; - for (const QString channel : channels) { + for (const QString& channel : channels) { [nsChannels addObject:channel.toNSString()]; channelsConfig += channel + " "; } From be963764eaa5e3721cc2d96598d10d22bdae9000 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Tue, 25 Mar 2025 00:09:15 +0200 Subject: [PATCH 287/695] propagate side as enum instead of Qstring Signed-off-by: Trial97 --- launcher/minecraft/mod/MetadataHandler.h | 27 +-------- launcher/minecraft/mod/Mod.cpp | 4 +- launcher/modplatform/ModIndex.cpp | 25 +++++++++ launcher/modplatform/ModIndex.h | 11 +++- launcher/modplatform/ResourceAPI.h | 2 +- launcher/modplatform/flame/FlameModIndex.cpp | 10 ++-- launcher/modplatform/modrinth/ModrinthAPI.h | 22 ++++---- .../modrinth/ModrinthPackExportTask.cpp | 3 +- .../modrinth/ModrinthPackExportTask.h | 3 +- .../modrinth/ModrinthPackIndex.cpp | 6 +- launcher/modplatform/packwiz/Packwiz.cpp | 55 +------------------ launcher/modplatform/packwiz/Packwiz.h | 17 +----- launcher/ui/pages/modplatform/ModModel.cpp | 6 +- .../ui/pages/modplatform/flame/FlameModel.cpp | 5 +- .../modplatform/modrinth/ModrinthModel.cpp | 5 +- launcher/ui/widgets/ModFilterWidget.cpp | 12 ++-- launcher/ui/widgets/ModFilterWidget.h | 2 +- tests/Packwiz_test.cpp | 5 +- 18 files changed, 89 insertions(+), 131 deletions(-) diff --git a/launcher/minecraft/mod/MetadataHandler.h b/launcher/minecraft/mod/MetadataHandler.h index 0b8cb124d..5f12348ae 100644 --- a/launcher/minecraft/mod/MetadataHandler.h +++ b/launcher/minecraft/mod/MetadataHandler.h @@ -19,27 +19,16 @@ #pragma once -#include - #include "modplatform/packwiz/Packwiz.h" -// launcher/minecraft/mod/Mod.h -class Mod; - namespace Metadata { using ModStruct = Packwiz::V1::Mod; -using ModSide = Packwiz::V1::Side; -inline auto create(const QDir& index_dir, ModPlatform::IndexedPack& mod_pack, ModPlatform::IndexedVersion& mod_version) -> ModStruct +inline ModStruct create(const QDir& index_dir, ModPlatform::IndexedPack& mod_pack, ModPlatform::IndexedVersion& mod_version) { return Packwiz::V1::createModFormat(index_dir, mod_pack, mod_version); } -inline auto create(const QDir& index_dir, Mod& internal_mod, QString mod_slug) -> ModStruct -{ - return Packwiz::V1::createModFormat(index_dir, internal_mod, std::move(mod_slug)); -} - inline void update(const QDir& index_dir, ModStruct& mod) { Packwiz::V1::updateModIndex(index_dir, mod); @@ -50,24 +39,14 @@ inline void remove(const QDir& index_dir, QString mod_slug) Packwiz::V1::deleteModIndex(index_dir, mod_slug); } -inline void remove(const QDir& index_dir, QVariant& mod_id) -{ - Packwiz::V1::deleteModIndex(index_dir, mod_id); -} - -inline auto get(const QDir& index_dir, QString mod_slug) -> ModStruct +inline ModStruct get(const QDir& index_dir, QString mod_slug) { return Packwiz::V1::getIndexForMod(index_dir, std::move(mod_slug)); } -inline auto get(const QDir& index_dir, QVariant& mod_id) -> ModStruct +inline ModStruct get(const QDir& index_dir, QVariant& mod_id) { return Packwiz::V1::getIndexForMod(index_dir, mod_id); } -inline auto modSideToString(ModSide side) -> QString -{ - return Packwiz::V1::sideToString(side); -} - }; // namespace Metadata diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp index 99fc39ce0..e9ca2e682 100644 --- a/launcher/minecraft/mod/Mod.cpp +++ b/launcher/minecraft/mod/Mod.cpp @@ -179,9 +179,9 @@ auto Mod::loaders() const -> QString auto Mod::side() const -> QString { if (metadata()) - return Metadata::modSideToString(metadata()->side); + return ModPlatform::SideUtils::toString(metadata()->side); - return Metadata::modSideToString(Metadata::ModSide::UniversalSide); + return ModPlatform::SideUtils::toString(ModPlatform::Side::UniversalSide); } auto Mod::mcVersions() const -> QString diff --git a/launcher/modplatform/ModIndex.cpp b/launcher/modplatform/ModIndex.cpp index 380ff660f..e18ccaefa 100644 --- a/launcher/modplatform/ModIndex.cpp +++ b/launcher/modplatform/ModIndex.cpp @@ -152,4 +152,29 @@ auto getModLoaderFromString(QString type) -> ModLoaderType return {}; } +QString SideUtils::toString(Side side) +{ + switch (side) { + case Side::ClientSide: + return "client"; + case Side::ServerSide: + return "server"; + case Side::UniversalSide: + return "both"; + case Side::NoSide: + break; + } + return {}; +} + +Side SideUtils::fromString(QString side) +{ + if (side == "client") + return Side::ClientSide; + if (side == "server") + return Side::ServerSide; + if (side == "both") + return Side::UniversalSide; + return Side::UniversalSide; +} } // namespace ModPlatform diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h index ad2503ea7..cfe4eba75 100644 --- a/launcher/modplatform/ModIndex.h +++ b/launcher/modplatform/ModIndex.h @@ -47,6 +47,13 @@ enum class ResourceType { MOD, RESOURCE_PACK, SHADER_PACK, MODPACK, DATA_PACK }; enum class DependencyType { REQUIRED, OPTIONAL, INCOMPATIBLE, EMBEDDED, TOOL, INCLUDE, UNKNOWN }; +enum class Side { NoSide = 0, ClientSide = 1 << 0, ServerSide = 1 << 1, UniversalSide = ClientSide | ServerSide }; + +namespace SideUtils { +QString toString(Side side); +Side fromString(QString side); +} // namespace SideUtils + namespace ProviderCapabilities { const char* name(ResourceProvider); QString readableName(ResourceProvider); @@ -114,7 +121,7 @@ struct IndexedVersion { bool is_preferred = true; QString changelog; QList dependencies; - QString side; // this is for flame API + Side side; // this is for flame API // For internal use, not provided by APIs bool is_currently_selected = false; @@ -145,7 +152,7 @@ struct IndexedPack { QString logoName; QString logoUrl; QString websiteUrl; - QString side; + Side side; bool versionsLoaded = false; QList versions; diff --git a/launcher/modplatform/ResourceAPI.h b/launcher/modplatform/ResourceAPI.h index 62a1ff199..bd6b90227 100644 --- a/launcher/modplatform/ResourceAPI.h +++ b/launcher/modplatform/ResourceAPI.h @@ -74,7 +74,7 @@ class ResourceAPI { std::optional sorting; std::optional loaders; std::optional> versions; - std::optional side; + std::optional side; std::optional categoryIds; bool openSource; }; diff --git a/launcher/modplatform/flame/FlameModIndex.cpp b/launcher/modplatform/flame/FlameModIndex.cpp index c1b9e67af..660dc159c 100644 --- a/launcher/modplatform/flame/FlameModIndex.cpp +++ b/launcher/modplatform/flame/FlameModIndex.cpp @@ -4,6 +4,7 @@ #include "Json.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" +#include "modplatform/ModIndex.h" #include "modplatform/flame/FlameAPI.h" static FlameAPI api; @@ -110,6 +111,7 @@ auto FlameMod::loadIndexedPackVersion(QJsonObject& obj, bool load_changelog) -> if (str.contains('.')) file.mcVersion.append(str); + file.side = ModPlatform::Side::NoSide; if (auto loader = str.toLower(); loader == "neoforge") file.loaders |= ModPlatform::NeoForge; else if (loader == "forge") @@ -123,10 +125,10 @@ auto FlameMod::loadIndexedPackVersion(QJsonObject& obj, bool load_changelog) -> else if (loader == "quilt") file.loaders |= ModPlatform::Quilt; else if (loader == "server" || loader == "client") { - if (file.side.isEmpty()) - file.side = loader; - else if (file.side != loader) - file.side = "both"; + if (file.side == ModPlatform::Side::NoSide) + file.side = ModPlatform::SideUtils::fromString(loader); + else if (file.side != ModPlatform::SideUtils::fromString(loader)) + file.side = ModPlatform::Side::UniversalSide; } } diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index 17b23723b..7c2592256 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -69,18 +69,20 @@ class ModrinthAPI : public NetworkResourceAPI { return l.join(','); } - static auto getSideFilters(QString side) -> const QString + static QString getSideFilters(ModPlatform::Side side) { - if (side.isEmpty()) { - return {}; + switch (side) { + case ModPlatform::Side::ClientSide: + return QString("\"client_side:required\",\"client_side:optional\"],[\"server_side:optional\",\"server_side:unsupported\""); + case ModPlatform::Side::ServerSide: + return QString("\"server_side:required\",\"server_side:optional\"],[\"client_side:optional\",\"client_side:unsupported\""); + case ModPlatform::Side::UniversalSide: + return QString("\"client_side:required\"],[\"server_side:required\""); + case ModPlatform::Side::NoSide: + // fallthrough + default: + return {}; } - if (side == "both") - return QString("\"client_side:required\"],[\"server_side:required\""); - if (side == "client") - return QString("\"client_side:required\",\"client_side:optional\"],[\"server_side:optional\",\"server_side:unsupported\""); - if (side == "server") - return QString("\"server_side:required\",\"server_side:optional\"],[\"client_side:optional\",\"client_side:unsupported\""); - return {}; } [[nodiscard]] static inline QString mapMCVersionFromModrinth(QString v) diff --git a/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp b/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp index 4b19acd3f..9ee4101e6 100644 --- a/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackExportTask.cpp @@ -28,6 +28,7 @@ #include "minecraft/PackProfile.h" #include "minecraft/mod/MetadataHandler.h" #include "minecraft/mod/ModFolderModel.h" +#include "modplatform/ModIndex.h" #include "modplatform/helpers/HashUtils.h" #include "tasks/Task.h" @@ -289,7 +290,7 @@ QByteArray ModrinthPackExportTask::generateIndex() // a server side mod does not imply that the mod does not work on the client // however, if a mrpack mod is marked as server-only it will not install on the client - if (iterator->side == Metadata::ModSide::ClientSide) + if (iterator->side == ModPlatform::Side::ClientSide) env["server"] = "unsupported"; fileOut["env"] = env; diff --git a/launcher/modplatform/modrinth/ModrinthPackExportTask.h b/launcher/modplatform/modrinth/ModrinthPackExportTask.h index ec4730de5..f9b86bbd7 100644 --- a/launcher/modplatform/modrinth/ModrinthPackExportTask.h +++ b/launcher/modplatform/modrinth/ModrinthPackExportTask.h @@ -23,6 +23,7 @@ #include "BaseInstance.h" #include "MMCZip.h" #include "minecraft/MinecraftInstance.h" +#include "modplatform/ModIndex.h" #include "modplatform/modrinth/ModrinthAPI.h" #include "tasks/Task.h" @@ -45,7 +46,7 @@ class ModrinthPackExportTask : public Task { struct ResolvedFile { QString sha1, sha512, url; qint64 size; - Metadata::ModSide side; + ModPlatform::Side side; }; static const QStringList PREFIXES; diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp index 744b058c0..42fda9df1 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp @@ -63,11 +63,11 @@ void Modrinth::loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) auto server = shouldDownloadOnSide(Json::ensureString(obj, "server_side")); if (server && client) { - pack.side = "both"; + pack.side = ModPlatform::Side::UniversalSide; } else if (server) { - pack.side = "server"; + pack.side = ModPlatform::Side::ServerSide; } else if (client) { - pack.side = "client"; + pack.side = ModPlatform::Side::ClientSide; } // Modrinth can have more data than what's provided by the basic search :) diff --git a/launcher/modplatform/packwiz/Packwiz.cpp b/launcher/modplatform/packwiz/Packwiz.cpp index a3bb74399..0660d611c 100644 --- a/launcher/modplatform/packwiz/Packwiz.cpp +++ b/launcher/modplatform/packwiz/Packwiz.cpp @@ -28,7 +28,6 @@ #include "FileSystem.h" #include "StringUtils.h" -#include "minecraft/mod/Mod.h" #include "modplatform/ModIndex.h" #include @@ -113,7 +112,7 @@ auto V1::createModFormat([[maybe_unused]] const QDir& index_dir, mod.provider = mod_pack.provider; mod.file_id = mod_version.fileId; mod.project_id = mod_pack.addonId; - mod.side = stringToSide(mod_version.side.isEmpty() ? mod_pack.side : mod_version.side); + mod.side = mod_version.side == ModPlatform::Side::NoSide ? mod_pack.side : mod_version.side; mod.loaders = mod_version.loaders; mod.mcVersions = mod_version.mcVersion; mod.mcVersions.sort(); @@ -126,18 +125,6 @@ auto V1::createModFormat([[maybe_unused]] const QDir& index_dir, return mod; } -auto V1::createModFormat(const QDir& index_dir, [[maybe_unused]] ::Mod& internal_mod, QString slug) -> Mod -{ - // Try getting metadata if it exists - Mod mod{ getIndexForMod(index_dir, slug) }; - if (mod.isValid()) - return mod; - - qWarning() << QString("Tried to create mod metadata with a Mod without metadata!"); - - return {}; -} - void V1::updateModIndex(const QDir& index_dir, Mod& mod) { if (!mod.isValid()) { @@ -208,7 +195,7 @@ void V1::updateModIndex(const QDir& index_dir, Mod& mod) { auto tbl = toml::table{ { "name", mod.name.toStdString() }, { "filename", mod.filename.toStdString() }, - { "side", sideToString(mod.side).toStdString() }, + { "side", ModPlatform::SideUtils::toString(mod.side).toStdString() }, { "x-prismlauncher-loaders", loaders }, { "x-prismlauncher-mc-versions", mcVersions }, { "x-prismlauncher-release-type", mod.releaseType.toString().toStdString() }, @@ -249,18 +236,6 @@ void V1::deleteModIndex(const QDir& index_dir, QString& mod_slug) } } -void V1::deleteModIndex(const QDir& index_dir, QVariant& mod_id) -{ - for (auto& file_name : index_dir.entryList(QDir::Filter::Files)) { - auto mod = getIndexForMod(index_dir, file_name); - - if (mod.mod_id() == mod_id) { - deleteModIndex(index_dir, mod.name); - break; - } - } -} - auto V1::getIndexForMod(const QDir& index_dir, QString slug) -> Mod { Mod mod; @@ -296,7 +271,7 @@ auto V1::getIndexForMod(const QDir& index_dir, QString slug) -> Mod { // Basic info mod.name = stringEntry(table, "name"); mod.filename = stringEntry(table, "filename"); - mod.side = stringToSide(stringEntry(table, "side")); + mod.side = ModPlatform::SideUtils::fromString(stringEntry(table, "side")); mod.releaseType = ModPlatform::IndexedVersionType(table["x-prismlauncher-release-type"].value_or("")); if (auto loaders = table["x-prismlauncher-loaders"]; loaders && loaders.is_array()) { for (auto&& loader : *loaders.as_array()) { @@ -371,28 +346,4 @@ auto V1::getIndexForMod(const QDir& index_dir, QVariant& mod_id) -> Mod return {}; } -auto V1::sideToString(Side side) -> QString -{ - switch (side) { - case Side::ClientSide: - return "client"; - case Side::ServerSide: - return "server"; - case Side::UniversalSide: - return "both"; - } - return {}; -} - -auto V1::stringToSide(QString side) -> Side -{ - if (side == "client") - return Side::ClientSide; - if (side == "server") - return Side::ServerSide; - if (side == "both") - return Side::UniversalSide; - return Side::UniversalSide; -} - } // namespace Packwiz diff --git a/launcher/modplatform/packwiz/Packwiz.h b/launcher/modplatform/packwiz/Packwiz.h index 44896e74c..ba9a0fe75 100644 --- a/launcher/modplatform/packwiz/Packwiz.h +++ b/launcher/modplatform/packwiz/Packwiz.h @@ -27,23 +27,18 @@ class QDir; -// Mod from launcher/minecraft/mod/Mod.h -class Mod; - namespace Packwiz { auto getRealIndexName(const QDir& index_dir, QString normalized_index_name, bool should_match = false) -> QString; class V1 { public: - enum class Side { ClientSide = 1 << 0, ServerSide = 1 << 1, UniversalSide = ClientSide | ServerSide }; - // can also represent other resources beside loader mods - but this is what packwiz calls it struct Mod { QString slug{}; QString name{}; QString filename{}; - Side side{ Side::UniversalSide }; + ModPlatform::Side side{ ModPlatform::Side::UniversalSide }; ModPlatform::ModLoaderTypes loaders; QStringList mcVersions; ModPlatform::IndexedVersionType releaseType; @@ -74,10 +69,6 @@ class V1 { * its common representation in the launcher, when downloading mods. * */ static auto createModFormat(const QDir& index_dir, ModPlatform::IndexedPack& mod_pack, ModPlatform::IndexedVersion& mod_version) -> Mod; - /* Generates the object representing the information in a mod.pw.toml file via - * its common representation in the launcher, plus a necessary slug. - * */ - static auto createModFormat(const QDir& index_dir, ::Mod& internal_mod, QString slug) -> Mod; /* Updates the mod index for the provided mod. * This creates a new index if one does not exist already @@ -88,9 +79,6 @@ class V1 { /* Deletes the metadata for the mod with the given slug. If the metadata doesn't exist, it does nothing. */ static void deleteModIndex(const QDir& index_dir, QString& mod_slug); - /* Deletes the metadata for the mod with the given id. If the metadata doesn't exist, it does nothing. */ - static void deleteModIndex(const QDir& index_dir, QVariant& mod_id); - /* Gets the metadata for a mod with a particular file name. * If the mod doesn't have a metadata, it simply returns an empty Mod object. * */ @@ -100,9 +88,6 @@ class V1 { * If the mod doesn't have a metadata, it simply returns an empty Mod object. * */ static auto getIndexForMod(const QDir& index_dir, QVariant& mod_id) -> Mod; - - static auto sideToString(Side side) -> QString; - static auto stringToSide(QString side) -> Side; }; } // namespace Packwiz diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp index 6e98a88bc..32e6f2146 100644 --- a/launcher/ui/pages/modplatform/ModModel.cpp +++ b/launcher/ui/pages/modplatform/ModModel.cpp @@ -7,6 +7,7 @@ #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" #include "minecraft/mod/ModFolderModel.h" +#include "modplatform/ModIndex.h" #include #include @@ -101,9 +102,10 @@ QVariant ModModel::getInstalledPackVersion(ModPlatform::IndexedPack::Ptr pack) c return {}; } -bool checkSide(QString filter, QString value) +bool checkSide(ModPlatform::Side filter, ModPlatform::Side value) { - return filter.isEmpty() || value.isEmpty() || filter == "both" || value == "both" || filter == value; + return filter == ModPlatform::Side::NoSide || value == ModPlatform::Side::NoSide || filter == ModPlatform::Side::UniversalSide || + value == ModPlatform::Side::UniversalSide || filter == value; } bool ModModel::checkFilters(ModPlatform::IndexedPack::Ptr pack) diff --git a/launcher/ui/pages/modplatform/flame/FlameModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModel.cpp index d501bf9f4..5f254597d 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModel.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModel.cpp @@ -186,8 +186,9 @@ void ListModel::performPaginatedSearch() sort.index = currentSort + 1; auto netJob = makeShared("Flame::Search", APPLICATION->network()); - auto searchUrl = FlameAPI().getSearchURL({ ModPlatform::ResourceType::MODPACK, nextSearchOffset, currentSearchTerm, sort, - m_filter->loaders, m_filter->versions, "", m_filter->categoryIds, m_filter->openSource }); + auto searchUrl = + FlameAPI().getSearchURL({ ModPlatform::ResourceType::MODPACK, nextSearchOffset, currentSearchTerm, sort, m_filter->loaders, + m_filter->versions, ModPlatform::Side::NoSide, m_filter->categoryIds, m_filter->openSource }); netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(searchUrl.value()), response)); jobPtr = netJob; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index 4681b1a7f..ae8e05a6f 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -152,8 +152,9 @@ void ModpackListModel::performPaginatedSearch() } // TODO: Move to standalone API ResourceAPI::SortingMethod sort{}; sort.name = currentSort; - auto searchUrl = ModrinthAPI().getSearchURL({ ModPlatform::ResourceType::MODPACK, nextSearchOffset, currentSearchTerm, sort, - m_filter->loaders, m_filter->versions, "", m_filter->categoryIds, m_filter->openSource }); + auto searchUrl = + ModrinthAPI().getSearchURL({ ModPlatform::ResourceType::MODPACK, nextSearchOffset, currentSearchTerm, sort, m_filter->loaders, + m_filter->versions, ModPlatform::Side::NoSide, m_filter->categoryIds, m_filter->openSource }); auto netJob = makeShared("Modrinth::SearchModpack", APPLICATION->network()); netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(searchUrl.value()), m_allResponse)); diff --git a/launcher/ui/widgets/ModFilterWidget.cpp b/launcher/ui/widgets/ModFilterWidget.cpp index 031ff0f94..6fbaf5944 100644 --- a/launcher/ui/widgets/ModFilterWidget.cpp +++ b/launcher/ui/widgets/ModFilterWidget.cpp @@ -218,7 +218,7 @@ void ModFilterWidget::prepareBasicFilter() m_filter->openSource = false; if (m_instance) { m_filter->hideInstalled = false; - m_filter->side = ""; // or "both" + m_filter->side = ModPlatform::Side::NoSide; // or "both" ModPlatform::ModLoaderTypes loaders; if (m_instance->settings()->get("OverrideModDownloadLoaders").toBool()) { for (auto loader : Json::toStringList(m_instance->settings()->get("ModDownloadLoaders").toString())) { @@ -287,16 +287,16 @@ void ModFilterWidget::onLoadersFilterChanged() void ModFilterWidget::onSideFilterChanged() { - QString side; + ModPlatform::Side side; if (ui->clientSide->isChecked() && !ui->serverSide->isChecked()) { - side = "client"; + side = ModPlatform::Side::ClientSide; } else if (!ui->clientSide->isChecked() && ui->serverSide->isChecked()) { - side = "server"; + side = ModPlatform::Side::ServerSide; } else if (ui->clientSide->isChecked() && ui->serverSide->isChecked()) { - side = "both"; + side = ModPlatform::Side::UniversalSide; } else { - side = ""; + side = ModPlatform::Side::NoSide; } m_filter_changed = side != m_filter->side; diff --git a/launcher/ui/widgets/ModFilterWidget.h b/launcher/ui/widgets/ModFilterWidget.h index 88f2593dd..be60ba70a 100644 --- a/launcher/ui/widgets/ModFilterWidget.h +++ b/launcher/ui/widgets/ModFilterWidget.h @@ -61,7 +61,7 @@ class ModFilterWidget : public QTabWidget { std::list versions; std::list releases; ModPlatform::ModLoaderTypes loaders; - QString side; + ModPlatform::Side side; bool hideInstalled; QStringList categoryIds; bool openSource; diff --git a/tests/Packwiz_test.cpp b/tests/Packwiz_test.cpp index e4abda9f9..1fcb1b9f9 100644 --- a/tests/Packwiz_test.cpp +++ b/tests/Packwiz_test.cpp @@ -19,6 +19,7 @@ #include #include +#include "modplatform/ModIndex.h" #include @@ -42,7 +43,7 @@ class PackwizTest : public QObject { QCOMPARE(metadata.name, "Borderless Mining"); QCOMPARE(metadata.filename, "borderless-mining-1.1.1+1.18.jar"); - QCOMPARE(metadata.side, Packwiz::V1::Side::ClientSide); + QCOMPARE(metadata.side, ModPlatform::Side::ClientSide); QCOMPARE(metadata.url, QUrl("https://cdn.modrinth.com/data/kYq5qkSL/versions/1.1.1+1.18/borderless-mining-1.1.1+1.18.jar")); QCOMPARE(metadata.hash_format, "sha512"); @@ -72,7 +73,7 @@ class PackwizTest : public QObject { QCOMPARE(metadata.name, "Screenshot to Clipboard (Fabric)"); QCOMPARE(metadata.filename, "screenshot-to-clipboard-1.0.7-fabric.jar"); - QCOMPARE(metadata.side, Packwiz::V1::Side::UniversalSide); + QCOMPARE(metadata.side, ModPlatform::Side::UniversalSide); QCOMPARE(metadata.url, QUrl("https://edge.forgecdn.net/files/3509/43/screenshot-to-clipboard-1.0.7-fabric.jar")); QCOMPARE(metadata.hash_format, "murmur2"); From 8c0ba43838ef5b1386f8f60420a5a53c25eab723 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Sat, 1 Feb 2025 20:30:55 +0200 Subject: [PATCH 288/695] add options to change the cat scalling Signed-off-by: Trial97 --- launcher/Application.cpp | 1 + launcher/CMakeLists.txt | 2 + launcher/ui/instanceview/InstanceView.cpp | 45 ++++++------- launcher/ui/instanceview/InstanceView.h | 10 +-- launcher/ui/themes/CatPainter.cpp | 51 +++++++++++++++ launcher/ui/themes/CatPainter.h | 38 +++++++++++ launcher/ui/widgets/AppearanceWidget.cpp | 16 +++-- launcher/ui/widgets/AppearanceWidget.ui | 80 +++++++++++++++++------ 8 files changed, 187 insertions(+), 56 deletions(-) create mode 100644 launcher/ui/themes/CatPainter.cpp create mode 100644 launcher/ui/themes/CatPainter.h diff --git a/launcher/Application.cpp b/launcher/Application.cpp index b683b8ab8..ed85106e9 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -786,6 +786,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) // The cat m_settings->registerSetting("TheCat", false); m_settings->registerSetting("CatOpacity", 100); + m_settings->registerSetting("CatFit", "fit"); m_settings->registerSetting("StatusBarVisible", true); diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index a7ccb809d..4981b5aec 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -891,6 +891,8 @@ SET(LAUNCHER_SOURCES ui/themes/ThemeManager.h ui/themes/CatPack.cpp ui/themes/CatPack.h + ui/themes/CatPainter.cpp + ui/themes/CatPainter.h # Processes LaunchController.h diff --git a/launcher/ui/instanceview/InstanceView.cpp b/launcher/ui/instanceview/InstanceView.cpp index 2349c684d..14d480e90 100644 --- a/launcher/ui/instanceview/InstanceView.cpp +++ b/launcher/ui/instanceview/InstanceView.cpp @@ -49,6 +49,7 @@ #include #include "VisualGroup.h" +#include "ui/themes/CatPainter.h" #include "ui/themes/ThemeManager.h" #include @@ -78,6 +79,9 @@ InstanceView::~InstanceView() { qDeleteAll(m_groups); m_groups.clear(); + if (m_cat) { + m_cat->deleteLater(); + } } void InstanceView::setModel(QAbstractItemModel* model) @@ -172,7 +176,7 @@ void InstanceView::updateScrollbar() void InstanceView::updateGeometries() { - geometryCache.clear(); + m_geometryCache.clear(); QMap cats; @@ -186,8 +190,8 @@ void InstanceView::updateGeometries() cat->update(); } else { auto cat = new VisualGroup(groupName, this); - if (fVisibility) { - cat->collapsed = fVisibility(groupName); + if (m_fVisibility) { + cat->collapsed = m_fVisibility(groupName); } cats.insert(groupName, cat); cat->update(); @@ -436,11 +440,15 @@ void InstanceView::mouseDoubleClickEvent(QMouseEvent* event) void InstanceView::setPaintCat(bool visible) { - m_catVisible = visible; - if (visible) - m_catPixmap.load(APPLICATION->themeManager()->getCatPack()); - else - m_catPixmap = QPixmap(); + if (m_cat) { + disconnect(m_cat, &CatPainter::updateFrame, this, nullptr); + delete m_cat; + m_cat = nullptr; + } + if (visible) { + m_cat = new CatPainter(APPLICATION->themeManager()->getCatPack(), this); + connect(m_cat, &CatPainter::updateFrame, this, [this] { viewport()->update(); }); + } } void InstanceView::paintEvent([[maybe_unused]] QPaintEvent* event) @@ -449,19 +457,8 @@ void InstanceView::paintEvent([[maybe_unused]] QPaintEvent* event) QPainter painter(this->viewport()); - if (m_catVisible) { - painter.setOpacity(APPLICATION->settings()->get("CatOpacity").toFloat() / 100); - int widWidth = this->viewport()->width(); - int widHeight = this->viewport()->height(); - if (m_catPixmap.width() < widWidth) - widWidth = m_catPixmap.width(); - if (m_catPixmap.height() < widHeight) - widHeight = m_catPixmap.height(); - auto pixmap = m_catPixmap.scaled(widWidth, widHeight, Qt::KeepAspectRatio, Qt::SmoothTransformation); - QRect rectOfPixmap = pixmap.rect(); - rectOfPixmap.moveBottomRight(this->viewport()->rect().bottomRight()); - painter.drawPixmap(rectOfPixmap.topLeft(), pixmap); - painter.setOpacity(1.0); + if (m_cat) { + m_cat->paint(&painter, this->viewport()->rect()); } QStyleOptionViewItem option; @@ -711,8 +708,8 @@ QRect InstanceView::geometryRect(const QModelIndex& index) const } int row = index.row(); - if (geometryCache.contains(row)) { - return *geometryCache[row]; + if (m_geometryCache.contains(row)) { + return *m_geometryCache[row]; } const VisualGroup* cat = category(index); @@ -727,7 +724,7 @@ QRect InstanceView::geometryRect(const QModelIndex& index) const out.setTop(cat->verticalPosition() + cat->headerHeight() + 5 + cat->rowTopOf(index)); out.setLeft(m_spacing + x * (itemWidth() + m_spacing)); out.setSize(itemDelegate()->sizeHint(option, index)); - geometryCache.insert(row, new QRect(out)); + m_geometryCache.insert(row, new QRect(out)); return out; } diff --git a/launcher/ui/instanceview/InstanceView.h b/launcher/ui/instanceview/InstanceView.h index dea8b1212..5d9dbf729 100644 --- a/launcher/ui/instanceview/InstanceView.h +++ b/launcher/ui/instanceview/InstanceView.h @@ -41,6 +41,7 @@ #include #include #include "VisualGroup.h" +#include "ui/themes/CatPainter.h" struct InstanceViewRoles { enum { GroupRole = Qt::UserRole, ProgressValueRole, ProgressMaximumRole }; @@ -56,7 +57,7 @@ class InstanceView : public QAbstractItemView { void setModel(QAbstractItemModel* model) override; using visibilityFunction = std::function; - void setSourceOfGroupCollapseStatus(visibilityFunction f) { fVisibility = f; } + void setSourceOfGroupCollapseStatus(visibilityFunction f) { m_fVisibility = f; } /// return geometry rectangle occupied by the specified model item QRect geometryRect(const QModelIndex& index) const; @@ -116,7 +117,7 @@ class InstanceView : public QAbstractItemView { friend struct VisualGroup; QList m_groups; - visibilityFunction fVisibility; + visibilityFunction m_fVisibility; // geometry int m_leftMargin = 5; @@ -127,9 +128,8 @@ class InstanceView : public QAbstractItemView { int m_itemWidth = 100; int m_currentItemsPerRow = -1; int m_currentCursorColumn = -1; - mutable QCache geometryCache; - bool m_catVisible = false; - QPixmap m_catPixmap; + mutable QCache m_geometryCache; + CatPainter* m_cat = nullptr; // point where the currently active mouse action started in geometry coordinates QPoint m_pressedPosition; diff --git a/launcher/ui/themes/CatPainter.cpp b/launcher/ui/themes/CatPainter.cpp new file mode 100644 index 000000000..7ff24932b --- /dev/null +++ b/launcher/ui/themes/CatPainter.cpp @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2025 Trial97 + * + * 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 "ui/themes/CatPainter.h" +#include +#include "Application.h" + +CatPainter::CatPainter(const QString& path, QObject* parent) : QObject(parent) +{ + m_image = QPixmap(path); +} + +void CatPainter::paint(QPainter* painter, const QRect& viewport) +{ + QPixmap frame = m_image; + + auto fit = APPLICATION->settings()->get("CatFit").toString(); + painter->setOpacity(APPLICATION->settings()->get("CatOpacity").toFloat() / 100); + int widWidth = viewport.width(); + int widHeight = viewport.height(); + auto aspectMode = Qt::IgnoreAspectRatio; + if (fit == "fill") { + aspectMode = Qt::KeepAspectRatio; + } else if (fit == "fit") { + aspectMode = Qt::KeepAspectRatio; + if (frame.width() < widWidth) + widWidth = frame.width(); + if (frame.height() < widHeight) + widHeight = frame.height(); + } + auto pixmap = frame.scaled(widWidth, widHeight, aspectMode, Qt::SmoothTransformation); + QRect rectOfPixmap = pixmap.rect(); + rectOfPixmap.moveBottomRight(viewport.bottomRight()); + painter->drawPixmap(rectOfPixmap.topLeft(), pixmap); + painter->setOpacity(1.0); +}; diff --git a/launcher/ui/themes/CatPainter.h b/launcher/ui/themes/CatPainter.h new file mode 100644 index 000000000..3b790c640 --- /dev/null +++ b/launcher/ui/themes/CatPainter.h @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2025 Trial97 + * + * 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 +#include + +class CatPainter : public QObject { + Q_OBJECT + public: + CatPainter(const QString& path, QObject* parent = nullptr); + virtual ~CatPainter() = default; + void paint(QPainter*, const QRect&); + + signals: + void updateFrame(); + + private: + QPixmap m_image; +}; diff --git a/launcher/ui/widgets/AppearanceWidget.cpp b/launcher/ui/widgets/AppearanceWidget.cpp index 731b72727..ab5c21872 100644 --- a/launcher/ui/widgets/AppearanceWidget.cpp +++ b/launcher/ui/widgets/AppearanceWidget.cpp @@ -97,22 +97,28 @@ void AppearanceWidget::applySettings() settings->set("ConsoleFont", consoleFontFamily); settings->set("ConsoleFontSize", m_ui->fontSizeBox->value()); settings->set("CatOpacity", m_ui->catOpacitySlider->value()); + auto catFit = m_ui->catFitComboBox->currentIndex(); + settings->set("CatFit", catFit == 0 ? "fit" : catFit == 1 ? "fill" : "strech"); } void AppearanceWidget::loadSettings() { - QString fontFamily = APPLICATION->settings()->get("ConsoleFont").toString(); + SettingsObjectPtr settings = APPLICATION->settings(); + QString fontFamily = settings->get("ConsoleFont").toString(); QFont consoleFont(fontFamily); m_ui->consoleFont->setCurrentFont(consoleFont); bool conversionOk = true; - int fontSize = APPLICATION->settings()->get("ConsoleFontSize").toInt(&conversionOk); + int fontSize = settings->get("ConsoleFontSize").toInt(&conversionOk); if (!conversionOk) { fontSize = 11; } m_ui->fontSizeBox->setValue(fontSize); - m_ui->catOpacitySlider->setValue(APPLICATION->settings()->get("CatOpacity").toInt()); + m_ui->catOpacitySlider->setValue(settings->get("CatOpacity").toInt()); + + auto catFit = settings->get("CatFit").toString(); + m_ui->catFitComboBox->setCurrentIndex(catFit == "fit" ? 0 : catFit == "fill" ? 1 : 2); } void AppearanceWidget::retranslateUi() @@ -245,9 +251,7 @@ void AppearanceWidget::updateConsolePreview() workCursor.insertBlock(); }; - print(QString("%1 version: %2\n") - .arg(BuildConfig.LAUNCHER_DISPLAYNAME, BuildConfig.printableVersionString()), - MessageLevel::Launcher); + print(QString("%1 version: %2\n").arg(BuildConfig.LAUNCHER_DISPLAYNAME, BuildConfig.printableVersionString()), MessageLevel::Launcher); QDate today = QDate::currentDate(); diff --git a/launcher/ui/widgets/AppearanceWidget.ui b/launcher/ui/widgets/AppearanceWidget.ui index c672279f0..99bf4a500 100644 --- a/launcher/ui/widgets/AppearanceWidget.ui +++ b/launcher/ui/widgets/AppearanceWidget.ui @@ -7,7 +7,7 @@ 0 0 600 - 700 + 711 @@ -203,6 +203,53 @@ + + + + + 0 + 0 + + + + Fit + + + + + + + + 0 + 0 + + + + + 77 + 30 + + + + 0 + + + + Fit + + + + + Fill + + + + + Stretch + + + + @@ -372,8 +419,7 @@ - - .. + true @@ -389,8 +435,7 @@ - - .. + true @@ -406,8 +451,7 @@ - - .. + true @@ -423,8 +467,7 @@ - - .. + true @@ -440,8 +483,7 @@ - - .. + true @@ -457,8 +499,7 @@ - - .. + true @@ -474,8 +515,7 @@ - - .. + true @@ -491,8 +531,7 @@ - - .. + true @@ -508,8 +547,7 @@ - - .. + true @@ -525,8 +563,7 @@ - - .. + true @@ -586,6 +623,7 @@ reloadThemesButton consoleFont fontSizeBox + catFitComboBox catOpacitySlider consolePreview From 2fc89d7e1146fe090a68ef938482df6abf966735 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Sat, 1 Feb 2025 20:06:22 +0200 Subject: [PATCH 289/695] improve blocked mods dialog Signed-off-by: Trial97 --- launcher/ui/dialogs/BlockedModsDialog.cpp | 45 ++++--- launcher/ui/dialogs/BlockedModsDialog.h | 9 +- launcher/ui/dialogs/BlockedModsDialog.ui | 139 ++++++++++++++++------ 3 files changed, 128 insertions(+), 65 deletions(-) diff --git a/launcher/ui/dialogs/BlockedModsDialog.cpp b/launcher/ui/dialogs/BlockedModsDialog.cpp index 0095f7af9..3691cbcd4 100644 --- a/launcher/ui/dialogs/BlockedModsDialog.cpp +++ b/launcher/ui/dialogs/BlockedModsDialog.cpp @@ -43,21 +43,18 @@ #include BlockedModsDialog::BlockedModsDialog(QWidget* parent, const QString& title, const QString& text, QList& mods, QString hash_type) - : QDialog(parent), ui(new Ui::BlockedModsDialog), m_mods(mods), m_hash_type(hash_type) + : QDialog(parent), ui(new Ui::BlockedModsDialog), m_mods(mods), m_hashType(hash_type) { - m_hashing_task = shared_qobject_ptr( + m_hashingTask = shared_qobject_ptr( new ConcurrentTask("MakeHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt())); - connect(m_hashing_task.get(), &Task::finished, this, &BlockedModsDialog::hashTaskFinished); + connect(m_hashingTask.get(), &Task::finished, this, &BlockedModsDialog::hashTaskFinished); ui->setupUi(this); ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); - m_openMissingButton = ui->buttonBox->addButton(tr("Open Missing"), QDialogButtonBox::ActionRole); - connect(m_openMissingButton, &QPushButton::clicked, this, [this]() { openAll(true); }); - - auto downloadFolderButton = ui->buttonBox->addButton(tr("Add Download Folder"), QDialogButtonBox::ActionRole); - connect(downloadFolderButton, &QPushButton::clicked, this, &BlockedModsDialog::addDownloadFolder); + connect(ui->openMissingButton, &QPushButton::clicked, this, [this]() { openAll(true); }); + connect(ui->downloadFolderButton, &QPushButton::clicked, this, &BlockedModsDialog::addDownloadFolder); connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &BlockedModsDialog::directoryChanged); @@ -174,10 +171,12 @@ void BlockedModsDialog::update() if (allModsMatched()) { ui->labelModsFound->setText("" + tr("All mods found")); - m_openMissingButton->setDisabled(true); + ui->openMissingButton->setDisabled(true); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); } else { ui->labelModsFound->setText(tr("Please download the missing mods.")); - m_openMissingButton->setDisabled(false); + ui->openMissingButton->setDisabled(false); + ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Skip")); } } @@ -260,7 +259,7 @@ void BlockedModsDialog::scanPath(QString path, bool start_task) void BlockedModsDialog::addHashTask(QString path) { qDebug() << "[Blocked Mods Dialog] adding a Hash task for" << path << "to the pending set."; - m_pending_hash_paths.insert(path); + m_pendingHashPaths.insert(path); } /// @brief add a hashing task for the file located at path and connect it to check that hash against @@ -268,14 +267,14 @@ void BlockedModsDialog::addHashTask(QString path) /// @param path the path to the local file being hashed void BlockedModsDialog::buildHashTask(QString path) { - auto hash_task = Hashing::createHasher(path, m_hash_type); + auto hash_task = Hashing::createHasher(path, m_hashType); qDebug() << "[Blocked Mods Dialog] Creating Hash task for path: " << path; connect(hash_task.get(), &Task::succeeded, this, [this, hash_task, path] { checkMatchHash(hash_task->getResult(), path); }); connect(hash_task.get(), &Task::failed, this, [path] { qDebug() << "Failed to hash path: " << path; }); - m_hashing_task->addTask(hash_task); + m_hashingTask->addTask(hash_task); } /// @brief check if the computed hash for the provided path matches a blocked @@ -406,31 +405,31 @@ void BlockedModsDialog::validateMatchedMods() /// @brief run hash task or mark a pending run if it is already running void BlockedModsDialog::runHashTask() { - if (!m_hashing_task->isRunning()) { - m_rehash_pending = false; + if (!m_hashingTask->isRunning()) { + m_rehashPending = false; - if (!m_pending_hash_paths.isEmpty()) { + if (!m_pendingHashPaths.isEmpty()) { qDebug() << "[Blocked Mods Dialog] there are pending hash tasks, building and running tasks"; - auto path = m_pending_hash_paths.begin(); - while (path != m_pending_hash_paths.end()) { + auto path = m_pendingHashPaths.begin(); + while (path != m_pendingHashPaths.end()) { buildHashTask(*path); - path = m_pending_hash_paths.erase(path); + path = m_pendingHashPaths.erase(path); } - m_hashing_task->start(); + m_hashingTask->start(); } } else { qDebug() << "[Blocked Mods Dialog] queueing another run of the hashing task"; - qDebug() << "[Blocked Mods Dialog] pending hash tasks:" << m_pending_hash_paths; - m_rehash_pending = true; + qDebug() << "[Blocked Mods Dialog] pending hash tasks:" << m_pendingHashPaths; + m_rehashPending = true; } } void BlockedModsDialog::hashTaskFinished() { qDebug() << "[Blocked Mods Dialog] All hash tasks finished"; - if (m_rehash_pending) { + if (m_rehashPending) { qDebug() << "[Blocked Mods Dialog] task finished with a rehash pending, rerunning"; runHashTask(); } diff --git a/launcher/ui/dialogs/BlockedModsDialog.h b/launcher/ui/dialogs/BlockedModsDialog.h index b2d2c0374..b24e76bbf 100644 --- a/launcher/ui/dialogs/BlockedModsDialog.h +++ b/launcher/ui/dialogs/BlockedModsDialog.h @@ -70,11 +70,10 @@ class BlockedModsDialog : public QDialog { Ui::BlockedModsDialog* ui; QList& m_mods; QFileSystemWatcher m_watcher; - shared_qobject_ptr m_hashing_task; - QSet m_pending_hash_paths; - bool m_rehash_pending; - QPushButton* m_openMissingButton; - QString m_hash_type; + shared_qobject_ptr m_hashingTask; + QSet m_pendingHashPaths; + bool m_rehashPending; + QString m_hashType; void openAll(bool missingOnly); void addDownloadFolder(); diff --git a/launcher/ui/dialogs/BlockedModsDialog.ui b/launcher/ui/dialogs/BlockedModsDialog.ui index 2292b99c0..850ad713e 100644 --- a/launcher/ui/dialogs/BlockedModsDialog.ui +++ b/launcher/ui/dialogs/BlockedModsDialog.ui @@ -6,20 +6,26 @@ 0 0 - 400 - 400 + 800 + 500 + + + 2 + 1 + + - 0 + 700 350 BlockedModsDialog - + @@ -36,47 +42,106 @@ - <html><head/><body><p>Your configured global mods folder and default downloads folder are automatically checked for the downloaded mods and they will be copied to the instance if found.</p><p>Optionally, you may drag and drop the downloaded mods onto this dialog or add a folder to watch if you did not download the mods to a default location.</p></body></html> + <html><head/><body><p>Your configured global mods folder and default downloads folder are automatically checked for the downloaded mods and they will be copied to the instance if found.</p><p>Optionally, you may drag and drop the downloaded mods onto this dialog or add a folder to watch if you did not download the mods to a default location.</p><p><span style=" font-weight:600;">Click 'Open Missing' to open all the download links in the browser. </span></p></body></html> true - - true - - - - - - - true - - - true - - - - Watched Folders: - - - - - - - - 0 - 12 - - - - true - - - false + + + 0 + + + Blocked Mods + + + + + + true + + + true + + + + + + + + + Open Missing + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + Watched Folders + + + + + + + 0 + 12 + + + + true + + + false + + + + + + + + + Add Download Folder + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + From 93e9d2fd868815fef2d2b4c83ed102d0ac85f870 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Mon, 2 Jun 2025 16:34:12 +0300 Subject: [PATCH 290/695] fix: warning with different signedness Signed-off-by: Trial97 --- launcher/modplatform/flame/FlameInstanceCreationTask.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp index 5d9c74ccf..c80187c42 100644 --- a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp +++ b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp @@ -426,7 +426,7 @@ bool FlameCreationTask::createInstance() const uint64_t sysMiB = Sys::getSystemRam() / Sys::mebibyte; const uint64_t max = sysMiB * 0.9; - if (recommendedRAM > max) { + if (static_cast(recommendedRAM) > max) { logWarning(tr("The recommended memory of the modpack exceeds 90% of your system RAM—reducing it from %1 MiB to %2 MiB!") .arg(recommendedRAM) .arg(max)); From 956f5ee180b0eac7bc8f4328b22b5125b4bb614b Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Mon, 2 Jun 2025 14:43:56 +0100 Subject: [PATCH 291/695] Fix crash and make loader override more consistent with other option groups Signed-off-by: TheKodeToad --- .../ui/widgets/MinecraftSettingsWidget.cpp | 82 +++++++++++-------- launcher/ui/widgets/MinecraftSettingsWidget.h | 4 +- 2 files changed, 50 insertions(+), 36 deletions(-) diff --git a/launcher/ui/widgets/MinecraftSettingsWidget.cpp b/launcher/ui/widgets/MinecraftSettingsWidget.cpp index 3307eeb79..f46786518 100644 --- a/launcher/ui/widgets/MinecraftSettingsWidget.cpp +++ b/launcher/ui/widgets/MinecraftSettingsWidget.cpp @@ -104,19 +104,21 @@ MinecraftSettingsWidget::MinecraftSettingsWidget(MinecraftInstancePtr instance, if (!value) m_instance->settings()->reset("GlobalDataPacksPath"); }); - connect(m_ui->dataPacksPathEdit, &QLineEdit::editingFinished, this, &MinecraftSettingsWidget::editedDataPacksPath); + connect(m_ui->dataPacksPathEdit, &QLineEdit::editingFinished, this, &MinecraftSettingsWidget::saveDataPacksPath); connect(m_ui->dataPacksPathBrowse, &QPushButton::clicked, this, &MinecraftSettingsWidget::selectDataPacksFolder); connect(m_ui->loaderGroup, &QGroupBox::toggled, this, [this](bool value) { m_instance->settings()->set("OverrideModDownloadLoaders", value); - if (!value) + if (value) + saveSelectedLoaders(); + else m_instance->settings()->reset("ModDownloadLoaders"); }); - connect(m_ui->neoForge, &QCheckBox::stateChanged, this, &MinecraftSettingsWidget::selectedLoadersChanged); - connect(m_ui->forge, &QCheckBox::stateChanged, this, &MinecraftSettingsWidget::selectedLoadersChanged); - connect(m_ui->fabric, &QCheckBox::stateChanged, this, &MinecraftSettingsWidget::selectedLoadersChanged); - connect(m_ui->quilt, &QCheckBox::stateChanged, this, &MinecraftSettingsWidget::selectedLoadersChanged); - connect(m_ui->liteLoader, &QCheckBox::stateChanged, this, &MinecraftSettingsWidget::selectedLoadersChanged); + connect(m_ui->neoForge, &QCheckBox::stateChanged, this, &MinecraftSettingsWidget::saveSelectedLoaders); + connect(m_ui->forge, &QCheckBox::stateChanged, this, &MinecraftSettingsWidget::saveSelectedLoaders); + connect(m_ui->fabric, &QCheckBox::stateChanged, this, &MinecraftSettingsWidget::saveSelectedLoaders); + connect(m_ui->quilt, &QCheckBox::stateChanged, this, &MinecraftSettingsWidget::saveSelectedLoaders); + connect(m_ui->liteLoader, &QCheckBox::stateChanged, this, &MinecraftSettingsWidget::saveSelectedLoaders); } m_ui->maximizedWarning->hide(); @@ -251,22 +253,28 @@ void MinecraftSettingsWidget::loadSettings() m_ui->fabric->blockSignals(true); m_ui->quilt->blockSignals(true); m_ui->liteLoader->blockSignals(true); - auto instLoaders = m_instance->getPackProfile()->getSupportedModLoaders().value(); - m_ui->loaderGroup->setChecked(settings->get("OverrideModDownloadLoaders").toBool()); - auto loaders = Json::toStringList(settings->get("ModDownloadLoaders").toString()); - if (loaders.isEmpty()) { - m_ui->neoForge->setChecked(instLoaders & ModPlatform::NeoForge); - m_ui->forge->setChecked(instLoaders & ModPlatform::Forge); - m_ui->fabric->setChecked(instLoaders & ModPlatform::Fabric); - m_ui->quilt->setChecked(instLoaders & ModPlatform::Quilt); - m_ui->liteLoader->setChecked(instLoaders & ModPlatform::LiteLoader); - } else { + + const bool overrideLoaders = settings->get("OverrideModDownloadLoaders").toBool(); + const QStringList loaders = Json::toStringList(settings->get("ModDownloadLoaders").toString()); + + m_ui->loaderGroup->setChecked(overrideLoaders); + + if (overrideLoaders) { m_ui->neoForge->setChecked(loaders.contains(getModLoaderAsString(ModPlatform::NeoForge))); m_ui->forge->setChecked(loaders.contains(getModLoaderAsString(ModPlatform::Forge))); m_ui->fabric->setChecked(loaders.contains(getModLoaderAsString(ModPlatform::Fabric))); m_ui->quilt->setChecked(loaders.contains(getModLoaderAsString(ModPlatform::Quilt))); m_ui->liteLoader->setChecked(loaders.contains(getModLoaderAsString(ModPlatform::LiteLoader))); + } else { + auto instLoaders = m_instance->getPackProfile()->getSupportedModLoaders().value_or(ModPlatform::ModLoaderTypes(0)); + + m_ui->neoForge->setChecked(instLoaders & ModPlatform::NeoForge); + m_ui->forge->setChecked(instLoaders & ModPlatform::Forge); + m_ui->fabric->setChecked(instLoaders & ModPlatform::Fabric); + m_ui->quilt->setChecked(instLoaders & ModPlatform::Quilt); + m_ui->liteLoader->setChecked(instLoaders & ModPlatform::LiteLoader); } + m_ui->loaderGroup->blockSignals(false); m_ui->neoForge->blockSignals(false); m_ui->forge->blockSignals(false); @@ -504,7 +512,29 @@ bool MinecraftSettingsWidget::isQuickPlaySupported() return m_instance->traits().contains("feature:is_quick_play_singleplayer"); } -void MinecraftSettingsWidget::editedDataPacksPath() +void MinecraftSettingsWidget::saveSelectedLoaders() +{ + QStringList loaders; + + if (m_ui->neoForge->isChecked()) + loaders << getModLoaderAsString(ModPlatform::NeoForge); + + if (m_ui->forge->isChecked()) + loaders << getModLoaderAsString(ModPlatform::Forge); + + if (m_ui->fabric->isChecked()) + loaders << getModLoaderAsString(ModPlatform::Fabric); + + if (m_ui->quilt->isChecked()) + loaders << getModLoaderAsString(ModPlatform::Quilt); + + if (m_ui->liteLoader->isChecked()) + loaders << getModLoaderAsString(ModPlatform::LiteLoader); + + m_instance->settings()->set("ModDownloadLoaders", Json::fromStringList(loaders)); +} + +void MinecraftSettingsWidget::saveDataPacksPath() { if (QDir::separator() != '/') m_ui->dataPacksPathEdit->setText(m_ui->dataPacksPathEdit->text().replace(QDir::separator(), '/')); @@ -531,19 +561,3 @@ void MinecraftSettingsWidget::selectDataPacksFolder() m_ui->dataPacksPathEdit->setText(path); m_instance->settings()->set("GlobalDataPacksPath", path); } - -void MinecraftSettingsWidget::selectedLoadersChanged() -{ - QStringList loaders; - if (m_ui->neoForge->isChecked()) - loaders << getModLoaderAsString(ModPlatform::NeoForge); - if (m_ui->forge->isChecked()) - loaders << getModLoaderAsString(ModPlatform::Forge); - if (m_ui->fabric->isChecked()) - loaders << getModLoaderAsString(ModPlatform::Fabric); - if (m_ui->quilt->isChecked()) - loaders << getModLoaderAsString(ModPlatform::Quilt); - if (m_ui->liteLoader->isChecked()) - loaders << getModLoaderAsString(ModPlatform::LiteLoader); - m_instance->settings()->set("ModDownloadLoaders", Json::fromStringList(loaders)); -} diff --git a/launcher/ui/widgets/MinecraftSettingsWidget.h b/launcher/ui/widgets/MinecraftSettingsWidget.h index 1481d0fae..9e3e7afb2 100644 --- a/launcher/ui/widgets/MinecraftSettingsWidget.h +++ b/launcher/ui/widgets/MinecraftSettingsWidget.h @@ -57,8 +57,8 @@ class MinecraftSettingsWidget : public QWidget { void updateAccountsMenu(const SettingsObject& settings); bool isQuickPlaySupported(); private slots: - void selectedLoadersChanged(); - void editedDataPacksPath(); + void saveSelectedLoaders(); + void saveDataPacksPath(); void selectDataPacksFolder(); MinecraftInstancePtr m_instance; From 99915018322f37530551175083a041fd35582e28 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Mon, 2 Jun 2025 19:28:52 +0300 Subject: [PATCH 292/695] feat: use build config url instead of hadcoded one Signed-off-by: Trial97 --- launcher/modplatform/flame/FlameAPI.cpp | 15 ++++++++------- launcher/modplatform/flame/FlameAPI.h | 12 +++++++----- launcher/ui/pages/modplatform/flame/FlamePage.cpp | 2 +- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/launcher/modplatform/flame/FlameAPI.cpp b/launcher/modplatform/flame/FlameAPI.cpp index 15eb7a696..0a5997ed9 100644 --- a/launcher/modplatform/flame/FlameAPI.cpp +++ b/launcher/modplatform/flame/FlameAPI.cpp @@ -5,6 +5,7 @@ #include "FlameAPI.h" #include #include +#include "BuildConfig.h" #include "FlameModIndex.h" #include "Application.h" @@ -29,7 +30,7 @@ Task::Ptr FlameAPI::matchFingerprints(const QList& fingerprints, std::shar QJsonDocument body(body_obj); auto body_raw = body.toJson(); - netJob->addNetAction(Net::ApiUpload::makeByteArray(QString("https://api.curseforge.com/v1/fingerprints"), response, body_raw)); + netJob->addNetAction(Net::ApiUpload::makeByteArray(QString(BuildConfig.FLAME_BASE_URL + "/fingerprints"), response, body_raw)); return netJob; } @@ -42,7 +43,7 @@ QString FlameAPI::getModFileChangelog(int modId, int fileId) auto netJob = makeShared(QString("Flame::FileChangelog"), APPLICATION->network()); auto response = std::make_shared(); netJob->addNetAction(Net::ApiDownload::makeByteArray( - QString("https://api.curseforge.com/v1/mods/%1/files/%2/changelog") + QString(BuildConfig.FLAME_BASE_URL + "/mods/%1/files/%2/changelog") .arg(QString::fromStdString(std::to_string(modId)), QString::fromStdString(std::to_string(fileId))), response)); @@ -77,7 +78,7 @@ QString FlameAPI::getModDescription(int modId) auto netJob = makeShared(QString("Flame::ModDescription"), APPLICATION->network()); auto response = std::make_shared(); netJob->addNetAction(Net::ApiDownload::makeByteArray( - QString("https://api.curseforge.com/v1/mods/%1/description").arg(QString::number(modId)), response)); + QString(BuildConfig.FLAME_BASE_URL + "/mods/%1/description").arg(QString::number(modId)), response)); QObject::connect(netJob.get(), &NetJob::succeeded, [&netJob, response, &description] { QJsonParseError parse_error{}; @@ -117,7 +118,7 @@ Task::Ptr FlameAPI::getProjects(QStringList addonIds, std::shared_ptraddNetAction(Net::ApiUpload::makeByteArray(QString("https://api.curseforge.com/v1/mods"), response, body_raw)); + netJob->addNetAction(Net::ApiUpload::makeByteArray(QString(BuildConfig.FLAME_BASE_URL + "/mods"), response, body_raw)); QObject::connect(netJob.get(), &NetJob::failed, [body_raw] { qDebug() << body_raw; }); @@ -139,7 +140,7 @@ Task::Ptr FlameAPI::getFiles(const QStringList& fileIds, std::shared_ptraddNetAction(Net::ApiUpload::makeByteArray(QString("https://api.curseforge.com/v1/mods/files"), response, body_raw)); + netJob->addNetAction(Net::ApiUpload::makeByteArray(QString(BuildConfig.FLAME_BASE_URL + "/mods/files"), response, body_raw)); QObject::connect(netJob.get(), &NetJob::failed, [body_raw] { qDebug() << body_raw; }); @@ -150,7 +151,7 @@ Task::Ptr FlameAPI::getFile(const QString& addonId, const QString& fileId, std:: { auto netJob = makeShared(QString("Flame::GetFile"), APPLICATION->network()); netJob->addNetAction( - Net::ApiDownload::makeByteArray(QUrl(QString("https://api.curseforge.com/v1/mods/%1/files/%2").arg(addonId, fileId)), response)); + Net::ApiDownload::makeByteArray(QUrl(QString(BuildConfig.FLAME_BASE_URL + "/mods/%1/files/%2").arg(addonId, fileId)), response)); QObject::connect(netJob.get(), &NetJob::failed, [addonId, fileId] { qDebug() << "Flame API file failure" << addonId << fileId; }); @@ -174,7 +175,7 @@ Task::Ptr FlameAPI::getCategories(std::shared_ptr response, ModPlatf { auto netJob = makeShared(QString("Flame::GetCategories"), APPLICATION->network()); netJob->addNetAction(Net::ApiDownload::makeByteArray( - QUrl(QString("https://api.curseforge.com/v1/categories?gameId=432&classId=%1").arg(getClassId(type))), response)); + QUrl(QString(BuildConfig.FLAME_BASE_URL + "/categories?gameId=432&classId=%1").arg(getClassId(type))), response)); QObject::connect(netJob.get(), &Task::failed, [](QString msg) { qDebug() << "Flame failed to get categories:" << msg; }); return netJob; } diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index 71282f36a..316d2e9c9 100644 --- a/launcher/modplatform/flame/FlameAPI.h +++ b/launcher/modplatform/flame/FlameAPI.h @@ -6,6 +6,7 @@ #include #include +#include "BuildConfig.h" #include "modplatform/ModIndex.h" #include "modplatform/ResourceAPI.h" #include "modplatform/helpers/NetworkResourceAPI.h" @@ -112,18 +113,19 @@ class FlameAPI : public NetworkResourceAPI { if (args.versions.has_value() && !args.versions.value().empty()) get_arguments.append(QString("gameVersion=%1").arg(args.versions.value().front().toString())); - return "https://api.curseforge.com/v1/mods/search?gameId=432&" + get_arguments.join('&'); + return BuildConfig.FLAME_BASE_URL + "/mods/search?gameId=432&" + get_arguments.join('&'); } [[nodiscard]] std::optional getVersionsURL(VersionSearchArgs const& args) const override { auto addonId = args.pack.addonId.toString(); - QString url = QString("https://api.curseforge.com/v1/mods/%1/files?pageSize=10000").arg(addonId); + QString url = QString(BuildConfig.FLAME_BASE_URL + "/mods/%1/files?pageSize=10000").arg(addonId); if (args.mcVersions.has_value()) url += QString("&gameVersion=%1").arg(args.mcVersions.value().front().toString()); - if (args.loaders.has_value() && args.loaders.value() != ModPlatform::ModLoaderType::DataPack && ModPlatform::hasSingleModLoaderSelected(args.loaders.value())) { + if (args.loaders.has_value() && args.loaders.value() != ModPlatform::ModLoaderType::DataPack && + ModPlatform::hasSingleModLoaderSelected(args.loaders.value())) { int mappedModLoader = getMappedModLoader(static_cast(static_cast(args.loaders.value()))); url += QString("&modLoaderType=%1").arg(mappedModLoader); } @@ -133,13 +135,13 @@ class FlameAPI : public NetworkResourceAPI { private: [[nodiscard]] std::optional getInfoURL(QString const& id) const override { - return QString("https://api.curseforge.com/v1/mods/%1").arg(id); + return QString(BuildConfig.FLAME_BASE_URL + "/mods/%1").arg(id); } [[nodiscard]] std::optional getDependencyURL(DependencySearchArgs const& args) const override { auto addonId = args.dependency.addonId.toString(); auto url = - QString("https://api.curseforge.com/v1/mods/%1/files?pageSize=10000&gameVersion=%2").arg(addonId, args.mcVersion.toString()); + QString(BuildConfig.FLAME_BASE_URL + "/mods/%1/files?pageSize=10000&gameVersion=%2").arg(addonId, args.mcVersion.toString()); if (args.loader && ModPlatform::hasSingleModLoaderSelected(args.loader)) { int mappedModLoader = getMappedModLoader(static_cast(static_cast(args.loader))); url += QString("&modLoaderType=%1").arg(mappedModLoader); diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.cpp b/launcher/ui/pages/modplatform/flame/FlamePage.cpp index bb91e5a64..d4a9a0a48 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlamePage.cpp @@ -174,7 +174,7 @@ void FlamePage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelInde auto response = std::make_shared(); int addonId = current.addonId; netJob->addNetAction( - Net::ApiDownload::makeByteArray(QString("https://api.curseforge.com/v1/mods/%1/files").arg(addonId), response)); + Net::ApiDownload::makeByteArray(QString(BuildConfig.FLAME_BASE_URL + "/mods/%1/files").arg(addonId), response)); QObject::connect(netJob, &NetJob::succeeded, this, [this, response, addonId, curr] { if (addonId != current.addonId) { From c58cc3396ab5b4b6e92ab2c74774d32490544802 Mon Sep 17 00:00:00 2001 From: Yihe Li Date: Tue, 3 Jun 2025 04:41:48 +0800 Subject: [PATCH 293/695] Basic support for launcher log page Signed-off-by: Yihe Li --- launcher/Application.cpp | 20 +- launcher/Application.h | 2 + launcher/CMakeLists.txt | 3 + launcher/MessageLevel.cpp | 18 ++ launcher/MessageLevel.h | 2 + launcher/minecraft/mod/ResourcePack.h | 2 +- launcher/ui/pages/global/LauncherLogPage.cpp | 293 +++++++++++++++++++ launcher/ui/pages/global/LauncherLogPage.h | 99 +++++++ launcher/ui/pages/global/LauncherLogPage.ui | 193 ++++++++++++ launcher/ui/pages/instance/LogPage.cpp | 76 ----- launcher/ui/pages/instance/LogPage.h | 14 +- 11 files changed, 631 insertions(+), 91 deletions(-) create mode 100644 launcher/ui/pages/global/LauncherLogPage.cpp create mode 100644 launcher/ui/pages/global/LauncherLogPage.h create mode 100644 launcher/ui/pages/global/LauncherLogPage.ui diff --git a/launcher/Application.cpp b/launcher/Application.cpp index b683b8ab8..e3018db0d 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -63,6 +63,7 @@ #include "ui/pages/global/ExternalToolsPage.h" #include "ui/pages/global/JavaPage.h" #include "ui/pages/global/LanguagePage.h" +#include "ui/pages/global/LauncherLogPage.h" #include "ui/pages/global/LauncherPage.h" #include "ui/pages/global/MinecraftPage.h" #include "ui/pages/global/ProxyPage.h" @@ -244,8 +245,11 @@ void appDebugOutput(QtMsgType type, const QMessageLogContext& context, const QSt } QString out = qFormatLogMessage(type, context, msg); - out += QChar::LineFeed; + if (APPLICATION->logModel) { + APPLICATION->logModel->append(MessageLevel::getLevel(type), out); + } + out += QChar::LineFeed; APPLICATION->logFile->write(out.toUtf8()); APPLICATION->logFile->flush(); @@ -538,6 +542,8 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) qInstallMessageHandler(appDebugOutput); qSetMessagePattern(defaultLogFormat); + logModel.reset(new LogModel(this)); + bool foundLoggingRules = false; auto logRulesFile = QStringLiteral("qtlogging.ini"); @@ -691,6 +697,17 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_settings->registerSetting("ConsoleMaxLines", 100000); m_settings->registerSetting("ConsoleOverflowStop", true); + auto lineSetting = settings()->getSetting("ConsoleMaxLines"); + bool conversionOk = false; + int maxLines = lineSetting->get().toInt(&conversionOk); + if (!conversionOk) { + maxLines = lineSetting->defValue().toInt(); + qWarning() << "ConsoleMaxLines has nonsensical value, defaulting to" << maxLines; + } + logModel->setMaxLines(maxLines); + logModel->setStopOnOverflow(settings()->get("ConsoleOverflowStop").toBool()); + logModel->setOverflowMessage(tr("Cannot display this log since the log length surpassed %1 lines.").arg(maxLines)); + // Folders m_settings->registerSetting("InstanceDir", "instances"); m_settings->registerSetting({ "CentralModsDir", "ModsDir" }, "mods"); @@ -895,6 +912,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); + m_globalSettingsProvider->addPage(); } PixmapCache::setInstance(new PixmapCache(this)); diff --git a/launcher/Application.h b/launcher/Application.h index 2daf6ef35..548345c18 100644 --- a/launcher/Application.h +++ b/launcher/Application.h @@ -48,6 +48,7 @@ #include +#include "launch/LogModel.h" #include "minecraft/launch/MinecraftTarget.h" class LaunchController; @@ -307,6 +308,7 @@ class Application : public QApplication { QList m_urlsToImport; QString m_instanceIdToShowWindowOf; std::unique_ptr logFile; + shared_qobject_ptr logModel; public: void addQSavePath(QString); diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index a7ccb809d..cd9903067 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -959,6 +959,8 @@ SET(LAUNCHER_SOURCES ui/pages/global/MinecraftPage.h ui/pages/global/LauncherPage.cpp ui/pages/global/LauncherPage.h + ui/pages/global/LauncherLogPage.cpp + ui/pages/global/LauncherLogPage.h ui/pages/global/AppearancePage.h ui/pages/global/ProxyPage.cpp ui/pages/global/ProxyPage.h @@ -1203,6 +1205,7 @@ qt_wrap_ui(LAUNCHER_UI ui/pages/global/AccountListPage.ui ui/pages/global/JavaPage.ui ui/pages/global/LauncherPage.ui + ui/pages/global/LauncherLogPage.ui ui/pages/global/APIPage.ui ui/pages/global/ProxyPage.ui ui/pages/global/ExternalToolsPage.ui diff --git a/launcher/MessageLevel.cpp b/launcher/MessageLevel.cpp index 2bd6ecc00..3516dbdd6 100644 --- a/launcher/MessageLevel.cpp +++ b/launcher/MessageLevel.cpp @@ -25,6 +25,24 @@ MessageLevel::Enum MessageLevel::getLevel(const QString& levelName) return MessageLevel::Unknown; } +MessageLevel::Enum MessageLevel::getLevel(QtMsgType type) +{ + switch (type) { + case QtDebugMsg: + return MessageLevel::Debug; + case QtInfoMsg: + return MessageLevel::Info; + case QtWarningMsg: + return MessageLevel::Warning; + case QtCriticalMsg: + return MessageLevel::Error; + case QtFatalMsg: + return MessageLevel::Fatal; + default: + return MessageLevel::Unknown; + } +} + MessageLevel::Enum MessageLevel::fromLine(QString& line) { // Level prefix diff --git a/launcher/MessageLevel.h b/launcher/MessageLevel.h index 321af9d92..794e2ac39 100644 --- a/launcher/MessageLevel.h +++ b/launcher/MessageLevel.h @@ -1,6 +1,7 @@ #pragma once #include +#include /** * @brief the MessageLevel Enum @@ -21,6 +22,7 @@ enum Enum { Fatal, /**< Fatal Errors */ }; MessageLevel::Enum getLevel(const QString& levelName); +MessageLevel::Enum getLevel(QtMsgType type); /* Get message level from a line. Line is modified if it was successful. */ MessageLevel::Enum fromLine(QString& line); diff --git a/launcher/minecraft/mod/ResourcePack.h b/launcher/minecraft/mod/ResourcePack.h index f214bedf2..bd161df87 100644 --- a/launcher/minecraft/mod/ResourcePack.h +++ b/launcher/minecraft/mod/ResourcePack.h @@ -24,5 +24,5 @@ class ResourcePack : public DataPack { /** Gets, respectively, the lower and upper versions supported by the set pack format. */ [[nodiscard]] std::pair compatibleVersions() const override; - virtual QString directory() { return "/assets"; } + QString directory() override { return "/assets"; } }; diff --git a/launcher/ui/pages/global/LauncherLogPage.cpp b/launcher/ui/pages/global/LauncherLogPage.cpp new file mode 100644 index 000000000..62c866b75 --- /dev/null +++ b/launcher/ui/pages/global/LauncherLogPage.cpp @@ -0,0 +1,293 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2024 TheKodeToad + * Copyright (c) 2025 Yihe Li + * + * 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 "LauncherLogPage.h" +#include "ui_LauncherLogPage.h" + +#include "Application.h" + +#include +#include +#include + +#include "launch/LaunchTask.h" +#include "settings/Setting.h" + +#include "ui/GuiUtil.h" +#include "ui/themes/ThemeManager.h" + +#include + +QVariant LogFormatProxyModel::data(const QModelIndex& index, int role) const +{ + const LogColors& colors = APPLICATION->themeManager()->getLogColors(); + + switch (role) { + case Qt::FontRole: + return m_font; + case Qt::ForegroundRole: { + auto level = static_cast(QIdentityProxyModel::data(index, LogModel::LevelRole).toInt()); + QColor result = colors.foreground.value(level); + + if (result.isValid()) + return result; + + break; + } + case Qt::BackgroundRole: { + auto level = static_cast(QIdentityProxyModel::data(index, LogModel::LevelRole).toInt()); + QColor result = colors.background.value(level); + + if (result.isValid()) + return result; + + break; + } + } + + return QIdentityProxyModel::data(index, role); +} + +QModelIndex LogFormatProxyModel::find(const QModelIndex& start, const QString& value, bool reverse) const +{ + QModelIndex parentIndex = parent(start); + auto compare = [this, start, parentIndex, value](int r) -> QModelIndex { + QModelIndex idx = index(r, start.column(), parentIndex); + if (!idx.isValid() || idx == start) { + return QModelIndex(); + } + QVariant v = data(idx, Qt::DisplayRole); + QString t = v.toString(); + if (t.contains(value, Qt::CaseInsensitive)) + return idx; + return QModelIndex(); + }; + if (reverse) { + int from = start.row(); + int to = 0; + + for (int i = 0; i < 2; ++i) { + for (int r = from; (r >= to); --r) { + auto idx = compare(r); + if (idx.isValid()) + return idx; + } + // prepare for the next iteration + from = rowCount() - 1; + to = start.row(); + } + } else { + int from = start.row(); + int to = rowCount(parentIndex); + + for (int i = 0; i < 2; ++i) { + for (int r = from; (r < to); ++r) { + auto idx = compare(r); + if (idx.isValid()) + return idx; + } + // prepare for the next iteration + from = 0; + to = start.row(); + } + } + return QModelIndex(); +} + +LauncherLogPage::LauncherLogPage(QWidget* parent) : QWidget(parent), ui(new Ui::LauncherLogPage) +{ + ui->setupUi(this); + ui->tabWidget->tabBar()->hide(); + + m_proxy = new LogFormatProxyModel(this); + + // set up fonts in the log proxy + { + QString fontFamily = APPLICATION->settings()->get("ConsoleFont").toString(); + bool conversionOk = false; + int fontSize = APPLICATION->settings()->get("ConsoleFontSize").toInt(&conversionOk); + if (!conversionOk) { + fontSize = 11; + } + m_proxy->setFont(QFont(fontFamily, fontSize)); + } + + ui->text->setModel(m_proxy); + m_proxy->setSourceModel(APPLICATION->logModel.get()); + modelStateToUI(); + + auto findShortcut = new QShortcut(QKeySequence(QKeySequence::Find), this); + connect(findShortcut, SIGNAL(activated()), SLOT(findActivated())); + auto findNextShortcut = new QShortcut(QKeySequence(QKeySequence::FindNext), this); + connect(findNextShortcut, SIGNAL(activated()), SLOT(findNextActivated())); + connect(ui->searchBar, SIGNAL(returnPressed()), SLOT(on_findButton_clicked())); + auto findPreviousShortcut = new QShortcut(QKeySequence(QKeySequence::FindPrevious), this); + connect(findPreviousShortcut, SIGNAL(activated()), SLOT(findPreviousActivated())); +} + +LauncherLogPage::~LauncherLogPage() +{ + delete ui; +} + +void LauncherLogPage::modelStateToUI() +{ + if (APPLICATION->logModel->wrapLines()) { + ui->text->setWordWrap(true); + ui->wrapCheckbox->setCheckState(Qt::Checked); + } else { + ui->text->setWordWrap(false); + ui->wrapCheckbox->setCheckState(Qt::Unchecked); + } + if (APPLICATION->logModel->colorLines()) { + ui->text->setColorLines(true); + ui->colorCheckbox->setCheckState(Qt::Checked); + } else { + ui->text->setColorLines(false); + ui->colorCheckbox->setCheckState(Qt::Unchecked); + } + if (APPLICATION->logModel->suspended()) { + ui->trackLogCheckbox->setCheckState(Qt::Unchecked); + } else { + ui->trackLogCheckbox->setCheckState(Qt::Checked); + } +} + +void LauncherLogPage::UIToModelState() +{ + if (!APPLICATION->logModel) { + return; + } + APPLICATION->logModel->setLineWrap(ui->wrapCheckbox->checkState() == Qt::Checked); + APPLICATION->logModel->setColorLines(ui->colorCheckbox->checkState() == Qt::Checked); + APPLICATION->logModel->suspend(ui->trackLogCheckbox->checkState() != Qt::Checked); +} + +void LauncherLogPage::on_btnPaste_clicked() +{ + if (!APPLICATION->logModel) + return; + + // FIXME: turn this into a proper task and move the upload logic out of GuiUtil! + APPLICATION->logModel->append(MessageLevel::Launcher, + QString("Log upload triggered at: %1").arg(QDateTime::currentDateTime().toString(Qt::RFC2822Date))); + auto url = GuiUtil::uploadPaste(tr("Launcher Log"), APPLICATION->logModel->toPlainText(), this); + if (!url.has_value()) { + APPLICATION->logModel->append(MessageLevel::Error, QString("Log upload canceled")); + } else if (url->isNull()) { + APPLICATION->logModel->append(MessageLevel::Error, QString("Log upload failed!")); + } else { + APPLICATION->logModel->append(MessageLevel::Launcher, QString("Log uploaded to: %1").arg(url.value())); + } +} + +void LauncherLogPage::on_btnCopy_clicked() +{ + if (!APPLICATION->logModel) + return; + APPLICATION->logModel->append(MessageLevel::Launcher, + QString("Clipboard copy at: %1").arg(QDateTime::currentDateTime().toString(Qt::RFC2822Date))); + GuiUtil::setClipboardText(APPLICATION->logModel->toPlainText()); +} + +void LauncherLogPage::on_btnClear_clicked() +{ + if (!APPLICATION->logModel) + return; + APPLICATION->logModel->clear(); + m_container->refreshContainer(); +} + +void LauncherLogPage::on_btnBottom_clicked() +{ + ui->text->scrollToBottom(); +} + +void LauncherLogPage::on_trackLogCheckbox_clicked(bool checked) +{ + if (!APPLICATION->logModel) + return; + APPLICATION->logModel->suspend(!checked); +} + +void LauncherLogPage::on_wrapCheckbox_clicked(bool checked) +{ + ui->text->setWordWrap(checked); + if (!APPLICATION->logModel) + return; + APPLICATION->logModel->setLineWrap(checked); +} + +void LauncherLogPage::on_colorCheckbox_clicked(bool checked) +{ + ui->text->setColorLines(checked); + if (!APPLICATION->logModel) + return; + APPLICATION->logModel->setColorLines(checked); +} + +void LauncherLogPage::on_findButton_clicked() +{ + auto modifiers = QApplication::keyboardModifiers(); + bool reverse = modifiers & Qt::ShiftModifier; + ui->text->findNext(ui->searchBar->text(), reverse); +} + +void LauncherLogPage::findNextActivated() +{ + ui->text->findNext(ui->searchBar->text(), false); +} + +void LauncherLogPage::findPreviousActivated() +{ + ui->text->findNext(ui->searchBar->text(), true); +} + +void LauncherLogPage::findActivated() +{ + // focus the search bar if it doesn't have focus + if (!ui->searchBar->hasFocus()) { + ui->searchBar->setFocus(); + ui->searchBar->selectAll(); + } +} + +void LauncherLogPage::retranslate() +{ + ui->retranslateUi(this); +} diff --git a/launcher/ui/pages/global/LauncherLogPage.h b/launcher/ui/pages/global/LauncherLogPage.h new file mode 100644 index 000000000..bab8a3a1a --- /dev/null +++ b/launcher/ui/pages/global/LauncherLogPage.h @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2022 Jamie Mansfield + * Copyright (c) 2025 Yihe Li + * + * 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 +#include + +#include +#include "BaseInstance.h" +#include "launch/LaunchTask.h" +#include "ui/pages/BasePage.h" + +namespace Ui { +class LauncherLogPage; +} +class QTextCharFormat; + +class LogFormatProxyModel : public QIdentityProxyModel { + public: + LogFormatProxyModel(QObject* parent = nullptr) : QIdentityProxyModel(parent) {} + QVariant data(const QModelIndex& index, int role) const override; + QFont getFont() const { return m_font; } + void setFont(QFont font) { m_font = font; } + QModelIndex find(const QModelIndex& start, const QString& value, bool reverse) const; + + private: + QFont m_font; +}; + +class LauncherLogPage : public QWidget, public BasePage { + Q_OBJECT + + public: + explicit LauncherLogPage(QWidget* parent = 0); + ~LauncherLogPage(); + + QString displayName() const override { return tr("Logs"); } + QIcon icon() const override { return APPLICATION->getThemedIcon("log"); } + QString id() const override { return "launcher-console"; } + QString helpPage() const override { return "Launcher-Logs"; } + void retranslate() override; + + private slots: + void on_btnPaste_clicked(); + void on_btnCopy_clicked(); + void on_btnClear_clicked(); + void on_btnBottom_clicked(); + + void on_trackLogCheckbox_clicked(bool checked); + void on_wrapCheckbox_clicked(bool checked); + void on_colorCheckbox_clicked(bool checked); + + void on_findButton_clicked(); + void findActivated(); + void findNextActivated(); + void findPreviousActivated(); + + private: + void modelStateToUI(); + void UIToModelState(); + + private: + Ui::LauncherLogPage* ui; + LogFormatProxyModel* m_proxy; +}; diff --git a/launcher/ui/pages/global/LauncherLogPage.ui b/launcher/ui/pages/global/LauncherLogPage.ui new file mode 100644 index 000000000..44e564f68 --- /dev/null +++ b/launcher/ui/pages/global/LauncherLogPage.ui @@ -0,0 +1,193 @@ + + + LauncherLogPage + + + + 0 + 0 + 825 + 782 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 0 + + + + Tab 1 + + + + + + false + + + true + + + + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + false + + + + + + + + + Keep updating + + + true + + + + + + + Wrap lines + + + true + + + + + + + Color lines + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Copy the whole log into the clipboard + + + &Copy + + + + + + + Upload the log to the paste service configured in preferences + + + Upload + + + + + + + Clear the log + + + Clear + + + + + + + + + Search: + + + + + + + Find + + + + + + + + + + Scroll all the way to bottom + + + Bottom + + + + + + + Qt::Vertical + + + + + + + + + + + + LogView + QPlainTextEdit +
    ui/widgets/LogView.h
    +
    +
    + + tabWidget + trackLogCheckbox + wrapCheckbox + colorCheckbox + btnCopy + btnPaste + btnClear + text + searchBar + findButton + + + +
    diff --git a/launcher/ui/pages/instance/LogPage.cpp b/launcher/ui/pages/instance/LogPage.cpp index 7897a2932..d1691ff16 100644 --- a/launcher/ui/pages/instance/LogPage.cpp +++ b/launcher/ui/pages/instance/LogPage.cpp @@ -52,82 +52,6 @@ #include -QVariant LogFormatProxyModel::data(const QModelIndex& index, int role) const -{ - const LogColors& colors = APPLICATION->themeManager()->getLogColors(); - - switch (role) { - case Qt::FontRole: - return m_font; - case Qt::ForegroundRole: { - auto level = static_cast(QIdentityProxyModel::data(index, LogModel::LevelRole).toInt()); - QColor result = colors.foreground.value(level); - - if (result.isValid()) - return result; - - break; - } - case Qt::BackgroundRole: { - auto level = static_cast(QIdentityProxyModel::data(index, LogModel::LevelRole).toInt()); - QColor result = colors.background.value(level); - - if (result.isValid()) - return result; - - break; - } - } - - return QIdentityProxyModel::data(index, role); -} - -QModelIndex LogFormatProxyModel::find(const QModelIndex& start, const QString& value, bool reverse) const -{ - QModelIndex parentIndex = parent(start); - auto compare = [this, start, parentIndex, value](int r) -> QModelIndex { - QModelIndex idx = index(r, start.column(), parentIndex); - if (!idx.isValid() || idx == start) { - return QModelIndex(); - } - QVariant v = data(idx, Qt::DisplayRole); - QString t = v.toString(); - if (t.contains(value, Qt::CaseInsensitive)) - return idx; - return QModelIndex(); - }; - if (reverse) { - int from = start.row(); - int to = 0; - - for (int i = 0; i < 2; ++i) { - for (int r = from; (r >= to); --r) { - auto idx = compare(r); - if (idx.isValid()) - return idx; - } - // prepare for the next iteration - from = rowCount() - 1; - to = start.row(); - } - } else { - int from = start.row(); - int to = rowCount(parentIndex); - - for (int i = 0; i < 2; ++i) { - for (int r = from; (r < to); ++r) { - auto idx = compare(r); - if (idx.isValid()) - return idx; - } - // prepare for the next iteration - from = 0; - to = start.row(); - } - } - return QModelIndex(); -} - LogPage::LogPage(InstancePtr instance, QWidget* parent) : QWidget(parent), ui(new Ui::LogPage), m_instance(instance) { ui->setupUi(this); diff --git a/launcher/ui/pages/instance/LogPage.h b/launcher/ui/pages/instance/LogPage.h index b4d74fb9c..caa870cbc 100644 --- a/launcher/ui/pages/instance/LogPage.h +++ b/launcher/ui/pages/instance/LogPage.h @@ -42,23 +42,11 @@ #include "BaseInstance.h" #include "launch/LaunchTask.h" #include "ui/pages/BasePage.h" +#include "ui/pages/global/LauncherLogPage.h" namespace Ui { class LogPage; } -class QTextCharFormat; - -class LogFormatProxyModel : public QIdentityProxyModel { - public: - LogFormatProxyModel(QObject* parent = nullptr) : QIdentityProxyModel(parent) {} - QVariant data(const QModelIndex& index, int role) const override; - QFont getFont() const { return m_font; } - void setFont(QFont font) { m_font = font; } - QModelIndex find(const QModelIndex& start, const QString& value, bool reverse) const; - - private: - QFont m_font; -}; class LogPage : public QWidget, public BasePage { Q_OBJECT From 289645266ad72f81cdb968241adb98cd802255ef Mon Sep 17 00:00:00 2001 From: Yihe Li Date: Tue, 3 Jun 2025 06:08:24 +0800 Subject: [PATCH 294/695] Add support for view older launcher logs Signed-off-by: Yihe Li --- launcher/Application.cpp | 30 +- launcher/Application.h | 3 + launcher/MessageLevel.cpp | 15 + launcher/MessageLevel.h | 3 + launcher/ui/pages/global/LauncherLogPage.cpp | 397 ++++++++++++++++--- launcher/ui/pages/global/LauncherLogPage.h | 22 +- launcher/ui/pages/global/LauncherLogPage.ui | 256 +++++++----- launcher/ui/pages/instance/OtherLogsPage.cpp | 3 +- 8 files changed, 553 insertions(+), 176 deletions(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index e3018db0d..ef2530e0d 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -697,16 +697,9 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_settings->registerSetting("ConsoleMaxLines", 100000); m_settings->registerSetting("ConsoleOverflowStop", true); - auto lineSetting = settings()->getSetting("ConsoleMaxLines"); - bool conversionOk = false; - int maxLines = lineSetting->get().toInt(&conversionOk); - if (!conversionOk) { - maxLines = lineSetting->defValue().toInt(); - qWarning() << "ConsoleMaxLines has nonsensical value, defaulting to" << maxLines; - } - logModel->setMaxLines(maxLines); - logModel->setStopOnOverflow(settings()->get("ConsoleOverflowStop").toBool()); - logModel->setOverflowMessage(tr("Cannot display this log since the log length surpassed %1 lines.").arg(maxLines)); + logModel->setMaxLines(getConsoleMaxLines()); + logModel->setStopOnOverflow(shouldStopOnConsoleOverflow()); + logModel->setOverflowMessage(tr("Cannot display this log since the log length surpassed %1 lines.").arg(logModel->getMaxLines())); // Folders m_settings->registerSetting("InstanceDir", "instances"); @@ -1614,6 +1607,23 @@ void Application::updateIsRunning(bool running) m_updateRunning = running; } +int Application::getConsoleMaxLines() const +{ + auto lineSetting = settings()->getSetting("ConsoleMaxLines"); + bool conversionOk = false; + int maxLines = lineSetting->get().toInt(&conversionOk); + if (!conversionOk) { + maxLines = lineSetting->defValue().toInt(); + qWarning() << "ConsoleMaxLines has nonsensical value, defaulting to" << maxLines; + } + return maxLines; +} + +bool Application::shouldStopOnConsoleOverflow() const +{ + return settings()->get("ConsoleOverflowStop").toBool(); +} + void Application::controllerSucceeded() { auto controller = qobject_cast(QObject::sender()); diff --git a/launcher/Application.h b/launcher/Application.h index 548345c18..3c2c6e11c 100644 --- a/launcher/Application.h +++ b/launcher/Application.h @@ -162,6 +162,9 @@ class Application : public QApplication { QString getModrinthAPIToken(); QString getUserAgent(); + int getConsoleMaxLines() const; + bool shouldStopOnConsoleOverflow() const; + /// this is the root of the 'installation'. Used for automatic updates const QString& root() { return m_rootPath; } diff --git a/launcher/MessageLevel.cpp b/launcher/MessageLevel.cpp index 3516dbdd6..2440f644e 100644 --- a/launcher/MessageLevel.cpp +++ b/launcher/MessageLevel.cpp @@ -54,3 +54,18 @@ MessageLevel::Enum MessageLevel::fromLine(QString& line) } return MessageLevel::Unknown; } + +MessageLevel::Enum MessageLevel::fromLauncherLine(QString& line) +{ + // Level prefix + int startMark = 0; + while (startMark < line.size() && (line[startMark].isDigit() || line[startMark].isSpace() || line[startMark] == '.')) + ++startMark; + int endmark = line.indexOf(":"); + if (startMark < line.size() && endmark != -1) { + auto level = MessageLevel::getLevel(line.left(endmark).mid(startMark)); + line = line.mid(endmark + 2); + return level; + } + return MessageLevel::Unknown; +} diff --git a/launcher/MessageLevel.h b/launcher/MessageLevel.h index 794e2ac39..ce4e8263f 100644 --- a/launcher/MessageLevel.h +++ b/launcher/MessageLevel.h @@ -26,4 +26,7 @@ MessageLevel::Enum getLevel(QtMsgType type); /* Get message level from a line. Line is modified if it was successful. */ MessageLevel::Enum fromLine(QString& line); + +/* Get message level from a line from the launcher log. Line is modified if it was successful. */ +MessageLevel::Enum fromLauncherLine(QString& line); } // namespace MessageLevel diff --git a/launcher/ui/pages/global/LauncherLogPage.cpp b/launcher/ui/pages/global/LauncherLogPage.cpp index 62c866b75..2f8dbac53 100644 --- a/launcher/ui/pages/global/LauncherLogPage.cpp +++ b/launcher/ui/pages/global/LauncherLogPage.cpp @@ -2,7 +2,6 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield - * Copyright (C) 2022 Sefa Eyeoglu * Copyright (C) 2024 TheKodeToad * Copyright (c) 2025 Yihe Li * @@ -39,19 +38,18 @@ #include "LauncherLogPage.h" #include "ui_LauncherLogPage.h" -#include "Application.h" - -#include -#include -#include - -#include "launch/LaunchTask.h" -#include "settings/Setting.h" +#include #include "ui/GuiUtil.h" #include "ui/themes/ThemeManager.h" -#include +#include +#include +#include +#include +#include +#include +#include QVariant LogFormatProxyModel::data(const QModelIndex& index, int role) const { @@ -129,7 +127,12 @@ QModelIndex LogFormatProxyModel::find(const QModelIndex& start, const QString& v return QModelIndex(); } -LauncherLogPage::LauncherLogPage(QWidget* parent) : QWidget(parent), ui(new Ui::LauncherLogPage) +LauncherLogPage::LauncherLogPage(QWidget* parent) + : QWidget(parent) + , ui(new Ui::LauncherLogPage) + , m_model(APPLICATION->logModel) + , m_basePath(APPLICATION->dataRoot()) + , m_logSearchPaths({ "logs" }) { ui->setupUi(this); ui->tabWidget->tabBar()->hide(); @@ -148,16 +151,21 @@ LauncherLogPage::LauncherLogPage(QWidget* parent) : QWidget(parent), ui(new Ui:: } ui->text->setModel(m_proxy); - m_proxy->setSourceModel(APPLICATION->logModel.get()); + m_proxy->setSourceModel(m_model.get()); modelStateToUI(); + connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &LauncherLogPage::populateSelectLogBox); + auto findShortcut = new QShortcut(QKeySequence(QKeySequence::Find), this); - connect(findShortcut, SIGNAL(activated()), SLOT(findActivated())); + connect(findShortcut, &QShortcut::activated, this, &LauncherLogPage::findActivated); + auto findNextShortcut = new QShortcut(QKeySequence(QKeySequence::FindNext), this); - connect(findNextShortcut, SIGNAL(activated()), SLOT(findNextActivated())); - connect(ui->searchBar, SIGNAL(returnPressed()), SLOT(on_findButton_clicked())); + connect(findNextShortcut, &QShortcut::activated, this, &LauncherLogPage::findNextActivated); + auto findPreviousShortcut = new QShortcut(QKeySequence(QKeySequence::FindPrevious), this); - connect(findPreviousShortcut, SIGNAL(activated()), SLOT(findPreviousActivated())); + connect(findPreviousShortcut, &QShortcut::activated, this, &LauncherLogPage::findPreviousActivated); + + connect(ui->searchBar, &QLineEdit::returnPressed, this, &LauncherLogPage::on_findButton_clicked); } LauncherLogPage::~LauncherLogPage() @@ -167,21 +175,21 @@ LauncherLogPage::~LauncherLogPage() void LauncherLogPage::modelStateToUI() { - if (APPLICATION->logModel->wrapLines()) { + if (m_model->wrapLines()) { ui->text->setWordWrap(true); ui->wrapCheckbox->setCheckState(Qt::Checked); } else { ui->text->setWordWrap(false); ui->wrapCheckbox->setCheckState(Qt::Unchecked); } - if (APPLICATION->logModel->colorLines()) { + if (m_model->colorLines()) { ui->text->setColorLines(true); ui->colorCheckbox->setCheckState(Qt::Checked); } else { ui->text->setColorLines(false); ui->colorCheckbox->setCheckState(Qt::Unchecked); } - if (APPLICATION->logModel->suspended()) { + if (m_model->suspended()) { ui->trackLogCheckbox->setCheckState(Qt::Unchecked); } else { ui->trackLogCheckbox->setCheckState(Qt::Checked); @@ -190,47 +198,205 @@ void LauncherLogPage::modelStateToUI() void LauncherLogPage::UIToModelState() { - if (!APPLICATION->logModel) { + if (!m_model) { return; } - APPLICATION->logModel->setLineWrap(ui->wrapCheckbox->checkState() == Qt::Checked); - APPLICATION->logModel->setColorLines(ui->colorCheckbox->checkState() == Qt::Checked); - APPLICATION->logModel->suspend(ui->trackLogCheckbox->checkState() != Qt::Checked); + m_model->setLineWrap(ui->wrapCheckbox->checkState() == Qt::Checked); + m_model->setColorLines(ui->colorCheckbox->checkState() == Qt::Checked); + m_model->suspend(ui->trackLogCheckbox->checkState() != Qt::Checked); } -void LauncherLogPage::on_btnPaste_clicked() +void LauncherLogPage::retranslate() { - if (!APPLICATION->logModel) - return; + ui->retranslateUi(this); +} + +void LauncherLogPage::openedImpl() +{ + const QStringList failedPaths = m_watcher.addPaths(m_logSearchPaths); + + for (const QString& path : m_logSearchPaths) { + if (failedPaths.contains(path)) + qDebug() << "Failed to start watching" << path; + else + qDebug() << "Started watching" << path; + } + + populateSelectLogBox(); +} + +void LauncherLogPage::closedImpl() +{ + const QStringList failedPaths = m_watcher.removePaths(m_logSearchPaths); + + for (const QString& path : m_logSearchPaths) { + if (failedPaths.contains(path)) + qDebug() << "Failed to stop watching" << path; + else + qDebug() << "Stopped watching" << path; + } +} + +void LauncherLogPage::populateSelectLogBox() +{ + const QString prevCurrentFile = m_currentFile; + + ui->selectLogBox->blockSignals(true); + ui->selectLogBox->clear(); + ui->selectLogBox->addItem("Current logs"); + ui->selectLogBox->addItems(getPaths()); + ui->selectLogBox->blockSignals(false); + + if (!prevCurrentFile.isEmpty()) { + const int index = ui->selectLogBox->findText(prevCurrentFile); + if (index != -1) { + ui->selectLogBox->blockSignals(true); + ui->selectLogBox->setCurrentIndex(index); + ui->selectLogBox->blockSignals(false); + setControlsEnabled(true); + // don't refresh file + return; + } else { + setControlsEnabled(false); + } + } else { + ui->selectLogBox->setCurrentIndex(0); + setControlsEnabled(true); + } - // FIXME: turn this into a proper task and move the upload logic out of GuiUtil! - APPLICATION->logModel->append(MessageLevel::Launcher, - QString("Log upload triggered at: %1").arg(QDateTime::currentDateTime().toString(Qt::RFC2822Date))); - auto url = GuiUtil::uploadPaste(tr("Launcher Log"), APPLICATION->logModel->toPlainText(), this); - if (!url.has_value()) { - APPLICATION->logModel->append(MessageLevel::Error, QString("Log upload canceled")); - } else if (url->isNull()) { - APPLICATION->logModel->append(MessageLevel::Error, QString("Log upload failed!")); + on_selectLogBox_currentIndexChanged(ui->selectLogBox->currentIndex()); +} + +void LauncherLogPage::on_selectLogBox_currentIndexChanged(const int index) +{ + QString file; + if (index > 0) { + file = ui->selectLogBox->itemText(index); + } + + if (index != 0 && (file.isEmpty() || !QFile::exists(FS::PathCombine(m_basePath, file)))) { + m_currentFile = QString(); + ui->text->clear(); + setControlsEnabled(false); } else { - APPLICATION->logModel->append(MessageLevel::Launcher, QString("Log uploaded to: %1").arg(url.value())); + m_currentFile = file; + reload(); + setControlsEnabled(true); } } -void LauncherLogPage::on_btnCopy_clicked() +void LauncherLogPage::on_btnReload_clicked() { - if (!APPLICATION->logModel) - return; - APPLICATION->logModel->append(MessageLevel::Launcher, - QString("Clipboard copy at: %1").arg(QDateTime::currentDateTime().toString(Qt::RFC2822Date))); - GuiUtil::setClipboardText(APPLICATION->logModel->toPlainText()); + if (m_currentFile.isEmpty()) { + if (!m_model) + return; + m_model->clear(); + m_container->refreshContainer(); + } else { + reload(); + } } -void LauncherLogPage::on_btnClear_clicked() +void LauncherLogPage::reload() { - if (!APPLICATION->logModel) + if (m_currentFile.isEmpty()) { + m_model = APPLICATION->logModel; + m_proxy->setSourceModel(m_model.get()); + ui->text->setModel(m_proxy); + ui->text->scrollToBottom(); + UIToModelState(); + setControlsEnabled(true); return; - APPLICATION->logModel->clear(); - m_container->refreshContainer(); + } + + QFile file(FS::PathCombine(m_basePath, m_currentFile)); + if (!file.open(QFile::ReadOnly)) { + setControlsEnabled(false); + ui->btnReload->setEnabled(true); // allow reload + m_currentFile = QString(); + QMessageBox::critical(this, tr("Error"), tr("Unable to open %1 for reading: %2").arg(m_currentFile, file.errorString())); + } else { + auto setPlainText = [this](const QString& text) { + QTextDocument* doc = ui->text->document(); + doc->setDefaultFont(m_proxy->getFont()); + ui->text->setPlainText(text); + }; + auto showTooBig = [setPlainText, &file]() { + setPlainText(tr("The file (%1) is too big. You may want to open it in a viewer optimized " + "for large files.") + .arg(file.fileName())); + }; + if (file.size() > (1024ll * 1024ll * 12ll)) { + showTooBig(); + return; + } + MessageLevel::Enum last = MessageLevel::Unknown; + + auto handleLine = [this, &last](QString line) { + if (line.isEmpty()) + return false; + if (line.back() == '\n') + line = line.remove(line.size() - 1, 1); + QString lineTemp = line; // don't edit out the time and level for clarity + MessageLevel::Enum level = MessageLevel::fromLauncherLine(lineTemp); + + last = level; + m_model->append(level, line); + return m_model->isOverFlow(); + }; + + // Try to determine a level for each line + ui->text->clear(); + ui->text->setModel(nullptr); + m_model.reset(new LogModel(this)); + m_model->setMaxLines(APPLICATION->getConsoleMaxLines()); + m_model->setStopOnOverflow(APPLICATION->shouldStopOnConsoleOverflow()); + m_model->setOverflowMessage(tr("Cannot display this log since the log length surpassed %1 lines.").arg(m_model->getMaxLines())); + m_model->clear(); + if (file.fileName().endsWith(".gz")) { + QString line; + auto error = GZip::readGzFileByBlocks(&file, [&line, handleLine](const QByteArray& d) { + auto block = d; + int newlineIndex = block.indexOf('\n'); + while (newlineIndex != -1) { + line += QString::fromUtf8(block).left(newlineIndex); + block.remove(0, newlineIndex + 1); + if (handleLine(line)) { + line.clear(); + return false; + } + line.clear(); + newlineIndex = block.indexOf('\n'); + } + line += QString::fromUtf8(block); + return true; + }); + if (!error.isEmpty()) { + setPlainText(tr("The file (%1) encountered an error when reading: %2.").arg(file.fileName(), error)); + return; + } else if (!line.isEmpty()) { + handleLine(line); + } + } else { + while (!file.atEnd() && !handleLine(QString::fromUtf8(file.readLine()))) { + } + } + m_proxy->setSourceModel(m_model.get()); + ui->text->setModel(m_proxy); + ui->text->scrollToBottom(); + UIToModelState(); + setControlsEnabled(true); + } +} + +void LauncherLogPage::on_btnPaste_clicked() +{ + GuiUtil::uploadPaste(m_currentFile, ui->text->toPlainText(), this); +} + +void LauncherLogPage::on_btnCopy_clicked() +{ + GuiUtil::setClipboardText(ui->text->toPlainText()); } void LauncherLogPage::on_btnBottom_clicked() @@ -240,25 +406,149 @@ void LauncherLogPage::on_btnBottom_clicked() void LauncherLogPage::on_trackLogCheckbox_clicked(bool checked) { - if (!APPLICATION->logModel) + if (!m_model) + return; + m_model->suspend(!checked); +} + +void LauncherLogPage::on_btnDelete_clicked() +{ + if (m_currentFile.isEmpty()) { + setControlsEnabled(false); + return; + } + if (QMessageBox::question(this, tr("Confirm Deletion"), + tr("You are about to delete \"%1\".\n" + "This may be permanent and it will be gone from the logs folder.\n\n" + "Are you sure?") + .arg(m_currentFile), + QMessageBox::Yes, QMessageBox::No) == QMessageBox::No) { + return; + } + QFile file(FS::PathCombine(m_basePath, m_currentFile)); + + if (FS::trash(file.fileName())) { + return; + } + + if (!file.remove()) { + QMessageBox::critical(this, tr("Error"), tr("Unable to delete %1: %2").arg(m_currentFile, file.errorString())); + } +} + +void LauncherLogPage::on_btnClean_clicked() +{ + auto toDelete = getPaths(); + if (toDelete.isEmpty()) { return; - APPLICATION->logModel->suspend(!checked); + } + QMessageBox* messageBox = new QMessageBox(this); + messageBox->setWindowTitle(tr("Confirm Cleanup")); + if (toDelete.size() > 5) { + messageBox->setText(tr("Are you sure you want to delete all log files?")); + messageBox->setDetailedText(toDelete.join('\n')); + } else { + messageBox->setText(tr("Are you sure you want to delete all these files?\n%1").arg(toDelete.join('\n'))); + } + messageBox->setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel); + messageBox->setDefaultButton(QMessageBox::Ok); + messageBox->setTextInteractionFlags(Qt::TextSelectableByMouse); + messageBox->setIcon(QMessageBox::Question); + messageBox->setTextInteractionFlags(Qt::TextBrowserInteraction); + + if (messageBox->exec() != QMessageBox::Ok) { + return; + } + QStringList failed; + for (auto item : toDelete) { + QString absolutePath = FS::PathCombine(m_basePath, item); + QFile file(absolutePath); + qDebug() << "Deleting log" << absolutePath; + if (FS::trash(file.fileName())) { + continue; + } + if (!file.remove()) { + failed.push_back(item); + } + } + if (!failed.empty()) { + QMessageBox* messageBoxFailure = new QMessageBox(this); + messageBoxFailure->setWindowTitle(tr("Error")); + if (failed.size() > 5) { + messageBoxFailure->setText(tr("Couldn't delete some files!")); + messageBoxFailure->setDetailedText(failed.join('\n')); + } else { + messageBoxFailure->setText(tr("Couldn't delete some files:\n%1").arg(failed.join('\n'))); + } + messageBoxFailure->setStandardButtons(QMessageBox::Ok); + messageBoxFailure->setDefaultButton(QMessageBox::Ok); + messageBoxFailure->setTextInteractionFlags(Qt::TextSelectableByMouse); + messageBoxFailure->setIcon(QMessageBox::Critical); + messageBoxFailure->setTextInteractionFlags(Qt::TextBrowserInteraction); + messageBoxFailure->exec(); + } } void LauncherLogPage::on_wrapCheckbox_clicked(bool checked) { ui->text->setWordWrap(checked); - if (!APPLICATION->logModel) + if (!m_model) return; - APPLICATION->logModel->setLineWrap(checked); + m_model->setLineWrap(checked); + ui->text->scrollToBottom(); } void LauncherLogPage::on_colorCheckbox_clicked(bool checked) { ui->text->setColorLines(checked); - if (!APPLICATION->logModel) + if (!m_model) return; - APPLICATION->logModel->setColorLines(checked); + m_model->setColorLines(checked); + ui->text->scrollToBottom(); +} + +void LauncherLogPage::setControlsEnabled(const bool enabled) +{ + if (!m_currentFile.isEmpty()) { + ui->btnReload->setText("&Reload"); + ui->btnReload->setToolTip("Reload the contents of the log from the disk"); + ui->btnDelete->setEnabled(enabled); + ui->btnClean->setEnabled(enabled); + ui->trackLogCheckbox->setEnabled(false); + } else { + ui->btnReload->setText("Clear"); + ui->btnReload->setToolTip("Clear the log"); + ui->btnDelete->setEnabled(false); + ui->btnClean->setEnabled(false); + ui->trackLogCheckbox->setEnabled(enabled); + } + ui->btnReload->setEnabled(enabled); + ui->btnCopy->setEnabled(enabled); + ui->btnPaste->setEnabled(enabled); + ui->text->setEnabled(enabled); +} + +QStringList LauncherLogPage::getPaths() +{ + QDir baseDir(m_basePath); + + QStringList result; + + for (QString searchPath : m_logSearchPaths) { + QDir searchDir(searchPath); + + QStringList filters{ "*.log", "*.log.gz" }; + + if (searchPath != m_basePath) + filters.append("*.txt"); + + QStringList entries = searchDir.entryList(filters, QDir::Files | QDir::Readable, QDir::SortFlag::Time); + + for (const QString& name : entries) + result.append(baseDir.relativeFilePath(searchDir.filePath(name))); + } + + return result; } void LauncherLogPage::on_findButton_clicked() @@ -286,8 +576,3 @@ void LauncherLogPage::findActivated() ui->searchBar->selectAll(); } } - -void LauncherLogPage::retranslate() -{ - ui->retranslateUi(this); -} diff --git a/launcher/ui/pages/global/LauncherLogPage.h b/launcher/ui/pages/global/LauncherLogPage.h index bab8a3a1a..4a6fb5882 100644 --- a/launcher/ui/pages/global/LauncherLogPage.h +++ b/launcher/ui/pages/global/LauncherLogPage.h @@ -36,6 +36,7 @@ #pragma once +#include #include #include @@ -48,6 +49,7 @@ namespace Ui { class LauncherLogPage; } class QTextCharFormat; +class RecursiveFileSystemWatcher; class LogFormatProxyModel : public QIdentityProxyModel { public: @@ -74,10 +76,17 @@ class LauncherLogPage : public QWidget, public BasePage { QString helpPage() const override { return "Launcher-Logs"; } void retranslate() override; + void openedImpl() override; + void closedImpl() override; + private slots: + void populateSelectLogBox(); + void on_selectLogBox_currentIndexChanged(int index); + void on_btnReload_clicked(); void on_btnPaste_clicked(); void on_btnCopy_clicked(); - void on_btnClear_clicked(); + void on_btnDelete_clicked(); + void on_btnClean_clicked(); void on_btnBottom_clicked(); void on_trackLogCheckbox_clicked(bool checked); @@ -90,10 +99,21 @@ class LauncherLogPage : public QWidget, public BasePage { void findPreviousActivated(); private: + void reload(); void modelStateToUI(); void UIToModelState(); + void setControlsEnabled(bool enabled); + + QStringList getPaths(); private: Ui::LauncherLogPage* ui; LogFormatProxyModel* m_proxy; + shared_qobject_ptr m_model; + + /** Path to display log paths relative to. */ + QString m_basePath; + QStringList m_logSearchPaths; + QString m_currentFile; + QFileSystemWatcher m_watcher; }; diff --git a/launcher/ui/pages/global/LauncherLogPage.ui b/launcher/ui/pages/global/LauncherLogPage.ui index 44e564f68..189f2fe78 100644 --- a/launcher/ui/pages/global/LauncherLogPage.ui +++ b/launcher/ui/pages/global/LauncherLogPage.ui @@ -33,102 +33,6 @@ Tab 1 - - - - false - - - true - - - - - - Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - false - - - - - - - - - Keep updating - - - true - - - - - - - Wrap lines - - - true - - - - - - - Color lines - - - true - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Copy the whole log into the clipboard - - - &Copy - - - - - - - Upload the log to the paste service configured in preferences - - - Upload - - - - - - - Clear the log - - - Clear - - - - - @@ -136,15 +40,22 @@ + + + - Find + &Find - - + + + + Qt::Vertical + + @@ -152,17 +63,144 @@ Scroll all the way to bottom
    - Bottom + &Bottom
    - - - - Qt::Vertical + + + + false + + + true + + + + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + false + + + + + + + + + 0 + 0 + + + + + + + + Delete the selected log + + + &Delete Selected + + + + + + + Delete all the logs + + + Delete &All + + + + + + + + + + + Keep updating + + + true + + + + + + + Wrap lines + + + true + + + + + + + Color lines + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Copy the whole log into the clipboard + + + &Copy + + + + + + + Upload the log to the paste service configured in preferences + + + &Upload + + + + + + + Reload the contents of the log from the disk + + + &Reload + + + + + + +
    @@ -178,12 +216,14 @@
    tabWidget - trackLogCheckbox - wrapCheckbox - colorCheckbox + selectLogBox + btnReload btnCopy btnPaste - btnClear + btnDelete + btnClean + wrapCheckbox + colorCheckbox text searchBar findButton diff --git a/launcher/ui/pages/instance/OtherLogsPage.cpp b/launcher/ui/pages/instance/OtherLogsPage.cpp index afd1ff1c1..a90969503 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.cpp +++ b/launcher/ui/pages/instance/OtherLogsPage.cpp @@ -211,7 +211,8 @@ void OtherLogsPage::on_btnReload_clicked() MessageLevel::Enum level = MessageLevel::Unknown; // if the launcher part set a log level, use it - auto innerLevel = MessageLevel::fromLine(line); + QString lineTemp = line; // don't edit out the time and level for clarity + auto innerLevel = MessageLevel::fromLine(lineTemp); if (innerLevel != MessageLevel::Unknown) { level = innerLevel; } From 1aa8d7bc13bf53d00eb60a4fd439404446f53105 Mon Sep 17 00:00:00 2001 From: Yihe Li Date: Tue, 3 Jun 2025 15:44:11 +0800 Subject: [PATCH 295/695] Reuse OtherLogsPage directly Signed-off-by: Yihe Li --- launcher/Application.cpp | 4 +- launcher/CMakeLists.txt | 3 - launcher/InstancePageProvider.h | 2 +- launcher/ui/pages/global/LauncherLogPage.cpp | 578 ------------------- launcher/ui/pages/global/LauncherLogPage.h | 119 ---- launcher/ui/pages/global/LauncherLogPage.ui | 233 -------- launcher/ui/pages/instance/LogPage.cpp | 76 +++ launcher/ui/pages/instance/LogPage.h | 14 +- launcher/ui/pages/instance/OtherLogsPage.cpp | 160 ++++- launcher/ui/pages/instance/OtherLogsPage.h | 16 +- launcher/ui/pages/instance/OtherLogsPage.ui | 10 + 11 files changed, 251 insertions(+), 964 deletions(-) delete mode 100644 launcher/ui/pages/global/LauncherLogPage.cpp delete mode 100644 launcher/ui/pages/global/LauncherLogPage.h delete mode 100644 launcher/ui/pages/global/LauncherLogPage.ui diff --git a/launcher/Application.cpp b/launcher/Application.cpp index ef2530e0d..86e454802 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -63,10 +63,10 @@ #include "ui/pages/global/ExternalToolsPage.h" #include "ui/pages/global/JavaPage.h" #include "ui/pages/global/LanguagePage.h" -#include "ui/pages/global/LauncherLogPage.h" #include "ui/pages/global/LauncherPage.h" #include "ui/pages/global/MinecraftPage.h" #include "ui/pages/global/ProxyPage.h" +#include "ui/pages/instance/OtherLogsPage.h" #include "ui/setupwizard/AutoJavaWizardPage.h" #include "ui/setupwizard/JavaWizardPage.h" @@ -905,7 +905,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); - m_globalSettingsProvider->addPage(); + m_globalSettingsProvider->addPageCreator([]() { return new OtherLogsPage("launcher-logs", tr("Logs"), "Launcher-Logs"); }); } PixmapCache::setInstance(new PixmapCache(this)); diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index cd9903067..a7ccb809d 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -959,8 +959,6 @@ SET(LAUNCHER_SOURCES ui/pages/global/MinecraftPage.h ui/pages/global/LauncherPage.cpp ui/pages/global/LauncherPage.h - ui/pages/global/LauncherLogPage.cpp - ui/pages/global/LauncherLogPage.h ui/pages/global/AppearancePage.h ui/pages/global/ProxyPage.cpp ui/pages/global/ProxyPage.h @@ -1205,7 +1203,6 @@ qt_wrap_ui(LAUNCHER_UI ui/pages/global/AccountListPage.ui ui/pages/global/JavaPage.ui ui/pages/global/LauncherPage.ui - ui/pages/global/LauncherLogPage.ui ui/pages/global/APIPage.ui ui/pages/global/ProxyPage.ui ui/pages/global/ExternalToolsPage.ui diff --git a/launcher/InstancePageProvider.h b/launcher/InstancePageProvider.h index 2c2b0b580..258ed5aa5 100644 --- a/launcher/InstancePageProvider.h +++ b/launcher/InstancePageProvider.h @@ -46,7 +46,7 @@ class InstancePageProvider : protected QObject, public BasePageProvider { // values.append(new GameOptionsPage(onesix.get())); values.append(new ScreenshotsPage(FS::PathCombine(onesix->gameRoot(), "screenshots"))); values.append(new InstanceSettingsPage(onesix)); - values.append(new OtherLogsPage(inst)); + values.append(new OtherLogsPage("logs", tr("Other logs"), "Other-Logs", inst)); return values; } diff --git a/launcher/ui/pages/global/LauncherLogPage.cpp b/launcher/ui/pages/global/LauncherLogPage.cpp deleted file mode 100644 index 2f8dbac53..000000000 --- a/launcher/ui/pages/global/LauncherLogPage.cpp +++ /dev/null @@ -1,578 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (c) 2022 Jamie Mansfield - * Copyright (C) 2024 TheKodeToad - * Copyright (c) 2025 Yihe Li - * - * 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 "LauncherLogPage.h" -#include "ui_LauncherLogPage.h" - -#include - -#include "ui/GuiUtil.h" -#include "ui/themes/ThemeManager.h" - -#include -#include -#include -#include -#include -#include -#include - -QVariant LogFormatProxyModel::data(const QModelIndex& index, int role) const -{ - const LogColors& colors = APPLICATION->themeManager()->getLogColors(); - - switch (role) { - case Qt::FontRole: - return m_font; - case Qt::ForegroundRole: { - auto level = static_cast(QIdentityProxyModel::data(index, LogModel::LevelRole).toInt()); - QColor result = colors.foreground.value(level); - - if (result.isValid()) - return result; - - break; - } - case Qt::BackgroundRole: { - auto level = static_cast(QIdentityProxyModel::data(index, LogModel::LevelRole).toInt()); - QColor result = colors.background.value(level); - - if (result.isValid()) - return result; - - break; - } - } - - return QIdentityProxyModel::data(index, role); -} - -QModelIndex LogFormatProxyModel::find(const QModelIndex& start, const QString& value, bool reverse) const -{ - QModelIndex parentIndex = parent(start); - auto compare = [this, start, parentIndex, value](int r) -> QModelIndex { - QModelIndex idx = index(r, start.column(), parentIndex); - if (!idx.isValid() || idx == start) { - return QModelIndex(); - } - QVariant v = data(idx, Qt::DisplayRole); - QString t = v.toString(); - if (t.contains(value, Qt::CaseInsensitive)) - return idx; - return QModelIndex(); - }; - if (reverse) { - int from = start.row(); - int to = 0; - - for (int i = 0; i < 2; ++i) { - for (int r = from; (r >= to); --r) { - auto idx = compare(r); - if (idx.isValid()) - return idx; - } - // prepare for the next iteration - from = rowCount() - 1; - to = start.row(); - } - } else { - int from = start.row(); - int to = rowCount(parentIndex); - - for (int i = 0; i < 2; ++i) { - for (int r = from; (r < to); ++r) { - auto idx = compare(r); - if (idx.isValid()) - return idx; - } - // prepare for the next iteration - from = 0; - to = start.row(); - } - } - return QModelIndex(); -} - -LauncherLogPage::LauncherLogPage(QWidget* parent) - : QWidget(parent) - , ui(new Ui::LauncherLogPage) - , m_model(APPLICATION->logModel) - , m_basePath(APPLICATION->dataRoot()) - , m_logSearchPaths({ "logs" }) -{ - ui->setupUi(this); - ui->tabWidget->tabBar()->hide(); - - m_proxy = new LogFormatProxyModel(this); - - // set up fonts in the log proxy - { - QString fontFamily = APPLICATION->settings()->get("ConsoleFont").toString(); - bool conversionOk = false; - int fontSize = APPLICATION->settings()->get("ConsoleFontSize").toInt(&conversionOk); - if (!conversionOk) { - fontSize = 11; - } - m_proxy->setFont(QFont(fontFamily, fontSize)); - } - - ui->text->setModel(m_proxy); - m_proxy->setSourceModel(m_model.get()); - modelStateToUI(); - - connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &LauncherLogPage::populateSelectLogBox); - - auto findShortcut = new QShortcut(QKeySequence(QKeySequence::Find), this); - connect(findShortcut, &QShortcut::activated, this, &LauncherLogPage::findActivated); - - auto findNextShortcut = new QShortcut(QKeySequence(QKeySequence::FindNext), this); - connect(findNextShortcut, &QShortcut::activated, this, &LauncherLogPage::findNextActivated); - - auto findPreviousShortcut = new QShortcut(QKeySequence(QKeySequence::FindPrevious), this); - connect(findPreviousShortcut, &QShortcut::activated, this, &LauncherLogPage::findPreviousActivated); - - connect(ui->searchBar, &QLineEdit::returnPressed, this, &LauncherLogPage::on_findButton_clicked); -} - -LauncherLogPage::~LauncherLogPage() -{ - delete ui; -} - -void LauncherLogPage::modelStateToUI() -{ - if (m_model->wrapLines()) { - ui->text->setWordWrap(true); - ui->wrapCheckbox->setCheckState(Qt::Checked); - } else { - ui->text->setWordWrap(false); - ui->wrapCheckbox->setCheckState(Qt::Unchecked); - } - if (m_model->colorLines()) { - ui->text->setColorLines(true); - ui->colorCheckbox->setCheckState(Qt::Checked); - } else { - ui->text->setColorLines(false); - ui->colorCheckbox->setCheckState(Qt::Unchecked); - } - if (m_model->suspended()) { - ui->trackLogCheckbox->setCheckState(Qt::Unchecked); - } else { - ui->trackLogCheckbox->setCheckState(Qt::Checked); - } -} - -void LauncherLogPage::UIToModelState() -{ - if (!m_model) { - return; - } - m_model->setLineWrap(ui->wrapCheckbox->checkState() == Qt::Checked); - m_model->setColorLines(ui->colorCheckbox->checkState() == Qt::Checked); - m_model->suspend(ui->trackLogCheckbox->checkState() != Qt::Checked); -} - -void LauncherLogPage::retranslate() -{ - ui->retranslateUi(this); -} - -void LauncherLogPage::openedImpl() -{ - const QStringList failedPaths = m_watcher.addPaths(m_logSearchPaths); - - for (const QString& path : m_logSearchPaths) { - if (failedPaths.contains(path)) - qDebug() << "Failed to start watching" << path; - else - qDebug() << "Started watching" << path; - } - - populateSelectLogBox(); -} - -void LauncherLogPage::closedImpl() -{ - const QStringList failedPaths = m_watcher.removePaths(m_logSearchPaths); - - for (const QString& path : m_logSearchPaths) { - if (failedPaths.contains(path)) - qDebug() << "Failed to stop watching" << path; - else - qDebug() << "Stopped watching" << path; - } -} - -void LauncherLogPage::populateSelectLogBox() -{ - const QString prevCurrentFile = m_currentFile; - - ui->selectLogBox->blockSignals(true); - ui->selectLogBox->clear(); - ui->selectLogBox->addItem("Current logs"); - ui->selectLogBox->addItems(getPaths()); - ui->selectLogBox->blockSignals(false); - - if (!prevCurrentFile.isEmpty()) { - const int index = ui->selectLogBox->findText(prevCurrentFile); - if (index != -1) { - ui->selectLogBox->blockSignals(true); - ui->selectLogBox->setCurrentIndex(index); - ui->selectLogBox->blockSignals(false); - setControlsEnabled(true); - // don't refresh file - return; - } else { - setControlsEnabled(false); - } - } else { - ui->selectLogBox->setCurrentIndex(0); - setControlsEnabled(true); - } - - on_selectLogBox_currentIndexChanged(ui->selectLogBox->currentIndex()); -} - -void LauncherLogPage::on_selectLogBox_currentIndexChanged(const int index) -{ - QString file; - if (index > 0) { - file = ui->selectLogBox->itemText(index); - } - - if (index != 0 && (file.isEmpty() || !QFile::exists(FS::PathCombine(m_basePath, file)))) { - m_currentFile = QString(); - ui->text->clear(); - setControlsEnabled(false); - } else { - m_currentFile = file; - reload(); - setControlsEnabled(true); - } -} - -void LauncherLogPage::on_btnReload_clicked() -{ - if (m_currentFile.isEmpty()) { - if (!m_model) - return; - m_model->clear(); - m_container->refreshContainer(); - } else { - reload(); - } -} - -void LauncherLogPage::reload() -{ - if (m_currentFile.isEmpty()) { - m_model = APPLICATION->logModel; - m_proxy->setSourceModel(m_model.get()); - ui->text->setModel(m_proxy); - ui->text->scrollToBottom(); - UIToModelState(); - setControlsEnabled(true); - return; - } - - QFile file(FS::PathCombine(m_basePath, m_currentFile)); - if (!file.open(QFile::ReadOnly)) { - setControlsEnabled(false); - ui->btnReload->setEnabled(true); // allow reload - m_currentFile = QString(); - QMessageBox::critical(this, tr("Error"), tr("Unable to open %1 for reading: %2").arg(m_currentFile, file.errorString())); - } else { - auto setPlainText = [this](const QString& text) { - QTextDocument* doc = ui->text->document(); - doc->setDefaultFont(m_proxy->getFont()); - ui->text->setPlainText(text); - }; - auto showTooBig = [setPlainText, &file]() { - setPlainText(tr("The file (%1) is too big. You may want to open it in a viewer optimized " - "for large files.") - .arg(file.fileName())); - }; - if (file.size() > (1024ll * 1024ll * 12ll)) { - showTooBig(); - return; - } - MessageLevel::Enum last = MessageLevel::Unknown; - - auto handleLine = [this, &last](QString line) { - if (line.isEmpty()) - return false; - if (line.back() == '\n') - line = line.remove(line.size() - 1, 1); - QString lineTemp = line; // don't edit out the time and level for clarity - MessageLevel::Enum level = MessageLevel::fromLauncherLine(lineTemp); - - last = level; - m_model->append(level, line); - return m_model->isOverFlow(); - }; - - // Try to determine a level for each line - ui->text->clear(); - ui->text->setModel(nullptr); - m_model.reset(new LogModel(this)); - m_model->setMaxLines(APPLICATION->getConsoleMaxLines()); - m_model->setStopOnOverflow(APPLICATION->shouldStopOnConsoleOverflow()); - m_model->setOverflowMessage(tr("Cannot display this log since the log length surpassed %1 lines.").arg(m_model->getMaxLines())); - m_model->clear(); - if (file.fileName().endsWith(".gz")) { - QString line; - auto error = GZip::readGzFileByBlocks(&file, [&line, handleLine](const QByteArray& d) { - auto block = d; - int newlineIndex = block.indexOf('\n'); - while (newlineIndex != -1) { - line += QString::fromUtf8(block).left(newlineIndex); - block.remove(0, newlineIndex + 1); - if (handleLine(line)) { - line.clear(); - return false; - } - line.clear(); - newlineIndex = block.indexOf('\n'); - } - line += QString::fromUtf8(block); - return true; - }); - if (!error.isEmpty()) { - setPlainText(tr("The file (%1) encountered an error when reading: %2.").arg(file.fileName(), error)); - return; - } else if (!line.isEmpty()) { - handleLine(line); - } - } else { - while (!file.atEnd() && !handleLine(QString::fromUtf8(file.readLine()))) { - } - } - m_proxy->setSourceModel(m_model.get()); - ui->text->setModel(m_proxy); - ui->text->scrollToBottom(); - UIToModelState(); - setControlsEnabled(true); - } -} - -void LauncherLogPage::on_btnPaste_clicked() -{ - GuiUtil::uploadPaste(m_currentFile, ui->text->toPlainText(), this); -} - -void LauncherLogPage::on_btnCopy_clicked() -{ - GuiUtil::setClipboardText(ui->text->toPlainText()); -} - -void LauncherLogPage::on_btnBottom_clicked() -{ - ui->text->scrollToBottom(); -} - -void LauncherLogPage::on_trackLogCheckbox_clicked(bool checked) -{ - if (!m_model) - return; - m_model->suspend(!checked); -} - -void LauncherLogPage::on_btnDelete_clicked() -{ - if (m_currentFile.isEmpty()) { - setControlsEnabled(false); - return; - } - if (QMessageBox::question(this, tr("Confirm Deletion"), - tr("You are about to delete \"%1\".\n" - "This may be permanent and it will be gone from the logs folder.\n\n" - "Are you sure?") - .arg(m_currentFile), - QMessageBox::Yes, QMessageBox::No) == QMessageBox::No) { - return; - } - QFile file(FS::PathCombine(m_basePath, m_currentFile)); - - if (FS::trash(file.fileName())) { - return; - } - - if (!file.remove()) { - QMessageBox::critical(this, tr("Error"), tr("Unable to delete %1: %2").arg(m_currentFile, file.errorString())); - } -} - -void LauncherLogPage::on_btnClean_clicked() -{ - auto toDelete = getPaths(); - if (toDelete.isEmpty()) { - return; - } - QMessageBox* messageBox = new QMessageBox(this); - messageBox->setWindowTitle(tr("Confirm Cleanup")); - if (toDelete.size() > 5) { - messageBox->setText(tr("Are you sure you want to delete all log files?")); - messageBox->setDetailedText(toDelete.join('\n')); - } else { - messageBox->setText(tr("Are you sure you want to delete all these files?\n%1").arg(toDelete.join('\n'))); - } - messageBox->setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel); - messageBox->setDefaultButton(QMessageBox::Ok); - messageBox->setTextInteractionFlags(Qt::TextSelectableByMouse); - messageBox->setIcon(QMessageBox::Question); - messageBox->setTextInteractionFlags(Qt::TextBrowserInteraction); - - if (messageBox->exec() != QMessageBox::Ok) { - return; - } - QStringList failed; - for (auto item : toDelete) { - QString absolutePath = FS::PathCombine(m_basePath, item); - QFile file(absolutePath); - qDebug() << "Deleting log" << absolutePath; - if (FS::trash(file.fileName())) { - continue; - } - if (!file.remove()) { - failed.push_back(item); - } - } - if (!failed.empty()) { - QMessageBox* messageBoxFailure = new QMessageBox(this); - messageBoxFailure->setWindowTitle(tr("Error")); - if (failed.size() > 5) { - messageBoxFailure->setText(tr("Couldn't delete some files!")); - messageBoxFailure->setDetailedText(failed.join('\n')); - } else { - messageBoxFailure->setText(tr("Couldn't delete some files:\n%1").arg(failed.join('\n'))); - } - messageBoxFailure->setStandardButtons(QMessageBox::Ok); - messageBoxFailure->setDefaultButton(QMessageBox::Ok); - messageBoxFailure->setTextInteractionFlags(Qt::TextSelectableByMouse); - messageBoxFailure->setIcon(QMessageBox::Critical); - messageBoxFailure->setTextInteractionFlags(Qt::TextBrowserInteraction); - messageBoxFailure->exec(); - } -} - -void LauncherLogPage::on_wrapCheckbox_clicked(bool checked) -{ - ui->text->setWordWrap(checked); - if (!m_model) - return; - m_model->setLineWrap(checked); - ui->text->scrollToBottom(); -} - -void LauncherLogPage::on_colorCheckbox_clicked(bool checked) -{ - ui->text->setColorLines(checked); - if (!m_model) - return; - m_model->setColorLines(checked); - ui->text->scrollToBottom(); -} - -void LauncherLogPage::setControlsEnabled(const bool enabled) -{ - if (!m_currentFile.isEmpty()) { - ui->btnReload->setText("&Reload"); - ui->btnReload->setToolTip("Reload the contents of the log from the disk"); - ui->btnDelete->setEnabled(enabled); - ui->btnClean->setEnabled(enabled); - ui->trackLogCheckbox->setEnabled(false); - } else { - ui->btnReload->setText("Clear"); - ui->btnReload->setToolTip("Clear the log"); - ui->btnDelete->setEnabled(false); - ui->btnClean->setEnabled(false); - ui->trackLogCheckbox->setEnabled(enabled); - } - ui->btnReload->setEnabled(enabled); - ui->btnCopy->setEnabled(enabled); - ui->btnPaste->setEnabled(enabled); - ui->text->setEnabled(enabled); -} - -QStringList LauncherLogPage::getPaths() -{ - QDir baseDir(m_basePath); - - QStringList result; - - for (QString searchPath : m_logSearchPaths) { - QDir searchDir(searchPath); - - QStringList filters{ "*.log", "*.log.gz" }; - - if (searchPath != m_basePath) - filters.append("*.txt"); - - QStringList entries = searchDir.entryList(filters, QDir::Files | QDir::Readable, QDir::SortFlag::Time); - - for (const QString& name : entries) - result.append(baseDir.relativeFilePath(searchDir.filePath(name))); - } - - return result; -} - -void LauncherLogPage::on_findButton_clicked() -{ - auto modifiers = QApplication::keyboardModifiers(); - bool reverse = modifiers & Qt::ShiftModifier; - ui->text->findNext(ui->searchBar->text(), reverse); -} - -void LauncherLogPage::findNextActivated() -{ - ui->text->findNext(ui->searchBar->text(), false); -} - -void LauncherLogPage::findPreviousActivated() -{ - ui->text->findNext(ui->searchBar->text(), true); -} - -void LauncherLogPage::findActivated() -{ - // focus the search bar if it doesn't have focus - if (!ui->searchBar->hasFocus()) { - ui->searchBar->setFocus(); - ui->searchBar->selectAll(); - } -} diff --git a/launcher/ui/pages/global/LauncherLogPage.h b/launcher/ui/pages/global/LauncherLogPage.h deleted file mode 100644 index 4a6fb5882..000000000 --- a/launcher/ui/pages/global/LauncherLogPage.h +++ /dev/null @@ -1,119 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (c) 2022 Jamie Mansfield - * Copyright (c) 2025 Yihe Li - * - * 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 -#include -#include - -#include -#include "BaseInstance.h" -#include "launch/LaunchTask.h" -#include "ui/pages/BasePage.h" - -namespace Ui { -class LauncherLogPage; -} -class QTextCharFormat; -class RecursiveFileSystemWatcher; - -class LogFormatProxyModel : public QIdentityProxyModel { - public: - LogFormatProxyModel(QObject* parent = nullptr) : QIdentityProxyModel(parent) {} - QVariant data(const QModelIndex& index, int role) const override; - QFont getFont() const { return m_font; } - void setFont(QFont font) { m_font = font; } - QModelIndex find(const QModelIndex& start, const QString& value, bool reverse) const; - - private: - QFont m_font; -}; - -class LauncherLogPage : public QWidget, public BasePage { - Q_OBJECT - - public: - explicit LauncherLogPage(QWidget* parent = 0); - ~LauncherLogPage(); - - QString displayName() const override { return tr("Logs"); } - QIcon icon() const override { return APPLICATION->getThemedIcon("log"); } - QString id() const override { return "launcher-console"; } - QString helpPage() const override { return "Launcher-Logs"; } - void retranslate() override; - - void openedImpl() override; - void closedImpl() override; - - private slots: - void populateSelectLogBox(); - void on_selectLogBox_currentIndexChanged(int index); - void on_btnReload_clicked(); - void on_btnPaste_clicked(); - void on_btnCopy_clicked(); - void on_btnDelete_clicked(); - void on_btnClean_clicked(); - void on_btnBottom_clicked(); - - void on_trackLogCheckbox_clicked(bool checked); - void on_wrapCheckbox_clicked(bool checked); - void on_colorCheckbox_clicked(bool checked); - - void on_findButton_clicked(); - void findActivated(); - void findNextActivated(); - void findPreviousActivated(); - - private: - void reload(); - void modelStateToUI(); - void UIToModelState(); - void setControlsEnabled(bool enabled); - - QStringList getPaths(); - - private: - Ui::LauncherLogPage* ui; - LogFormatProxyModel* m_proxy; - shared_qobject_ptr m_model; - - /** Path to display log paths relative to. */ - QString m_basePath; - QStringList m_logSearchPaths; - QString m_currentFile; - QFileSystemWatcher m_watcher; -}; diff --git a/launcher/ui/pages/global/LauncherLogPage.ui b/launcher/ui/pages/global/LauncherLogPage.ui deleted file mode 100644 index 189f2fe78..000000000 --- a/launcher/ui/pages/global/LauncherLogPage.ui +++ /dev/null @@ -1,233 +0,0 @@ - - - LauncherLogPage - - - - 0 - 0 - 825 - 782 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - 0 - - - - Tab 1 - - - - - - Search: - - - - - - - - - - &Find - - - - - - - Qt::Vertical - - - - - - - Scroll all the way to bottom - - - &Bottom - - - - - - - false - - - true - - - - - - Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - false - - - - - - - - - - - - 0 - 0 - - - - - - - - Delete the selected log - - - &Delete Selected - - - - - - - Delete all the logs - - - Delete &All - - - - - - - - - - - Keep updating - - - true - - - - - - - Wrap lines - - - true - - - - - - - Color lines - - - true - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Copy the whole log into the clipboard - - - &Copy - - - - - - - Upload the log to the paste service configured in preferences - - - &Upload - - - - - - - Reload the contents of the log from the disk - - - &Reload - - - - - - - - - - - - - - - - LogView - QPlainTextEdit -
    ui/widgets/LogView.h
    -
    -
    - - tabWidget - selectLogBox - btnReload - btnCopy - btnPaste - btnDelete - btnClean - wrapCheckbox - colorCheckbox - text - searchBar - findButton - - - -
    diff --git a/launcher/ui/pages/instance/LogPage.cpp b/launcher/ui/pages/instance/LogPage.cpp index d1691ff16..7897a2932 100644 --- a/launcher/ui/pages/instance/LogPage.cpp +++ b/launcher/ui/pages/instance/LogPage.cpp @@ -52,6 +52,82 @@ #include +QVariant LogFormatProxyModel::data(const QModelIndex& index, int role) const +{ + const LogColors& colors = APPLICATION->themeManager()->getLogColors(); + + switch (role) { + case Qt::FontRole: + return m_font; + case Qt::ForegroundRole: { + auto level = static_cast(QIdentityProxyModel::data(index, LogModel::LevelRole).toInt()); + QColor result = colors.foreground.value(level); + + if (result.isValid()) + return result; + + break; + } + case Qt::BackgroundRole: { + auto level = static_cast(QIdentityProxyModel::data(index, LogModel::LevelRole).toInt()); + QColor result = colors.background.value(level); + + if (result.isValid()) + return result; + + break; + } + } + + return QIdentityProxyModel::data(index, role); +} + +QModelIndex LogFormatProxyModel::find(const QModelIndex& start, const QString& value, bool reverse) const +{ + QModelIndex parentIndex = parent(start); + auto compare = [this, start, parentIndex, value](int r) -> QModelIndex { + QModelIndex idx = index(r, start.column(), parentIndex); + if (!idx.isValid() || idx == start) { + return QModelIndex(); + } + QVariant v = data(idx, Qt::DisplayRole); + QString t = v.toString(); + if (t.contains(value, Qt::CaseInsensitive)) + return idx; + return QModelIndex(); + }; + if (reverse) { + int from = start.row(); + int to = 0; + + for (int i = 0; i < 2; ++i) { + for (int r = from; (r >= to); --r) { + auto idx = compare(r); + if (idx.isValid()) + return idx; + } + // prepare for the next iteration + from = rowCount() - 1; + to = start.row(); + } + } else { + int from = start.row(); + int to = rowCount(parentIndex); + + for (int i = 0; i < 2; ++i) { + for (int r = from; (r < to); ++r) { + auto idx = compare(r); + if (idx.isValid()) + return idx; + } + // prepare for the next iteration + from = 0; + to = start.row(); + } + } + return QModelIndex(); +} + LogPage::LogPage(InstancePtr instance, QWidget* parent) : QWidget(parent), ui(new Ui::LogPage), m_instance(instance) { ui->setupUi(this); diff --git a/launcher/ui/pages/instance/LogPage.h b/launcher/ui/pages/instance/LogPage.h index caa870cbc..b4d74fb9c 100644 --- a/launcher/ui/pages/instance/LogPage.h +++ b/launcher/ui/pages/instance/LogPage.h @@ -42,11 +42,23 @@ #include "BaseInstance.h" #include "launch/LaunchTask.h" #include "ui/pages/BasePage.h" -#include "ui/pages/global/LauncherLogPage.h" namespace Ui { class LogPage; } +class QTextCharFormat; + +class LogFormatProxyModel : public QIdentityProxyModel { + public: + LogFormatProxyModel(QObject* parent = nullptr) : QIdentityProxyModel(parent) {} + QVariant data(const QModelIndex& index, int role) const override; + QFont getFont() const { return m_font; } + void setFont(QFont font) { m_font = font; } + QModelIndex find(const QModelIndex& start, const QString& value, bool reverse) const; + + private: + QFont m_font; +}; class LogPage : public QWidget, public BasePage { Q_OBJECT diff --git a/launcher/ui/pages/instance/OtherLogsPage.cpp b/launcher/ui/pages/instance/OtherLogsPage.cpp index a90969503..281e5be27 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.cpp +++ b/launcher/ui/pages/instance/OtherLogsPage.cpp @@ -40,6 +40,7 @@ #include #include "ui/GuiUtil.h" +#include "ui/themes/ThemeManager.h" #include #include @@ -49,18 +50,26 @@ #include #include -OtherLogsPage::OtherLogsPage(InstancePtr instance, QWidget* parent) +OtherLogsPage::OtherLogsPage(QString id, QString displayName, QString helpPage, InstancePtr instance, QWidget* parent) : QWidget(parent) + , m_id(id) + , m_displayName(displayName) + , m_helpPage(helpPage) , ui(new Ui::OtherLogsPage) , m_instance(instance) - , m_basePath(instance->gameRoot()) - , m_logSearchPaths(instance->getLogFileSearchPaths()) - , m_model(new LogModel(this)) + , m_basePath(instance ? instance->gameRoot() : APPLICATION->dataRoot()) + , m_logSearchPaths(instance ? instance->getLogFileSearchPaths() : QStringList{ "logs" }) { ui->setupUi(this); ui->tabWidget->tabBar()->hide(); m_proxy = new LogFormatProxyModel(this); + if (m_instance) { + m_model.reset(new LogModel(this)); + ui->trackLogCheckbox->setVisible(false); + } else { + m_model = APPLICATION->logModel; + } // set up fonts in the log proxy { @@ -75,9 +84,13 @@ OtherLogsPage::OtherLogsPage(InstancePtr instance, QWidget* parent) ui->text->setModel(m_proxy); - m_model->setMaxLines(m_instance->getConsoleMaxLines()); - m_model->setStopOnOverflow(m_instance->shouldStopOnConsoleOverflow()); - m_model->setOverflowMessage(tr("Cannot display this log since the log length surpassed %1 lines.").arg(m_model->getMaxLines())); + if (m_instance) { + m_model->setMaxLines(m_instance->getConsoleMaxLines()); + m_model->setStopOnOverflow(m_instance->shouldStopOnConsoleOverflow()); + m_model->setOverflowMessage(tr("Cannot display this log since the log length surpassed %1 lines.").arg(m_model->getMaxLines())); + } else { + modelStateToUI(); + } m_proxy->setSourceModel(m_model.get()); connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &OtherLogsPage::populateSelectLogBox); @@ -99,6 +112,39 @@ OtherLogsPage::~OtherLogsPage() delete ui; } +void OtherLogsPage::modelStateToUI() +{ + if (m_model->wrapLines()) { + ui->text->setWordWrap(true); + ui->wrapCheckbox->setCheckState(Qt::Checked); + } else { + ui->text->setWordWrap(false); + ui->wrapCheckbox->setCheckState(Qt::Unchecked); + } + if (m_model->colorLines()) { + ui->text->setColorLines(true); + ui->colorCheckbox->setCheckState(Qt::Checked); + } else { + ui->text->setColorLines(false); + ui->colorCheckbox->setCheckState(Qt::Unchecked); + } + if (m_model->suspended()) { + ui->trackLogCheckbox->setCheckState(Qt::Unchecked); + } else { + ui->trackLogCheckbox->setCheckState(Qt::Checked); + } +} + +void OtherLogsPage::UIToModelState() +{ + if (!m_model) { + return; + } + m_model->setLineWrap(ui->wrapCheckbox->checkState() == Qt::Checked); + m_model->setColorLines(ui->colorCheckbox->checkState() == Qt::Checked); + m_model->suspend(ui->trackLogCheckbox->checkState() != Qt::Checked); +} + void OtherLogsPage::retranslate() { ui->retranslateUi(this); @@ -136,6 +182,8 @@ void OtherLogsPage::populateSelectLogBox() ui->selectLogBox->blockSignals(true); ui->selectLogBox->clear(); + if (!m_instance) + ui->selectLogBox->addItem("Current logs"); ui->selectLogBox->addItems(getPaths()); ui->selectLogBox->blockSignals(false); @@ -151,6 +199,9 @@ void OtherLogsPage::populateSelectLogBox() } else { setControlsEnabled(false); } + } else if (!m_instance) { + ui->selectLogBox->setCurrentIndex(0); + setControlsEnabled(true); } on_selectLogBox_currentIndexChanged(ui->selectLogBox->currentIndex()); @@ -159,27 +210,49 @@ void OtherLogsPage::populateSelectLogBox() void OtherLogsPage::on_selectLogBox_currentIndexChanged(const int index) { QString file; - if (index != -1) { + if (index > 0 || (index == 0 && m_instance)) { file = ui->selectLogBox->itemText(index); } - if (file.isEmpty() || !QFile::exists(FS::PathCombine(m_basePath, file))) { + if ((index != 0 || m_instance) && (file.isEmpty() || !QFile::exists(FS::PathCombine(m_basePath, file)))) { m_currentFile = QString(); ui->text->clear(); setControlsEnabled(false); } else { m_currentFile = file; - on_btnReload_clicked(); + reload(); setControlsEnabled(true); } } void OtherLogsPage::on_btnReload_clicked() +{ + if (!m_instance && m_currentFile.isEmpty()) { + if (!m_model) + return; + m_model->clear(); + m_container->refreshContainer(); + } else { + reload(); + } +} + +void OtherLogsPage::reload() { if (m_currentFile.isEmpty()) { - setControlsEnabled(false); + if (m_instance) { + setControlsEnabled(false); + } else { + m_model = APPLICATION->logModel; + m_proxy->setSourceModel(m_model.get()); + ui->text->setModel(m_proxy); + ui->text->scrollToBottom(); + UIToModelState(); + setControlsEnabled(true); + } return; } + QFile file(FS::PathCombine(m_basePath, m_currentFile)); if (!file.open(QFile::ReadOnly)) { setControlsEnabled(false); @@ -210,16 +283,20 @@ void OtherLogsPage::on_btnReload_clicked() line = line.remove(line.size() - 1, 1); MessageLevel::Enum level = MessageLevel::Unknown; - // if the launcher part set a log level, use it QString lineTemp = line; // don't edit out the time and level for clarity - auto innerLevel = MessageLevel::fromLine(lineTemp); - if (innerLevel != MessageLevel::Unknown) { - level = innerLevel; - } + if (!m_instance) { + level = MessageLevel::fromLauncherLine(lineTemp); + } else { + // if the launcher part set a log level, use it + auto innerLevel = MessageLevel::fromLine(lineTemp); + if (innerLevel != MessageLevel::Unknown) { + level = innerLevel; + } - // If the level is still undetermined, guess level - if (level == MessageLevel::StdErr || level == MessageLevel::StdOut || level == MessageLevel::Unknown) { - level = LogParser::guessLevel(line, last); + // If the level is still undetermined, guess level + if (level == MessageLevel::StdErr || level == MessageLevel::StdOut || level == MessageLevel::Unknown) { + level = LogParser::guessLevel(line, last); + } } last = level; @@ -230,6 +307,12 @@ void OtherLogsPage::on_btnReload_clicked() // Try to determine a level for each line ui->text->clear(); ui->text->setModel(nullptr); + if (!m_instance) { + m_model.reset(new LogModel(this)); + m_model->setMaxLines(APPLICATION->getConsoleMaxLines()); + m_model->setStopOnOverflow(APPLICATION->shouldStopOnConsoleOverflow()); + m_model->setOverflowMessage(tr("Cannot display this log since the log length surpassed %1 lines.").arg(m_model->getMaxLines())); + } m_model->clear(); if (file.fileName().endsWith(".gz")) { QString line; @@ -259,8 +342,17 @@ void OtherLogsPage::on_btnReload_clicked() while (!file.atEnd() && !handleLine(QString::fromUtf8(file.readLine()))) { } } - ui->text->setModel(m_proxy); - ui->text->scrollToBottom(); + + if (m_instance) { + ui->text->setModel(m_proxy); + ui->text->scrollToBottom(); + } else { + m_proxy->setSourceModel(m_model.get()); + ui->text->setModel(m_proxy); + ui->text->scrollToBottom(); + UIToModelState(); + setControlsEnabled(true); + } } } @@ -279,6 +371,13 @@ void OtherLogsPage::on_btnBottom_clicked() ui->text->scrollToBottom(); } +void OtherLogsPage::on_trackLogCheckbox_clicked(bool checked) +{ + if (!m_model) + return; + m_model->suspend(!checked); +} + void OtherLogsPage::on_btnDelete_clicked() { if (m_currentFile.isEmpty()) { @@ -377,12 +476,27 @@ void OtherLogsPage::on_colorCheckbox_clicked(bool checked) void OtherLogsPage::setControlsEnabled(const bool enabled) { + if (m_instance) { + ui->btnDelete->setEnabled(enabled); + ui->btnClean->setEnabled(enabled); + } else if (!m_currentFile.isEmpty()) { + ui->btnReload->setText("&Reload"); + ui->btnReload->setToolTip("Reload the contents of the log from the disk"); + ui->btnDelete->setEnabled(enabled); + ui->btnClean->setEnabled(enabled); + ui->trackLogCheckbox->setEnabled(false); + } else { + ui->btnReload->setText("Clear"); + ui->btnReload->setToolTip("Clear the log"); + ui->btnDelete->setEnabled(false); + ui->btnClean->setEnabled(false); + ui->trackLogCheckbox->setEnabled(enabled); + } + ui->btnReload->setEnabled(enabled); - ui->btnDelete->setEnabled(enabled); ui->btnCopy->setEnabled(enabled); ui->btnPaste->setEnabled(enabled); ui->text->setEnabled(enabled); - ui->btnClean->setEnabled(enabled); } QStringList OtherLogsPage::getPaths() diff --git a/launcher/ui/pages/instance/OtherLogsPage.h b/launcher/ui/pages/instance/OtherLogsPage.h index 70eb145fb..4104d8f3c 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.h +++ b/launcher/ui/pages/instance/OtherLogsPage.h @@ -53,13 +53,13 @@ class OtherLogsPage : public QWidget, public BasePage { Q_OBJECT public: - explicit OtherLogsPage(InstancePtr instance, QWidget* parent = 0); + explicit OtherLogsPage(QString id, QString displayName, QString helpPage, InstancePtr instance = nullptr, QWidget* parent = 0); ~OtherLogsPage(); - QString id() const override { return "logs"; } - QString displayName() const override { return tr("Other logs"); } + QString id() const override { return m_id; } + QString displayName() const override { return m_displayName; } QIcon icon() const override { return APPLICATION->getThemedIcon("log"); } - QString helpPage() const override { return "other-Logs"; } + QString helpPage() const override { return m_helpPage; } void retranslate() override; void openedImpl() override; @@ -75,6 +75,7 @@ class OtherLogsPage : public QWidget, public BasePage { void on_btnClean_clicked(); void on_btnBottom_clicked(); + void on_trackLogCheckbox_clicked(bool checked); void on_wrapCheckbox_clicked(bool checked); void on_colorCheckbox_clicked(bool checked); @@ -84,11 +85,18 @@ class OtherLogsPage : public QWidget, public BasePage { void findPreviousActivated(); private: + void reload(); + void modelStateToUI(); + void UIToModelState(); void setControlsEnabled(bool enabled); QStringList getPaths(); private: + QString m_id; + QString m_displayName; + QString m_helpPage; + Ui::OtherLogsPage* ui; InstancePtr m_instance; /** Path to display log paths relative to. */ diff --git a/launcher/ui/pages/instance/OtherLogsPage.ui b/launcher/ui/pages/instance/OtherLogsPage.ui index 6d1a46139..7d60de5c4 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.ui +++ b/launcher/ui/pages/instance/OtherLogsPage.ui @@ -127,6 +127,16 @@ + + + + Keep updating + + + true + + + From 4f5db2e49f6dc987335f90e3d90a93dd94746473 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Thu, 5 Jun 2025 00:34:45 +0300 Subject: [PATCH 296/695] chore: fixe some codeql warnings Signed-off-by: Trial97 --- launcher/SysInfo.h | 1 + launcher/logs/LogParser.h | 2 +- launcher/minecraft/AssetsUtils.cpp | 2 +- launcher/minecraft/Component.cpp | 2 +- launcher/minecraft/Component.h | 2 +- launcher/minecraft/ComponentUpdateTask.cpp | 2 +- launcher/minecraft/auth/AccountData.cpp | 2 +- launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp | 7 ++++--- launcher/modplatform/atlauncher/ATLPackInstallTask.cpp | 2 +- launcher/modplatform/atlauncher/ATLPackInstallTask.h | 2 +- launcher/ui/pages/instance/McClient.h | 1 + 11 files changed, 14 insertions(+), 11 deletions(-) diff --git a/launcher/SysInfo.h b/launcher/SysInfo.h index f3688d60d..f6c04d702 100644 --- a/launcher/SysInfo.h +++ b/launcher/SysInfo.h @@ -1,3 +1,4 @@ +#pragma once #include namespace SysInfo { diff --git a/launcher/logs/LogParser.h b/launcher/logs/LogParser.h index 1a1d86dd1..aaf21e397 100644 --- a/launcher/logs/LogParser.h +++ b/launcher/logs/LogParser.h @@ -16,7 +16,7 @@ * along with this program. If not, see . * */ - +#pragma once #include #include #include diff --git a/launcher/minecraft/AssetsUtils.cpp b/launcher/minecraft/AssetsUtils.cpp index 4406d9b34..083924dc6 100644 --- a/launcher/minecraft/AssetsUtils.cpp +++ b/launcher/minecraft/AssetsUtils.cpp @@ -159,7 +159,7 @@ bool loadAssetsIndexJson(const QString& assetsId, const QString& path, AssetsInd if (key == "hash") { object.hash = value.toString(); } else if (key == "size") { - object.size = value.toDouble(); + object.size = value.toLongLong(); } } diff --git a/launcher/minecraft/Component.cpp b/launcher/minecraft/Component.cpp index ad7ef545c..5f114e942 100644 --- a/launcher/minecraft/Component.cpp +++ b/launcher/minecraft/Component.cpp @@ -462,7 +462,7 @@ void Component::waitLoadMeta() } } -void Component::setUpdateAction(UpdateAction action) +void Component::setUpdateAction(const UpdateAction& action) { m_updateAction = action; } diff --git a/launcher/minecraft/Component.h b/launcher/minecraft/Component.h index 203cc2241..eafdb8ed7 100644 --- a/launcher/minecraft/Component.h +++ b/launcher/minecraft/Component.h @@ -106,7 +106,7 @@ class Component : public QObject, public ProblemProvider { void waitLoadMeta(); - void setUpdateAction(UpdateAction action); + void setUpdateAction(const UpdateAction& action); void clearUpdateAction(); UpdateAction getUpdateAction(); diff --git a/launcher/minecraft/ComponentUpdateTask.cpp b/launcher/minecraft/ComponentUpdateTask.cpp index 36a07ee72..c77ed248d 100644 --- a/launcher/minecraft/ComponentUpdateTask.cpp +++ b/launcher/minecraft/ComponentUpdateTask.cpp @@ -570,7 +570,7 @@ void ComponentUpdateTask::performUpdateActions() component->setVersion(cv.targetVersion); component->waitLoadMeta(); }, - [&component, &instance](const UpdateActionLatestRecommendedCompatible lrc) { + [&component, &instance](const UpdateActionLatestRecommendedCompatible& lrc) { qCDebug(instanceProfileResolveC) << instance->name() << "|" << "UpdateActionLatestRecommendedCompatible" << component->getID() << ":" << component->getVersion() diff --git a/launcher/minecraft/auth/AccountData.cpp b/launcher/minecraft/auth/AccountData.cpp index 9dbe7ffc0..161fd968c 100644 --- a/launcher/minecraft/auth/AccountData.cpp +++ b/launcher/minecraft/auth/AccountData.cpp @@ -41,7 +41,7 @@ #include namespace { -void tokenToJSONV3(QJsonObject& parent, Token t, const char* tokenName) +void tokenToJSONV3(QJsonObject& parent, const Token& t, const char* tokenName) { if (!t.persistent) { return; diff --git a/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp b/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp index b63d36361..29b48c0ef 100644 --- a/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp +++ b/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp @@ -87,7 +87,7 @@ ModPlatform::Dependency GetModDependenciesTask::getOverride(const ModPlatform::D { if (auto isQuilt = m_loaderType & ModPlatform::Quilt; isQuilt || m_loaderType & ModPlatform::Fabric) { auto overide = ModPlatform::getOverrideDeps(); - auto over = std::find_if(overide.cbegin(), overide.cend(), [dep, providerName, isQuilt](auto o) { + auto over = std::find_if(overide.cbegin(), overide.cend(), [dep, providerName, isQuilt](const auto& o) { return o.provider == providerName && dep.addonId == (isQuilt ? o.fabric : o.quilt); }); if (over != overide.cend()) { @@ -207,8 +207,9 @@ Task::Ptr GetModDependenciesTask::prepareDependencyTask(const ModPlatform::Depen if (!pDep->version.addonId.isValid()) { if (m_loaderType & ModPlatform::Quilt) { // falback for quilt auto overide = ModPlatform::getOverrideDeps(); - auto over = std::find_if(overide.cbegin(), overide.cend(), - [dep, provider](auto o) { return o.provider == provider.name && dep.addonId == o.quilt; }); + auto over = std::find_if(overide.cbegin(), overide.cend(), [dep, provider](const auto& o) { + return o.provider == provider.name && dep.addonId == o.quilt; + }); if (over != overide.cend()) { removePack(dep.addonId); addTask(prepareDependencyTask({ over->fabric, dep.type }, provider.name, level)); diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp index 261ef786d..139c4f0c8 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp @@ -391,7 +391,7 @@ QString PackInstallTask::getVersionForLoader(QString uid) return m_version.loader.version; } -QString PackInstallTask::detectLibrary(VersionLibrary library) +QString PackInstallTask::detectLibrary(const VersionLibrary& library) { // Try to detect what the library is if (!library.server.isEmpty() && library.server.split("/").length() >= 3) { diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.h b/launcher/modplatform/atlauncher/ATLPackInstallTask.h index ce8bb636d..8024286e8 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.h +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.h @@ -105,7 +105,7 @@ class PackInstallTask : public InstanceTask { private: QString getDirForModType(ModType type, QString raw); QString getVersionForLoader(QString uid); - QString detectLibrary(VersionLibrary library); + QString detectLibrary(const VersionLibrary& library); bool createLibrariesComponent(QString instanceRoot, std::shared_ptr profile); bool createPackComponent(QString instanceRoot, std::shared_ptr profile); diff --git a/launcher/ui/pages/instance/McClient.h b/launcher/ui/pages/instance/McClient.h index 832b70d40..633e7aaed 100644 --- a/launcher/ui/pages/instance/McClient.h +++ b/launcher/ui/pages/instance/McClient.h @@ -1,3 +1,4 @@ +#pragma once #include #include #include From e4a801fdf7c1a17835a5c78bc90721af66b1678c Mon Sep 17 00:00:00 2001 From: Yihe Li Date: Thu, 5 Jun 2025 07:18:07 +0800 Subject: [PATCH 297/695] Use separate window for viewing logs Signed-off-by: Yihe Li --- launcher/Application.cpp | 2 -- launcher/CMakeLists.txt | 3 ++ launcher/ui/MainWindow.cpp | 14 ++++---- launcher/ui/MainWindow.ui | 8 ++--- launcher/ui/dialogs/ViewLogDialog.cpp | 21 ++++++++++++ launcher/ui/dialogs/ViewLogDialog.h | 22 +++++++++++++ launcher/ui/dialogs/ViewLogDialog.ui | 34 ++++++++++++++++++++ launcher/ui/pages/instance/OtherLogsPage.cpp | 6 ++-- 8 files changed, 94 insertions(+), 16 deletions(-) create mode 100644 launcher/ui/dialogs/ViewLogDialog.cpp create mode 100644 launcher/ui/dialogs/ViewLogDialog.h create mode 100644 launcher/ui/dialogs/ViewLogDialog.ui diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 86e454802..a641a9d8a 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -66,7 +66,6 @@ #include "ui/pages/global/LauncherPage.h" #include "ui/pages/global/MinecraftPage.h" #include "ui/pages/global/ProxyPage.h" -#include "ui/pages/instance/OtherLogsPage.h" #include "ui/setupwizard/AutoJavaWizardPage.h" #include "ui/setupwizard/JavaWizardPage.h" @@ -905,7 +904,6 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); m_globalSettingsProvider->addPage(); - m_globalSettingsProvider->addPageCreator([]() { return new OtherLogsPage("launcher-logs", tr("Logs"), "Launcher-Logs"); }); } PixmapCache::setInstance(new PixmapCache(this)); diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index a7ccb809d..e9e32d481 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -1098,6 +1098,8 @@ SET(LAUNCHER_SOURCES ui/dialogs/ResourceUpdateDialog.h ui/dialogs/InstallLoaderDialog.cpp ui/dialogs/InstallLoaderDialog.h + ui/dialogs/ViewLogDialog.cpp + ui/dialogs/ViewLogDialog.h ui/dialogs/skins/SkinManageDialog.cpp ui/dialogs/skins/SkinManageDialog.h @@ -1256,6 +1258,7 @@ qt_wrap_ui(LAUNCHER_UI ui/dialogs/ScrollMessageBox.ui ui/dialogs/BlockedModsDialog.ui ui/dialogs/ChooseProviderDialog.ui + ui/dialogs/ViewLogDialog.ui ui/dialogs/skins/SkinManageDialog.ui ) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 455a95837..f68b94aca 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -103,6 +103,7 @@ #include "ui/dialogs/NewInstanceDialog.h" #include "ui/dialogs/NewsDialog.h" #include "ui/dialogs/ProgressDialog.h" +#include "ui/dialogs/ViewLogDialog.h" #include "ui/instanceview/InstanceDelegate.h" #include "ui/instanceview/InstanceProxyModel.h" #include "ui/instanceview/InstanceView.h" @@ -238,14 +239,11 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi ui->actionViewJavaFolder->setEnabled(BuildConfig.JAVA_DOWNLOADER_ENABLED); } - { // logs upload - - auto menu = new QMenu(this); - for (auto file : QDir("logs").entryInfoList(QDir::Files)) { - auto action = menu->addAction(file.fileName()); - connect(action, &QAction::triggered, this, [this, file] { GuiUtil::uploadPaste(file.fileName(), file, this); }); - } - ui->actionUploadLog->setMenu(menu); + { // logs viewing + connect(ui->actionViewLog, &QAction::triggered, this, [this] { + ViewLogDialog dialog(this); + dialog.exec(); + }); } // add the toolbar toggles to the view menu diff --git a/launcher/ui/MainWindow.ui b/launcher/ui/MainWindow.ui index 1499ec872..1d29ff628 100644 --- a/launcher/ui/MainWindow.ui +++ b/launcher/ui/MainWindow.ui @@ -215,7 +215,7 @@ - + @@ -663,16 +663,16 @@ Clear cached metadata - + .. - Upload logs + View logs - Upload launcher logs to the selected log provider + View current and previous launcher logs diff --git a/launcher/ui/dialogs/ViewLogDialog.cpp b/launcher/ui/dialogs/ViewLogDialog.cpp new file mode 100644 index 000000000..47c63d9cc --- /dev/null +++ b/launcher/ui/dialogs/ViewLogDialog.cpp @@ -0,0 +1,21 @@ +#include "ViewLogDialog.h" +#include "ui_ViewLogDialog.h" + +#include "ui/pages/instance/OtherLogsPage.h" + +ViewLogDialog::ViewLogDialog(QWidget* parent) + : QDialog(parent) + , ui(new Ui::ViewLogDialog) + , m_page(new OtherLogsPage("launcher-logs", tr("Launcher Logs"), "Launcher-Logs", nullptr, parent)) +{ + ui->setupUi(this); + ui->verticalLayout->insertWidget(0, m_page); + connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); + m_page->opened(); +} + +ViewLogDialog::~ViewLogDialog() +{ + m_page->closed(); + delete ui; +} diff --git a/launcher/ui/dialogs/ViewLogDialog.h b/launcher/ui/dialogs/ViewLogDialog.h new file mode 100644 index 000000000..ebb9ef650 --- /dev/null +++ b/launcher/ui/dialogs/ViewLogDialog.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include + +namespace Ui { +class ViewLogDialog; +} + +class OtherLogsPage; + +class ViewLogDialog : public QDialog { + Q_OBJECT + + public: + explicit ViewLogDialog(QWidget* parent = nullptr); + ~ViewLogDialog(); + + private: + Ui::ViewLogDialog* ui; + OtherLogsPage* m_page; +}; diff --git a/launcher/ui/dialogs/ViewLogDialog.ui b/launcher/ui/dialogs/ViewLogDialog.ui new file mode 100644 index 000000000..4a7bb789e --- /dev/null +++ b/launcher/ui/dialogs/ViewLogDialog.ui @@ -0,0 +1,34 @@ + + + ViewLogDialog + + + + 0 + 0 + 825 + 782 + + + + View Launcher Logs + + + true + + + + + + Qt::Horizontal + + + QDialogButtonBox::Close + + + + + + + + diff --git a/launcher/ui/pages/instance/OtherLogsPage.cpp b/launcher/ui/pages/instance/OtherLogsPage.cpp index 281e5be27..b1f0c6507 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.cpp +++ b/launcher/ui/pages/instance/OtherLogsPage.cpp @@ -231,7 +231,8 @@ void OtherLogsPage::on_btnReload_clicked() if (!m_model) return; m_model->clear(); - m_container->refreshContainer(); + if (m_container) + m_container->refreshContainer(); } else { reload(); } @@ -358,7 +359,8 @@ void OtherLogsPage::reload() void OtherLogsPage::on_btnPaste_clicked() { - GuiUtil::uploadPaste(m_currentFile, ui->text->toPlainText(), this); + QString name = m_currentFile.isEmpty() ? displayName() : m_currentFile; + GuiUtil::uploadPaste(name, ui->text->toPlainText(), this); } void OtherLogsPage::on_btnCopy_clicked() From 7ea15c31a101869f7ee09973b90b68ea83e0ba7d Mon Sep 17 00:00:00 2001 From: Trial97 Date: Thu, 5 Jun 2025 09:26:00 +0300 Subject: [PATCH 298/695] chore: fix some codeql warnings Signed-off-by: Trial97 --- launcher/minecraft/mod/ResourcePack.h | 2 +- launcher/modplatform/atlauncher/ATLPackIndex.h | 1 + launcher/modplatform/flame/FlamePackIndex.h | 1 + .../modplatform/modrinth/ModrinthPackManifest.h | 1 + launcher/ui/pages/instance/McResolver.cpp | 6 +++--- .../modplatform/atlauncher/AtlFilterModel.cpp | 13 ++++++++++--- .../ui/pages/modplatform/atlauncher/AtlPage.cpp | 4 +++- .../ui/pages/modplatform/flame/FlameModel.cpp | 1 + .../ui/pages/modplatform/flame/FlamePage.cpp | 4 +++- .../modplatform/import_ftb/ImportFTBPage.cpp | 7 +++++-- .../pages/modplatform/import_ftb/ListModel.cpp | 12 +++++++++--- .../pages/modplatform/legacy_ftb/ListModel.cpp | 12 +++++++++--- .../ui/pages/modplatform/legacy_ftb/Page.cpp | 16 ++++++++++++---- .../pages/modplatform/modrinth/ModrinthModel.cpp | 5 +++-- .../pages/modplatform/modrinth/ModrinthPage.cpp | 4 +++- .../ui/pages/modplatform/technic/TechnicData.h | 1 + .../ui/pages/modplatform/technic/TechnicPage.cpp | 4 +++- tests/FileSystem_test.cpp | 4 +++- 18 files changed, 72 insertions(+), 26 deletions(-) diff --git a/launcher/minecraft/mod/ResourcePack.h b/launcher/minecraft/mod/ResourcePack.h index f214bedf2..45883c259 100644 --- a/launcher/minecraft/mod/ResourcePack.h +++ b/launcher/minecraft/mod/ResourcePack.h @@ -24,5 +24,5 @@ class ResourcePack : public DataPack { /** Gets, respectively, the lower and upper versions supported by the set pack format. */ [[nodiscard]] std::pair compatibleVersions() const override; - virtual QString directory() { return "/assets"; } + virtual QString directory() override { return "/assets"; } }; diff --git a/launcher/modplatform/atlauncher/ATLPackIndex.h b/launcher/modplatform/atlauncher/ATLPackIndex.h index 187bc05ec..0df5e237c 100644 --- a/launcher/modplatform/atlauncher/ATLPackIndex.h +++ b/launcher/modplatform/atlauncher/ATLPackIndex.h @@ -45,3 +45,4 @@ void loadIndexedPack(IndexedPack& m, QJsonObject& obj); } // namespace ATLauncher Q_DECLARE_METATYPE(ATLauncher::IndexedPack) +Q_DECLARE_METATYPE(QList) \ No newline at end of file diff --git a/launcher/modplatform/flame/FlamePackIndex.h b/launcher/modplatform/flame/FlamePackIndex.h index 30391288b..d2cf2a6aa 100644 --- a/launcher/modplatform/flame/FlamePackIndex.h +++ b/launcher/modplatform/flame/FlamePackIndex.h @@ -50,3 +50,4 @@ void loadIndexedPackVersions(IndexedPack& m, QJsonArray& arr); } // namespace Flame Q_DECLARE_METATYPE(Flame::IndexedPack) +Q_DECLARE_METATYPE(QList) diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.h b/launcher/modplatform/modrinth/ModrinthPackManifest.h index 97b8ab712..6d970b264 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.h +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.h @@ -124,3 +124,4 @@ auto validateDownloadUrl(QUrl) -> bool; Q_DECLARE_METATYPE(Modrinth::Modpack) Q_DECLARE_METATYPE(Modrinth::ModpackVersion) +Q_DECLARE_METATYPE(QList) diff --git a/launcher/ui/pages/instance/McResolver.cpp b/launcher/ui/pages/instance/McResolver.cpp index 2a769762c..5e2b8239c 100644 --- a/launcher/ui/pages/instance/McResolver.cpp +++ b/launcher/ui/pages/instance/McResolver.cpp @@ -37,9 +37,9 @@ void McResolver::pingWithDomainSRV(QString domain, int port) } const auto& firstRecord = records.at(0); - QString domain = firstRecord.target(); - int port = firstRecord.port(); - pingWithDomainA(domain, port); + QString newDomain = firstRecord.target(); + int newPort = firstRecord.port(); + pingWithDomainA(newDomain, newPort); }); lookup->lookup(); diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.cpp index dee3784e5..6868ce736 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.cpp @@ -68,7 +68,10 @@ bool FilterModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParen return true; } QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); - ATLauncher::IndexedPack pack = sourceModel()->data(index, Qt::UserRole).value(); + QVariant raw = sourceModel()->data(index, Qt::UserRole); + Q_ASSERT(raw.canConvert()); + auto pack = raw.value(); + if (searchTerm.startsWith("#")) return QString::number(pack.id) == searchTerm.mid(1); return pack.name.contains(searchTerm, Qt::CaseInsensitive); @@ -76,8 +79,12 @@ bool FilterModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParen bool FilterModel::lessThan(const QModelIndex& left, const QModelIndex& right) const { - ATLauncher::IndexedPack leftPack = sourceModel()->data(left, Qt::UserRole).value(); - ATLauncher::IndexedPack rightPack = sourceModel()->data(right, Qt::UserRole).value(); + QVariant leftRaw = sourceModel()->data(left, Qt::UserRole); + Q_ASSERT(leftRaw.canConvert()); + auto leftPack = leftRaw.value(); + QVariant rightRaw = sourceModel()->data(right, Qt::UserRole); + Q_ASSERT(rightRaw.canConvert()); + auto rightPack = rightRaw.value(); if (currentSorting == ByPopularity) { return leftPack.position > rightPack.position; diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp index 7c69b315e..ad49d940e 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp @@ -143,7 +143,9 @@ void AtlPage::onSelectionChanged(QModelIndex first, [[maybe_unused]] QModelIndex return; } - selected = filterModel->data(first, Qt::UserRole).value(); + QVariant raw = filterModel->data(first, Qt::UserRole); + Q_ASSERT(raw.canConvert()); + selected = raw.value(); ui->packDescription->setHtml(StringUtils::htmlListPatch(selected.description.replace("\n", "
    "))); diff --git a/launcher/ui/pages/modplatform/flame/FlameModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModel.cpp index d501bf9f4..5562e34b8 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModel.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModel.cpp @@ -79,6 +79,7 @@ bool ListModel::setData(const QModelIndex& index, const QVariant& value, [[maybe if (pos >= modpacks.size() || pos < 0 || !index.isValid()) return false; + Q_ASSERT(value.canConvert()); modpacks[pos] = value.value(); return true; diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.cpp b/launcher/ui/pages/modplatform/flame/FlamePage.cpp index bb91e5a64..015cc165e 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlamePage.cpp @@ -166,7 +166,9 @@ void FlamePage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelInde return; } - current = listModel->data(curr, Qt::UserRole).value(); + QVariant raw = listModel->data(curr, Qt::UserRole); + Q_ASSERT(raw.canConvert()); + current = raw.value(); if (!current.versionsLoaded || m_filterWidget->changed()) { qDebug() << "Loading flame modpack versions"; diff --git a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.cpp b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.cpp index 15303bb22..35e1dc110 100644 --- a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.cpp +++ b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.cpp @@ -103,13 +103,16 @@ void ImportFTBPage::suggestCurrent() dialog->setSuggestedIconFromFile(FS::PathCombine(selected.path, "folder.jpg"), editedLogoName); } -void ImportFTBPage::onPublicPackSelectionChanged(QModelIndex now, QModelIndex prev) +void ImportFTBPage::onPublicPackSelectionChanged(QModelIndex now, QModelIndex) { if (!now.isValid()) { onPackSelectionChanged(); return; } - Modpack selectedPack = currentModel->data(now, Qt::UserRole).value(); + + QVariant raw = currentModel->data(now, Qt::UserRole); + Q_ASSERT(raw.canConvert()); + auto selectedPack = raw.value(); onPackSelectionChanged(&selectedPack); } diff --git a/launcher/ui/pages/modplatform/import_ftb/ListModel.cpp b/launcher/ui/pages/modplatform/import_ftb/ListModel.cpp index f99ac8d65..8d3beea01 100644 --- a/launcher/ui/pages/modplatform/import_ftb/ListModel.cpp +++ b/launcher/ui/pages/modplatform/import_ftb/ListModel.cpp @@ -143,8 +143,12 @@ FilterModel::FilterModel(QObject* parent) : QSortFilterProxyModel(parent) bool FilterModel::lessThan(const QModelIndex& left, const QModelIndex& right) const { - Modpack leftPack = sourceModel()->data(left, Qt::UserRole).value(); - Modpack rightPack = sourceModel()->data(right, Qt::UserRole).value(); + QVariant leftRaw = sourceModel()->data(left, Qt::UserRole); + Q_ASSERT(leftRaw.canConvert()); + auto leftPack = leftRaw.value(); + QVariant rightRaw = sourceModel()->data(right, Qt::UserRole); + Q_ASSERT(rightRaw.canConvert()); + auto rightPack = rightRaw.value(); if (m_currentSorting == Sorting::ByGameVersion) { Version lv(leftPack.mcVersion); @@ -166,7 +170,9 @@ bool FilterModel::filterAcceptsRow([[maybe_unused]] int sourceRow, [[maybe_unuse return true; } QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); - Modpack pack = sourceModel()->data(index, Qt::UserRole).value(); + QVariant raw = sourceModel()->data(index, Qt::UserRole); + Q_ASSERT(raw.canConvert()); + auto pack = raw.value(); return pack.name.contains(m_searchTerm, Qt::CaseInsensitive); } diff --git a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp index b68fcd34a..97960845d 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp +++ b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp @@ -61,8 +61,12 @@ FilterModel::FilterModel(QObject* parent) : QSortFilterProxyModel(parent) bool FilterModel::lessThan(const QModelIndex& left, const QModelIndex& right) const { - Modpack leftPack = sourceModel()->data(left, Qt::UserRole).value(); - Modpack rightPack = sourceModel()->data(right, Qt::UserRole).value(); + QVariant leftRaw = sourceModel()->data(left, Qt::UserRole); + Q_ASSERT(leftRaw.canConvert()); + auto leftPack = leftRaw.value(); + QVariant rightRaw = sourceModel()->data(right, Qt::UserRole); + Q_ASSERT(rightRaw.canConvert()); + auto rightPack = rightRaw.value(); if (currentSorting == Sorting::ByGameVersion) { Version lv(leftPack.mcVersion); @@ -84,7 +88,9 @@ bool FilterModel::filterAcceptsRow([[maybe_unused]] int sourceRow, [[maybe_unuse return true; } QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); - Modpack pack = sourceModel()->data(index, Qt::UserRole).value(); + QVariant raw = sourceModel()->data(index, Qt::UserRole); + Q_ASSERT(raw.canConvert()); + auto pack = raw.value(); if (searchTerm.startsWith("#")) return pack.packCode == searchTerm.mid(1); return pack.name.contains(searchTerm, Qt::CaseInsensitive); diff --git a/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp b/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp index 5752b6c61..1576f52fe 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp +++ b/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp @@ -233,7 +233,9 @@ void Page::onPublicPackSelectionChanged(QModelIndex now, [[maybe_unused]] QModel onPackSelectionChanged(); return; } - Modpack selectedPack = publicFilterModel->data(now, Qt::UserRole).value(); + QVariant raw = publicFilterModel->data(now, Qt::UserRole); + Q_ASSERT(raw.canConvert()); + auto selectedPack = raw.value(); onPackSelectionChanged(&selectedPack); } @@ -243,7 +245,9 @@ void Page::onThirdPartyPackSelectionChanged(QModelIndex now, [[maybe_unused]] QM onPackSelectionChanged(); return; } - Modpack selectedPack = thirdPartyFilterModel->data(now, Qt::UserRole).value(); + QVariant raw = thirdPartyFilterModel->data(now, Qt::UserRole); + Q_ASSERT(raw.canConvert()); + auto selectedPack = raw.value(); onPackSelectionChanged(&selectedPack); } @@ -253,7 +257,9 @@ void Page::onPrivatePackSelectionChanged(QModelIndex now, [[maybe_unused]] QMode onPackSelectionChanged(); return; } - Modpack selectedPack = privateFilterModel->data(now, Qt::UserRole).value(); + QVariant raw = privateFilterModel->data(now, Qt::UserRole); + Q_ASSERT(raw.canConvert()); + auto selectedPack = raw.value(); onPackSelectionChanged(&selectedPack); } @@ -328,7 +334,9 @@ void Page::onTabChanged(int tab) currentList->selectionModel()->reset(); QModelIndex idx = currentList->currentIndex(); if (idx.isValid()) { - auto pack = currentModel->data(idx, Qt::UserRole).value(); + QVariant raw = currentModel->data(idx, Qt::UserRole); + Q_ASSERT(raw.canConvert()); + auto pack = raw.value(); onPackSelectionChanged(&pack); } else { onPackSelectionChanged(); diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index 4681b1a7f..323300af9 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -121,6 +121,7 @@ bool ModpackListModel::setData(const QModelIndex& index, const QVariant& value, if (pos >= modpacks.size() || pos < 0 || !index.isValid()) return false; + Q_ASSERT(value.canConvert()); modpacks[pos] = value.value(); return true; @@ -137,7 +138,7 @@ void ModpackListModel::performPaginatedSearch() ResourceAPI::ProjectInfoCallbacks callbacks; callbacks.on_fail = [this](QString reason) { searchRequestFailed(reason); }; - callbacks.on_succeed = [this](auto& doc, auto& pack) { searchRequestForOneSucceeded(doc); }; + callbacks.on_succeed = [this](auto& doc, auto&) { searchRequestForOneSucceeded(doc); }; callbacks.on_abort = [this] { qCritical() << "Search task aborted by an unknown reason!"; searchRequestFailed("Aborted"); @@ -345,7 +346,7 @@ void ModpackListModel::searchRequestForOneSucceeded(QJsonDocument& doc) endInsertRows(); } -void ModpackListModel::searchRequestFailed(QString reason) +void ModpackListModel::searchRequestFailed(QString) { auto failed_action = dynamic_cast(jobPtr.get())->getFailedActions().at(0); if (failed_action->replyStatusCode() == -1) { diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index 701bb9f72..f8214e8bb 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -150,7 +150,9 @@ void ModrinthPage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelI return; } - current = m_model->data(curr, Qt::UserRole).value(); + QVariant raw = m_model->data(curr, Qt::UserRole); + Q_ASSERT(raw.canConvert()); + current = raw.value(); auto name = current.name; if (!current.extraInfoLoaded) { diff --git a/launcher/ui/pages/modplatform/technic/TechnicData.h b/launcher/ui/pages/modplatform/technic/TechnicData.h index 11d57f071..1049d1f2e 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicData.h +++ b/launcher/ui/pages/modplatform/technic/TechnicData.h @@ -36,6 +36,7 @@ #pragma once #include +#include #include namespace Technic { diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp index 50d267b1f..6319eb06e 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp @@ -135,7 +135,9 @@ void TechnicPage::onSelectionChanged(QModelIndex first, [[maybe_unused]] QModelI return; } - current = model->data(first, Qt::UserRole).value(); + QVariant raw = model->data(first, Qt::UserRole); + Q_ASSERT(raw.canConvert()); + current = raw.value(); suggestCurrent(); } diff --git a/tests/FileSystem_test.cpp b/tests/FileSystem_test.cpp index 995867e46..8aa25d82d 100644 --- a/tests/FileSystem_test.cpp +++ b/tests/FileSystem_test.cpp @@ -69,7 +69,9 @@ class LinkTask : public Task { } FS::create_link* m_lnk; - [[maybe_unused]] bool m_useHard = false; +#if defined Q_OS_WIN32 + bool m_useHard = false; +#endif bool m_linkRecursive = true; }; From ef3bf75715dfcdb39f38d5fb525342e37b0b213f Mon Sep 17 00:00:00 2001 From: Yihe Li Date: Thu, 5 Jun 2025 16:15:29 +0800 Subject: [PATCH 299/695] Remove some duplicate code Signed-off-by: Yihe Li --- launcher/Application.cpp | 21 ++---------- launcher/Application.h | 3 -- launcher/BaseInstance.cpp | 34 ++++++++++---------- launcher/BaseInstance.h | 7 ++-- launcher/MessageLevel.h | 2 +- launcher/launch/LaunchTask.cpp | 4 +-- launcher/ui/pages/instance/OtherLogsPage.cpp | 10 +++--- 7 files changed, 31 insertions(+), 50 deletions(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index a641a9d8a..b0ef1405a 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -696,8 +696,8 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_settings->registerSetting("ConsoleMaxLines", 100000); m_settings->registerSetting("ConsoleOverflowStop", true); - logModel->setMaxLines(getConsoleMaxLines()); - logModel->setStopOnOverflow(shouldStopOnConsoleOverflow()); + logModel->setMaxLines(getConsoleMaxLines(settings())); + logModel->setStopOnOverflow(shouldStopOnConsoleOverflow(settings())); logModel->setOverflowMessage(tr("Cannot display this log since the log length surpassed %1 lines.").arg(logModel->getMaxLines())); // Folders @@ -1605,23 +1605,6 @@ void Application::updateIsRunning(bool running) m_updateRunning = running; } -int Application::getConsoleMaxLines() const -{ - auto lineSetting = settings()->getSetting("ConsoleMaxLines"); - bool conversionOk = false; - int maxLines = lineSetting->get().toInt(&conversionOk); - if (!conversionOk) { - maxLines = lineSetting->defValue().toInt(); - qWarning() << "ConsoleMaxLines has nonsensical value, defaulting to" << maxLines; - } - return maxLines; -} - -bool Application::shouldStopOnConsoleOverflow() const -{ - return settings()->get("ConsoleOverflowStop").toBool(); -} - void Application::controllerSucceeded() { auto controller = qobject_cast(QObject::sender()); diff --git a/launcher/Application.h b/launcher/Application.h index 3c2c6e11c..548345c18 100644 --- a/launcher/Application.h +++ b/launcher/Application.h @@ -162,9 +162,6 @@ class Application : public QApplication { QString getModrinthAPIToken(); QString getUserAgent(); - int getConsoleMaxLines() const; - bool shouldStopOnConsoleOverflow() const; - /// this is the root of the 'installation'. Used for automatic updates const QString& root() { return m_rootPath; } diff --git a/launcher/BaseInstance.cpp b/launcher/BaseInstance.cpp index f4bc7e30b..fdbcc11fe 100644 --- a/launcher/BaseInstance.cpp +++ b/launcher/BaseInstance.cpp @@ -53,6 +53,23 @@ #include "Commandline.h" #include "FileSystem.h" +int getConsoleMaxLines(SettingsObjectPtr settings) +{ + auto lineSetting = settings->getSetting("ConsoleMaxLines"); + bool conversionOk = false; + int maxLines = lineSetting->get().toInt(&conversionOk); + if (!conversionOk) { + maxLines = lineSetting->defValue().toInt(); + qWarning() << "ConsoleMaxLines has nonsensical value, defaulting to" << maxLines; + } + return maxLines; +} + +bool shouldStopOnConsoleOverflow(SettingsObjectPtr settings) +{ + return settings->get("ConsoleOverflowStop").toBool(); +} + BaseInstance::BaseInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString& rootDir) : QObject() { m_settings = settings; @@ -184,23 +201,6 @@ void BaseInstance::copyManagedPack(BaseInstance& other) } } -int BaseInstance::getConsoleMaxLines() const -{ - auto lineSetting = m_settings->getSetting("ConsoleMaxLines"); - bool conversionOk = false; - int maxLines = lineSetting->get().toInt(&conversionOk); - if (!conversionOk) { - maxLines = lineSetting->defValue().toInt(); - qWarning() << "ConsoleMaxLines has nonsensical value, defaulting to" << maxLines; - } - return maxLines; -} - -bool BaseInstance::shouldStopOnConsoleOverflow() const -{ - return m_settings->get("ConsoleOverflowStop").toBool(); -} - QStringList BaseInstance::getLinkedInstances() const { auto setting = m_settings->get("linkedInstances").toString(); diff --git a/launcher/BaseInstance.h b/launcher/BaseInstance.h index 3509c0155..6baac4ce8 100644 --- a/launcher/BaseInstance.h +++ b/launcher/BaseInstance.h @@ -78,6 +78,10 @@ struct ShortcutData { ShortcutTarget target = ShortcutTarget::Other; }; +/// Console settings +int getConsoleMaxLines(SettingsObjectPtr settings); +bool shouldStopOnConsoleOverflow(SettingsObjectPtr settings); + /*! * \brief Base class for instances. * This class implements many functions that are common between instances and @@ -272,9 +276,6 @@ class BaseInstance : public QObject, public std::enable_shared_from_this LaunchTask::getLogModel() { if (!m_logModel) { m_logModel.reset(new LogModel()); - m_logModel->setMaxLines(m_instance->getConsoleMaxLines()); - m_logModel->setStopOnOverflow(m_instance->shouldStopOnConsoleOverflow()); + m_logModel->setMaxLines(getConsoleMaxLines(m_instance->settings())); + m_logModel->setStopOnOverflow(shouldStopOnConsoleOverflow(m_instance->settings())); // FIXME: should this really be here? m_logModel->setOverflowMessage(tr("Stopped watching the game log because the log length surpassed %1 lines.\n" "You may have to fix your mods because the game is still logging to files and" diff --git a/launcher/ui/pages/instance/OtherLogsPage.cpp b/launcher/ui/pages/instance/OtherLogsPage.cpp index b1f0c6507..6f98db4a8 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.cpp +++ b/launcher/ui/pages/instance/OtherLogsPage.cpp @@ -66,7 +66,7 @@ OtherLogsPage::OtherLogsPage(QString id, QString displayName, QString helpPage, m_proxy = new LogFormatProxyModel(this); if (m_instance) { m_model.reset(new LogModel(this)); - ui->trackLogCheckbox->setVisible(false); + ui->trackLogCheckbox->hide(); } else { m_model = APPLICATION->logModel; } @@ -85,8 +85,8 @@ OtherLogsPage::OtherLogsPage(QString id, QString displayName, QString helpPage, ui->text->setModel(m_proxy); if (m_instance) { - m_model->setMaxLines(m_instance->getConsoleMaxLines()); - m_model->setStopOnOverflow(m_instance->shouldStopOnConsoleOverflow()); + m_model->setMaxLines(getConsoleMaxLines(m_instance->settings())); + m_model->setStopOnOverflow(shouldStopOnConsoleOverflow(m_instance->settings())); m_model->setOverflowMessage(tr("Cannot display this log since the log length surpassed %1 lines.").arg(m_model->getMaxLines())); } else { modelStateToUI(); @@ -310,8 +310,8 @@ void OtherLogsPage::reload() ui->text->setModel(nullptr); if (!m_instance) { m_model.reset(new LogModel(this)); - m_model->setMaxLines(APPLICATION->getConsoleMaxLines()); - m_model->setStopOnOverflow(APPLICATION->shouldStopOnConsoleOverflow()); + m_model->setMaxLines(getConsoleMaxLines(APPLICATION->settings())); + m_model->setStopOnOverflow(shouldStopOnConsoleOverflow(APPLICATION->settings())); m_model->setOverflowMessage(tr("Cannot display this log since the log length surpassed %1 lines.").arg(m_model->getMaxLines())); } m_model->clear(); From 58a28f319ad17bd147ebac28a8cf25ed61c05fb0 Mon Sep 17 00:00:00 2001 From: Yihe Li Date: Thu, 5 Jun 2025 20:20:14 +0800 Subject: [PATCH 300/695] More intuitive version changing for modpacks Signed-off-by: Yihe Li --- launcher/modplatform/flame/FlamePackIndex.cpp | 8 ++++++++ launcher/modplatform/flame/FlamePackIndex.h | 2 ++ .../modplatform/modrinth/ModrinthPackManifest.cpp | 10 ++++++++++ launcher/modplatform/modrinth/ModrinthPackManifest.h | 2 ++ launcher/ui/pages/instance/ManagedPackPage.cpp | 11 ++++------- launcher/ui/pages/modplatform/flame/FlamePage.cpp | 9 ++------- .../ui/pages/modplatform/modrinth/ModrinthPage.cpp | 10 ++-------- 7 files changed, 30 insertions(+), 22 deletions(-) diff --git a/launcher/modplatform/flame/FlamePackIndex.cpp b/launcher/modplatform/flame/FlamePackIndex.cpp index 8a7734be5..db2061d99 100644 --- a/launcher/modplatform/flame/FlamePackIndex.cpp +++ b/launcher/modplatform/flame/FlamePackIndex.cpp @@ -140,3 +140,11 @@ void Flame::loadIndexedPackVersions(Flame::IndexedPack& pack, QJsonArray& arr) pack.versions = unsortedVersions; pack.versionsLoaded = true; } + +auto Flame::getVersionDisplayString(const IndexedVersion& version) -> QString +{ + auto release_type = version.version_type.isValid() ? QString(" [%1]").arg(version.version_type.toString()) : ""; + auto mcVersion = + !version.mcVersion.isEmpty() && !version.version.contains(version.mcVersion) ? QObject::tr(" for %1").arg(version.mcVersion) : ""; + return QString("%1%2%3").arg(version.version, mcVersion, release_type); +} diff --git a/launcher/modplatform/flame/FlamePackIndex.h b/launcher/modplatform/flame/FlamePackIndex.h index 30391288b..71cadf8e1 100644 --- a/launcher/modplatform/flame/FlamePackIndex.h +++ b/launcher/modplatform/flame/FlamePackIndex.h @@ -47,6 +47,8 @@ struct IndexedPack { void loadIndexedPack(IndexedPack& m, QJsonObject& obj); void loadIndexedInfo(IndexedPack&, QJsonObject&); void loadIndexedPackVersions(IndexedPack& m, QJsonArray& arr); + +auto getVersionDisplayString(const IndexedVersion&) -> QString; } // namespace Flame Q_DECLARE_METATYPE(Flame::IndexedPack) diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp index be565bf11..1e90f713e 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp @@ -183,4 +183,14 @@ auto loadIndexedVersion(QJsonObject& obj) -> ModpackVersion return file; } +auto getVersionDisplayString(const ModpackVersion& version) -> QString +{ + auto release_type = version.version_type.isValid() ? QString(" [%1]").arg(version.version_type.toString()) : ""; + auto mcVersion = !version.gameVersion.isEmpty() && !version.name.contains(version.gameVersion) + ? QObject::tr(" for %1").arg(version.gameVersion) + : ""; + auto versionStr = !version.name.contains(version.version) ? version.version : ""; + return QString("%1%2 — %3%4").arg(version.name, mcVersion, versionStr, release_type); +} + } // namespace Modrinth diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.h b/launcher/modplatform/modrinth/ModrinthPackManifest.h index 97b8ab712..bfc6782a4 100644 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.h +++ b/launcher/modplatform/modrinth/ModrinthPackManifest.h @@ -120,6 +120,8 @@ auto loadIndexedVersion(QJsonObject&) -> ModpackVersion; auto validateDownloadUrl(QUrl) -> bool; +auto getVersionDisplayString(const ModpackVersion&) -> QString; + } // namespace Modrinth Q_DECLARE_METATYPE(Modrinth::Modpack) diff --git a/launcher/ui/pages/instance/ManagedPackPage.cpp b/launcher/ui/pages/instance/ManagedPackPage.cpp index 1738c9cde..facd2d639 100644 --- a/launcher/ui/pages/instance/ManagedPackPage.cpp +++ b/launcher/ui/pages/instance/ManagedPackPage.cpp @@ -292,11 +292,8 @@ void ModrinthManagedPackPage::parseManagedPack() ui->versionsComboBox->clear(); ui->versionsComboBox->blockSignals(false); - for (auto version : m_pack.versions) { - QString name = version.version; - - if (!version.name.contains(version.version)) - name = QString("%1 — %2").arg(version.name, version.version); + for (const auto& version : m_pack.versions) { + QString name = Modrinth::getVersionDisplayString(version); // NOTE: the id from version isn't the same id in the modpack format spec... // e.g. HexMC's 4.4.0 has versionId 4.0.0 in the modpack index.............. @@ -489,8 +486,8 @@ void FlameManagedPackPage::parseManagedPack() ui->versionsComboBox->clear(); ui->versionsComboBox->blockSignals(false); - for (auto version : m_pack.versions) { - QString name = version.version; + for (const auto& version : m_pack.versions) { + QString name = Flame::getVersionDisplayString(version); if (version.fileId == m_inst->getManagedPackVersionID().toInt()) name = tr("%1 (Current)").arg(name); diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.cpp b/launcher/ui/pages/modplatform/flame/FlamePage.cpp index bb91e5a64..fb3759730 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlamePage.cpp @@ -206,13 +206,8 @@ void FlamePage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelInde else ++it; #endif - for (auto version : current.versions) { - auto release_type = version.version_type.isValid() ? QString(" [%1]").arg(version.version_type.toString()) : ""; - auto mcVersion = !version.mcVersion.isEmpty() && !version.version.contains(version.mcVersion) - ? QString(" for %1").arg(version.mcVersion) - : ""; - ui->versionSelectionBox->addItem(QString("%1%2%3").arg(version.version, mcVersion, release_type), - QVariant(version.downloadUrl)); + for (const auto& version : current.versions) { + ui->versionSelectionBox->addItem(Flame::getVersionDisplayString(version), QVariant(version.downloadUrl)); } QVariant current_updated; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index 701bb9f72..44c1d5f7a 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -244,14 +244,8 @@ void ModrinthPage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelI else ++it; #endif - for (auto version : current.versions) { - auto release_type = version.version_type.isValid() ? QString(" [%1]").arg(version.version_type.toString()) : ""; - auto mcVersion = !version.gameVersion.isEmpty() && !version.name.contains(version.gameVersion) - ? QString(" for %1").arg(version.gameVersion) - : ""; - auto versionStr = !version.name.contains(version.version) ? version.version : ""; - ui->versionSelectionBox->addItem(QString("%1%2 — %3%4").arg(version.name, mcVersion, versionStr, release_type), - QVariant(version.id)); + for (const auto& version : current.versions) { + ui->versionSelectionBox->addItem(Modrinth::getVersionDisplayString(version), QVariant(version.id)); } QVariant current_updated; From 4a9b3d2f5fad264ae0008f84c75c44b34f921433 Mon Sep 17 00:00:00 2001 From: Yihe Li Date: Thu, 5 Jun 2025 21:35:13 +0800 Subject: [PATCH 301/695] Remove usage of SIGNAL/SLOT macro Signed-off-by: Yihe Li --- launcher/LoggedProcess.cpp | 2 +- launcher/java/JavaChecker.cpp | 2 +- launcher/settings/SettingsObject.cpp | 4 +-- launcher/tools/JProfiler.cpp | 2 +- launcher/tools/JVisualVM.cpp | 2 +- launcher/ui/MainWindow.cpp | 10 ++++---- launcher/ui/dialogs/AboutDialog.cpp | 2 +- launcher/ui/dialogs/ExportInstanceDialog.cpp | 2 +- launcher/ui/dialogs/ExportToModListDialog.cpp | 2 +- launcher/ui/dialogs/IconPickerDialog.cpp | 9 +++---- launcher/ui/dialogs/ImportResourceDialog.cpp | 5 ++-- launcher/ui/dialogs/ProfileSelectDialog.cpp | 2 +- .../ui/dialogs/skins/SkinManageDialog.cpp | 5 ++-- launcher/ui/pages/global/ProxyPage.cpp | 2 +- launcher/ui/pages/instance/LogPage.cpp | 8 +++--- .../ui/pages/instance/ManagedPackPage.cpp | 4 +-- .../ui/pages/instance/ScreenshotsPage.cpp | 10 ++++---- launcher/ui/pages/instance/ServersPage.cpp | 2 +- .../ui/pages/modplatform/ResourcePage.cpp | 4 +-- .../ui/pages/modplatform/flame/FlamePage.cpp | 4 +-- .../modplatform/flame/FlameResourcePages.cpp | 24 ++++++++---------- .../modplatform/modrinth/ModrinthPage.cpp | 4 +-- .../modrinth/ModrinthResourcePages.cpp | 25 ++++++++----------- launcher/ui/widgets/AppearanceWidget.cpp | 8 +++--- launcher/ui/widgets/CheckComboBox.cpp | 2 +- launcher/ui/widgets/JavaSettingsWidget.cpp | 4 +-- launcher/ui/widgets/JavaWizardWidget.cpp | 6 ++--- launcher/ui/widgets/ModFilterWidget.cpp | 2 +- launcher/ui/widgets/PageContainer.cpp | 2 +- libraries/LocalPeer/src/LocalPeer.cpp | 2 +- 30 files changed, 75 insertions(+), 87 deletions(-) diff --git a/launcher/LoggedProcess.cpp b/launcher/LoggedProcess.cpp index 35ce4e0e5..b1efc8bd3 100644 --- a/launcher/LoggedProcess.cpp +++ b/launcher/LoggedProcess.cpp @@ -45,7 +45,7 @@ LoggedProcess::LoggedProcess(const QTextCodec* output_codec, QObject* parent) // QProcess has a strange interface... let's map a lot of those into a few. connect(this, &QProcess::readyReadStandardOutput, this, &LoggedProcess::on_stdOut); connect(this, &QProcess::readyReadStandardError, this, &LoggedProcess::on_stdErr); - connect(this, QOverload::of(&QProcess::finished), this, &LoggedProcess::on_exit); + connect(this, &QProcess::finished, this, &LoggedProcess::on_exit); connect(this, &QProcess::errorOccurred, this, &LoggedProcess::on_error); connect(this, &QProcess::stateChanged, this, &LoggedProcess::on_stateChange); } diff --git a/launcher/java/JavaChecker.cpp b/launcher/java/JavaChecker.cpp index 0aa725705..23bd1b73e 100644 --- a/launcher/java/JavaChecker.cpp +++ b/launcher/java/JavaChecker.cpp @@ -84,7 +84,7 @@ void JavaChecker::executeTask() process->setProcessEnvironment(CleanEnviroment()); qDebug() << "Running java checker:" << m_path << args.join(" "); - connect(process.get(), QOverload::of(&QProcess::finished), this, &JavaChecker::finished); + connect(process.get(), &QProcess::finished, this, &JavaChecker::finished); connect(process.get(), &QProcess::errorOccurred, this, &JavaChecker::error); connect(process.get(), &QProcess::readyReadStandardOutput, this, &JavaChecker::stdoutReady); connect(process.get(), &QProcess::readyReadStandardError, this, &JavaChecker::stderrReady); diff --git a/launcher/settings/SettingsObject.cpp b/launcher/settings/SettingsObject.cpp index 0df22b42d..7501d6748 100644 --- a/launcher/settings/SettingsObject.cpp +++ b/launcher/settings/SettingsObject.cpp @@ -119,10 +119,10 @@ bool SettingsObject::reload() void SettingsObject::connectSignals(const Setting& setting) { connect(&setting, &Setting::SettingChanged, this, &SettingsObject::changeSetting); - connect(&setting, SIGNAL(SettingChanged(const Setting&, QVariant)), this, SIGNAL(SettingChanged(const Setting&, QVariant))); + connect(&setting, &Setting::SettingChanged, this, &SettingsObject::SettingChanged); connect(&setting, &Setting::settingReset, this, &SettingsObject::resetSetting); - connect(&setting, SIGNAL(settingReset(Setting)), this, SIGNAL(settingReset(const Setting&))); + connect(&setting, &Setting::settingReset, this, &SettingsObject::settingReset); } std::shared_ptr SettingsObject::getOrRegisterSetting(const QString& id, QVariant defVal) diff --git a/launcher/tools/JProfiler.cpp b/launcher/tools/JProfiler.cpp index 7a532a3d2..8550038d2 100644 --- a/launcher/tools/JProfiler.cpp +++ b/launcher/tools/JProfiler.cpp @@ -57,7 +57,7 @@ void JProfiler::beginProfilingImpl(shared_qobject_ptr process) profiler->setProgram(profilerProgram); connect(profiler, &QProcess::started, this, &JProfiler::profilerStarted); - connect(profiler, QOverload::of(&QProcess::finished), this, &JProfiler::profilerFinished); + connect(profiler, &QProcess::finished, this, &JProfiler::profilerFinished); m_profilerProcess = profiler; profiler->start(); diff --git a/launcher/tools/JVisualVM.cpp b/launcher/tools/JVisualVM.cpp index 0cae8e37b..2e1cf69f7 100644 --- a/launcher/tools/JVisualVM.cpp +++ b/launcher/tools/JVisualVM.cpp @@ -48,7 +48,7 @@ void JVisualVM::beginProfilingImpl(shared_qobject_ptr process) profiler->setProgram(programPath); connect(profiler, &QProcess::started, this, &JVisualVM::profilerStarted); - connect(profiler, QOverload::of(&QProcess::finished), this, &JVisualVM::profilerFinished); + connect(profiler, &QProcess::finished, this, &JVisualVM::profilerFinished); profiler->start(); m_profilerProcess = profiler; diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 455a95837..0216792e5 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -568,7 +568,7 @@ void MainWindow::showInstanceContextMenu(const QPoint& pos) actionCreateInstance->setData(instance_action_data); } - connect(actionCreateInstance, SIGNAL(triggered(bool)), SLOT(on_actionAddInstance_triggered())); + connect(actionCreateInstance, &QAction::triggered, this, &MainWindow::on_actionAddInstance_triggered); actions.prepend(actionSep); actions.prepend(actionVoid); @@ -698,7 +698,7 @@ void MainWindow::repopulateAccountsMenu() } ui->accountsMenu->addAction(action); - connect(action, SIGNAL(triggered(bool)), SLOT(changeActiveAccount())); + connect(action, &QAction::triggered, this, &MainWindow::changeActiveAccount); } } @@ -710,7 +710,7 @@ void MainWindow::repopulateAccountsMenu() ui->accountsMenu->addAction(ui->actionNoDefaultAccount); - connect(ui->actionNoDefaultAccount, SIGNAL(triggered(bool)), SLOT(changeActiveAccount())); + connect(ui->actionNoDefaultAccount, &QAction::triggered, this, &MainWindow::changeActiveAccount); ui->accountsMenu->addSeparator(); ui->accountsMenu->addAction(ui->actionManageAccounts); @@ -1559,8 +1559,8 @@ void MainWindow::taskEnd() void MainWindow::startTask(Task* task) { - connect(task, SIGNAL(succeeded()), SLOT(taskEnd())); - connect(task, SIGNAL(failed(QString)), SLOT(taskEnd())); + connect(task, &Task::succeeded, this, &MainWindow::taskEnd); + connect(task, &Task::failed, this, &MainWindow::taskEnd); task->start(); } diff --git a/launcher/ui/dialogs/AboutDialog.cpp b/launcher/ui/dialogs/AboutDialog.cpp index a8d60aef1..5b7d44ff7 100644 --- a/launcher/ui/dialogs/AboutDialog.cpp +++ b/launcher/ui/dialogs/AboutDialog.cpp @@ -177,7 +177,7 @@ AboutDialog::AboutDialog(QWidget* parent) : QDialog(parent), ui(new Ui::AboutDia ui->copyLabel->setText(BuildConfig.LAUNCHER_COPYRIGHT); - connect(ui->closeButton, SIGNAL(clicked()), SLOT(close())); + connect(ui->closeButton, &QPushButton::clicked, this, &AboutDialog::close); connect(ui->aboutQt, &QPushButton::clicked, &QApplication::aboutQt); } diff --git a/launcher/ui/dialogs/ExportInstanceDialog.cpp b/launcher/ui/dialogs/ExportInstanceDialog.cpp index 51e338503..8d98b0513 100644 --- a/launcher/ui/dialogs/ExportInstanceDialog.cpp +++ b/launcher/ui/dialogs/ExportInstanceDialog.cpp @@ -79,7 +79,7 @@ ExportInstanceDialog::ExportInstanceDialog(InstancePtr instance, QWidget* parent m_ui->treeView->setRootIndex(m_proxyModel->mapFromSource(model->index(root))); m_ui->treeView->sortByColumn(0, Qt::AscendingOrder); - connect(m_proxyModel, SIGNAL(rowsInserted(QModelIndex, int, int)), SLOT(rowsInserted(QModelIndex, int, int))); + connect(m_proxyModel, &QAbstractItemModel::rowsInserted, this, &ExportInstanceDialog::rowsInserted); model->setFilter(QDir::AllEntries | QDir::NoDotAndDotDot | QDir::AllDirs | QDir::Hidden); model->setRootPath(root); diff --git a/launcher/ui/dialogs/ExportToModListDialog.cpp b/launcher/ui/dialogs/ExportToModListDialog.cpp index c2ba68f7a..e8873f9b4 100644 --- a/launcher/ui/dialogs/ExportToModListDialog.cpp +++ b/launcher/ui/dialogs/ExportToModListDialog.cpp @@ -46,7 +46,7 @@ ExportToModListDialog::ExportToModListDialog(QString name, QList mods, QWi ui->setupUi(this); enableCustom(false); - connect(ui->formatComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &ExportToModListDialog::formatChanged); + connect(ui->formatComboBox, &QComboBox::currentIndexChanged, this, &ExportToModListDialog::formatChanged); connect(ui->authorsCheckBox, &QCheckBox::stateChanged, this, &ExportToModListDialog::trigger); connect(ui->versionCheckBox, &QCheckBox::stateChanged, this, &ExportToModListDialog::trigger); connect(ui->urlCheckBox, &QCheckBox::stateChanged, this, &ExportToModListDialog::trigger); diff --git a/launcher/ui/dialogs/IconPickerDialog.cpp b/launcher/ui/dialogs/IconPickerDialog.cpp index 8f53995f9..f4d352c86 100644 --- a/launcher/ui/dialogs/IconPickerDialog.cpp +++ b/launcher/ui/dialogs/IconPickerDialog.cpp @@ -77,13 +77,12 @@ IconPickerDialog::IconPickerDialog(QWidget* parent) : QDialog(parent), ui(new Ui ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); - connect(buttonAdd, SIGNAL(clicked(bool)), SLOT(addNewIcon())); - connect(buttonRemove, SIGNAL(clicked(bool)), SLOT(removeSelectedIcon())); + connect(buttonAdd, &QPushButton::clicked, this, &IconPickerDialog::addNewIcon); + connect(buttonRemove, &QPushButton::clicked, this, &IconPickerDialog::removeSelectedIcon); - connect(contentsWidget, SIGNAL(doubleClicked(QModelIndex)), SLOT(activated(QModelIndex))); + connect(contentsWidget, &QAbstractItemView::doubleClicked, this, &IconPickerDialog::activated); - connect(contentsWidget->selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)), - SLOT(selectionChanged(QItemSelection, QItemSelection))); + connect(contentsWidget->selectionModel(), &QItemSelectionModel::selectionChanged, this, &IconPickerDialog::selectionChanged); auto buttonFolder = ui->buttonBox->addButton(tr("Open Folder"), QDialogButtonBox::ResetRole); connect(buttonFolder, &QPushButton::clicked, this, &IconPickerDialog::openFolder); diff --git a/launcher/ui/dialogs/ImportResourceDialog.cpp b/launcher/ui/dialogs/ImportResourceDialog.cpp index e3a1e9a6c..97c8f22c5 100644 --- a/launcher/ui/dialogs/ImportResourceDialog.cpp +++ b/launcher/ui/dialogs/ImportResourceDialog.cpp @@ -38,9 +38,8 @@ ImportResourceDialog::ImportResourceDialog(QString file_path, PackedResourceType proxyModel->sort(0); contentsWidget->setModel(proxyModel); - connect(contentsWidget, SIGNAL(doubleClicked(QModelIndex)), SLOT(activated(QModelIndex))); - connect(contentsWidget->selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)), - SLOT(selectionChanged(QItemSelection, QItemSelection))); + connect(contentsWidget, &QAbstractItemView::doubleClicked, this, &ImportResourceDialog::activated); + connect(contentsWidget->selectionModel(), &QItemSelectionModel::selectionChanged, this, &ImportResourceDialog::selectionChanged); ui->label->setText( tr("Choose the instance you would like to import this %1 to.").arg(ResourceUtils::getPackedTypeName(m_resource_type))); diff --git a/launcher/ui/dialogs/ProfileSelectDialog.cpp b/launcher/ui/dialogs/ProfileSelectDialog.cpp index 95bdf99a9..90588ce05 100644 --- a/launcher/ui/dialogs/ProfileSelectDialog.cpp +++ b/launcher/ui/dialogs/ProfileSelectDialog.cpp @@ -70,7 +70,7 @@ ProfileSelectDialog::ProfileSelectDialog(const QString& message, int flags, QWid // Select the first entry in the list. ui->listView->setCurrentIndex(ui->listView->model()->index(0, 0)); - connect(ui->listView, SIGNAL(doubleClicked(QModelIndex)), SLOT(on_buttonBox_accepted())); + connect(ui->listView, &QAbstractItemView::doubleClicked, this, &ProfileSelectDialog::on_buttonBox_accepted); ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("OK")); diff --git a/launcher/ui/dialogs/skins/SkinManageDialog.cpp b/launcher/ui/dialogs/skins/SkinManageDialog.cpp index 8e661d37c..e7c06d048 100644 --- a/launcher/ui/dialogs/skins/SkinManageDialog.cpp +++ b/launcher/ui/dialogs/skins/SkinManageDialog.cpp @@ -87,10 +87,9 @@ SkinManageDialog::SkinManageDialog(QWidget* parent, MinecraftAccountPtr acct) contentsWidget->installEventFilter(this); contentsWidget->setModel(&m_list); - connect(contentsWidget, SIGNAL(doubleClicked(QModelIndex)), SLOT(activated(QModelIndex))); + connect(contentsWidget, &QAbstractItemView::doubleClicked, this, &SkinManageDialog::activated); - connect(contentsWidget->selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)), - SLOT(selectionChanged(QItemSelection, QItemSelection))); + connect(contentsWidget->selectionModel(), &QItemSelectionModel::selectionChanged, this, &SkinManageDialog::selectionChanged); connect(m_ui->listView, &QListView::customContextMenuRequested, this, &SkinManageDialog::show_context_menu); connect(m_ui->elytraCB, &QCheckBox::stateChanged, this, [this]() { m_skinPreview->setElytraVisible(m_ui->elytraCB->isChecked()); diff --git a/launcher/ui/pages/global/ProxyPage.cpp b/launcher/ui/pages/global/ProxyPage.cpp index 979f07c6a..0629cc648 100644 --- a/launcher/ui/pages/global/ProxyPage.cpp +++ b/launcher/ui/pages/global/ProxyPage.cpp @@ -49,7 +49,7 @@ ProxyPage::ProxyPage(QWidget* parent) : QWidget(parent), ui(new Ui::ProxyPage) loadSettings(); updateCheckboxStuff(); - connect(ui->proxyGroup, QOverload::of(&QButtonGroup::buttonClicked), this, &ProxyPage::proxyGroupChanged); + connect(ui->proxyGroup, &QButtonGroup::buttonClicked, this, &ProxyPage::proxyGroupChanged); } ProxyPage::~ProxyPage() diff --git a/launcher/ui/pages/instance/LogPage.cpp b/launcher/ui/pages/instance/LogPage.cpp index 7897a2932..9a7ce6039 100644 --- a/launcher/ui/pages/instance/LogPage.cpp +++ b/launcher/ui/pages/instance/LogPage.cpp @@ -158,12 +158,12 @@ LogPage::LogPage(InstancePtr instance, QWidget* parent) : QWidget(parent), ui(ne } auto findShortcut = new QShortcut(QKeySequence(QKeySequence::Find), this); - connect(findShortcut, SIGNAL(activated()), SLOT(findActivated())); + connect(findShortcut, &QShortcut::activated, this, &LogPage::findActivated); auto findNextShortcut = new QShortcut(QKeySequence(QKeySequence::FindNext), this); - connect(findNextShortcut, SIGNAL(activated()), SLOT(findNextActivated())); - connect(ui->searchBar, SIGNAL(returnPressed()), SLOT(on_findButton_clicked())); + connect(findNextShortcut, &QShortcut::activated, this, &LogPage::findNextActivated); + connect(ui->searchBar, &QLineEdit::returnPressed, this, &LogPage::on_findButton_clicked); auto findPreviousShortcut = new QShortcut(QKeySequence(QKeySequence::FindPrevious), this); - connect(findPreviousShortcut, SIGNAL(activated()), SLOT(findPreviousActivated())); + connect(findPreviousShortcut, &QShortcut::activated, this, &LogPage::findPreviousActivated); } LogPage::~LogPage() diff --git a/launcher/ui/pages/instance/ManagedPackPage.cpp b/launcher/ui/pages/instance/ManagedPackPage.cpp index 1738c9cde..176f406af 100644 --- a/launcher/ui/pages/instance/ManagedPackPage.cpp +++ b/launcher/ui/pages/instance/ManagedPackPage.cpp @@ -239,7 +239,7 @@ ModrinthManagedPackPage::ModrinthManagedPackPage(BaseInstance* inst, InstanceWin : ManagedPackPage(inst, instance_window, parent) { Q_ASSERT(inst->isManagedPack()); - connect(ui->versionsComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(suggestVersion())); + connect(ui->versionsComboBox, &QComboBox::currentIndexChanged, this, &ModrinthManagedPackPage::suggestVersion); connect(ui->updateButton, &QPushButton::clicked, this, &ModrinthManagedPackPage::update); connect(ui->updateFromFileButton, &QPushButton::clicked, this, &ModrinthManagedPackPage::updateFromFile); } @@ -418,7 +418,7 @@ FlameManagedPackPage::FlameManagedPackPage(BaseInstance* inst, InstanceWindow* i : ManagedPackPage(inst, instance_window, parent) { Q_ASSERT(inst->isManagedPack()); - connect(ui->versionsComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(suggestVersion())); + connect(ui->versionsComboBox, &QComboBox::currentIndexChanged, this, &FlameManagedPackPage::suggestVersion); connect(ui->updateButton, &QPushButton::clicked, this, &FlameManagedPackPage::update); connect(ui->updateFromFileButton, &QPushButton::clicked, this, &FlameManagedPackPage::updateFromFile); } diff --git a/launcher/ui/pages/instance/ScreenshotsPage.cpp b/launcher/ui/pages/instance/ScreenshotsPage.cpp index c9a1b406a..082b44308 100644 --- a/launcher/ui/pages/instance/ScreenshotsPage.cpp +++ b/launcher/ui/pages/instance/ScreenshotsPage.cpp @@ -135,7 +135,7 @@ class FilterModel : public QIdentityProxyModel { m_thumbnailingPool.setMaxThreadCount(4); m_thumbnailCache = std::make_shared(); m_thumbnailCache->add("placeholder", APPLICATION->getThemedIcon("screenshot-placeholder")); - connect(&watcher, SIGNAL(fileChanged(QString)), SLOT(fileChanged(QString))); + connect(&watcher, &QFileSystemWatcher::fileChanged, this, &FilterModel::fileChanged); } virtual ~FilterModel() { @@ -191,9 +191,9 @@ class FilterModel : public QIdentityProxyModel { void thumbnailImage(QString path) { auto runnable = new ThumbnailRunnable(path, m_thumbnailCache); - connect(&(runnable->m_resultEmitter), SIGNAL(resultsReady(QString)), SLOT(thumbnailReady(QString))); - connect(&(runnable->m_resultEmitter), SIGNAL(resultsFailed(QString)), SLOT(thumbnailFailed(QString))); - ((QThreadPool&)m_thumbnailingPool).start(runnable); + connect(&runnable->m_resultEmitter, &ThumbnailingResult::resultsReady, this, &FilterModel::thumbnailReady); + connect(&runnable->m_resultEmitter, &ThumbnailingResult::resultsFailed, this, &FilterModel::thumbnailFailed); + m_thumbnailingPool.start(runnable); } private slots: void thumbnailReady(QString path) { emit layoutChanged(); } @@ -266,7 +266,7 @@ ScreenshotsPage::ScreenshotsPage(QString path, QWidget* parent) : QMainWindow(pa ui->listView->setItemDelegate(new CenteredEditingDelegate(this)); ui->listView->setContextMenuPolicy(Qt::CustomContextMenu); connect(ui->listView, &QListView::customContextMenuRequested, this, &ScreenshotsPage::ShowContextMenu); - connect(ui->listView, SIGNAL(activated(QModelIndex)), SLOT(onItemActivated(QModelIndex))); + connect(ui->listView, &QAbstractItemView::activated, this, &ScreenshotsPage::onItemActivated); } bool ScreenshotsPage::eventFilter(QObject* obj, QEvent* evt) diff --git a/launcher/ui/pages/instance/ServersPage.cpp b/launcher/ui/pages/instance/ServersPage.cpp index 2f12c3523..d1f39bb88 100644 --- a/launcher/ui/pages/instance/ServersPage.cpp +++ b/launcher/ui/pages/instance/ServersPage.cpp @@ -578,7 +578,7 @@ ServersPage::ServersPage(InstancePtr inst, QWidget* parent) : QMainWindow(parent connect(m_inst.get(), &MinecraftInstance::runningStatusChanged, this, &ServersPage::runningStateChanged); connect(ui->nameLine, &QLineEdit::textEdited, this, &ServersPage::nameEdited); connect(ui->addressLine, &QLineEdit::textEdited, this, &ServersPage::addressEdited); - connect(ui->resourceComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(resourceIndexChanged(int))); + connect(ui->resourceComboBox, &QComboBox::currentIndexChanged, this, &ServersPage::resourceIndexChanged); connect(m_model, &QAbstractItemModel::rowsRemoved, this, &ServersPage::rowsRemoved); m_locked = m_inst->isRunning(); diff --git a/launcher/ui/pages/modplatform/ResourcePage.cpp b/launcher/ui/pages/modplatform/ResourcePage.cpp index ea8e8d5e9..061e96491 100644 --- a/launcher/ui/pages/modplatform/ResourcePage.cpp +++ b/launcher/ui/pages/modplatform/ResourcePage.cpp @@ -85,7 +85,7 @@ ResourcePage::ResourcePage(ResourceDownloadDialog* parent, BaseInstance& base_in connect(m_ui->packDescription, &QTextBrowser::anchorClicked, this, &ResourcePage::openUrl); - connect(m_ui->packView, &QListView::doubleClicked, this, &ResourcePage::onResourceToggle); + connect(m_ui->packView, &QAbstractItemView::doubleClicked, this, &ResourcePage::onResourceToggle); connect(delegate, &ProjectItemDelegate::checkboxClicked, this, &ResourcePage::onResourceToggle); } @@ -554,7 +554,7 @@ void ResourcePage::openProject(QVariant projectID) connect(cancelBtn, &QPushButton::clicked, m_parentDialog, &ResourceDownloadDialog::reject); m_ui->gridLayout_4->addWidget(buttonBox, 1, 2); - connect(m_ui->versionSelectionBox, QOverload::of(&QComboBox::currentIndexChanged), this, + connect(m_ui->versionSelectionBox, &QComboBox::currentIndexChanged, this, [this, okBtn](int index) { okBtn->setEnabled(m_ui->versionSelectionBox->itemData(index).toInt() >= 0); }); auto jump = [this] { diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.cpp b/launcher/ui/pages/modplatform/flame/FlamePage.cpp index bb91e5a64..e087abff1 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlamePage.cpp @@ -86,9 +86,9 @@ FlamePage::FlamePage(NewInstanceDialog* dialog, QWidget* parent) 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->sortByBox, &QComboBox::currentIndexChanged, this, &FlamePage::triggerSearch); connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlamePage::onSelectionChanged); - connect(ui->versionSelectionBox, QOverload::of(&QComboBox::currentIndexChanged), this, &FlamePage::onVersionSelectionChanged); + connect(ui->versionSelectionBox, &QComboBox::currentIndexChanged, this, &FlamePage::onVersionSelectionChanged); ui->packView->setItemDelegate(new ProjectItemDelegate(this)); ui->packDescription->setMetaEntry("FlamePacks"); diff --git a/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp index 32175a356..3a5503683 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp @@ -58,9 +58,9 @@ FlameModPage::FlameModPage(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 contructor... - connect(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); + connect(m_ui->sortByBox, &QComboBox::currentIndexChanged, this, &FlameModPage::triggerSearch); connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlameModPage::onSelectionChanged); - connect(m_ui->versionSelectionBox, QOverload::of(&QComboBox::currentIndexChanged), this, &FlameModPage::onVersionSelectionChanged); + connect(m_ui->versionSelectionBox, &QComboBox::currentIndexChanged, this, &FlameModPage::onVersionSelectionChanged); connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &FlameModPage::onResourceSelected); m_ui->packDescription->setMetaEntry(metaEntryBase()); @@ -92,10 +92,9 @@ FlameResourcePackPage::FlameResourcePackPage(ResourcePackDownloadDialog* dialog, // 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 contructor... - connect(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); + connect(m_ui->sortByBox, &QComboBox::currentIndexChanged, this, &FlameResourcePackPage::triggerSearch); connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlameResourcePackPage::onSelectionChanged); - connect(m_ui->versionSelectionBox, QOverload::of(&QComboBox::currentIndexChanged), this, - &FlameResourcePackPage::onVersionSelectionChanged); + connect(m_ui->versionSelectionBox, &QComboBox::currentIndexChanged, this, &FlameResourcePackPage::onVersionSelectionChanged); connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &FlameResourcePackPage::onResourceSelected); m_ui->packDescription->setMetaEntry(metaEntryBase()); @@ -127,10 +126,9 @@ FlameTexturePackPage::FlameTexturePackPage(TexturePackDownloadDialog* dialog, Ba // 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 contructor... - connect(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); + connect(m_ui->sortByBox, &QComboBox::currentIndexChanged, this, &FlameTexturePackPage::triggerSearch); connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlameTexturePackPage::onSelectionChanged); - connect(m_ui->versionSelectionBox, QOverload::of(&QComboBox::currentIndexChanged), this, - &FlameTexturePackPage::onVersionSelectionChanged); + connect(m_ui->versionSelectionBox, &QComboBox::currentIndexChanged, this, &FlameTexturePackPage::onVersionSelectionChanged); connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &FlameTexturePackPage::onResourceSelected); m_ui->packDescription->setMetaEntry(metaEntryBase()); @@ -178,10 +176,9 @@ FlameShaderPackPage::FlameShaderPackPage(ShaderPackDownloadDialog* dialog, BaseI // 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(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); + connect(m_ui->sortByBox, &QComboBox::currentIndexChanged, this, &FlameShaderPackPage::triggerSearch); connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlameShaderPackPage::onSelectionChanged); - connect(m_ui->versionSelectionBox, QOverload::of(&QComboBox::currentIndexChanged), this, - &FlameShaderPackPage::onVersionSelectionChanged); + connect(m_ui->versionSelectionBox, &QComboBox::currentIndexChanged, this, &FlameShaderPackPage::onVersionSelectionChanged); connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &FlameShaderPackPage::onResourceSelected); m_ui->packDescription->setMetaEntry(metaEntryBase()); @@ -197,10 +194,9 @@ FlameDataPackPage::FlameDataPackPage(DataPackDownloadDialog* dialog, BaseInstanc // 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(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); + connect(m_ui->sortByBox, &QComboBox::currentIndexChanged, this, &FlameDataPackPage::triggerSearch); connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlameDataPackPage::onSelectionChanged); - connect(m_ui->versionSelectionBox, QOverload::of(&QComboBox::currentIndexChanged), this, - &FlameDataPackPage::onVersionSelectionChanged); + connect(m_ui->versionSelectionBox, &QComboBox::currentIndexChanged, this, &FlameDataPackPage::onVersionSelectionChanged); connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &FlameDataPackPage::onResourceSelected); m_ui->packDescription->setMetaEntry(metaEntryBase()); diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index 701bb9f72..04f246a52 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -86,9 +86,9 @@ ModrinthPage::ModrinthPage(NewInstanceDialog* dialog, QWidget* parent) 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->sortByBox, &QComboBox::currentIndexChanged, this, &ModrinthPage::triggerSearch); connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthPage::onSelectionChanged); - connect(ui->versionSelectionBox, QOverload::of(&QComboBox::currentIndexChanged), this, &ModrinthPage::onVersionSelectionChanged); + connect(ui->versionSelectionBox, &QComboBox::currentIndexChanged, this, &ModrinthPage::onVersionSelectionChanged); ui->packView->setItemDelegate(new ProjectItemDelegate(this)); ui->packDescription->setMetaEntry(metaEntryBase()); diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp index f75323a28..e42474cc8 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp @@ -56,10 +56,9 @@ ModrinthModPage::ModrinthModPage(ModDownloadDialog* dialog, BaseInstance& instan // 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(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); + connect(m_ui->sortByBox, &QComboBox::currentIndexChanged, this, &ModrinthModPage::triggerSearch); connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthModPage::onSelectionChanged); - connect(m_ui->versionSelectionBox, QOverload::of(&QComboBox::currentIndexChanged), this, - &ModrinthModPage::onVersionSelectionChanged); + connect(m_ui->versionSelectionBox, &QComboBox::currentIndexChanged, this, &ModrinthModPage::onVersionSelectionChanged); connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &ModrinthModPage::onResourceSelected); m_ui->packDescription->setMetaEntry(metaEntryBase()); @@ -75,10 +74,9 @@ ModrinthResourcePackPage::ModrinthResourcePackPage(ResourcePackDownloadDialog* d // 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(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); + connect(m_ui->sortByBox, &QComboBox::currentIndexChanged, this, &ModrinthResourcePackPage::triggerSearch); connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthResourcePackPage::onSelectionChanged); - connect(m_ui->versionSelectionBox, QOverload::of(&QComboBox::currentIndexChanged), this, - &ModrinthResourcePackPage::onVersionSelectionChanged); + connect(m_ui->versionSelectionBox, &QComboBox::currentIndexChanged, this, &ModrinthResourcePackPage::onVersionSelectionChanged); connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &ModrinthResourcePackPage::onResourceSelected); m_ui->packDescription->setMetaEntry(metaEntryBase()); @@ -94,10 +92,9 @@ ModrinthTexturePackPage::ModrinthTexturePackPage(TexturePackDownloadDialog* dial // 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(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); + connect(m_ui->sortByBox, &QComboBox::currentIndexChanged, this, &ModrinthTexturePackPage::triggerSearch); connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthTexturePackPage::onSelectionChanged); - connect(m_ui->versionSelectionBox, QOverload::of(&QComboBox::currentIndexChanged), this, - &ModrinthTexturePackPage::onVersionSelectionChanged); + connect(m_ui->versionSelectionBox, &QComboBox::currentIndexChanged, this, &ModrinthTexturePackPage::onVersionSelectionChanged); connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &ModrinthTexturePackPage::onResourceSelected); m_ui->packDescription->setMetaEntry(metaEntryBase()); @@ -113,10 +110,9 @@ ModrinthShaderPackPage::ModrinthShaderPackPage(ShaderPackDownloadDialog* dialog, // 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(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); + connect(m_ui->sortByBox, &QComboBox::currentIndexChanged, this, &ModrinthShaderPackPage::triggerSearch); connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthShaderPackPage::onSelectionChanged); - connect(m_ui->versionSelectionBox, QOverload::of(&QComboBox::currentIndexChanged), this, - &ModrinthShaderPackPage::onVersionSelectionChanged); + connect(m_ui->versionSelectionBox, &QComboBox::currentIndexChanged, this, &ModrinthShaderPackPage::onVersionSelectionChanged); connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &ModrinthShaderPackPage::onResourceSelected); m_ui->packDescription->setMetaEntry(metaEntryBase()); @@ -132,10 +128,9 @@ ModrinthDataPackPage::ModrinthDataPackPage(DataPackDownloadDialog* dialog, BaseI // 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(m_ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); + connect(m_ui->sortByBox, &QComboBox::currentIndexChanged, this, &ModrinthDataPackPage::triggerSearch); connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthDataPackPage::onSelectionChanged); - connect(m_ui->versionSelectionBox, QOverload::of(&QComboBox::currentIndexChanged), this, - &ModrinthDataPackPage::onVersionSelectionChanged); + connect(m_ui->versionSelectionBox, &QComboBox::currentIndexChanged, this, &ModrinthDataPackPage::onVersionSelectionChanged); connect(m_ui->resourceSelectionButton, &QPushButton::clicked, this, &ModrinthDataPackPage::onResourceSelected); m_ui->packDescription->setMetaEntry(metaEntryBase()); diff --git a/launcher/ui/widgets/AppearanceWidget.cpp b/launcher/ui/widgets/AppearanceWidget.cpp index 731b72727..ae9c6ee5e 100644 --- a/launcher/ui/widgets/AppearanceWidget.cpp +++ b/launcher/ui/widgets/AppearanceWidget.cpp @@ -68,12 +68,12 @@ AppearanceWidget::AppearanceWidget(bool themesOnly, QWidget* parent) updateCatPreview(); } - connect(m_ui->fontSizeBox, QOverload::of(&QSpinBox::valueChanged), this, &AppearanceWidget::updateConsolePreview); + connect(m_ui->fontSizeBox, &QSpinBox::valueChanged, this, &AppearanceWidget::updateConsolePreview); connect(m_ui->consoleFont, &QFontComboBox::currentFontChanged, this, &AppearanceWidget::updateConsolePreview); - connect(m_ui->iconsComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &AppearanceWidget::applyIconTheme); - connect(m_ui->widgetStyleComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &AppearanceWidget::applyWidgetTheme); - connect(m_ui->catPackComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &AppearanceWidget::applyCatTheme); + connect(m_ui->iconsComboBox, &QComboBox::currentIndexChanged, this, &AppearanceWidget::applyIconTheme); + connect(m_ui->widgetStyleComboBox, &QComboBox::currentIndexChanged, this, &AppearanceWidget::applyWidgetTheme); + connect(m_ui->catPackComboBox, &QComboBox::currentIndexChanged, this, &AppearanceWidget::applyCatTheme); connect(m_ui->catOpacitySlider, &QAbstractSlider::valueChanged, this, &AppearanceWidget::updateCatPreview); connect(m_ui->iconsFolder, &QPushButton::clicked, this, diff --git a/launcher/ui/widgets/CheckComboBox.cpp b/launcher/ui/widgets/CheckComboBox.cpp index 57d98ea7f..961709650 100644 --- a/launcher/ui/widgets/CheckComboBox.cpp +++ b/launcher/ui/widgets/CheckComboBox.cpp @@ -84,7 +84,7 @@ void CheckComboBox::setSourceModel(QAbstractItemModel* new_model) proxy->setSourceModel(new_model); model()->disconnect(this); QComboBox::setModel(proxy); - connect(this, QOverload::of(&QComboBox::activated), this, &CheckComboBox::toggleCheckState); + connect(this, &QComboBox::activated, this, &CheckComboBox::toggleCheckState); connect(proxy, &CheckComboModel::checkStateChanged, this, &CheckComboBox::emitCheckedItemsChanged); connect(model(), &CheckComboModel::rowsInserted, this, &CheckComboBox::emitCheckedItemsChanged); connect(model(), &CheckComboModel::rowsRemoved, this, &CheckComboBox::emitCheckedItemsChanged); diff --git a/launcher/ui/widgets/JavaSettingsWidget.cpp b/launcher/ui/widgets/JavaSettingsWidget.cpp index f5435d16f..0a0d1968b 100644 --- a/launcher/ui/widgets/JavaSettingsWidget.cpp +++ b/launcher/ui/widgets/JavaSettingsWidget.cpp @@ -101,8 +101,8 @@ JavaSettingsWidget::JavaSettingsWidget(InstancePtr instance, QWidget* parent) connect(m_ui->javaDetectBtn, &QPushButton::clicked, this, &JavaSettingsWidget::onJavaAutodetect); connect(m_ui->javaBrowseBtn, &QPushButton::clicked, this, &JavaSettingsWidget::onJavaBrowse); - connect(m_ui->maxMemSpinBox, QOverload::of(&QSpinBox::valueChanged), this, &JavaSettingsWidget::updateThresholds); - connect(m_ui->minMemSpinBox, QOverload::of(&QSpinBox::valueChanged), this, &JavaSettingsWidget::updateThresholds); + connect(m_ui->maxMemSpinBox, &QSpinBox::valueChanged, this, &JavaSettingsWidget::updateThresholds); + connect(m_ui->minMemSpinBox, &QSpinBox::valueChanged, this, &JavaSettingsWidget::updateThresholds); loadSettings(); updateThresholds(); diff --git a/launcher/ui/widgets/JavaWizardWidget.cpp b/launcher/ui/widgets/JavaWizardWidget.cpp index 02bb57474..6158dc7a3 100644 --- a/launcher/ui/widgets/JavaWizardWidget.cpp +++ b/launcher/ui/widgets/JavaWizardWidget.cpp @@ -39,9 +39,9 @@ JavaWizardWidget::JavaWizardWidget(QWidget* parent) : QWidget(parent) m_memoryTimer = new QTimer(this); setupUi(); - connect(m_minMemSpinBox, SIGNAL(valueChanged(int)), this, SLOT(onSpinBoxValueChanged(int))); - connect(m_maxMemSpinBox, SIGNAL(valueChanged(int)), this, SLOT(onSpinBoxValueChanged(int))); - connect(m_permGenSpinBox, SIGNAL(valueChanged(int)), this, SLOT(onSpinBoxValueChanged(int))); + connect(m_minMemSpinBox, &QSpinBox::valueChanged, this, &JavaWizardWidget::onSpinBoxValueChanged); + connect(m_maxMemSpinBox, &QSpinBox::valueChanged, this, &JavaWizardWidget::onSpinBoxValueChanged); + connect(m_permGenSpinBox, &QSpinBox::valueChanged, this, &JavaWizardWidget::onSpinBoxValueChanged); connect(m_memoryTimer, &QTimer::timeout, this, &JavaWizardWidget::memoryValueChanged); connect(m_versionWidget, &VersionSelectWidget::selectedVersionChanged, this, &JavaWizardWidget::javaVersionSelected); connect(m_javaBrowseBtn, &QPushButton::clicked, this, &JavaWizardWidget::on_javaBrowseBtn_clicked); diff --git a/launcher/ui/widgets/ModFilterWidget.cpp b/launcher/ui/widgets/ModFilterWidget.cpp index 031ff0f94..527efc32a 100644 --- a/launcher/ui/widgets/ModFilterWidget.cpp +++ b/launcher/ui/widgets/ModFilterWidget.cpp @@ -141,7 +141,7 @@ ModFilterWidget::ModFilterWidget(MinecraftInstance* instance, bool extended) ui->versions->setStyleSheet("combobox-popup: 0;"); ui->version->setStyleSheet("combobox-popup: 0;"); connect(ui->showAllVersions, &QCheckBox::stateChanged, this, &ModFilterWidget::onShowAllVersionsChanged); - connect(ui->versions, QOverload::of(&QComboBox::currentIndexChanged), this, &ModFilterWidget::onVersionFilterChanged); + connect(ui->versions, &QComboBox::currentIndexChanged, this, &ModFilterWidget::onVersionFilterChanged); connect(ui->versions, &CheckComboBox::checkedItemsChanged, this, [this] { onVersionFilterChanged(0); }); connect(ui->version, &QComboBox::currentTextChanged, this, &ModFilterWidget::onVersionFilterTextChanged); diff --git a/launcher/ui/widgets/PageContainer.cpp b/launcher/ui/widgets/PageContainer.cpp index 514e1d25c..e3054c17a 100644 --- a/launcher/ui/widgets/PageContainer.cpp +++ b/launcher/ui/widgets/PageContainer.cpp @@ -103,7 +103,7 @@ PageContainer::PageContainer(BasePageProvider* pageProvider, QString defaultId, m_pageList->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); m_pageList->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents); m_pageList->setModel(m_proxyModel); - connect(m_pageList->selectionModel(), SIGNAL(currentRowChanged(QModelIndex, QModelIndex)), this, SLOT(currentChanged(QModelIndex))); + connect(m_pageList->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &PageContainer::currentChanged); m_pageStack->setStackingMode(QStackedLayout::StackOne); m_pageList->setFocus(); selectPage(defaultId); diff --git a/libraries/LocalPeer/src/LocalPeer.cpp b/libraries/LocalPeer/src/LocalPeer.cpp index cb74c031b..7c97579fc 100644 --- a/libraries/LocalPeer/src/LocalPeer.cpp +++ b/libraries/LocalPeer/src/LocalPeer.cpp @@ -146,7 +146,7 @@ bool LocalPeer::isClient() #endif if (!res) qWarning("QtSingleCoreApplication: listen on local socket failed, %s", qPrintable(server->errorString())); - QObject::connect(server.get(), SIGNAL(newConnection()), SLOT(receiveConnection())); + connect(server.get(), &QLocalServer::newConnection, this, &LocalPeer::receiveConnection); return false; } From c57ba911cff6f898a80814b40980b20911992af1 Mon Sep 17 00:00:00 2001 From: Yihe Li Date: Thu, 5 Jun 2025 21:40:59 +0800 Subject: [PATCH 302/695] Remove unnecessary QObject:: Signed-off-by: Yihe Li --- launcher/Application.cpp | 8 ++--- launcher/meta/Index.cpp | 2 +- launcher/meta/VersionList.cpp | 2 +- .../minecraft/mod/ResourceFolderModel.cpp | 8 ++--- .../mod/tasks/GetModDependenciesTask.cpp | 2 +- .../atlauncher/ATLPackInstallTask.cpp | 6 ++-- .../modplatform/legacy_ftb/PackFetchTask.cpp | 12 +++---- launcher/news/NewsChecker.cpp | 4 +-- launcher/ui/MainWindow.cpp | 4 +-- .../ui/pages/instance/ManagedPackPage.cpp | 12 +++---- launcher/ui/pages/instance/ServerPingTask.cpp | 12 +++---- .../modplatform/atlauncher/AtlListModel.cpp | 8 ++--- .../ui/pages/modplatform/flame/FlameModel.cpp | 8 ++--- .../ui/pages/modplatform/flame/FlamePage.cpp | 6 ++-- .../modplatform/flame/FlameResourcePages.cpp | 2 +- .../modplatform/legacy_ftb/ListModel.cpp | 4 +-- .../modplatform/modrinth/ModrinthModel.cpp | 8 ++--- .../modplatform/modrinth/ModrinthPage.cpp | 10 +++--- .../modrinth/ModrinthResourcePages.cpp | 2 +- .../modplatform/technic/TechnicModel.cpp | 8 ++--- .../pages/modplatform/technic/TechnicPage.cpp | 4 +-- launcher/ui/widgets/InfoFrame.cpp | 4 +-- tests/FileSystem_test.cpp | 35 ++++++++----------- tests/Task_test.cpp | 12 +++---- 24 files changed, 88 insertions(+), 95 deletions(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index b683b8ab8..b59d13824 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -1598,7 +1598,7 @@ void Application::updateIsRunning(bool running) void Application::controllerSucceeded() { - auto controller = qobject_cast(QObject::sender()); + auto controller = qobject_cast(sender()); if (!controller) return; auto id = controller->id(); @@ -1625,7 +1625,7 @@ void Application::controllerSucceeded() void Application::controllerFailed(const QString& error) { Q_UNUSED(error); - auto controller = qobject_cast(QObject::sender()); + auto controller = qobject_cast(sender()); if (!controller) return; auto id = controller->id(); @@ -1723,7 +1723,7 @@ InstanceWindow* Application::showInstanceWindow(InstancePtr instance, QString pa void Application::on_windowClose() { m_openWindows--; - auto instWindow = qobject_cast(QObject::sender()); + auto instWindow = qobject_cast(sender()); if (instWindow) { QMutexLocker locker(&m_instanceExtrasMutex); auto& extras = m_instanceExtras[instWindow->instanceId()]; @@ -1732,7 +1732,7 @@ void Application::on_windowClose() extras.controller->setParentWidget(m_mainWindow); } } - auto mainWindow = qobject_cast(QObject::sender()); + auto mainWindow = qobject_cast(sender()); if (mainWindow) { m_mainWindow = nullptr; } diff --git a/launcher/meta/Index.cpp b/launcher/meta/Index.cpp index 25a4cd146..d0c7075cd 100644 --- a/launcher/meta/Index.cpp +++ b/launcher/meta/Index.cpp @@ -154,7 +154,7 @@ Version::Ptr Index::getLoadedVersion(const QString& uid, const QString& version) { QEventLoop ev; auto task = loadVersion(uid, version); - QObject::connect(task.get(), &Task::finished, &ev, &QEventLoop::quit); + connect(task.get(), &Task::finished, &ev, &QEventLoop::quit); task->start(); ev.exec(); return get(uid, version); diff --git a/launcher/meta/VersionList.cpp b/launcher/meta/VersionList.cpp index 1f4a969fa..dfca52d87 100644 --- a/launcher/meta/VersionList.cpp +++ b/launcher/meta/VersionList.cpp @@ -282,7 +282,7 @@ void VersionList::waitToLoad() return; QEventLoop ev; auto task = getLoadTask(); - QObject::connect(task.get(), &Task::finished, &ev, &QEventLoop::quit); + connect(task.get(), &Task::finished, &ev, &QEventLoop::quit); task->start(); ev.exec(); } diff --git a/launcher/minecraft/mod/ResourceFolderModel.cpp b/launcher/minecraft/mod/ResourceFolderModel.cpp index ed33ed5cd..fbcd1b0bc 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.cpp +++ b/launcher/minecraft/mod/ResourceFolderModel.cpp @@ -175,9 +175,9 @@ void ResourceFolderModel::installResourceWithFlameMetadata(QString path, ModPlat auto response = std::make_shared(); auto job = FlameAPI().getProject(vers.addonId.toString(), response); - QObject::connect(job.get(), &Task::failed, this, install); - QObject::connect(job.get(), &Task::aborted, this, install); - QObject::connect(job.get(), &Task::succeeded, [response, this, &vers, install, &pack] { + connect(job.get(), &Task::failed, this, install); + connect(job.get(), &Task::aborted, this, install); + connect(job.get(), &Task::succeeded, [response, this, &vers, install, &pack] { QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { @@ -194,7 +194,7 @@ void ResourceFolderModel::installResourceWithFlameMetadata(QString path, ModPlat qWarning() << "Error while reading mod info: " << e.cause(); } LocalResourceUpdateTask update_metadata(indexDir(), pack, vers); - QObject::connect(&update_metadata, &Task::finished, this, install); + connect(&update_metadata, &Task::finished, this, install); update_metadata.start(); }); diff --git a/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp b/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp index b63d36361..af00c3b9a 100644 --- a/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp +++ b/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp @@ -147,7 +147,7 @@ Task::Ptr GetModDependenciesTask::getProjectInfoTask(std::shared_ptrpack->provider == m_flame_provider.name ? m_flame_provider : m_modrinth_provider; auto responseInfo = std::make_shared(); auto info = provider.api->getProject(pDep->pack->addonId.toString(), responseInfo); - QObject::connect(info.get(), &NetJob::succeeded, [this, responseInfo, provider, pDep] { + connect(info.get(), &NetJob::succeeded, [this, responseInfo, provider, pDep] { QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(*responseInfo, &parse_error); if (parse_error.error != QJsonParseError::NoError) { diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp index 261ef786d..042214c84 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp @@ -91,9 +91,9 @@ void PackInstallTask::executeTask() QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "packs/%1/versions/%2/Configs.json").arg(m_pack_safe_name).arg(m_version_name); netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(searchUrl), response)); - QObject::connect(netJob.get(), &NetJob::succeeded, this, &PackInstallTask::onDownloadSucceeded); - QObject::connect(netJob.get(), &NetJob::failed, this, &PackInstallTask::onDownloadFailed); - QObject::connect(netJob.get(), &NetJob::aborted, this, &PackInstallTask::onDownloadAborted); + connect(netJob.get(), &NetJob::succeeded, this, &PackInstallTask::onDownloadSucceeded); + connect(netJob.get(), &NetJob::failed, this, &PackInstallTask::onDownloadFailed); + connect(netJob.get(), &NetJob::aborted, this, &PackInstallTask::onDownloadAborted); jobPtr = netJob; jobPtr->start(); diff --git a/launcher/modplatform/legacy_ftb/PackFetchTask.cpp b/launcher/modplatform/legacy_ftb/PackFetchTask.cpp index aea9cefad..717e63084 100644 --- a/launcher/modplatform/legacy_ftb/PackFetchTask.cpp +++ b/launcher/modplatform/legacy_ftb/PackFetchTask.cpp @@ -59,9 +59,9 @@ void PackFetchTask::fetch() qDebug() << "Downloading thirdparty version info from" << thirdPartyUrl.toString(); jobPtr->addNetAction(Net::Download::makeByteArray(thirdPartyUrl, thirdPartyModpacksXmlFileData)); - QObject::connect(jobPtr.get(), &NetJob::succeeded, this, &PackFetchTask::fileDownloadFinished); - QObject::connect(jobPtr.get(), &NetJob::failed, this, &PackFetchTask::fileDownloadFailed); - QObject::connect(jobPtr.get(), &NetJob::aborted, this, &PackFetchTask::fileDownloadAborted); + connect(jobPtr.get(), &NetJob::succeeded, this, &PackFetchTask::fileDownloadFinished); + connect(jobPtr.get(), &NetJob::failed, this, &PackFetchTask::fileDownloadFailed); + connect(jobPtr.get(), &NetJob::aborted, this, &PackFetchTask::fileDownloadAborted); jobPtr->start(); } @@ -76,7 +76,7 @@ void PackFetchTask::fetchPrivate(const QStringList& toFetch) job->addNetAction(Net::ApiDownload::makeByteArray(privatePackBaseUrl.arg(packCode), data)); job->setAskRetry(false); - QObject::connect(job, &NetJob::succeeded, this, [this, job, data, packCode] { + connect(job, &NetJob::succeeded, this, [this, job, data, packCode] { ModpackList packs; parseAndAddPacks(*data, PackType::Private, packs); for (auto& currentPack : packs) { @@ -89,14 +89,14 @@ void PackFetchTask::fetchPrivate(const QStringList& toFetch) data->clear(); }); - QObject::connect(job, &NetJob::failed, this, [this, job, packCode, data](QString reason) { + connect(job, &NetJob::failed, this, [this, job, packCode, data](QString reason) { emit privateFileDownloadFailed(reason, packCode); job->deleteLater(); data->clear(); }); - QObject::connect(job, &NetJob::aborted, this, [this, job, data] { + connect(job, &NetJob::aborted, this, [this, job, data] { emit aborted(); job->deleteLater(); diff --git a/launcher/news/NewsChecker.cpp b/launcher/news/NewsChecker.cpp index 169589f78..dc4447aba 100644 --- a/launcher/news/NewsChecker.cpp +++ b/launcher/news/NewsChecker.cpp @@ -59,8 +59,8 @@ void NewsChecker::reloadNews() NetJob::Ptr job{ new NetJob("News RSS Feed", m_network) }; job->addNetAction(Net::Download::makeByteArray(m_feedUrl, newsData)); job->setAskRetry(false); - QObject::connect(job.get(), &NetJob::succeeded, this, &NewsChecker::rssDownloadFinished); - QObject::connect(job.get(), &NetJob::failed, this, &NewsChecker::rssDownloadFailed); + connect(job.get(), &NetJob::succeeded, this, &NewsChecker::rssDownloadFinished); + connect(job.get(), &NetJob::failed, this, &NewsChecker::rssDownloadFailed); m_newsNetJob.reset(job); job->start(); } diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 0216792e5..88d8c3353 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -288,8 +288,8 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi newsLabel->setFocusPolicy(Qt::NoFocus); ui->newsToolBar->insertWidget(ui->actionMoreNews, newsLabel); - QObject::connect(newsLabel, &QAbstractButton::clicked, this, &MainWindow::newsButtonClicked); - QObject::connect(m_newsChecker.get(), &NewsChecker::newsLoaded, this, &MainWindow::updateNewsLabel); + connect(newsLabel, &QAbstractButton::clicked, this, &MainWindow::newsButtonClicked); + connect(m_newsChecker.get(), &NewsChecker::newsLoaded, this, &MainWindow::updateNewsLabel); updateNewsLabel(); } diff --git a/launcher/ui/pages/instance/ManagedPackPage.cpp b/launcher/ui/pages/instance/ManagedPackPage.cpp index 176f406af..3230268c4 100644 --- a/launcher/ui/pages/instance/ManagedPackPage.cpp +++ b/launcher/ui/pages/instance/ManagedPackPage.cpp @@ -264,7 +264,7 @@ void ModrinthManagedPackPage::parseManagedPack() m_fetch_job->addNetAction( Net::ApiDownload::makeByteArray(QString("%1/project/%2/version").arg(BuildConfig.MODRINTH_PROD_URL, id), response)); - QObject::connect(m_fetch_job.get(), &NetJob::succeeded, this, [this, response, id] { + connect(m_fetch_job.get(), &NetJob::succeeded, this, [this, response, id] { QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { @@ -310,8 +310,8 @@ void ModrinthManagedPackPage::parseManagedPack() m_loaded = true; }); - QObject::connect(m_fetch_job.get(), &NetJob::failed, this, &ModrinthManagedPackPage::setFailState); - QObject::connect(m_fetch_job.get(), &NetJob::aborted, this, &ModrinthManagedPackPage::setFailState); + connect(m_fetch_job.get(), &NetJob::failed, this, &ModrinthManagedPackPage::setFailState); + connect(m_fetch_job.get(), &NetJob::aborted, this, &ModrinthManagedPackPage::setFailState); ui->changelogTextBrowser->setText(tr("Fetching changelogs...")); @@ -459,7 +459,7 @@ void FlameManagedPackPage::parseManagedPack() m_fetch_job->addNetAction(Net::ApiDownload::makeByteArray(QString("%1/mods/%2/files").arg(BuildConfig.FLAME_BASE_URL, id), response)); - QObject::connect(m_fetch_job.get(), &NetJob::succeeded, this, [this, response, id] { + connect(m_fetch_job.get(), &NetJob::succeeded, this, [this, response, id] { QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { @@ -502,8 +502,8 @@ void FlameManagedPackPage::parseManagedPack() m_loaded = true; }); - QObject::connect(m_fetch_job.get(), &NetJob::failed, this, &FlameManagedPackPage::setFailState); - QObject::connect(m_fetch_job.get(), &NetJob::aborted, this, &FlameManagedPackPage::setFailState); + connect(m_fetch_job.get(), &NetJob::failed, this, &FlameManagedPackPage::setFailState); + connect(m_fetch_job.get(), &NetJob::aborted, this, &FlameManagedPackPage::setFailState); m_fetch_job->start(); } diff --git a/launcher/ui/pages/instance/ServerPingTask.cpp b/launcher/ui/pages/instance/ServerPingTask.cpp index 4a9215ce5..cd2ff31d8 100644 --- a/launcher/ui/pages/instance/ServerPingTask.cpp +++ b/launcher/ui/pages/instance/ServerPingTask.cpp @@ -16,26 +16,26 @@ void ServerPingTask::executeTask() // Resolve the actual IP and port for the server McResolver* resolver = new McResolver(nullptr, m_domain, m_port); - QObject::connect(resolver, &McResolver::succeeded, this, [this](QString ip, int port) { + connect(resolver, &McResolver::succeeded, this, [this](QString ip, int port) { qDebug() << "Resolved Address for" << m_domain << ": " << ip << ":" << port; // Now that we have the IP and port, query the server McClient* client = new McClient(nullptr, m_domain, ip, port); - QObject::connect(client, &McClient::succeeded, this, [this](QJsonObject data) { + connect(client, &McClient::succeeded, this, [this](QJsonObject data) { m_outputOnlinePlayers = getOnlinePlayers(data); qDebug() << "Online players: " << m_outputOnlinePlayers; emitSucceeded(); }); - QObject::connect(client, &McClient::failed, this, [this](QString error) { emitFailed(error); }); + connect(client, &McClient::failed, this, [this](QString error) { emitFailed(error); }); // Delete McClient object when done - QObject::connect(client, &McClient::finished, this, [client]() { client->deleteLater(); }); + connect(client, &McClient::finished, this, [client]() { client->deleteLater(); }); client->getStatusData(); }); - QObject::connect(resolver, &McResolver::failed, this, [this](QString error) { emitFailed(error); }); + connect(resolver, &McResolver::failed, this, [this](QString error) { emitFailed(error); }); // Delete McResolver object when done - QObject::connect(resolver, &McResolver::finished, [resolver]() { resolver->deleteLater(); }); + connect(resolver, &McResolver::finished, [resolver]() { resolver->deleteLater(); }); resolver->ping(); } \ No newline at end of file diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp index e381f2a16..9e2e7a2ca 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp @@ -103,8 +103,8 @@ void ListModel::request() jobPtr = netJob; jobPtr->start(); - QObject::connect(netJob.get(), &NetJob::succeeded, this, &ListModel::requestFinished); - QObject::connect(netJob.get(), &NetJob::failed, this, &ListModel::requestFailed); + connect(netJob.get(), &NetJob::succeeded, this, &ListModel::requestFinished); + connect(netJob.get(), &NetJob::failed, this, &ListModel::requestFailed); } void ListModel::requestFinished() @@ -197,7 +197,7 @@ void ListModel::requestLogo(QString file, QString url) job->addNetAction(Net::ApiDownload::makeCached(QUrl(url), entry)); auto fullPath = entry->getFullPath(); - QObject::connect(job, &NetJob::succeeded, this, [this, file, fullPath, job] { + connect(job, &NetJob::succeeded, this, [this, file, fullPath, job] { job->deleteLater(); emit logoLoaded(file, QIcon(fullPath)); if (waitingCallbacks.contains(file)) { @@ -205,7 +205,7 @@ void ListModel::requestLogo(QString file, QString url) } }); - QObject::connect(job, &NetJob::failed, this, [this, file, job] { + connect(job, &NetJob::failed, this, [this, file, job] { job->deleteLater(); emit logoFailed(file); }); diff --git a/launcher/ui/pages/modplatform/flame/FlameModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModel.cpp index d501bf9f4..20005a8b2 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModel.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModel.cpp @@ -113,7 +113,7 @@ void ListModel::requestLogo(QString logo, QString url) job->addNetAction(Net::ApiDownload::makeCached(QUrl(url), entry)); auto fullPath = entry->getFullPath(); - QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] { + connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] { job->deleteLater(); emit logoLoaded(logo, QIcon(fullPath)); if (waitingCallbacks.contains(logo)) { @@ -121,7 +121,7 @@ void ListModel::requestLogo(QString logo, QString url) } }); - QObject::connect(job, &NetJob::failed, this, [this, logo, job] { + connect(job, &NetJob::failed, this, [this, logo, job] { job->deleteLater(); emit logoFailed(logo); }); @@ -192,8 +192,8 @@ void ListModel::performPaginatedSearch() netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(searchUrl.value()), response)); jobPtr = netJob; jobPtr->start(); - QObject::connect(netJob.get(), &NetJob::succeeded, this, &ListModel::searchRequestFinished); - QObject::connect(netJob.get(), &NetJob::failed, this, &ListModel::searchRequestFailed); + connect(netJob.get(), &NetJob::succeeded, this, &ListModel::searchRequestFinished); + connect(netJob.get(), &NetJob::failed, this, &ListModel::searchRequestFailed); } void ListModel::searchWithTerm(const QString& term, int sort, std::shared_ptr filter, bool filterChanged) diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.cpp b/launcher/ui/pages/modplatform/flame/FlamePage.cpp index e087abff1..b60e5800b 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlamePage.cpp @@ -176,7 +176,7 @@ void FlamePage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelInde netJob->addNetAction( Net::ApiDownload::makeByteArray(QString("https://api.curseforge.com/v1/mods/%1/files").arg(addonId), response)); - QObject::connect(netJob, &NetJob::succeeded, this, [this, response, addonId, curr] { + connect(netJob, &NetJob::succeeded, this, [this, response, addonId, curr] { if (addonId != current.addonId) { return; // wrong request } @@ -227,7 +227,7 @@ void FlamePage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelInde } suggestCurrent(); }); - QObject::connect(netJob, &NetJob::finished, this, [response, netJob] { netJob->deleteLater(); }); + connect(netJob, &NetJob::finished, this, [response, netJob] { netJob->deleteLater(); }); connect(netJob, &NetJob::failed, [this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); netJob->start(); @@ -354,7 +354,7 @@ void FlamePage::createFilterWidget() connect(m_filterWidget.get(), &ModFilterWidget::filterChanged, this, &FlamePage::triggerSearch); auto response = std::make_shared(); m_categoriesTask = FlameAPI::getCategories(response, ModPlatform::ResourceType::MODPACK); - QObject::connect(m_categoriesTask.get(), &Task::succeeded, [this, response]() { + connect(m_categoriesTask.get(), &Task::succeeded, [this, response]() { auto categories = FlameAPI::loadModCategories(response); m_filterWidget->setCategories(categories); }); diff --git a/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp index 3a5503683..bf421c036 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp @@ -251,7 +251,7 @@ void FlameModPage::prepareProviderCategories() { auto response = std::make_shared(); m_categoriesTask = FlameAPI::getModCategories(response); - QObject::connect(m_categoriesTask.get(), &Task::succeeded, [this, response]() { + connect(m_categoriesTask.get(), &Task::succeeded, [this, response]() { auto categories = FlameAPI::loadModCategories(response); m_filter_widget->setCategories(categories); }); diff --git a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp index b68fcd34a..9c5041126 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp +++ b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp @@ -266,7 +266,7 @@ void ListModel::requestLogo(QString file) job->addNetAction(Net::ApiDownload::makeCached(QUrl(QString(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/%1").arg(file)), entry)); auto fullPath = entry->getFullPath(); - QObject::connect(job, &NetJob::finished, this, [this, file, fullPath, job] { + connect(job, &NetJob::finished, this, [this, file, fullPath, job] { job->deleteLater(); emit logoLoaded(file, QIcon(fullPath)); if (waitingCallbacks.contains(file)) { @@ -274,7 +274,7 @@ void ListModel::requestLogo(QString file) } }); - QObject::connect(job, &NetJob::failed, this, [this, file, job] { + connect(job, &NetJob::failed, this, [this, file, job] { job->deleteLater(); emit logoFailed(file); }); diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index 4681b1a7f..717dd359f 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -158,7 +158,7 @@ void ModpackListModel::performPaginatedSearch() auto netJob = makeShared("Modrinth::SearchModpack", APPLICATION->network()); netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(searchUrl.value()), m_allResponse)); - QObject::connect(netJob.get(), &NetJob::succeeded, this, [this] { + connect(netJob.get(), &NetJob::succeeded, this, [this] { QJsonParseError parseError{}; QJsonDocument doc = QJsonDocument::fromJson(*m_allResponse, &parseError); @@ -171,7 +171,7 @@ void ModpackListModel::performPaginatedSearch() searchRequestFinished(doc); }); - QObject::connect(netJob.get(), &NetJob::failed, this, &ModpackListModel::searchRequestFailed); + connect(netJob.get(), &NetJob::failed, this, &ModpackListModel::searchRequestFailed); jobPtr = netJob; jobPtr->start(); @@ -253,7 +253,7 @@ void ModpackListModel::requestLogo(QString logo, QString url) job->addNetAction(Net::ApiDownload::makeCached(QUrl(url), entry)); auto fullPath = entry->getFullPath(); - QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] { + connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] { job->deleteLater(); emit logoLoaded(logo, QIcon(fullPath)); if (waitingCallbacks.contains(logo)) { @@ -261,7 +261,7 @@ void ModpackListModel::requestLogo(QString logo, QString url) } }); - QObject::connect(job, &NetJob::failed, this, [this, logo, job] { + connect(job, &NetJob::failed, this, [this, logo, job] { job->deleteLater(); emit logoFailed(logo); }); diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index 04f246a52..1b5b8d4de 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -163,7 +163,7 @@ void ModrinthPage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelI netJob->addNetAction(Net::ApiDownload::makeByteArray(QString("%1/project/%2").arg(BuildConfig.MODRINTH_PROD_URL, id), response)); - QObject::connect(netJob, &NetJob::succeeded, this, [this, response, id, curr] { + connect(netJob, &NetJob::succeeded, this, [this, response, id, curr] { if (id != current.id) { return; // wrong request? } @@ -196,7 +196,7 @@ void ModrinthPage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelI suggestCurrent(); }); - QObject::connect(netJob, &NetJob::finished, this, [response, netJob] { netJob->deleteLater(); }); + connect(netJob, &NetJob::finished, this, [response, netJob] { netJob->deleteLater(); }); connect(netJob, &NetJob::failed, [this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); netJob->start(); @@ -214,7 +214,7 @@ void ModrinthPage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelI netJob->addNetAction( Net::ApiDownload::makeByteArray(QString("%1/project/%2/version").arg(BuildConfig.MODRINTH_PROD_URL, id), response)); - QObject::connect(netJob, &NetJob::succeeded, this, [this, response, id, curr] { + connect(netJob, &NetJob::succeeded, this, [this, response, id, curr] { if (id != current.id) { return; // wrong request? } @@ -262,7 +262,7 @@ void ModrinthPage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelI suggestCurrent(); }); - QObject::connect(netJob, &NetJob::finished, this, [response, netJob] { netJob->deleteLater(); }); + connect(netJob, &NetJob::finished, this, [response, netJob] { netJob->deleteLater(); }); connect(netJob, &NetJob::failed, [this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); netJob->start(); @@ -404,7 +404,7 @@ void ModrinthPage::createFilterWidget() connect(m_filterWidget.get(), &ModFilterWidget::filterChanged, this, &ModrinthPage::triggerSearch); auto response = std::make_shared(); m_categoriesTask = ModrinthAPI::getModCategories(response); - QObject::connect(m_categoriesTask.get(), &Task::succeeded, [this, response]() { + connect(m_categoriesTask.get(), &Task::succeeded, [this, response]() { auto categories = ModrinthAPI::loadCategories(response, "modpack"); m_filterWidget->setCategories(categories); }); diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp index e42474cc8..aca71cde5 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp @@ -169,7 +169,7 @@ void ModrinthModPage::prepareProviderCategories() { auto response = std::make_shared(); m_categoriesTask = ModrinthAPI::getModCategories(response); - QObject::connect(m_categoriesTask.get(), &Task::succeeded, [this, response]() { + connect(m_categoriesTask.get(), &Task::succeeded, [this, response]() { auto categories = ModrinthAPI::loadModCategories(response); m_filter_widget->setCategories(categories); }); diff --git a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp index c689ab0d2..c425044a2 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp @@ -159,8 +159,8 @@ void Technic::ListModel::performSearch() netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(searchUrl), response)); jobPtr = netJob; jobPtr->start(); - QObject::connect(netJob.get(), &NetJob::succeeded, this, &ListModel::searchRequestFinished); - QObject::connect(netJob.get(), &NetJob::failed, this, &ListModel::searchRequestFailed); + connect(netJob.get(), &NetJob::succeeded, this, &ListModel::searchRequestFinished); + connect(netJob.get(), &NetJob::failed, this, &ListModel::searchRequestFailed); } void Technic::ListModel::searchRequestFinished() @@ -299,12 +299,12 @@ void Technic::ListModel::requestLogo(QString logo, QString url) auto fullPath = entry->getFullPath(); - QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] { + connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] { job->deleteLater(); logoLoaded(logo, fullPath); }); - QObject::connect(job, &NetJob::failed, this, [this, logo, job] { + connect(job, &NetJob::failed, this, [this, logo, job] { job->deleteLater(); logoFailed(logo); }); diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp index 50d267b1f..63a51d363 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.cpp @@ -162,7 +162,7 @@ void TechnicPage::suggestCurrent() QString slug = current.slug; netJob->addNetAction(Net::ApiDownload::makeByteArray( QString("%1modpack/%2?build=%3").arg(BuildConfig.TECHNIC_API_BASE_URL, slug, BuildConfig.TECHNIC_API_BUILD), response)); - QObject::connect(netJob.get(), &NetJob::succeeded, this, [this, slug] { + connect(netJob.get(), &NetJob::succeeded, this, [this, slug] { jobPtr.reset(); if (current.slug != slug) { @@ -260,7 +260,7 @@ void TechnicPage::metadataLoaded() auto url = QString("%1/modpack/%2").arg(current.url, current.slug); netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(url), response)); - QObject::connect(netJob.get(), &NetJob::succeeded, this, &TechnicPage::onSolderLoaded); + connect(netJob.get(), &NetJob::succeeded, this, &TechnicPage::onSolderLoaded); connect(jobPtr.get(), &NetJob::failed, [this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); diff --git a/launcher/ui/widgets/InfoFrame.cpp b/launcher/ui/widgets/InfoFrame.cpp index 8d6505655..2363b6592 100644 --- a/launcher/ui/widgets/InfoFrame.cpp +++ b/launcher/ui/widgets/InfoFrame.cpp @@ -323,7 +323,7 @@ void InfoFrame::setDescription(QString text) cursor.insertHtml("..."); labeltext.append(doc.toHtml()); - QObject::connect(ui->descriptionLabel, &QLabel::linkActivated, this, &InfoFrame::descriptionEllipsisHandler); + connect(ui->descriptionLabel, &QLabel::linkActivated, this, &InfoFrame::descriptionEllipsisHandler); } else { ui->descriptionLabel->setTextFormat(Qt::TextFormat::AutoText); labeltext.append(finaltext); @@ -362,7 +362,7 @@ void InfoFrame::setLicense(QString text) m_license = text; // This allows injecting HTML here. labeltext.append("" + finaltext.left(287) + "..."); - QObject::connect(ui->licenseLabel, &QLabel::linkActivated, this, &InfoFrame::licenseEllipsisHandler); + connect(ui->licenseLabel, &QLabel::linkActivated, this, &InfoFrame::licenseEllipsisHandler); } else { ui->licenseLabel->setTextFormat(Qt::TextFormat::AutoText); labeltext.append(finaltext); diff --git a/tests/FileSystem_test.cpp b/tests/FileSystem_test.cpp index 995867e46..050fafd3c 100644 --- a/tests/FileSystem_test.cpp +++ b/tests/FileSystem_test.cpp @@ -320,9 +320,8 @@ class FileSystemTest : public QObject { LinkTask lnk_tsk(folder, target_dir.path()); lnk_tsk.linkRecursively(false); - QObject::connect(&lnk_tsk, &Task::finished, [&lnk_tsk] { - QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been."); - }); + connect(&lnk_tsk, &Task::finished, + [&lnk_tsk] { QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been."); }); lnk_tsk.start(); QVERIFY2(QTest::qWaitFor([&lnk_tsk]() { return lnk_tsk.isFinished(); }, 100000), "Task didn't finish as it should."); @@ -417,9 +416,8 @@ class FileSystemTest : public QObject { RegexpMatcher::Ptr re = std::make_shared("[.]?mcmeta"); lnk_tsk.matcher(re); lnk_tsk.linkRecursively(true); - QObject::connect(&lnk_tsk, &Task::finished, [&lnk_tsk] { - QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been."); - }); + connect(&lnk_tsk, &Task::finished, + [&lnk_tsk] { QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been."); }); lnk_tsk.start(); QVERIFY2(QTest::qWaitFor([&lnk_tsk]() { return lnk_tsk.isFinished(); }, 100000), "Task didn't finish as it should."); @@ -465,9 +463,8 @@ class FileSystemTest : public QObject { lnk_tsk.matcher(re); lnk_tsk.linkRecursively(true); lnk_tsk.whitelist(true); - QObject::connect(&lnk_tsk, &Task::finished, [&lnk_tsk] { - QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been."); - }); + connect(&lnk_tsk, &Task::finished, + [&lnk_tsk] { QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been."); }); lnk_tsk.start(); QVERIFY2(QTest::qWaitFor([&lnk_tsk]() { return lnk_tsk.isFinished(); }, 100000), "Task didn't finish as it should."); @@ -510,9 +507,8 @@ class FileSystemTest : public QObject { LinkTask lnk_tsk(folder, target_dir.path()); lnk_tsk.linkRecursively(true); - QObject::connect(&lnk_tsk, &Task::finished, [&lnk_tsk] { - QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been."); - }); + connect(&lnk_tsk, &Task::finished, + [&lnk_tsk] { QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been."); }); lnk_tsk.start(); QVERIFY2(QTest::qWaitFor([&lnk_tsk]() { return lnk_tsk.isFinished(); }, 100000), "Task didn't finish as it should."); @@ -559,9 +555,8 @@ class FileSystemTest : public QObject { qDebug() << target_dir.path(); LinkTask lnk_tsk(file, target_dir.filePath("pack.mcmeta")); - QObject::connect(&lnk_tsk, &Task::finished, [&lnk_tsk] { - QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been."); - }); + connect(&lnk_tsk, &Task::finished, + [&lnk_tsk] { QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been."); }); lnk_tsk.start(); QVERIFY2(QTest::qWaitFor([&lnk_tsk]() { return lnk_tsk.isFinished(); }, 100000), "Task didn't finish as it should."); @@ -595,9 +590,8 @@ class FileSystemTest : public QObject { LinkTask lnk_tsk(folder, target_dir.path()); lnk_tsk.linkRecursively(true); lnk_tsk.setMaxDepth(0); - QObject::connect(&lnk_tsk, &Task::finished, [&lnk_tsk] { - QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been."); - }); + connect(&lnk_tsk, &Task::finished, + [&lnk_tsk] { QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been."); }); lnk_tsk.start(); QVERIFY2(QTest::qWaitFor([&lnk_tsk]() { return lnk_tsk.isFinished(); }, 100000), "Task didn't finish as it should."); @@ -646,9 +640,8 @@ class FileSystemTest : public QObject { LinkTask lnk_tsk(folder, target_dir.path()); lnk_tsk.linkRecursively(true); lnk_tsk.setMaxDepth(-1); - QObject::connect(&lnk_tsk, &Task::finished, [&lnk_tsk] { - QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been."); - }); + connect(&lnk_tsk, &Task::finished, + [&lnk_tsk] { QVERIFY2(lnk_tsk.wasSuccessful(), "Task finished but was not successful when it should have been."); }); lnk_tsk.start(); QVERIFY2(QTest::qWaitFor([&lnk_tsk]() { return lnk_tsk.isFinished(); }, 100000), "Task didn't finish as it should."); diff --git a/tests/Task_test.cpp b/tests/Task_test.cpp index 8333840c1..1c95f702d 100644 --- a/tests/Task_test.cpp +++ b/tests/Task_test.cpp @@ -127,8 +127,8 @@ class TaskTest : public QObject { void test_basicRun() { BasicTask t; - QObject::connect(&t, &Task::finished, - [&t] { QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been."); }); + connect(&t, &Task::finished, + [&t] { QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been."); }); t.start(); QVERIFY2(QTest::qWaitFor([&t]() { return t.isFinished(); }, 1000), "Task didn't finish as it should."); @@ -146,7 +146,7 @@ class TaskTest : public QObject { t.addTask(t2); t.addTask(t3); - QObject::connect(&t, &Task::finished, [&t, &t1, &t2, &t3] { + connect(&t, &Task::finished, [&t, &t1, &t2, &t3] { QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been."); QVERIFY(t1->wasSuccessful()); QVERIFY(t2->wasSuccessful()); @@ -182,7 +182,7 @@ class TaskTest : public QObject { t.addTask(t8); t.addTask(t9); - QObject::connect(&t, &Task::finished, [&t, &t1, &t2, &t3, &t4, &t5, &t6, &t7, &t8, &t9] { + connect(&t, &Task::finished, [&t, &t1, &t2, &t3, &t4, &t5, &t6, &t7, &t8, &t9] { QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been."); QVERIFY(t1->wasSuccessful()); QVERIFY(t2->wasSuccessful()); @@ -211,7 +211,7 @@ class TaskTest : public QObject { t.addTask(t2); t.addTask(t3); - QObject::connect(&t, &Task::finished, [&t, &t1, &t2, &t3] { + connect(&t, &Task::finished, [&t, &t1, &t2, &t3] { QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been."); QVERIFY(t1->wasSuccessful()); QVERIFY(t2->wasSuccessful()); @@ -234,7 +234,7 @@ class TaskTest : public QObject { t.addTask(t2); t.addTask(t3); - QObject::connect(&t, &Task::finished, [&t, &t1, &t2, &t3] { + connect(&t, &Task::finished, [&t, &t1, &t2, &t3] { QVERIFY2(t.wasSuccessful(), "Task finished but was not successful when it should have been."); QVERIFY(t1->wasSuccessful()); QVERIFY(!t2->wasSuccessful()); From 37f8b2f56371d873de0e31210b0a448e26fab6c1 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Thu, 5 Jun 2025 17:45:47 +0300 Subject: [PATCH 303/695] fix: crash with global datapacks on export Signed-off-by: Trial97 --- launcher/ui/dialogs/ExportPackDialog.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/launcher/ui/dialogs/ExportPackDialog.cpp b/launcher/ui/dialogs/ExportPackDialog.cpp index 15420616e..e6c17972d 100644 --- a/launcher/ui/dialogs/ExportPackDialog.cpp +++ b/launcher/ui/dialogs/ExportPackDialog.cpp @@ -102,9 +102,10 @@ ExportPackDialog::ExportPackDialog(MinecraftInstancePtr instance, QWidget* paren MinecraftInstance* mcInstance = dynamic_cast(instance.get()); if (mcInstance) { - for (auto& resourceModel : mcInstance->resourceLists()) - if (resourceModel->indexDir().exists()) + for (auto resourceModel : mcInstance->resourceLists()) { + if (resourceModel && resourceModel->indexDir().exists()) m_proxy->ignoreFilesWithPath().insert(instanceRoot.relativeFilePath(resourceModel->indexDir().absolutePath())); + } } m_ui->files->setModel(m_proxy); From d77889f26d06574d2e635045d236a7e4fec89b4e Mon Sep 17 00:00:00 2001 From: Yihe Li Date: Thu, 5 Jun 2025 23:12:20 +0800 Subject: [PATCH 304/695] Change log to be a QMainWindow Signed-off-by: Yihe Li --- launcher/Application.cpp | 15 ++++++++++++ launcher/Application.h | 5 ++++ launcher/CMakeLists.txt | 5 ++-- launcher/MessageLevel.cpp | 2 +- launcher/ui/MainWindow.cpp | 7 ++---- launcher/ui/ViewLogWindow.cpp | 25 ++++++++++++++++++++ launcher/ui/ViewLogWindow.h | 23 ++++++++++++++++++ launcher/ui/dialogs/ViewLogDialog.cpp | 21 ----------------- launcher/ui/dialogs/ViewLogDialog.h | 22 ----------------- launcher/ui/dialogs/ViewLogDialog.ui | 34 --------------------------- 10 files changed, 73 insertions(+), 86 deletions(-) create mode 100644 launcher/ui/ViewLogWindow.cpp create mode 100644 launcher/ui/ViewLogWindow.h delete mode 100644 launcher/ui/dialogs/ViewLogDialog.cpp delete mode 100644 launcher/ui/dialogs/ViewLogDialog.h delete mode 100644 launcher/ui/dialogs/ViewLogDialog.ui diff --git a/launcher/Application.cpp b/launcher/Application.cpp index b0ef1405a..fd76d4671 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -52,6 +52,7 @@ #include "tools/GenericProfiler.h" #include "ui/InstanceWindow.h" #include "ui/MainWindow.h" +#include "ui/ViewLogWindow.h" #include "ui/dialogs/ProgressDialog.h" #include "ui/instanceview/AccessibleInstanceView.h" @@ -1691,6 +1692,20 @@ MainWindow* Application::showMainWindow(bool minimized) return m_mainWindow; } +ViewLogWindow* Application::showLogWindow() +{ + if (m_viewLogWindow) { + m_viewLogWindow->setWindowState(m_viewLogWindow->windowState() & ~Qt::WindowMinimized); + m_viewLogWindow->raise(); + m_viewLogWindow->activateWindow(); + } else { + m_viewLogWindow = new ViewLogWindow(); + connect(m_viewLogWindow, &ViewLogWindow::isClosing, this, &Application::on_windowClose); + m_openWindows++; + } + return m_viewLogWindow; +} + InstanceWindow* Application::showInstanceWindow(InstancePtr instance, QString page) { if (!instance) diff --git a/launcher/Application.h b/launcher/Application.h index 548345c18..52a84b461 100644 --- a/launcher/Application.h +++ b/launcher/Application.h @@ -55,6 +55,7 @@ class LaunchController; class LocalPeer; class InstanceWindow; class MainWindow; +class ViewLogWindow; class SetupWizard; class GenericPageProvider; class QFile; @@ -183,6 +184,7 @@ class Application : public QApplication { InstanceWindow* showInstanceWindow(InstancePtr instance, QString page = QString()); MainWindow* showMainWindow(bool minimized = false); + ViewLogWindow* showLogWindow(); void updateIsRunning(bool running); bool updatesAreAllowed(); @@ -290,6 +292,9 @@ class Application : public QApplication { // main window, if any MainWindow* m_mainWindow = nullptr; + // log window, if any + ViewLogWindow* m_viewLogWindow = nullptr; + // peer launcher instance connector - used to implement single instance launcher and signalling LocalPeer* m_peerInstance = nullptr; diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index e9e32d481..ada0af75f 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -847,6 +847,8 @@ SET(LAUNCHER_SOURCES ui/MainWindow.cpp ui/InstanceWindow.h ui/InstanceWindow.cpp + ui/ViewLogWindow.h + ui/ViewLogWindow.cpp # FIXME: maybe find a better home for this. FileIgnoreProxy.cpp @@ -1098,8 +1100,6 @@ SET(LAUNCHER_SOURCES ui/dialogs/ResourceUpdateDialog.h ui/dialogs/InstallLoaderDialog.cpp ui/dialogs/InstallLoaderDialog.h - ui/dialogs/ViewLogDialog.cpp - ui/dialogs/ViewLogDialog.h ui/dialogs/skins/SkinManageDialog.cpp ui/dialogs/skins/SkinManageDialog.h @@ -1258,7 +1258,6 @@ qt_wrap_ui(LAUNCHER_UI ui/dialogs/ScrollMessageBox.ui ui/dialogs/BlockedModsDialog.ui ui/dialogs/ChooseProviderDialog.ui - ui/dialogs/ViewLogDialog.ui ui/dialogs/skins/SkinManageDialog.ui ) diff --git a/launcher/MessageLevel.cpp b/launcher/MessageLevel.cpp index 2440f644e..c1c190c72 100644 --- a/launcher/MessageLevel.cpp +++ b/launcher/MessageLevel.cpp @@ -15,7 +15,7 @@ MessageLevel::Enum MessageLevel::getLevel(const QString& levelName) return MessageLevel::Message; else if (name == "WARNING" || name == "WARN") return MessageLevel::Warning; - else if (name == "ERROR") + else if (name == "ERROR" || name == "CRITICAL") return MessageLevel::Error; else if (name == "FATAL") return MessageLevel::Fatal; diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index f68b94aca..4fc4044cf 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -92,6 +92,7 @@ #include "InstanceWindow.h" #include "ui/GuiUtil.h" +#include "ui/ViewLogWindow.h" #include "ui/dialogs/AboutDialog.h" #include "ui/dialogs/CopyInstanceDialog.h" #include "ui/dialogs/CreateShortcutDialog.h" @@ -103,7 +104,6 @@ #include "ui/dialogs/NewInstanceDialog.h" #include "ui/dialogs/NewsDialog.h" #include "ui/dialogs/ProgressDialog.h" -#include "ui/dialogs/ViewLogDialog.h" #include "ui/instanceview/InstanceDelegate.h" #include "ui/instanceview/InstanceProxyModel.h" #include "ui/instanceview/InstanceView.h" @@ -240,10 +240,7 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi } { // logs viewing - connect(ui->actionViewLog, &QAction::triggered, this, [this] { - ViewLogDialog dialog(this); - dialog.exec(); - }); + connect(ui->actionViewLog, &QAction::triggered, this, [] { APPLICATION->showLogWindow(); }); } // add the toolbar toggles to the view menu diff --git a/launcher/ui/ViewLogWindow.cpp b/launcher/ui/ViewLogWindow.cpp new file mode 100644 index 000000000..c0c56f3ee --- /dev/null +++ b/launcher/ui/ViewLogWindow.cpp @@ -0,0 +1,25 @@ +#include + +#include "ViewLogWindow.h" + +#include "ui/pages/instance/OtherLogsPage.h" + +ViewLogWindow::ViewLogWindow(QWidget* parent) + : QMainWindow(parent), m_page(new OtherLogsPage("launcher-logs", tr("Launcher Logs"), "Launcher-Logs", nullptr, parent)) +{ + setAttribute(Qt::WA_DeleteOnClose); + setWindowIcon(APPLICATION->getThemedIcon("log")); + setWindowTitle(tr("View Launcher Logs")); + setCentralWidget(m_page); + setMinimumSize(m_page->size()); + setContentsMargins(0, 0, 0, 0); + m_page->opened(); + show(); +} + +void ViewLogWindow::closeEvent(QCloseEvent* event) +{ + m_page->closed(); + emit isClosing(); + event->accept(); +} diff --git a/launcher/ui/ViewLogWindow.h b/launcher/ui/ViewLogWindow.h new file mode 100644 index 000000000..bb10683aa --- /dev/null +++ b/launcher/ui/ViewLogWindow.h @@ -0,0 +1,23 @@ +#pragma once + +#include + +#include "Application.h" + +class OtherLogsPage; + +class ViewLogWindow : public QMainWindow { + Q_OBJECT + + public: + explicit ViewLogWindow(QWidget* parent = nullptr); + + signals: + void isClosing(); + + protected: + void closeEvent(QCloseEvent*) override; + + private: + OtherLogsPage* m_page; +}; diff --git a/launcher/ui/dialogs/ViewLogDialog.cpp b/launcher/ui/dialogs/ViewLogDialog.cpp deleted file mode 100644 index 47c63d9cc..000000000 --- a/launcher/ui/dialogs/ViewLogDialog.cpp +++ /dev/null @@ -1,21 +0,0 @@ -#include "ViewLogDialog.h" -#include "ui_ViewLogDialog.h" - -#include "ui/pages/instance/OtherLogsPage.h" - -ViewLogDialog::ViewLogDialog(QWidget* parent) - : QDialog(parent) - , ui(new Ui::ViewLogDialog) - , m_page(new OtherLogsPage("launcher-logs", tr("Launcher Logs"), "Launcher-Logs", nullptr, parent)) -{ - ui->setupUi(this); - ui->verticalLayout->insertWidget(0, m_page); - connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); - m_page->opened(); -} - -ViewLogDialog::~ViewLogDialog() -{ - m_page->closed(); - delete ui; -} diff --git a/launcher/ui/dialogs/ViewLogDialog.h b/launcher/ui/dialogs/ViewLogDialog.h deleted file mode 100644 index ebb9ef650..000000000 --- a/launcher/ui/dialogs/ViewLogDialog.h +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once - -#include -#include - -namespace Ui { -class ViewLogDialog; -} - -class OtherLogsPage; - -class ViewLogDialog : public QDialog { - Q_OBJECT - - public: - explicit ViewLogDialog(QWidget* parent = nullptr); - ~ViewLogDialog(); - - private: - Ui::ViewLogDialog* ui; - OtherLogsPage* m_page; -}; diff --git a/launcher/ui/dialogs/ViewLogDialog.ui b/launcher/ui/dialogs/ViewLogDialog.ui deleted file mode 100644 index 4a7bb789e..000000000 --- a/launcher/ui/dialogs/ViewLogDialog.ui +++ /dev/null @@ -1,34 +0,0 @@ - - - ViewLogDialog - - - - 0 - 0 - 825 - 782 - - - - View Launcher Logs - - - true - - - - - - Qt::Horizontal - - - QDialogButtonBox::Close - - - - - - - - From 25907ea8c652cd37d5d4907e6822cf99bc88743a Mon Sep 17 00:00:00 2001 From: Yihe Li Date: Thu, 5 Jun 2025 23:22:49 +0800 Subject: [PATCH 305/695] Forgot to reset pointer to nullptr Signed-off-by: Yihe Li --- launcher/Application.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 5e70286ac..a92c6b1e7 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -1760,6 +1760,10 @@ void Application::on_windowClose() if (mainWindow) { m_mainWindow = nullptr; } + auto logWindow = qobject_cast(sender()); + if (logWindow) { + m_viewLogWindow = nullptr; + } // quit when there are no more windows. if (shouldExitNow()) { exit(0); From 09dcfa4b65f3ecbb04f10987d35ad5ae20ed6405 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Fri, 2 May 2025 20:31:42 +0300 Subject: [PATCH 306/695] feat: fade installed resources in the download dialog Signed-off-by: Trial97 --- launcher/ui/widgets/ProjectItem.cpp | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/launcher/ui/widgets/ProjectItem.cpp b/launcher/ui/widgets/ProjectItem.cpp index 03fa659c9..91cf0956f 100644 --- a/launcher/ui/widgets/ProjectItem.cpp +++ b/launcher/ui/widgets/ProjectItem.cpp @@ -16,13 +16,20 @@ void ProjectItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& o QStyleOptionViewItem opt(option); initStyleOption(&opt, index); + auto isInstalled = index.data(UserDataTypes::INSTALLED).toBool(); + auto isChecked = opt.checkState == Qt::Checked; + auto isSelected = option.state & QStyle::State_Selected; + + if (!isSelected && !isChecked && isInstalled) { + painter->setOpacity(0.4); // Fade out the entire item + } const QStyle* style = opt.widget == nullptr ? QApplication::style() : opt.widget->style(); auto rect = opt.rect; style->drawPrimitive(QStyle::PE_PanelItemViewItem, &opt, painter, opt.widget); - if (option.state & QStyle::State_Selected && style->objectName() != "windowsvista") + if (isSelected && style->objectName() != "windowsvista") painter->setPen(opt.palette.highlightedText().color()); if (opt.features & QStyleOptionViewItem::HasCheckIndicator) { @@ -68,12 +75,16 @@ void ProjectItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& o { // Title painting auto title = index.data(UserDataTypes::TITLE).toString(); - if (index.data(UserDataTypes::INSTALLED).toBool()) - title = tr("%1 [installed]").arg(title); - painter->save(); auto font = opt.font; + if (isChecked) { + font.setBold(true); + } + if (isInstalled) { + title = tr("%1 [installed]").arg(title); + } + font.setPointSize(font.pointSize() + 2); painter->setFont(font); From f0388d04bf8cbadd9721e6d9be740c277df67cbd Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Fri, 6 Jun 2025 22:40:39 +0000 Subject: [PATCH 307/695] Fix MessageLevel.h error when compiling on Debian Stable Signed-off-by: TheKodeToad --- launcher/MessageLevel.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/MessageLevel.h b/launcher/MessageLevel.h index 4ffd6bfc2..4c840dfc1 100644 --- a/launcher/MessageLevel.h +++ b/launcher/MessageLevel.h @@ -1,7 +1,7 @@ #pragma once +#include #include -#include /** * @brief the MessageLevel Enum From 8a80ccae3a13b10e5f77f0b3f32347ce8cd9a258 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Sat, 7 Jun 2025 09:10:03 +0300 Subject: [PATCH 308/695] chore: move oppacity after checkbox draw Signed-off-by: Trial97 --- launcher/ui/widgets/ProjectItem.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/launcher/ui/widgets/ProjectItem.cpp b/launcher/ui/widgets/ProjectItem.cpp index 91cf0956f..b6701535d 100644 --- a/launcher/ui/widgets/ProjectItem.cpp +++ b/launcher/ui/widgets/ProjectItem.cpp @@ -20,9 +20,6 @@ void ProjectItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& o auto isChecked = opt.checkState == Qt::Checked; auto isSelected = option.state & QStyle::State_Selected; - if (!isSelected && !isChecked && isInstalled) { - painter->setOpacity(0.4); // Fade out the entire item - } const QStyle* style = opt.widget == nullptr ? QApplication::style() : opt.widget->style(); auto rect = opt.rect; @@ -39,6 +36,9 @@ void ProjectItemDelegate::paint(QPainter* painter, const QStyleOptionViewItem& o rect.setX(checkboxOpt.rect.right()); } + if (!isSelected && !isChecked && isInstalled) { + painter->setOpacity(0.4); // Fade out the entire item + } // The default icon size will be a square (and height is usually the lower value). auto icon_width = rect.height(), icon_height = rect.height(); int icon_x_margin = (rect.height() - icon_width) / 2; From 834eb5a90dd943888075274aca12cf5208d21484 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Sat, 7 Jun 2025 16:32:40 +0100 Subject: [PATCH 309/695] Don't retry account refresh if aborted Signed-off-by: TheKodeToad --- launcher/LaunchController.cpp | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/launcher/LaunchController.cpp b/launcher/LaunchController.cpp index b1a956b49..824738b6c 100644 --- a/launcher/LaunchController.cpp +++ b/launcher/LaunchController.cpp @@ -200,7 +200,7 @@ void LaunchController::login() if ((m_accountToUse->accountType() != AccountType::Offline && m_accountToUse->accountState() == AccountState::Offline) || m_accountToUse->shouldRefresh()) { // Force account refresh on the account used to launch the instance updating the AccountState - // only on first try and if it is not meant to be offline + // only on first try and if it is not meant to be offline m_accountToUse->refresh(); } while (tryagain) { @@ -296,11 +296,21 @@ void LaunchController::login() case AccountState::Working: { // refresh is in progress, we need to wait for it to finish to proceed. ProgressDialog progDialog(m_parentWidget); - if (m_online) { - progDialog.setSkipButton(true, tr("Play Offline")); - } + progDialog.setSkipButton(true, tr("Abort")); + auto task = accountToCheck->currentTask(); + + bool aborted = false; + auto abortListener = connect(task.get(), &Task::aborted, [&aborted] { aborted = true; }); + progDialog.execWithTask(task.get()); + + disconnect(abortListener); + + // don't retry if aborted + if (aborted) + tryagain = false; + continue; } case AccountState::Expired: { From 06aece111ac223c076c0042af86f09428a727050 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Sat, 7 Jun 2025 20:24:32 +0100 Subject: [PATCH 310/695] Reset account state on abort Signed-off-by: TheKodeToad --- launcher/minecraft/auth/MinecraftAccount.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/launcher/minecraft/auth/MinecraftAccount.cpp b/launcher/minecraft/auth/MinecraftAccount.cpp index 86e9cc511..ca052c378 100644 --- a/launcher/minecraft/auth/MinecraftAccount.cpp +++ b/launcher/minecraft/auth/MinecraftAccount.cpp @@ -181,8 +181,10 @@ void MinecraftAccount::authFailed(QString reason) data.validity_ = Validity::None; emit changed(); } break; + case AccountTaskState::STATE_WORKING: { + data.accountState = AccountState::Unchecked; + } break; case AccountTaskState::STATE_CREATED: - case AccountTaskState::STATE_WORKING: case AccountTaskState::STATE_SUCCEEDED: { // Not reachable here, as they are not failures. } From a195b9981d5f42849ed565c58dff673550c42f01 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Sat, 7 Jun 2025 22:41:09 +0100 Subject: [PATCH 311/695] Use Task::getState Signed-off-by: TheKodeToad --- launcher/LaunchController.cpp | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/launcher/LaunchController.cpp b/launcher/LaunchController.cpp index 824738b6c..26f539e15 100644 --- a/launcher/LaunchController.cpp +++ b/launcher/LaunchController.cpp @@ -299,16 +299,10 @@ void LaunchController::login() progDialog.setSkipButton(true, tr("Abort")); auto task = accountToCheck->currentTask(); - - bool aborted = false; - auto abortListener = connect(task.get(), &Task::aborted, [&aborted] { aborted = true; }); - progDialog.execWithTask(task.get()); - disconnect(abortListener); - // don't retry if aborted - if (aborted) + if (task->getState() == Task::State::AbortedByUser) tryagain = false; continue; From ca545b7a5b66f15870936862b7878123c3afd0de Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 8 Jun 2025 00:30:27 +0000 Subject: [PATCH 312/695] chore(nix): update lockfile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'nixpkgs': 'github:NixOS/nixpkgs/96ec055edbe5ee227f28cdbc3f1ddf1df5965102?narHash=sha256-7doLyJBzCllvqX4gszYtmZUToxKvMUrg45EUWaUYmBg%3D' (2025-05-28) → 'github:NixOS/nixpkgs/d3d2d80a2191a73d1e86456a751b83aa13085d7d?narHash=sha256-QuUtALJpVrPnPeozlUG/y%2BoIMSLdptHxb3GK6cpSVhA%3D' (2025-06-05) --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 2d2f820f4..444e07d05 100644 --- a/flake.lock +++ b/flake.lock @@ -18,11 +18,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1748460289, - "narHash": "sha256-7doLyJBzCllvqX4gszYtmZUToxKvMUrg45EUWaUYmBg=", + "lastModified": 1749143949, + "narHash": "sha256-QuUtALJpVrPnPeozlUG/y+oIMSLdptHxb3GK6cpSVhA=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "96ec055edbe5ee227f28cdbc3f1ddf1df5965102", + "rev": "d3d2d80a2191a73d1e86456a751b83aa13085d7d", "type": "github" }, "original": { From ea1a0daddaae35f448d69fd9c3e008a7b6a5552b Mon Sep 17 00:00:00 2001 From: Trial97 Date: Mon, 9 Jun 2025 23:04:20 +0300 Subject: [PATCH 313/695] fix: curseforge optinal blocked mods Signed-off-by: Trial97 --- .../flame/FlameInstanceCreationTask.cpp | 49 +++++++++++-------- .../flame/FlameInstanceCreationTask.h | 2 + launcher/ui/dialogs/BlockedModsDialog.h | 1 + 3 files changed, 31 insertions(+), 21 deletions(-) diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp index c80187c42..adf4c1065 100644 --- a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp +++ b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp @@ -493,16 +493,35 @@ bool FlameCreationTask::createInstance() void FlameCreationTask::idResolverSucceeded(QEventLoop& loop) { - auto results = m_modIdResolver->getResults(); + auto results = m_modIdResolver->getResults().files; + + QStringList optionalFiles; + for (auto& result : results) { + if (!result.required) { + optionalFiles << FS::PathCombine(result.targetFolder, result.version.fileName); + } + } + + if (!optionalFiles.empty()) { + OptionalModDialog optionalModDialog(m_parent, optionalFiles); + if (optionalModDialog.exec() == QDialog::Rejected) { + emitAborted(); + loop.quit(); + return; + } + + m_selectedOptionalMods = optionalModDialog.getResult(); + } // first check for blocked mods QList blocked_mods; auto anyBlocked = false; - for (const auto& result : results.files.values()) { + for (const auto& result : results.values()) { if (result.resourceType != PackedResourceType::Mod) { m_otherResources.append(std::make_pair(result.version.fileName, result.targetFolder)); } + // skip optional mods that were not selected if (result.version.downloadUrl.isEmpty()) { BlockedMod blocked_mod; blocked_mod.name = result.version.fileName; @@ -511,6 +530,10 @@ void FlameCreationTask::idResolverSucceeded(QEventLoop& loop) blocked_mod.matched = false; blocked_mod.localPath = ""; blocked_mod.targetFolder = result.targetFolder; + auto fileName = result.version.fileName; + fileName = FS::RemoveInvalidPathChars(fileName); + auto relpath = FS::PathCombine(result.targetFolder, fileName); + blocked_mod.disabled = !result.required && !m_selectedOptionalMods.contains(relpath); blocked_mods.append(blocked_mod); @@ -546,30 +569,12 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop) m_filesJob.reset(new NetJob(tr("Mod Download Flame"), APPLICATION->network())); auto results = m_modIdResolver->getResults().files; - QStringList optionalFiles; - for (auto& result : results) { - if (!result.required) { - optionalFiles << FS::PathCombine(result.targetFolder, result.version.fileName); - } - } - - QStringList selectedOptionalMods; - if (!optionalFiles.empty()) { - OptionalModDialog optionalModDialog(m_parent, optionalFiles); - if (optionalModDialog.exec() == QDialog::Rejected) { - emitAborted(); - loop.quit(); - return; - } - - selectedOptionalMods = optionalModDialog.getResult(); - } for (const auto& result : results) { auto fileName = result.version.fileName; fileName = FS::RemoveInvalidPathChars(fileName); auto relpath = FS::PathCombine(result.targetFolder, fileName); - if (!result.required && !selectedOptionalMods.contains(relpath)) { + if (!result.required && !m_selectedOptionalMods.contains(relpath)) { relpath += ".disabled"; } @@ -617,6 +622,8 @@ void FlameCreationTask::copyBlockedMods(QList const& blocked_mods) } auto destPath = FS::PathCombine(m_stagingPath, "minecraft", mod.targetFolder, mod.name); + if (mod.disabled) + destPath += ".disabled"; setStatus(tr("Copying Blocked Mods (%1 out of %2 are done)").arg(QString::number(i), QString::number(total))); diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.h b/launcher/modplatform/flame/FlameInstanceCreationTask.h index 3e586a416..e41ce742e 100644 --- a/launcher/modplatform/flame/FlameInstanceCreationTask.h +++ b/launcher/modplatform/flame/FlameInstanceCreationTask.h @@ -92,4 +92,6 @@ class FlameCreationTask final : public InstanceCreationTask { QList> m_otherResources; std::optional m_instance; + + QStringList m_selectedOptionalMods; }; diff --git a/launcher/ui/dialogs/BlockedModsDialog.h b/launcher/ui/dialogs/BlockedModsDialog.h index b24e76bbf..15d4d4770 100644 --- a/launcher/ui/dialogs/BlockedModsDialog.h +++ b/launcher/ui/dialogs/BlockedModsDialog.h @@ -42,6 +42,7 @@ struct BlockedMod { bool matched; QString localPath; QString targetFolder; + bool disabled = false; bool move = false; }; From 8df20a372c0a7ee3ce536496af159c18156d5329 Mon Sep 17 00:00:00 2001 From: Seth Flynn Date: Thu, 12 Jun 2025 21:37:39 -0400 Subject: [PATCH 314/695] ci: don't run ci on custom in-tree branches This basically duplicates runs when a PR is opened from an in-tree branch. If we want to run one without a PR, we can use workflow_dispatch anyways Signed-off-by: Seth Flynn --- .github/workflows/build.yml | 5 +++-- .github/workflows/codeql.yml | 3 +++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f4cdae97c..f744e28ba 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,8 +2,9 @@ name: Build on: push: - branches-ignore: - - "renovate/**" + branches: + - "develop" + - "release-*" paths: # File types - "**.cpp" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index f8fae8ecf..8df64878b 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -2,6 +2,9 @@ name: "CodeQL Code Scanning" on: push: + branches: + - "develop" + - "release-*" paths: # File types - "**.cpp" From d8ac52bd8c97ea34d6a54613e27431d5f0fe93ee Mon Sep 17 00:00:00 2001 From: Seth Flynn Date: Thu, 12 Jun 2025 21:39:11 -0400 Subject: [PATCH 315/695] ci: ensure all workflows are actually run on push to branches Oops Signed-off-by: Seth Flynn --- .github/workflows/flatpak.yml | 3 +++ .github/workflows/nix.yml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml index cab0edeb7..26a44d679 100644 --- a/.github/workflows/flatpak.yml +++ b/.github/workflows/flatpak.yml @@ -2,6 +2,9 @@ name: Flatpak on: push: + branches: + - "develop" + - "release-*" # We don't do anything with these artifacts on releases. They go to Flathub tags-ignore: - "*" diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index 80b41161a..7b9412c87 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -2,6 +2,9 @@ name: Nix on: push: + branches: + - "develop" + - "release-*" tags: - "*" paths: From 04ecd447bc0df43631face1c20c87f1fd03d3fc0 Mon Sep 17 00:00:00 2001 From: Seth Flynn Date: Thu, 12 Jun 2025 19:58:38 -0400 Subject: [PATCH 316/695] ci(package/linux): use dpkg to determine file paths and variables Should hopefully make things less brittle across different architectures Signed-off-by: Seth Flynn --- .github/actions/package/linux/action.yml | 58 +++++++++++++------ .../setup-dependencies/linux/action.yml | 5 +- .github/workflows/build.yml | 1 + 3 files changed, 45 insertions(+), 19 deletions(-) diff --git a/.github/actions/package/linux/action.yml b/.github/actions/package/linux/action.yml index b71e62592..1b4df5de2 100644 --- a/.github/actions/package/linux/action.yml +++ b/.github/actions/package/linux/action.yml @@ -31,6 +31,28 @@ runs: using: composite steps: + - name: Setup build variables + shell: bash + run: | + # Fixup architecture naming for AppImages + dpkg_arch="$(dpkg-architecture -q DEB_HOST_ARCH_CPU)" + case "$dpkg_arch" in + "amd64") + APPIMAGE_ARCH="x86_64" + ;; + "arm64") + APPIMAGE_ARCH="aarch64" + ;; + *) + echo "# 🚨 The Debian architecture \"$deb_arch\" is not recognized!" >> "$GITHUB_STEP_SUMMARY" + exit 1 + ;; + esac + echo "APPIMAGE_ARCH=$APPIMAGE_ARCH" >> "$GITHUB_ENV" + + # Used for the file paths of libraries + echo "DEB_HOST_MULTIARCH=$(dpkg-architecture -q DEB_HOST_MULTIARCH)" >> "$GITHUB_ENV" + - name: Package AppImage shell: bash env: @@ -45,7 +67,7 @@ runs: mv ${{ env.INSTALL_APPIMAGE_DIR }}/usr/share/metainfo/org.prismlauncher.PrismLauncher.metainfo.xml ${{ env.INSTALL_APPIMAGE_DIR }}/usr/share/metainfo/org.prismlauncher.PrismLauncher.appdata.xml export "NO_APPSTREAM=1" # we have to skip appstream checking because appstream on ubuntu 20.04 is outdated - export OUTPUT="PrismLauncher-Linux-x86_64.AppImage" + export OUTPUT="PrismLauncher-Linux-$APPIMAGE_ARCH.AppImage" chmod +x linuxdeploy-*.AppImage @@ -54,17 +76,17 @@ runs: cp -r ${{ runner.workspace }}/Qt/${{ inputs.qt-version }}/gcc_64/plugins/iconengines/* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/plugins/iconengines - cp /usr/lib/x86_64-linux-gnu/libcrypto.so.* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/ - cp /usr/lib/x86_64-linux-gnu/libssl.so.* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/ - cp /usr/lib/x86_64-linux-gnu/libOpenGL.so.0* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/ + cp /usr/lib/"$DEB_HOST_MULTIARCH"/libcrypto.so.* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/ + cp /usr/lib/"$DEB_HOST_MULTIARCH"/libssl.so.* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/ + cp /usr/lib/"$DEB_HOST_MULTIARCH"/libOpenGL.so.0* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/ LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib" export LD_LIBRARY_PATH - chmod +x AppImageUpdate-x86_64.AppImage - cp AppImageUpdate-x86_64.AppImage ${{ env.INSTALL_APPIMAGE_DIR }}/usr/bin + chmod +x AppImageUpdate-"$APPIMAGE_ARCH".AppImage + cp AppImageUpdate-"$APPIMAGE_ARCH".AppImage ${{ env.INSTALL_APPIMAGE_DIR }}/usr/bin - export UPDATE_INFORMATION="gh-releases-zsync|${{ github.repository_owner }}|${{ github.event.repository.name }}|latest|PrismLauncher-Linux-x86_64.AppImage.zsync" + export UPDATE_INFORMATION="gh-releases-zsync|${{ github.repository_owner }}|${{ github.event.repository.name }}|latest|PrismLauncher-Linux-$APPIMAGE_ARCH.AppImage.zsync" if [ '${{ inputs.gpg-private-key-id }}' != '' ]; then export SIGN=1 @@ -76,9 +98,9 @@ runs: echo ":warning: Skipped code signing for Linux AppImage, as gpg key was not present." >> $GITHUB_STEP_SUMMARY fi - ./linuxdeploy-x86_64.AppImage --appdir ${{ env.INSTALL_APPIMAGE_DIR }} --output appimage --plugin qt -i ${{ env.INSTALL_APPIMAGE_DIR }}/usr/share/icons/hicolor/scalable/apps/org.prismlauncher.PrismLauncher.svg + ./linuxdeploy-"$APPIMAGE_ARCH".AppImage --appdir ${{ env.INSTALL_APPIMAGE_DIR }} --output appimage --plugin qt -i ${{ env.INSTALL_APPIMAGE_DIR }}/usr/share/icons/hicolor/scalable/apps/org.prismlauncher.PrismLauncher.svg - mv "PrismLauncher-Linux-x86_64.AppImage" "PrismLauncher-Linux-${{ env.VERSION }}-${{ inputs.build-type }}-x86_64.AppImage" + mv "PrismLauncher-Linux-$APPIMAGE_ARCH.AppImage" "PrismLauncher-Linux-${{ env.VERSION }}-${{ inputs.build-type }}-$APPIMAGE_ARCH.AppImage" - name: Package portable tarball shell: bash @@ -94,11 +116,11 @@ runs: cmake --install ${{ env.BUILD_DIR }} --component portable mkdir ${{ env.INSTALL_PORTABLE_DIR }}/lib - cp /lib/x86_64-linux-gnu/libbz2.so.1.0 ${{ env.INSTALL_PORTABLE_DIR }}/lib - cp /usr/lib/x86_64-linux-gnu/libgobject-2.0.so.0 ${{ env.INSTALL_PORTABLE_DIR }}/lib - cp /usr/lib/x86_64-linux-gnu/libcrypto.so.* ${{ env.INSTALL_PORTABLE_DIR }}/lib - cp /usr/lib/x86_64-linux-gnu/libssl.so.* ${{ env.INSTALL_PORTABLE_DIR }}/lib - cp /usr/lib/x86_64-linux-gnu/libffi.so.*.* ${{ env.INSTALL_PORTABLE_DIR }}/lib + cp /lib/"$DEB_HOST_MULTIARCH"/libbz2.so.1.0 ${{ env.INSTALL_PORTABLE_DIR }}/lib + cp /usr/lib/"$DEB_HOST_MULTIARCH"/libgobject-2.0.so.0 ${{ env.INSTALL_PORTABLE_DIR }}/lib + cp /usr/lib/"$DEB_HOST_MULTIARCH"/libcrypto.so.* ${{ env.INSTALL_PORTABLE_DIR }}/lib + cp /usr/lib/"$DEB_HOST_MULTIARCH"/libssl.so.* ${{ env.INSTALL_PORTABLE_DIR }}/lib + cp /usr/lib/"$DEB_HOST_MULTIARCH"/libffi.so.*.* ${{ env.INSTALL_PORTABLE_DIR }}/lib mv ${{ env.INSTALL_PORTABLE_DIR }}/bin/*.so* ${{ env.INSTALL_PORTABLE_DIR }}/lib for l in $(find ${{ env.INSTALL_PORTABLE_DIR }} -type f); do l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_PORTABLE_DIR }}/}; l=${l#./}; echo $l; done > ${{ env.INSTALL_PORTABLE_DIR }}/manifest.txt @@ -114,11 +136,11 @@ runs: - name: Upload AppImage uses: actions/upload-artifact@v4 with: - name: PrismLauncher-${{ inputs.artifact-name }}-${{ inputs.version }}-${{ inputs.build-type }}-x86_64.AppImage - path: PrismLauncher-${{ runner.os }}-${{ inputs.version }}-${{ inputs.build-type }}-x86_64.AppImage + name: PrismLauncher-${{ runner.os }}-${{ inputs.version }}-${{ inputs.build-type }}-${{ env.APPIMAGE_ARCH }}.AppImage + path: PrismLauncher-${{ runner.os }}-${{ inputs.version }}-${{ inputs.build-type }}-${{ env.APPIMAGE_ARCH }}.AppImage - name: Upload AppImage Zsync uses: actions/upload-artifact@v4 with: - name: PrismLauncher-${{ inputs.artifact-name }}-${{ inputs.version }}-${{ inputs.build-type }}-x86_64.AppImage.zsync - path: PrismLauncher-Linux-x86_64.AppImage.zsync + name: PrismLauncher-${{ runner.os }}-${{ inputs.version }}-${{ inputs.build-type }}-${{ env.APPIMAGE_ARCH }}.AppImage.zsync + path: PrismLauncher-${{ runner.os }}-${{ env.APPIMAGE_ARCH }}.AppImage.zsync diff --git a/.github/actions/setup-dependencies/linux/action.yml b/.github/actions/setup-dependencies/linux/action.yml index dd0d28364..0569b3d4c 100644 --- a/.github/actions/setup-dependencies/linux/action.yml +++ b/.github/actions/setup-dependencies/linux/action.yml @@ -8,7 +8,10 @@ runs: shell: bash run: | sudo apt-get -y update - sudo apt-get -y install ninja-build extra-cmake-modules scdoc appstream libxcb-cursor-dev + sudo apt-get -y install \ + dpkg-dev \ + ninja-build extra-cmake-modules scdoc \ + appstream libxcb-cursor-dev - name: Setup AppImage tooling shell: bash diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f4cdae97c..602efbeae 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -164,6 +164,7 @@ jobs: with: version: ${{ steps.short-version.outputs.version }} build-type: ${{ steps.setup-dependencies.outputs.build-type }} + artifact-name: ${{ matrix.artifact-name }} cmake-preset: ${{ steps.cmake-preset.outputs.preset }} qt-version: ${{ steps.setup-dependencies.outputs.qt-version }} From 6d960b9c3c73385d035bb88744ea33bee1cf2e93 Mon Sep 17 00:00:00 2001 From: Seth Flynn Date: Thu, 12 Jun 2025 20:10:51 -0400 Subject: [PATCH 317/695] ci(setup-deps): always use sccache, simplify restore key sccache is available on arm runners. we can use the restore key for an easy, unique restore key in the cache too (it also prevents us from re-using the ccache caches!) Signed-off-by: Seth Flynn --- .github/actions/setup-dependencies/action.yml | 11 +++++++---- .github/workflows/build.yml | 1 + 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/actions/setup-dependencies/action.yml b/.github/actions/setup-dependencies/action.yml index e97abd1df..47a9819ea 100644 --- a/.github/actions/setup-dependencies/action.yml +++ b/.github/actions/setup-dependencies/action.yml @@ -6,6 +6,9 @@ inputs: description: Type for the build required: true default: Debug + artifact-name: + description: Name of the uploaded artifact + required: true msystem: description: MSYS2 subsystem to use required: false @@ -53,16 +56,16 @@ runs: if: ${{ (runner.os != 'Windows' || inputs.msystem == '') && inputs.build-type == 'Debug' }} uses: hendrikmuhs/ccache-action@v1.2.18 with: - variant: ${{ runner.os == 'Windows' && 'sccache' || 'ccache' }} + variant: sccache create-symlink: ${{ runner.os != 'Windows' }} - key: ${{ runner.os }}-qt${{ inputs.qt_ver }}-${{ inputs.architecture }} + key: ${{ runner.os }}-${{ runner.arch }}-${{ inputs.artifact-name }}-sccache - name: Use ccache on debug builds if: ${{ inputs.build-type == 'Debug' }} shell: bash env: - # Only use sccache on MSVC - CCACHE_VARIANT: ${{ (runner.os == 'Windows' && inputs.msystem == '') && 'sccache' || 'ccache' }} + # Only use ccache on MSYS2 + CCACHE_VARIANT: ${{ (runner.os == 'Windows' && inputs.msystem != '') && 'ccache' || 'sccache' }} run: | echo "CMAKE_C_COMPILER_LAUNCHER=$CCACHE_VARIANT" >> "$GITHUB_ENV" echo "CMAKE_CXX_COMPILER_LAUNCHER=$CCACHE_VARIANT" >> "$GITHUB_ENV" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 602efbeae..7df86c3ec 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -126,6 +126,7 @@ jobs: uses: ./.github/actions/setup-dependencies with: build-type: ${{ inputs.build-type || 'Debug' }} + artifact-name: ${{ matrix.artifact-name }} msystem: ${{ matrix.msystem }} vcvars-arch: ${{ matrix.vcvars-arch }} qt-architecture: ${{ matrix.qt-architecture }} From 3718c60844cf080b158108e53d6a4f1c3f7a4428 Mon Sep 17 00:00:00 2001 From: Seth Flynn Date: Thu, 12 Jun 2025 20:42:54 -0400 Subject: [PATCH 318/695] cmake: enforce explicit artifact name It's much easier to determine this in CI and ensure our artifact names are correct (I have made some accidents). They (and thus the updater) can also easily be left out of local builds -- and probably should've always been Signed-off-by: Seth Flynn --- .github/workflows/build.yml | 1 + cmake/commonPresets.json | 1 + cmake/linuxPreset.json | 4 ---- cmake/macosPreset.json | 8 ++------ cmake/windowsMSVCPreset.json | 18 +++--------------- cmake/windowsMinGWPreset.json | 10 ++-------- 6 files changed, 9 insertions(+), 33 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7df86c3ec..c1ee0c761 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -146,6 +146,7 @@ jobs: - name: Run CMake workflow env: CMAKE_PRESET: ${{ steps.cmake-preset.outputs.preset }} + ARTIFACT_NAME: ${{ matrix.artifact-name }}-Qt6 run: | cmake --workflow --preset "$CMAKE_PRESET" diff --git a/cmake/commonPresets.json b/cmake/commonPresets.json index 9cdf51649..2f4cbfa15 100644 --- a/cmake/commonPresets.json +++ b/cmake/commonPresets.json @@ -8,6 +8,7 @@ "binaryDir": "build", "installDir": "install", "cacheVariables": { + "Launcher_BUILD_ARTIFACT": "$penv{ARTIFACT_NAME}", "Launcher_BUILD_PLATFORM": "custom" } }, diff --git a/cmake/linuxPreset.json b/cmake/linuxPreset.json index b8bfe4ff0..984defa5d 100644 --- a/cmake/linuxPreset.json +++ b/cmake/linuxPreset.json @@ -15,7 +15,6 @@ }, "generator": "Ninja", "cacheVariables": { - "Launcher_BUILD_ARTIFACT": "Linux-Qt6", "Launcher_ENABLE_JAVA_DOWNLOADER": "ON" } }, @@ -42,9 +41,6 @@ "linux_base" ], "displayName": "Linux (CI)", - "cacheVariables": { - "Launcher_BUILD_ARTIFACT": "Linux-Qt6" - }, "installDir": "/usr" } ], diff --git a/cmake/macosPreset.json b/cmake/macosPreset.json index 726949934..de503d7a2 100644 --- a/cmake/macosPreset.json +++ b/cmake/macosPreset.json @@ -22,8 +22,7 @@ "macos_base" ], "cacheVariables": { - "CMAKE_OSX_ARCHITECTURES": "x86_64;arm64", - "Launcher_BUILD_ARTIFACT": "macOS-Qt6" + "CMAKE_OSX_ARCHITECTURES": "x86_64;arm64" } }, { @@ -64,10 +63,7 @@ "base_ci", "macos_universal_base" ], - "displayName": "macOS (CI)", - "cacheVariables": { - "Launcher_BUILD_ARTIFACT": "macOS-Qt6" - } + "displayName": "macOS (CI)" } ], "buildPresets": [ diff --git a/cmake/windowsMSVCPreset.json b/cmake/windowsMSVCPreset.json index eb6a38b19..603a0a9ff 100644 --- a/cmake/windowsMSVCPreset.json +++ b/cmake/windowsMSVCPreset.json @@ -12,9 +12,6 @@ "type": "equals", "lhs": "${hostSystemName}", "rhs": "Windows" - }, - "cacheVariables": { - "Launcher_BUILD_ARTIFACT": "Windows-MSVC-Qt6" } }, { @@ -23,10 +20,7 @@ "inherits": [ "windows_msvc_base" ], - "architecture": "arm64", - "cacheVariables": { - "Launcher_BUILD_ARTIFACT": "Windows-MSVC-arm64-Qt6" - } + "architecture": "arm64" }, { "name": "windows_msvc_debug", @@ -67,10 +61,7 @@ "base_ci", "windows_msvc_base" ], - "displayName": "Windows MSVC (CI)", - "cacheVariables": { - "Launcher_BUILD_ARTIFACT": "Windows-MSVC-Qt6" - } + "displayName": "Windows MSVC (CI)" }, { "name": "windows_msvc_arm64_cross_ci", @@ -78,10 +69,7 @@ "base_ci", "windows_msvc_arm64_cross_base" ], - "displayName": "Windows MSVC (ARM64 cross, CI)", - "cacheVariables": { - "Launcher_BUILD_ARTIFACT": "Windows-MSVC-arm64-Qt6" - } + "displayName": "Windows MSVC (ARM64 cross, CI)" } ], "buildPresets": [ diff --git a/cmake/windowsMinGWPreset.json b/cmake/windowsMinGWPreset.json index 984caadd6..7c4adbcf2 100644 --- a/cmake/windowsMinGWPreset.json +++ b/cmake/windowsMinGWPreset.json @@ -13,10 +13,7 @@ "lhs": "${hostSystemName}", "rhs": "Windows" }, - "generator": "Ninja", - "cacheVariables": { - "Launcher_BUILD_ARTIFACT": "Windows-MinGW-w64-Qt6" - } + "generator": "Ninja" }, { "name": "windows_mingw_debug", @@ -40,10 +37,7 @@ "base_ci", "windows_mingw_base" ], - "displayName": "Windows MinGW (CI)", - "cacheVariables": { - "Launcher_BUILD_ARTIFACT": "Windows-MinGW-w64-Qt6" - } + "displayName": "Windows MinGW (CI)" } ], "buildPresets": [ From c03f854fb8a56bc45179cb54c493eda77019e5ce Mon Sep 17 00:00:00 2001 From: Seth Flynn Date: Fri, 13 Jun 2025 02:00:11 -0400 Subject: [PATCH 319/695] cmake: use build platform from environment This allows all CI builds to be deemed "official" Signed-off-by: Seth Flynn --- .github/workflows/build.yml | 2 ++ cmake/commonPresets.json | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c1ee0c761..e656ffcca 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -146,7 +146,9 @@ jobs: - name: Run CMake workflow env: CMAKE_PRESET: ${{ steps.cmake-preset.outputs.preset }} + ARTIFACT_NAME: ${{ matrix.artifact-name }}-Qt6 + BUILD_PLATFORM: official run: | cmake --workflow --preset "$CMAKE_PRESET" diff --git a/cmake/commonPresets.json b/cmake/commonPresets.json index 2f4cbfa15..9be0fb447 100644 --- a/cmake/commonPresets.json +++ b/cmake/commonPresets.json @@ -9,7 +9,7 @@ "installDir": "install", "cacheVariables": { "Launcher_BUILD_ARTIFACT": "$penv{ARTIFACT_NAME}", - "Launcher_BUILD_PLATFORM": "custom" + "Launcher_BUILD_PLATFORM": "$penv{BUILD_PLATFORM}" } }, { @@ -40,7 +40,6 @@ "base_release" ], "cacheVariables": { - "Launcher_BUILD_PLATFORM": "official", "Launcher_FORCE_BUNDLED_LIBS": "ON" } } From 03c714cccf61b685cfe349199a1ed06ed3c329ad Mon Sep 17 00:00:00 2001 From: seth Date: Fri, 17 Jan 2025 00:49:32 -0500 Subject: [PATCH 320/695] ci: build for arm on linux Signed-off-by: seth --- .github/actions/package/linux/action.yml | 2 +- .../setup-dependencies/linux/action.yml | 21 ++++++++++++++++--- .github/workflows/build.yml | 8 +++++++ .github/workflows/release.yml | 9 ++++++-- 4 files changed, 34 insertions(+), 6 deletions(-) diff --git a/.github/actions/package/linux/action.yml b/.github/actions/package/linux/action.yml index 1b4df5de2..d5b0d73f7 100644 --- a/.github/actions/package/linux/action.yml +++ b/.github/actions/package/linux/action.yml @@ -74,7 +74,7 @@ runs: mkdir -p ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib mkdir -p ${{ env.INSTALL_APPIMAGE_DIR }}/usr/plugins/iconengines - cp -r ${{ runner.workspace }}/Qt/${{ inputs.qt-version }}/gcc_64/plugins/iconengines/* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/plugins/iconengines + cp -r ${{ runner.workspace }}/Qt/${{ inputs.qt-version }}/gcc_*64/plugins/iconengines/* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/plugins/iconengines cp /usr/lib/"$DEB_HOST_MULTIARCH"/libcrypto.so.* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/ cp /usr/lib/"$DEB_HOST_MULTIARCH"/libssl.so.* ${{ env.INSTALL_APPIMAGE_DIR }}/usr/lib/ diff --git a/.github/actions/setup-dependencies/linux/action.yml b/.github/actions/setup-dependencies/linux/action.yml index 0569b3d4c..94c04abe5 100644 --- a/.github/actions/setup-dependencies/linux/action.yml +++ b/.github/actions/setup-dependencies/linux/action.yml @@ -17,9 +17,24 @@ runs: shell: bash run: | declare -A appimage_deps - appimage_deps["https://github.com/linuxdeploy/linuxdeploy/releases/download/1-alpha-20250213-2/linuxdeploy-x86_64.AppImage"]="4648f278ab3ef31f819e67c30d50f462640e5365a77637d7e6f2ad9fd0b4522a linuxdeploy-x86_64.AppImage" - appimage_deps["https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/1-alpha-20250213-1/linuxdeploy-plugin-qt-x86_64.AppImage"]="15106be885c1c48a021198e7e1e9a48ce9d02a86dd0a1848f00bdbf3c1c92724 linuxdeploy-plugin-qt-x86_64.AppImage" - appimage_deps["https://github.com/AppImageCommunity/AppImageUpdate/releases/download/2.0.0-alpha-1-20241225/AppImageUpdate-x86_64.AppImage"]="f1747cf60058e99f1bb9099ee9787d16c10241313b7acec81810ea1b1e568c11 AppImageUpdate-x86_64.AppImage" + + deb_arch="$(dpkg-architecture -q DEB_HOST_ARCH)" + case "$deb_arch" in + "amd64") + appimage_deps["https://github.com/linuxdeploy/linuxdeploy/releases/download/1-alpha-20250213-2/linuxdeploy-x86_64.AppImage"]="4648f278ab3ef31f819e67c30d50f462640e5365a77637d7e6f2ad9fd0b4522a linuxdeploy-x86_64.AppImage" + appimage_deps["https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/1-alpha-20250213-1/linuxdeploy-plugin-qt-x86_64.AppImage"]="15106be885c1c48a021198e7e1e9a48ce9d02a86dd0a1848f00bdbf3c1c92724 linuxdeploy-plugin-qt-x86_64.AppImage" + appimage_deps["https://github.com/AppImageCommunity/AppImageUpdate/releases/download/2.0.0-alpha-1-20241225/AppImageUpdate-x86_64.AppImage"]="f1747cf60058e99f1bb9099ee9787d16c10241313b7acec81810ea1b1e568c11 AppImageUpdate-x86_64.AppImage" + ;; + "arm64") + appimage_deps["https://github.com/linuxdeploy/linuxdeploy/releases/download/1-alpha-20250213-2/linuxdeploy-aarch64.AppImage"]="06706ac8189797dccd36bd384105892cb5e6e71f784f4df526cc958adc223cd6 linuxdeploy-aarch64.AppImage" + appimage_deps["https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/1-alpha-20250213-1/linuxdeploy-plugin-qt-aarch64.AppImage"]="bf1c24aff6d749b5cf423afad6f15abd4440f81dec1aab95706b25f6667cdcf1 linuxdeploy-plugin-qt-aarch64.AppImage" + appimage_deps["https://github.com/AppImageCommunity/AppImageUpdate/releases/download/2.0.0-alpha-1-20241225/AppImageUpdate-aarch64.AppImage"]="cf27f810dfe5eda41f130769e4a4b562b9d93665371c15ebeffb84ee06a41550 AppImageUpdate-aarch64.AppImage" + ;; + *) + echo "# 🚨 The Debian architecture \"$deb_arch\" is not recognized!" >> "$GITHUB_STEP_SUMMARY" + exit 1 + ;; + esac for url in "${!appimage_deps[@]}"; do curl -LO "$url" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e656ffcca..5eb201fe1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -73,6 +73,14 @@ jobs: artifact-name: Linux base-cmake-preset: linux + # NOTE(@getchoo): Yes, we're intentionally using 24.04 here!!! + # + # It's not really documented anywhere AFAICT, but upstream Qt binaries + # *for the same version* are compiled against 24.04 on ARM, and *not* 22.04 like x64 + - os: ubuntu-24.04-arm + artifact-name: Linux-aarch64 + base-cmake-preset: linux + - os: windows-2022 artifact-name: Windows-MinGW-w64 base-cmake-preset: windows_mingw diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6e879cfd7..264dffbc0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -34,8 +34,10 @@ jobs: run: | mv ${{ github.workspace }}/PrismLauncher-source PrismLauncher-${{ env.VERSION }} mv PrismLauncher-Linux-Qt6-Portable*/PrismLauncher-portable.tar.gz PrismLauncher-Linux-Qt6-Portable-${{ env.VERSION }}.tar.gz - mv PrismLauncher-*.AppImage/PrismLauncher-*.AppImage PrismLauncher-Linux-x86_64.AppImage - mv PrismLauncher-*.AppImage.zsync/PrismLauncher-*.AppImage.zsync PrismLauncher-Linux-x86_64.AppImage.zsync + mv PrismLauncher-*.AppImage/PrismLauncher-*-x86_64.AppImage PrismLauncher-Linux-x86_64.AppImage + mv PrismLauncher-*.AppImage.zsync/PrismLauncher-*-x86_64.AppImage.zsync PrismLauncher-Linux-x86_64.AppImage.zsync + mv PrismLauncher-*.AppImage/PrismLauncher-*-aarch64.AppImage PrismLauncher-Linux-aarch64.AppImage + mv PrismLauncher-*.AppImage.zsync/PrismLauncher-*-aarch64.AppImage.zsync PrismLauncher-Linux-aarch64.AppImage.zsync mv PrismLauncher-macOS*/PrismLauncher.zip PrismLauncher-macOS-${{ env.VERSION }}.zip tar --exclude='.git' -czf PrismLauncher-${{ env.VERSION }}.tar.gz PrismLauncher-${{ env.VERSION }} @@ -89,7 +91,10 @@ jobs: files: | PrismLauncher-Linux-x86_64.AppImage PrismLauncher-Linux-x86_64.AppImage.zsync + PrismLauncher-Linux-aarch64.AppImage + PrismLauncher-Linux-aarch64.AppImage.zsync PrismLauncher-Linux-Qt6-Portable-${{ env.VERSION }}.tar.gz + PrismLauncher-Linux-aarch64-Qt6-Portable-${{ env.VERSION }}.tar.gz PrismLauncher-Windows-MinGW-w64-${{ env.VERSION }}.zip PrismLauncher-Windows-MinGW-w64-Portable-${{ env.VERSION }}.zip PrismLauncher-Windows-MinGW-w64-Setup-${{ env.VERSION }}.exe From 45027279fa73548dd026903d1953075608ca929d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 15 Jun 2025 00:30:46 +0000 Subject: [PATCH 321/695] chore(nix): update lockfile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'nixpkgs': 'github:NixOS/nixpkgs/d3d2d80a2191a73d1e86456a751b83aa13085d7d?narHash=sha256-QuUtALJpVrPnPeozlUG/y%2BoIMSLdptHxb3GK6cpSVhA%3D' (2025-06-05) → 'github:NixOS/nixpkgs/ee930f9755f58096ac6e8ca94a1887e0534e2d81?narHash=sha256-Kh9K4taXbVuaLC0IL%2B9HcfvxsSUx8dPB5s5weJcc9pc%3D' (2025-06-13) --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 444e07d05..18ecd28e2 100644 --- a/flake.lock +++ b/flake.lock @@ -18,11 +18,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1749143949, - "narHash": "sha256-QuUtALJpVrPnPeozlUG/y+oIMSLdptHxb3GK6cpSVhA=", + "lastModified": 1749794982, + "narHash": "sha256-Kh9K4taXbVuaLC0IL+9HcfvxsSUx8dPB5s5weJcc9pc=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "d3d2d80a2191a73d1e86456a751b83aa13085d7d", + "rev": "ee930f9755f58096ac6e8ca94a1887e0534e2d81", "type": "github" }, "original": { From 4bf6e59f3b1ff22e09c9d085d34db6d70ed1d2cd Mon Sep 17 00:00:00 2001 From: clague <93119153+clague@users.noreply.github.com> Date: Sat, 14 Jun 2025 23:10:49 +0800 Subject: [PATCH 322/695] feat: add ability to change assets download server Signed-off-by: clague <93119153+clague@users.noreply.github.com> --- buildconfig/BuildConfig.h | 2 +- launcher/Application.cpp | 9 ++++++ launcher/minecraft/AssetsUtils.cpp | 7 +++- launcher/minecraft/update/AssetUpdateTask.cpp | 10 +++++- launcher/ui/pages/global/APIPage.cpp | 16 ++++++++++ launcher/ui/pages/global/APIPage.ui | 32 +++++++++++++++++++ 6 files changed, 73 insertions(+), 3 deletions(-) diff --git a/buildconfig/BuildConfig.h b/buildconfig/BuildConfig.h index 10c38e3d6..045d987d4 100644 --- a/buildconfig/BuildConfig.h +++ b/buildconfig/BuildConfig.h @@ -166,7 +166,7 @@ class Config { QString DISCORD_URL; QString SUBREDDIT_URL; - QString RESOURCE_BASE = "https://resources.download.minecraft.net/"; + QString DEFAULT_RESOURCE_BASE = "https://resources.download.minecraft.net/"; QString LIBRARY_BASE = "https://libraries.minecraft.net/"; QString IMGUR_BASE_URL = "https://api.imgur.com/3/"; QString FMLLIBS_BASE_URL; diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 99b72870b..a4c96d645 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -864,6 +864,15 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) // get rid of invalid meta urls if (!metaUrl.isValid() || (metaUrl.scheme() != "http" && metaUrl.scheme() != "https")) m_settings->reset("MetaURLOverride"); + + // Resource URL + m_settings->registerSetting("ResourceURLOverride", ""); + + QUrl resourceUrl(m_settings->get("ResourceURLOverride").toString()); + + // get rid of invalid resource urls + if (!resourceUrl.isValid() || (resourceUrl.scheme() != "http" && resourceUrl.scheme() != "https")) + m_settings->reset("ResourceURLOverride"); } m_settings->registerSetting("CloseAfterLaunch", false); diff --git a/launcher/minecraft/AssetsUtils.cpp b/launcher/minecraft/AssetsUtils.cpp index 083924dc6..d723efc7a 100644 --- a/launcher/minecraft/AssetsUtils.cpp +++ b/launcher/minecraft/AssetsUtils.cpp @@ -298,7 +298,12 @@ QString AssetObject::getLocalPath() QUrl AssetObject::getUrl() { - return BuildConfig.RESOURCE_BASE + getRelPath(); + auto s = APPLICATION->settings(); + auto resourceURL = s->get("ResourceURLOverride").toString(); + if (resourceURL.isEmpty()) { + return BuildConfig.DEFAULT_RESOURCE_BASE + getRelPath(); + } + return resourceURL + getRelPath(); } QString AssetObject::getRelPath() diff --git a/launcher/minecraft/update/AssetUpdateTask.cpp b/launcher/minecraft/update/AssetUpdateTask.cpp index acdddc833..54cb46e53 100644 --- a/launcher/minecraft/update/AssetUpdateTask.cpp +++ b/launcher/minecraft/update/AssetUpdateTask.cpp @@ -71,7 +71,15 @@ void AssetUpdateTask::assetIndexFinished() auto job = index.getDownloadJob(); if (job) { - setStatus(tr("Getting the assets files from Mojang...")); + QString resourceURLRaw; + auto statusText = tr("Getting the assets files from Mojang..."); + resourceURLRaw = APPLICATION->settings()->get("ResourceURLOverride").toString(); + // FIXME: Need translation + if (!resourceURLRaw.isEmpty()) { + QUrl resourceURL = QUrl(resourceURLRaw); + statusText.replace("Mojang", resourceURL.host()); + } + setStatus(statusText); downloadJob = job; connect(downloadJob.get(), &NetJob::succeeded, this, &AssetUpdateTask::emitSucceeded); connect(downloadJob.get(), &NetJob::failed, this, &AssetUpdateTask::assetsFailed); diff --git a/launcher/ui/pages/global/APIPage.cpp b/launcher/ui/pages/global/APIPage.cpp index a030bf316..83f30f639 100644 --- a/launcher/ui/pages/global/APIPage.cpp +++ b/launcher/ui/pages/global/APIPage.cpp @@ -76,6 +76,7 @@ APIPage::APIPage(QWidget* parent) : QWidget(parent), ui(new Ui::APIPage) updateBaseURLPlaceholder(ui->pasteTypeComboBox->currentIndex()); // NOTE: this allows http://, but we replace that with https later anyway ui->metaURL->setValidator(new QRegularExpressionValidator(s_validUrlRegExp, ui->metaURL)); + ui->resourceURL->setValidator(new QRegularExpressionValidator(s_validUrlRegExp, ui->resourceURL)); ui->baseURLEntry->setValidator(new QRegularExpressionValidator(s_validUrlRegExp, ui->baseURLEntry)); ui->msaClientID->setValidator(new QRegularExpressionValidator(s_validMSAClientID, ui->msaClientID)); ui->flameKey->setValidator(new QRegularExpressionValidator(s_validFlameKey, ui->flameKey)); @@ -137,6 +138,8 @@ void APIPage::loadSettings() ui->msaClientID->setText(msaClientID); QString metaURL = s->get("MetaURLOverride").toString(); ui->metaURL->setText(metaURL); + QString resourceURL = s->get("ResourceURLOverride").toString(); + ui->resourceURL->setText(resourceURL); QString flameKey = s->get("FlameKeyOverride").toString(); ui->flameKey->setText(flameKey); QString modrinthToken = s->get("ModrinthToken").toString(); @@ -156,18 +159,31 @@ void APIPage::applySettings() QString msaClientID = ui->msaClientID->text(); s->set("MSAClientIDOverride", msaClientID); QUrl metaURL(ui->metaURL->text()); + QUrl resourceURL(ui->resourceURL->text()); // Add required trailing slash if (!metaURL.isEmpty() && !metaURL.path().endsWith('/')) { QString path = metaURL.path(); path.append('/'); metaURL.setPath(path); } + + if (!resourceURL.isEmpty() && !resourceURL.path().endsWith('/')) { + QString path = resourceURL.path(); + path.append('/'); + resourceURL.setPath(path); + } // Don't allow HTTP, since meta is basically RCE with all the jar files. if (!metaURL.isEmpty() && metaURL.scheme() == "http") { metaURL.setScheme("https"); } + // Also don't allow HTTP + if (!resourceURL.isEmpty() && resourceURL.scheme() == "http") { + resourceURL.setScheme("https"); + } + s->set("MetaURLOverride", metaURL.toString()); + s->set("ResourceURLOverride", resourceURL.toString()); QString flameKey = ui->flameKey->text(); s->set("FlameKeyOverride", flameKey); QString modrinthToken = ui->modrinthToken->text(); diff --git a/launcher/ui/pages/global/APIPage.ui b/launcher/ui/pages/global/APIPage.ui index c6a4593fc..cc36ff7b2 100644 --- a/launcher/ui/pages/global/APIPage.ui +++ b/launcher/ui/pages/global/APIPage.ui @@ -129,6 +129,38 @@
    + + + + Assets Server + + + + + + You can set this to another server if you have problem in downloading assets. + + + Qt::RichText + + + true + + + true + + + + + + + Use Default + + + + + + From 09ec3eb621e11ab0d6668fbecc33a463c001ea37 Mon Sep 17 00:00:00 2001 From: clague <93119153+clague@users.noreply.github.com> Date: Mon, 16 Jun 2025 21:56:23 +0800 Subject: [PATCH 323/695] resolve problems Signed-off-by: clague <93119153+clague@users.noreply.github.com> --- launcher/Application.cpp | 6 +++--- launcher/minecraft/AssetsUtils.cpp | 6 +----- launcher/minecraft/update/AssetUpdateTask.cpp | 14 ++++++-------- launcher/ui/pages/global/APIPage.cpp | 4 ++-- 4 files changed, 12 insertions(+), 18 deletions(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index a4c96d645..d7182c48d 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -866,13 +866,13 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) m_settings->reset("MetaURLOverride"); // Resource URL - m_settings->registerSetting("ResourceURLOverride", ""); + m_settings->registerSetting("ResourceURL", BuildConfig.DEFAULT_RESOURCE_BASE); - QUrl resourceUrl(m_settings->get("ResourceURLOverride").toString()); + QUrl resourceUrl(m_settings->get("ResourceURL").toString()); // get rid of invalid resource urls if (!resourceUrl.isValid() || (resourceUrl.scheme() != "http" && resourceUrl.scheme() != "https")) - m_settings->reset("ResourceURLOverride"); + m_settings->reset("ResourceURL"); } m_settings->registerSetting("CloseAfterLaunch", false); diff --git a/launcher/minecraft/AssetsUtils.cpp b/launcher/minecraft/AssetsUtils.cpp index d723efc7a..410d1e689 100644 --- a/launcher/minecraft/AssetsUtils.cpp +++ b/launcher/minecraft/AssetsUtils.cpp @@ -298,11 +298,7 @@ QString AssetObject::getLocalPath() QUrl AssetObject::getUrl() { - auto s = APPLICATION->settings(); - auto resourceURL = s->get("ResourceURLOverride").toString(); - if (resourceURL.isEmpty()) { - return BuildConfig.DEFAULT_RESOURCE_BASE + getRelPath(); - } + auto resourceURL = APPLICATION->settings()->get("ResourceURL").toString(); return resourceURL + getRelPath(); } diff --git a/launcher/minecraft/update/AssetUpdateTask.cpp b/launcher/minecraft/update/AssetUpdateTask.cpp index 54cb46e53..f4a4022e9 100644 --- a/launcher/minecraft/update/AssetUpdateTask.cpp +++ b/launcher/minecraft/update/AssetUpdateTask.cpp @@ -1,5 +1,6 @@ #include "AssetUpdateTask.h" +#include "BuildConfig.h" #include "launch/LaunchStep.h" #include "minecraft/AssetsUtils.h" #include "minecraft/MinecraftInstance.h" @@ -71,15 +72,12 @@ void AssetUpdateTask::assetIndexFinished() auto job = index.getDownloadJob(); if (job) { - QString resourceURLRaw; - auto statusText = tr("Getting the assets files from Mojang..."); - resourceURLRaw = APPLICATION->settings()->get("ResourceURLOverride").toString(); - // FIXME: Need translation - if (!resourceURLRaw.isEmpty()) { - QUrl resourceURL = QUrl(resourceURLRaw); - statusText.replace("Mojang", resourceURL.host()); + QString resourceURL = APPLICATION->settings()->get("ResourceURL").toString(); + QString source = tr("Mojang"); + if (resourceURL != BuildConfig.DEFAULT_RESOURCE_BASE) { + source = QUrl(resourceURL).host(); } - setStatus(statusText); + setStatus(tr("Getting the assets files from %1...").arg(source)); downloadJob = job; connect(downloadJob.get(), &NetJob::succeeded, this, &AssetUpdateTask::emitSucceeded); connect(downloadJob.get(), &NetJob::failed, this, &AssetUpdateTask::assetsFailed); diff --git a/launcher/ui/pages/global/APIPage.cpp b/launcher/ui/pages/global/APIPage.cpp index 83f30f639..3fedbff72 100644 --- a/launcher/ui/pages/global/APIPage.cpp +++ b/launcher/ui/pages/global/APIPage.cpp @@ -138,7 +138,7 @@ void APIPage::loadSettings() ui->msaClientID->setText(msaClientID); QString metaURL = s->get("MetaURLOverride").toString(); ui->metaURL->setText(metaURL); - QString resourceURL = s->get("ResourceURLOverride").toString(); + QString resourceURL = s->get("ResourceURL").toString(); ui->resourceURL->setText(resourceURL); QString flameKey = s->get("FlameKeyOverride").toString(); ui->flameKey->setText(flameKey); @@ -183,7 +183,7 @@ void APIPage::applySettings() } s->set("MetaURLOverride", metaURL.toString()); - s->set("ResourceURLOverride", resourceURL.toString()); + s->set("ResourceURL", resourceURL.toString()); QString flameKey = ui->flameKey->text(); s->set("FlameKeyOverride", flameKey); QString modrinthToken = ui->modrinthToken->text(); From e9899e3af38d96514bad3eff3e777c9e490bd8f1 Mon Sep 17 00:00:00 2001 From: renner Date: Sun, 15 Jun 2025 13:35:17 +0200 Subject: [PATCH 324/695] chore: refresh metainfo.xml.in adds a donation + FAQ URL and developer id property Signed-off-by: renner --- ...rismlauncher.PrismLauncher.metainfo.xml.in | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/program_info/org.prismlauncher.PrismLauncher.metainfo.xml.in b/program_info/org.prismlauncher.PrismLauncher.metainfo.xml.in index 95bb86a27..747e1bc24 100644 --- a/program_info/org.prismlauncher.PrismLauncher.metainfo.xml.in +++ b/program_info/org.prismlauncher.PrismLauncher.metainfo.xml.in @@ -1,19 +1,13 @@ @Launcher_AppID@ - @Launcher_AppID@.desktop Prism Launcher - Prism Launcher Contributors Custom Minecraft Launcher to easily manage multiple Minecraft installations at once + + Prism Launcher Contributors + CC0-1.0 GPL-3.0-only - https://prismlauncher.org/ - https://prismlauncher.org/wiki/ - https://github.com/PrismLauncher/PrismLauncher/issues - https://prismlauncher.org/discord - https://github.com/PrismLauncher/PrismLauncher - https://github.com/PrismLauncher/PrismLauncher/blob/develop/CONTRIBUTING.md - https://hosted.weblate.org/projects/prismlauncher/launcher

    Prism Launcher is a custom launcher for Minecraft that focuses on predictability, long term stability and simplicity.

    Features:

    @@ -67,8 +61,18 @@ + https://prismlauncher.org/ + https://github.com/PrismLauncher/PrismLauncher/issues + https://prismlauncher.org/wiki/overview/faq/ + https://prismlauncher.org/wiki/ + https://opencollective.com/prismlauncher + https://hosted.weblate.org/projects/prismlauncher/launcher + https://prismlauncher.org/discord + https://github.com/PrismLauncher/PrismLauncher + https://github.com/PrismLauncher/PrismLauncher/blob/develop/CONTRIBUTING.md moderate intense + @Launcher_AppID@.desktop
    From 21de7a2d97b14064fd2220b45e14a0a353f46f49 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Wed, 11 Jun 2025 12:03:59 +0300 Subject: [PATCH 325/695] fix: crash when component version can't be loaded from atlauncher file Signed-off-by: Trial97 --- .../atlauncher/ATLPackInstallTask.cpp | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp index 4f2cca80f..ba3a25aa3 100644 --- a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp +++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp @@ -434,14 +434,15 @@ bool PackInstallTask::createLibrariesComponent(QString instanceRoot, std::shared QList exempt; for (const auto& componentUid : componentsToInstall.keys()) { auto componentVersion = componentsToInstall.value(componentUid); - - for (const auto& library : componentVersion->data()->libraries) { - GradleSpecifier lib(library->rawName()); - exempt.append(lib); + if (componentVersion->data()) { + for (const auto& library : componentVersion->data()->libraries) { + GradleSpecifier lib(library->rawName()); + exempt.append(lib); + } } } - { + if (minecraftVersion->data()) { for (const auto& library : minecraftVersion->data()->libraries) { GradleSpecifier lib(library->rawName()); exempt.append(lib); @@ -582,10 +583,12 @@ bool PackInstallTask::createPackComponent(QString instanceRoot, std::shared_ptr< for (const auto& componentUid : componentsToInstall.keys()) { auto componentVersion = componentsToInstall.value(componentUid); - if (componentVersion->data()->mainClass != QString("")) { - mainClasses.append(componentVersion->data()->mainClass); + if (componentVersion->data()) { + if (componentVersion->data()->mainClass != QString("")) { + mainClasses.append(componentVersion->data()->mainClass); + } + tweakers.append(componentVersion->data()->addTweakers); } - tweakers.append(componentVersion->data()->addTweakers); } auto f = std::make_shared(); From 8711913ac3d65845ab13f668ca682ad901651b0d Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Tue, 17 Jun 2025 00:43:03 +0100 Subject: [PATCH 326/695] Improve the message when component metadata fails to download Signed-off-by: TheKodeToad --- launcher/minecraft/ComponentUpdateTask.cpp | 5 +++-- launcher/minecraft/ComponentUpdateTask_p.h | 1 - 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/launcher/minecraft/ComponentUpdateTask.cpp b/launcher/minecraft/ComponentUpdateTask.cpp index c77ed248d..56db20205 100644 --- a/launcher/minecraft/ComponentUpdateTask.cpp +++ b/launcher/minecraft/ComponentUpdateTask.cpp @@ -748,7 +748,6 @@ void ComponentUpdateTask::remoteLoadFailed(size_t taskIndex, const QString& msg) d->remoteLoadSuccessful = false; taskSlot.succeeded = false; taskSlot.finished = true; - taskSlot.error = msg; d->remoteTasksInProgress--; checkIfAllFinished(); } @@ -769,7 +768,9 @@ void ComponentUpdateTask::checkIfAllFinished() QStringList allErrorsList; for (auto& item : d->remoteLoadStatusList) { if (!item.succeeded) { - allErrorsList.append(item.error); + const ComponentPtr component = d->m_profile->getComponent(item.PackProfileIndex); + allErrorsList.append(tr("Could not download metadata for %1 %2. Please change the version or try again later.") + .arg(component->getName(), component->m_version)); } } auto allErrors = allErrorsList.join("\n"); diff --git a/launcher/minecraft/ComponentUpdateTask_p.h b/launcher/minecraft/ComponentUpdateTask_p.h index 2fc0b6d9a..8ffb9c71e 100644 --- a/launcher/minecraft/ComponentUpdateTask_p.h +++ b/launcher/minecraft/ComponentUpdateTask_p.h @@ -15,7 +15,6 @@ struct RemoteLoadStatus { size_t PackProfileIndex = 0; bool finished = false; bool succeeded = false; - QString error; Task::Ptr task; }; From 7eb07451cea7facee8698d65e6cb56239fd5e636 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Sat, 21 Jun 2025 17:35:51 +0300 Subject: [PATCH 327/695] fix: nightly link Signed-off-by: Trial97 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 361864dfe..868fa1fe1 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ Please understand that these builds are not intended for most users. There may b There are development builds available through: - [GitHub Actions](https://github.com/PrismLauncher/PrismLauncher/actions) (includes builds from pull requests opened by contribuitors) -- [nightly.link](https://nightly.link/PrismLauncher/PrismLauncher/workflows/trigger_builds/develop) (this will always point only to the latest version of develop) +- [nightly.link](https://nightly.link/PrismLauncher/PrismLauncher/workflows/build/develop) (this will always point only to the latest version of develop) These have debug information in the binaries, so their file sizes are relatively larger. From f29c5f55819cb9801eda28313a80773c4d0e6df8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 22 Jun 2025 00:30:42 +0000 Subject: [PATCH 328/695] chore(nix): update lockfile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'nixpkgs': 'github:NixOS/nixpkgs/ee930f9755f58096ac6e8ca94a1887e0534e2d81?narHash=sha256-Kh9K4taXbVuaLC0IL%2B9HcfvxsSUx8dPB5s5weJcc9pc%3D' (2025-06-13) → 'github:NixOS/nixpkgs/08f22084e6085d19bcfb4be30d1ca76ecb96fe54?narHash=sha256-XE/lFNhz5lsriMm/yjXkvSZz5DfvKJLUjsS6pP8EC50%3D' (2025-06-19) --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 18ecd28e2..9e592824c 100644 --- a/flake.lock +++ b/flake.lock @@ -18,11 +18,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1749794982, - "narHash": "sha256-Kh9K4taXbVuaLC0IL+9HcfvxsSUx8dPB5s5weJcc9pc=", + "lastModified": 1750365781, + "narHash": "sha256-XE/lFNhz5lsriMm/yjXkvSZz5DfvKJLUjsS6pP8EC50=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "ee930f9755f58096ac6e8ca94a1887e0534e2d81", + "rev": "08f22084e6085d19bcfb4be30d1ca76ecb96fe54", "type": "github" }, "original": { From ec63f54f48d498eae852242fe285f6556faf868c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 24 Jun 2025 17:38:33 +0000 Subject: [PATCH 329/695] chore(deps): update cachix/install-nix-action digest to f0fe604 --- .github/workflows/update-flake.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/update-flake.yml b/.github/workflows/update-flake.yml index 7480ba46e..3c66a9ef3 100644 --- a/.github/workflows/update-flake.yml +++ b/.github/workflows/update-flake.yml @@ -17,7 +17,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: cachix/install-nix-action@17fe5fb4a23ad6cbbe47d6b3f359611ad276644c # v31 + - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31 - uses: DeterminateSystems/update-flake-lock@v25 with: From cdf8ad2c944c1a1196465e0a79a6bc8a43a5a7d1 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Wed, 25 Jun 2025 13:13:42 +0300 Subject: [PATCH 330/695] fix: escape quetes in shorcut creation arguments Signed-off-by: Trial97 --- launcher/FileSystem.cpp | 48 +++++++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp index 5136e7954..308f8620e 100644 --- a/launcher/FileSystem.cpp +++ b/launcher/FileSystem.cpp @@ -897,6 +897,29 @@ QString getApplicationsDir() return QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation); } +QString quoteArgs(const QStringList& args, const QString& wrap, const QString& escapeChar, bool wrapOnlyIfNeeded = false) +{ + QString result; + + auto size = args.size(); + for (int i = 0; i < size; ++i) { + QString arg = args[i]; + arg.replace(wrap, escapeChar); + + bool needsWrapping = !wrapOnlyIfNeeded || arg.contains(' ') || arg.contains('\t') || arg.contains(wrap); + + if (needsWrapping) + result += wrap + arg + wrap; + else + result += arg; + + if (i < size - 1) + result += ' '; + } + + return result; +} + // Cross-platform Shortcut creation QString createShortcut(QString destination, QString target, QStringList args, QString name, QString icon) { @@ -940,9 +963,7 @@ QString createShortcut(QString destination, QString target, QStringList args, QS f.open(QIODevice::WriteOnly | QIODevice::Text); QTextStream stream(&f); - QString argstring; - if (!args.empty()) - argstring = " \"" + args.join("\" \"") + "\""; + auto argstring = quoteArgs(args, "\"", "\\\""); stream << "#!/bin/bash" << "\n"; stream << "\"" << target << "\" " << argstring << "\n"; @@ -984,14 +1005,12 @@ QString createShortcut(QString destination, QString target, QStringList args, QS f.open(QIODevice::WriteOnly | QIODevice::Text); QTextStream stream(&f); - QString argstring; - if (!args.empty()) - argstring = " '" + args.join("' '") + "'"; + auto argstring = quoteArgs(args, "'", "'\\''"); stream << "[Desktop Entry]" << "\n"; stream << "Type=Application" << "\n"; stream << "Categories=Game;ActionGame;AdventureGame;Simulation" << "\n"; - stream << "Exec=\"" << target.toLocal8Bit() << "\"" << argstring.toLocal8Bit() << "\n"; + stream << "Exec=\"" << target.toLocal8Bit() << "\" " << argstring.toLocal8Bit() << "\n"; stream << "Name=" << name.toLocal8Bit() << "\n"; if (!icon.isEmpty()) { stream << "Icon=" << icon.toLocal8Bit() << "\n"; @@ -1030,20 +1049,7 @@ QString createShortcut(QString destination, QString target, QStringList args, QS return QString(); } - QString argStr; - int argCount = args.count(); - for (int i = 0; i < argCount; i++) { - if (args[i].contains(' ')) { - argStr.append('"').append(args[i]).append('"'); - } else { - argStr.append(args[i]); - } - - if (i < argCount - 1) { - argStr.append(" "); - } - } - + auto argStr = quoteArgs(args, "\"", "\\\"", true); if (argStr.length() >= MAX_PATH) { qWarning() << "Arguments string is too long!"; return QString(); From bcdbbab7c0ea52404d9b745ca26338aeea2c75f9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 26 Jun 2025 08:37:29 +0000 Subject: [PATCH 331/695] chore(deps): update korthout/backport-action action to v3.2.1 --- .github/workflows/backport.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index c46f8e192..d8f9688d7 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -25,7 +25,7 @@ jobs: with: ref: ${{ github.event.pull_request.head.sha }} - name: Create backport PRs - uses: korthout/backport-action@v3.2.0 + uses: korthout/backport-action@v3.2.1 with: # Config README: https://github.com/korthout/backport-action#backport-action pull_description: |- From 24d70c773e278f155639b66a1808fe96b221e66d Mon Sep 17 00:00:00 2001 From: Ismail Date: Fri, 27 Jun 2025 00:48:31 +0100 Subject: [PATCH 332/695] fix typo in APIPage.ui Signed-off-by: Ismail --- launcher/ui/pages/global/APIPage.ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/pages/global/APIPage.ui b/launcher/ui/pages/global/APIPage.ui index c6a4593fc..a822f2b99 100644 --- a/launcher/ui/pages/global/APIPage.ui +++ b/launcher/ui/pages/global/APIPage.ui @@ -170,7 +170,7 @@ - &Microsoft Authentation + &Microsoft Authentication Qt::RichText From 53dcc1576689feebcf12c3004449da66eb84cfac Mon Sep 17 00:00:00 2001 From: Trial97 Date: Sat, 28 Jun 2025 23:34:13 +0300 Subject: [PATCH 333/695] fix: icon import with dot in name Signed-off-by: Trial97 --- launcher/InstanceImportTask.cpp | 10 +++++----- launcher/icons/IconList.cpp | 7 ++++--- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp index c489f6112..77298e2ce 100644 --- a/launcher/InstanceImportTask.cpp +++ b/launcher/InstanceImportTask.cpp @@ -263,9 +263,9 @@ void InstanceImportTask::extractFinished() } } -bool installIcon(QString root, QString instIcon) +bool installIcon(QString root, QString instIconKey) { - auto importIconPath = IconUtils::findBestIconIn(root, instIcon); + auto importIconPath = IconUtils::findBestIconIn(root, instIconKey); if (importIconPath.isNull() || !QFile::exists(importIconPath)) importIconPath = IconUtils::findBestIconIn(root, "icon.png"); if (importIconPath.isNull() || !QFile::exists(importIconPath)) @@ -273,10 +273,10 @@ bool installIcon(QString root, QString instIcon) if (!importIconPath.isNull() && QFile::exists(importIconPath)) { // import icon auto iconList = APPLICATION->icons(); - if (iconList->iconFileExists(instIcon)) { - iconList->deleteIcon(instIcon); + if (iconList->iconFileExists(instIconKey)) { + iconList->deleteIcon(instIconKey); } - iconList->installIcon(importIconPath, instIcon); + iconList->installIcon(importIconPath, instIconKey + ".png"); return true; } return false; diff --git a/launcher/icons/IconList.cpp b/launcher/icons/IconList.cpp index 8a2a482e1..fb80f89da 100644 --- a/launcher/icons/IconList.cpp +++ b/launcher/icons/IconList.cpp @@ -55,7 +55,7 @@ IconList::IconList(const QStringList& builtinPaths, const QString& path, QObject QDir instanceIcons(builtinPath); auto fileInfoList = instanceIcons.entryInfoList(QDir::Files, QDir::Name); for (const auto& fileInfo : fileInfoList) { - builtinNames.insert(fileInfo.baseName()); + builtinNames.insert(fileInfo.completeBaseName()); } } for (const auto& builtinName : builtinNames) { @@ -127,10 +127,11 @@ QStringList IconList::getIconFilePaths() const QString formatName(const QDir& iconsDir, const QFileInfo& iconFile) { if (iconFile.dir() == iconsDir) - return iconFile.baseName(); + return iconFile.completeBaseName(); constexpr auto delimiter = " » "; - QString relativePathWithoutExtension = iconsDir.relativeFilePath(iconFile.dir().path()) + QDir::separator() + iconFile.baseName(); + QString relativePathWithoutExtension = + iconsDir.relativeFilePath(iconFile.dir().path()) + QDir::separator() + iconFile.completeBaseName(); return relativePathWithoutExtension.replace(QDir::separator(), delimiter); } From be62a7d0a25b398debe3f84796e5a288463e9b6c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 29 Jun 2025 00:31:19 +0000 Subject: [PATCH 334/695] chore(nix): update lockfile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'nixpkgs': 'github:NixOS/nixpkgs/08f22084e6085d19bcfb4be30d1ca76ecb96fe54?narHash=sha256-XE/lFNhz5lsriMm/yjXkvSZz5DfvKJLUjsS6pP8EC50%3D' (2025-06-19) → 'github:NixOS/nixpkgs/30e2e2857ba47844aa71991daa6ed1fc678bcbb7?narHash=sha256-krGXKxvkBhnrSC/kGBmg5MyupUUT5R6IBCLEzx9jhMM%3D' (2025-06-27) --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 9e592824c..fdcbd87ed 100644 --- a/flake.lock +++ b/flake.lock @@ -18,11 +18,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1750365781, - "narHash": "sha256-XE/lFNhz5lsriMm/yjXkvSZz5DfvKJLUjsS6pP8EC50=", + "lastModified": 1751011381, + "narHash": "sha256-krGXKxvkBhnrSC/kGBmg5MyupUUT5R6IBCLEzx9jhMM=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "08f22084e6085d19bcfb4be30d1ca76ecb96fe54", + "rev": "30e2e2857ba47844aa71991daa6ed1fc678bcbb7", "type": "github" }, "original": { From dac73b394a99bae86dea2f55eb66224bd46fd307 Mon Sep 17 00:00:00 2001 From: seth Date: Mon, 30 Jun 2025 14:54:06 -0400 Subject: [PATCH 335/695] build: emit pdbs on mingw Signed-off-by: seth --- CMakeLists.txt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index e3d60a102..515ea5f1f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -79,6 +79,15 @@ else() if(WIN32) set(CMAKE_EXE_LINKER_FLAGS "-Wl,--stack,8388608 ${CMAKE_EXE_LINKER_FLAGS}") + # Emit PDBs for WinDbg, etc. + if(CMAKE_BUILD_TYPE STREQUAL "Debug" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo") + set(CMAKE_EXE_LINKER_FLAGS "-Wl,--pdb= ${CMAKE_EXE_LINKER_FLAGS}") + + foreach(lang C CXX) + set("CMAKE_${lang}_FLAGS" "-gcodeview ${CMAKE_${lang}_FLAGS}") + endforeach() + endif() + # -ffunction-sections and -fdata-sections help reduce binary size # -mguard=cf enables Control Flow Guard # TODO: Look into -gc-sections to further reduce binary size From 71be6eb7d83cefe652f9bf230a2085fd9768e25f Mon Sep 17 00:00:00 2001 From: seth Date: Mon, 30 Jun 2025 14:58:23 -0400 Subject: [PATCH 336/695] build: deploy pdbs with windows builds Signed-off-by: seth --- CMakeLists.txt | 4 ++++ launcher/CMakeLists.txt | 15 +++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 515ea5f1f..6202f63bd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -85,6 +85,10 @@ else() foreach(lang C CXX) set("CMAKE_${lang}_FLAGS" "-gcodeview ${CMAKE_${lang}_FLAGS}") + + # Force-enabling this to use generator expressions like TARGET_PDB_FILE + # (and because we can actually emit PDBs) + set("CMAKE_${lang}_LINKER_SUPPORTS_PDB" ON) endforeach() endif() diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 23b2fbfad..5246de19d 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -1391,6 +1391,11 @@ install(TARGETS ${Launcher_Name} FRAMEWORK DESTINATION ${FRAMEWORK_DEST_DIR} COMPONENT Runtime ) +# Deploy PDBs +if(WIN32 AND (CMAKE_BUILD_TYPE STREQUAL "Debug" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo")) + install(FILES $ DESTINATION ${BINARY_DEST_DIR}) +endif() + if(Launcher_BUILD_UPDATER) # Updater add_library(prism_updater_logic STATIC ${PRISMUPDATER_SOURCES} ${TASKS_SOURCES} ${PRISMUPDATER_UI}) @@ -1424,6 +1429,11 @@ if(Launcher_BUILD_UPDATER) RUNTIME DESTINATION ${BINARY_DEST_DIR} COMPONENT Runtime FRAMEWORK DESTINATION ${FRAMEWORK_DEST_DIR} COMPONENT Runtime ) + + # Deploy PDBs + if(WIN32 AND (CMAKE_BUILD_TYPE STREQUAL "Debug" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo")) + install(FILES $ DESTINATION ${BINARY_DEST_DIR}) + endif() endif() if(WIN32 OR (DEFINED Launcher_BUILD_FILELINKER AND Launcher_BUILD_FILELINKER)) @@ -1464,6 +1474,11 @@ if(WIN32 OR (DEFINED Launcher_BUILD_FILELINKER AND Launcher_BUILD_FILELINKER)) RUNTIME DESTINATION ${BINARY_DEST_DIR} COMPONENT Runtime FRAMEWORK DESTINATION ${FRAMEWORK_DEST_DIR} COMPONENT Runtime ) + + # Deploy PDBs + if(WIN32 AND (CMAKE_BUILD_TYPE STREQUAL "Debug" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo")) + install(FILES $ DESTINATION ${BINARY_DEST_DIR}) + endif() endif() if (UNIX AND APPLE AND Launcher_ENABLE_UPDATER) From 56fa6586abe6b90dbecf603efafa79b1a9b0647b Mon Sep 17 00:00:00 2001 From: Seth Flynn Date: Thu, 3 Jul 2025 01:15:29 -0400 Subject: [PATCH 337/695] fix(ui/CustomCommands): memory corruption in labelPostExitCmd Making a QLabel a buddy of itself causes a double free and crashing on deconstruction in some cases (like MSVC) Signed-off-by: Seth Flynn --- launcher/ui/widgets/CustomCommands.ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/widgets/CustomCommands.ui b/launcher/ui/widgets/CustomCommands.ui index c1b4558a8..6c1366c06 100644 --- a/launcher/ui/widgets/CustomCommands.ui +++ b/launcher/ui/widgets/CustomCommands.ui @@ -86,7 +86,7 @@ P&ost-exit Command - labelPostExitCmd + postExitCmdTextBox From 364ebbcbe63b13e21f1640deaf05b0c8dcd21eee Mon Sep 17 00:00:00 2001 From: Seth Flynn Date: Thu, 3 Jul 2025 01:25:49 -0400 Subject: [PATCH 338/695] ci: run on changes to .ui files Signed-off-by: Seth Flynn --- .github/workflows/build.yml | 1 + .github/workflows/codeql.yml | 1 + .github/workflows/flatpak.yml | 1 + .github/workflows/nix.yml | 1 + 4 files changed, 4 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 98be31627..4954328a9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,6 +10,7 @@ on: - "**.cpp" - "**.h" - "**.java" + - "**.ui" # Directories - "buildconfig/" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 8df64878b..f14bacc02 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -10,6 +10,7 @@ on: - "**.cpp" - "**.h" - "**.java" + - "**.ui" # Directories - "buildconfig/" diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml index 26a44d679..dcfd2ea14 100644 --- a/.github/workflows/flatpak.yml +++ b/.github/workflows/flatpak.yml @@ -13,6 +13,7 @@ on: - "**.cpp" - "**.h" - "**.java" + - "**.ui" # Build files - "flatpak/" diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index 7b9412c87..702f8df28 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -12,6 +12,7 @@ on: - "**.cpp" - "**.h" - "**.java" + - "**.ui" # Build files - "**.nix" From 59ed25fad1220c43ed56fa3ed06be42a9a21fb04 Mon Sep 17 00:00:00 2001 From: Seth Flynn Date: Thu, 3 Jul 2025 04:20:52 -0400 Subject: [PATCH 339/695] chore: update to qt 6.9 https://doc.qt.io/qt-6/whatsnew69.html Signed-off-by: Seth Flynn --- .github/actions/setup-dependencies/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/setup-dependencies/action.yml b/.github/actions/setup-dependencies/action.yml index 47a9819ea..e61509c44 100644 --- a/.github/actions/setup-dependencies/action.yml +++ b/.github/actions/setup-dependencies/action.yml @@ -21,7 +21,7 @@ inputs: qt-version: description: Version of Qt to use required: true - default: 6.8.1 + default: 6.9.1 outputs: build-type: From 1deda8cdf4c84a8120d0577778e12200fd085b78 Mon Sep 17 00:00:00 2001 From: Seth Flynn Date: Thu, 3 Jul 2025 04:46:18 -0400 Subject: [PATCH 340/695] build(cmake): set minimum version to 3.22 This requirement was introduced in Qt 6.9 https://doc.qt.io/qt-6/whatsnew69.html#build-system-changes Signed-off-by: Seth Flynn --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6202f63bd..a4d6df243 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.15) # minimum version required by QuaZip +cmake_minimum_required(VERSION 3.22) # minimum version required by Qt project(Launcher) From e12c4d0abcb2cc1d135bc3543fb9759ccf28dc92 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 3 Jul 2025 17:40:41 +0000 Subject: [PATCH 341/695] chore(deps): update determinatesystems/nix-installer-action action to v18 --- .github/workflows/nix.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index 702f8df28..2a9e2db6a 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -108,7 +108,7 @@ jobs: ref: ${{ steps.merge-commit.outputs.merge-commit-sha || github.sha }} - name: Install Nix - uses: DeterminateSystems/nix-installer-action@v17 + uses: DeterminateSystems/nix-installer-action@v18 with: determinate: ${{ env.USE_DETERMINATE }} From 907f661c576d10177672c87409b886215b399887 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 3 Jul 2025 17:40:45 +0000 Subject: [PATCH 342/695] chore(deps): update determinatesystems/update-flake-lock action to v26 --- .github/workflows/update-flake.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/update-flake.yml b/.github/workflows/update-flake.yml index 3c66a9ef3..123028ff2 100644 --- a/.github/workflows/update-flake.yml +++ b/.github/workflows/update-flake.yml @@ -19,7 +19,7 @@ jobs: - uses: actions/checkout@v4 - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31 - - uses: DeterminateSystems/update-flake-lock@v25 + - uses: DeterminateSystems/update-flake-lock@v26 with: commit-msg: "chore(nix): update lockfile" pr-title: "chore(nix): update lockfile" From fde66a11ce102aa2411744c12dec4e7798591af0 Mon Sep 17 00:00:00 2001 From: Seth Flynn Date: Fri, 4 Jul 2025 05:13:01 -0400 Subject: [PATCH 343/695] build(cmake): apply workaround for ninja in release mode on msvc Signed-off-by: Seth Flynn --- launcher/CMakeLists.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 5246de19d..6204acabd 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -1458,6 +1458,12 @@ if(WIN32 OR (DEFINED Launcher_BUILD_FILELINKER AND Launcher_BUILD_FILELINKER)) add_executable("${Launcher_Name}_filelink" WIN32 filelink/filelink_main.cpp) target_sources("${Launcher_Name}_filelink" PRIVATE filelink/filelink.exe.manifest) + # HACK: Fix manifest issues with Ninja in release mode (and only release mode) and MSVC + # I have no idea why this works or why it's needed. UPDATE THIS IF YOU EDIT THE MANIFEST!!! -@getchoo + # Thank you 2018 CMake mailing list thread https://cmake.cmake.narkive.com/LnotZXus/conflicting-msvc-manifests + if(MSVC) + set_property(TARGET "${Launcher_Name}_filelink" PROPERTY LINK_FLAGS "/MANIFESTUAC:level='requireAdministrator'") + endif() target_link_libraries("${Launcher_Name}_filelink" filelink_logic) From 3257abaa34f912d5828f7b5554a91b3d16b1601c Mon Sep 17 00:00:00 2001 From: Seth Flynn Date: Fri, 4 Jul 2025 05:16:35 -0400 Subject: [PATCH 344/695] build(cmake): use ninja for msvc release and cross builds Signed-off-by: Seth Flynn --- cmake/windowsMSVCPreset.json | 39 +++++++++--------------------------- 1 file changed, 9 insertions(+), 30 deletions(-) diff --git a/cmake/windowsMSVCPreset.json b/cmake/windowsMSVCPreset.json index 603a0a9ff..278f09b94 100644 --- a/cmake/windowsMSVCPreset.json +++ b/cmake/windowsMSVCPreset.json @@ -12,15 +12,15 @@ "type": "equals", "lhs": "${hostSystemName}", "rhs": "Windows" - } + }, + "generator": "Ninja" }, { "name": "windows_msvc_arm64_cross_base", "hidden": true, "inherits": [ "windows_msvc_base" - ], - "architecture": "arm64" + ] }, { "name": "windows_msvc_debug", @@ -28,8 +28,7 @@ "base_debug", "windows_msvc_base" ], - "displayName": "Windows MSVC (Debug)", - "generator": "Ninja" + "displayName": "Windows MSVC (Debug)" }, { "name": "windows_msvc_release", @@ -98,11 +97,7 @@ ], "displayName": "Windows MSVC (Release)", "configurePreset": "windows_msvc_release", - "configuration": "Release", - "nativeToolOptions": [ - "/p:UseMultiToolTask=true", - "/p:EnforceProcessCountAcrossBuilds=true" - ] + "configuration": "Release" }, { "name": "windows_msvc_arm64_cross_debug", @@ -111,11 +106,7 @@ ], "displayName": "Windows MSVC (ARM64 cross, Debug)", "configurePreset": "windows_msvc_arm64_cross_debug", - "configuration": "Debug", - "nativeToolOptions": [ - "/p:UseMultiToolTask=true", - "/p:EnforceProcessCountAcrossBuilds=true" - ] + "configuration": "Debug" }, { "name": "windows_msvc_arm64_cross_release", @@ -124,11 +115,7 @@ ], "displayName": "Windows MSVC (ARM64 cross, Release)", "configurePreset": "windows_msvc_arm64_cross_release", - "configuration": "Release", - "nativeToolOptions": [ - "/p:UseMultiToolTask=true", - "/p:EnforceProcessCountAcrossBuilds=true" - ] + "configuration": "Release" }, { "name": "windows_msvc_ci", @@ -137,11 +124,7 @@ ], "displayName": "Windows MSVC (CI)", "configurePreset": "windows_msvc_ci", - "configuration": "Release", - "nativeToolOptions": [ - "/p:UseMultiToolTask=true", - "/p:EnforceProcessCountAcrossBuilds=true" - ] + "configuration": "Release" }, { "name": "windows_msvc_arm64_cross_ci", @@ -150,11 +133,7 @@ ], "displayName": "Windows MSVC (ARM64 cross, CI)", "configurePreset": "windows_msvc_arm64_cross_ci", - "configuration": "Release", - "nativeToolOptions": [ - "/p:UseMultiToolTask=true", - "/p:EnforceProcessCountAcrossBuilds=true" - ] + "configuration": "Release" } ], "testPresets": [ From ba6f49b891a85226f57d9a0488b5eee4b4966897 Mon Sep 17 00:00:00 2001 From: Seth Flynn Date: Fri, 4 Jul 2025 05:27:41 -0400 Subject: [PATCH 345/695] ci: glob directory matches This makes sure *all* changes to directories trigger the given workflows Signed-off-by: Seth Flynn --- .github/workflows/build.yml | 34 ++++++++++++++++++---------------- .github/workflows/codeql.yml | 34 ++++++++++++++++++---------------- .github/workflows/flatpak.yml | 30 ++++++++++++++++-------------- .github/workflows/nix.yml | 30 ++++++++++++++++-------------- 4 files changed, 68 insertions(+), 60 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4954328a9..15de6f70f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,12 +13,12 @@ on: - "**.ui" # Directories - - "buildconfig/" - - "cmake/" - - "launcher/" - - "libraries/" - - "program_info/" - - "tests/" + - "buildconfig/**" + - "cmake/**" + - "launcher/**" + - "libraries/**" + - "program_info/**" + - "tests/**" # Files - "CMakeLists.txt" @@ -26,21 +26,23 @@ on: # Workflows - ".github/workflows/build.yml" - - ".github/actions/package/" - - ".github/actions/setup-dependencies/" + - ".github/actions/package/**" + - ".github/actions/setup-dependencies/**" pull_request: paths: # File types - "**.cpp" - "**.h" + - "**.java" + - "**.ui" # Directories - - "buildconfig/" - - "cmake/" - - "launcher/" - - "libraries/" - - "program_info/" - - "tests/" + - "buildconfig/**" + - "cmake/**" + - "launcher/**" + - "libraries/**" + - "program_info/**" + - "tests/**" # Files - "CMakeLists.txt" @@ -48,8 +50,8 @@ on: # Workflows - ".github/workflows/build.yml" - - ".github/actions/package/" - - ".github/actions/setup-dependencies/" + - ".github/actions/package/**" + - ".github/actions/setup-dependencies/**" workflow_call: inputs: build-type: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index f14bacc02..964e322b1 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -13,43 +13,45 @@ on: - "**.ui" # Directories - - "buildconfig/" - - "cmake/" - - "launcher/" - - "libraries/" - - "program_info/" - - "tests/" + - "buildconfig/**" + - "cmake/**" + - "launcher/**" + - "libraries/**" + - "program_info/**" + - "tests/**" # Files - "CMakeLists.txt" - "COPYING.md" # Workflows - - ".github/codeql" + - ".github/codeql/**" - ".github/workflows/codeql.yml" - - ".github/actions/setup-dependencies/" + - ".github/actions/setup-dependencies/**" pull_request: paths: # File types - "**.cpp" - "**.h" + - "**.java" + - "**.ui" # Directories - - "buildconfig/" - - "cmake/" - - "launcher/" - - "libraries/" - - "program_info/" - - "tests/" + - "buildconfig/**" + - "cmake/**" + - "launcher/**" + - "libraries/**" + - "program_info/**" + - "tests/**" # Files - "CMakeLists.txt" - "COPYING.md" # Workflows - - ".github/codeql" + - ".github/codeql/**" - ".github/workflows/codeql.yml" - - ".github/actions/setup-dependencies/" + - ".github/actions/setup-dependencies/**" workflow_dispatch: jobs: diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml index dcfd2ea14..1d5c5e9cc 100644 --- a/.github/workflows/flatpak.yml +++ b/.github/workflows/flatpak.yml @@ -16,15 +16,15 @@ on: - "**.ui" # Build files - - "flatpak/" + - "flatpak/**" # Directories - - "buildconfig/" - - "cmake/" - - "launcher/" - - "libraries/" - - "program_info/" - - "tests/" + - "buildconfig/**" + - "cmake/**" + - "launcher/**" + - "libraries/**" + - "program_info/**" + - "tests/**" # Files - "CMakeLists.txt" @@ -37,17 +37,19 @@ on: # File types - "**.cpp" - "**.h" + - "**.java" + - "**.ui" # Build files - - "flatpak/" + - "flatpak/**" # Directories - - "buildconfig/" - - "cmake/" - - "launcher/" - - "libraries/" - - "program_info/" - - "tests/" + - "buildconfig/**" + - "cmake/**" + - "launcher/**" + - "libraries/**" + - "program_info/**" + - "tests/**" # Files - "CMakeLists.txt" diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index 2a9e2db6a..ecd71977e 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -16,16 +16,16 @@ on: # Build files - "**.nix" - - "nix/" + - "nix/**" - "flake.lock" # Directories - - "buildconfig/" - - "cmake/" - - "launcher/" - - "libraries/" - - "program_info/" - - "tests/" + - "buildconfig/**" + - "cmake/**" + - "launcher/**" + - "libraries/**" + - "program_info/**" + - "tests/**" # Files - "CMakeLists.txt" @@ -38,19 +38,21 @@ on: # File types - "**.cpp" - "**.h" + - "**.java" + - "**.ui" # Build files - "**.nix" - - "nix/" + - "nix/**" - "flake.lock" # Directories - - "buildconfig/" - - "cmake/" - - "launcher/" - - "libraries/" - - "program_info/" - - "tests/" + - "buildconfig/**" + - "cmake/**" + - "launcher/**" + - "libraries/**" + - "program_info/**" + - "tests/**" # Files - "CMakeLists.txt" From 50c8cddb5b4a708f76433c566abdcc3bc8d215d9 Mon Sep 17 00:00:00 2001 From: Seth Flynn Date: Sat, 5 Jul 2025 02:19:57 -0400 Subject: [PATCH 346/695] build(linux): don't bundle qt with portable zip This was mainly implemented to work around an ABI incompatibility in Arch Linux, which is no longer a major issue as they have an official binary package for us now. Many ABI incompatibility issues still remain (as not every distribution is, or similar to, Ubuntu) which this doesn't even begin to scratch the surface of fixing, and isn't a very supported use case in Linux-land outside of our mostly self-rolled `fixup_bundle` Users who experience ABI incompatibilities with our binaries would be *much* better served using Flatpak or AppImage, as they can guarntee^* compatibility with any host system through a complete bundle; packagers who experience ABI incompatibilities should probably build the launcher against their own distribution, like Arch and many others do Signed-off-by: Seth Flynn --- .github/actions/package/linux/action.yml | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/.github/actions/package/linux/action.yml b/.github/actions/package/linux/action.yml index d5b0d73f7..c5848b089 100644 --- a/.github/actions/package/linux/action.yml +++ b/.github/actions/package/linux/action.yml @@ -111,17 +111,8 @@ runs: INSTALL_PORTABLE_DIR: install-portable run: | - cmake --preset "$CMAKE_PRESET" -DCMAKE_INSTALL_PREFIX=${{ env.INSTALL_PORTABLE_DIR }} -DINSTALL_BUNDLE=full - cmake --install ${{ env.BUILD_DIR }} - cmake --install ${{ env.BUILD_DIR }} --component portable - - mkdir ${{ env.INSTALL_PORTABLE_DIR }}/lib - cp /lib/"$DEB_HOST_MULTIARCH"/libbz2.so.1.0 ${{ env.INSTALL_PORTABLE_DIR }}/lib - cp /usr/lib/"$DEB_HOST_MULTIARCH"/libgobject-2.0.so.0 ${{ env.INSTALL_PORTABLE_DIR }}/lib - cp /usr/lib/"$DEB_HOST_MULTIARCH"/libcrypto.so.* ${{ env.INSTALL_PORTABLE_DIR }}/lib - cp /usr/lib/"$DEB_HOST_MULTIARCH"/libssl.so.* ${{ env.INSTALL_PORTABLE_DIR }}/lib - cp /usr/lib/"$DEB_HOST_MULTIARCH"/libffi.so.*.* ${{ env.INSTALL_PORTABLE_DIR }}/lib - mv ${{ env.INSTALL_PORTABLE_DIR }}/bin/*.so* ${{ env.INSTALL_PORTABLE_DIR }}/lib + cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} + cmake --install ${{ env.BUILD_DIR }} --prefix ${{ env.INSTALL_PORTABLE_DIR }} --component portable for l in $(find ${{ env.INSTALL_PORTABLE_DIR }} -type f); do l=${l#$(pwd)/}; l=${l#${{ env.INSTALL_PORTABLE_DIR }}/}; l=${l#./}; echo $l; done > ${{ env.INSTALL_PORTABLE_DIR }}/manifest.txt cd ${{ env.INSTALL_PORTABLE_DIR }} From fe1a488651ebe5140156a86869ff8c9bcab4d693 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 6 Jul 2025 00:30:52 +0000 Subject: [PATCH 347/695] chore(nix): update lockfile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'nixpkgs': 'github:NixOS/nixpkgs/30e2e2857ba47844aa71991daa6ed1fc678bcbb7?narHash=sha256-krGXKxvkBhnrSC/kGBmg5MyupUUT5R6IBCLEzx9jhMM%3D' (2025-06-27) → 'github:NixOS/nixpkgs/5c724ed1388e53cc231ed98330a60eb2f7be4be3?narHash=sha256-xVNy/XopSfIG9c46nRmPaKfH1Gn/56vQ8%2B%2BxWA8itO4%3D' (2025-07-04) --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index fdcbd87ed..17b77e22b 100644 --- a/flake.lock +++ b/flake.lock @@ -18,11 +18,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1751011381, - "narHash": "sha256-krGXKxvkBhnrSC/kGBmg5MyupUUT5R6IBCLEzx9jhMM=", + "lastModified": 1751637120, + "narHash": "sha256-xVNy/XopSfIG9c46nRmPaKfH1Gn/56vQ8++xWA8itO4=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "30e2e2857ba47844aa71991daa6ed1fc678bcbb7", + "rev": "5c724ed1388e53cc231ed98330a60eb2f7be4be3", "type": "github" }, "original": { From 29d73a474f910934047a213d63ed6f21773b5049 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Mon, 7 Jul 2025 20:27:06 +0100 Subject: [PATCH 348/695] Reduce usage of [[nodiscard]] attributes Signed-off-by: TheKodeToad --- launcher/InstanceCopyPrefs.h | 32 ++++----- launcher/InstanceTask.h | 12 ++-- launcher/Version.h | 4 +- launcher/java/JavaInstallList.h | 2 +- launcher/java/download/ArchiveDownloadTask.h | 2 +- launcher/java/download/ManifestDownloadTask.h | 2 +- launcher/meta/Version.h | 2 +- launcher/meta/VersionList.h | 2 +- launcher/minecraft/MinecraftInstance.h | 2 +- launcher/minecraft/World.h | 6 +- launcher/minecraft/auth/MinecraftAccount.h | 2 +- launcher/minecraft/mod/DataPack.h | 8 +-- launcher/minecraft/mod/DataPackFolderModel.h | 6 +- launcher/minecraft/mod/Mod.h | 2 +- launcher/minecraft/mod/Resource.h | 54 +++++++------- .../minecraft/mod/ResourceFolderModel.cpp | 4 +- launcher/minecraft/mod/ResourceFolderModel.h | 40 +++++------ launcher/minecraft/mod/ResourcePack.h | 2 +- .../minecraft/mod/ResourcePackFolderModel.h | 6 +- launcher/minecraft/mod/ShaderPack.h | 2 +- launcher/minecraft/mod/TexturePack.h | 4 +- .../minecraft/mod/TexturePackFolderModel.h | 6 +- launcher/minecraft/mod/WorldSave.h | 4 +- .../mod/tasks/LocalDataPackParseTask.h | 2 +- .../minecraft/mod/tasks/LocalModParseTask.h | 4 +- .../mod/tasks/LocalShaderPackParseTask.h | 4 +- .../mod/tasks/LocalTexturePackParseTask.h | 4 +- .../mod/tasks/LocalWorldSaveParseTask.h | 4 +- .../mod/tasks/ResourceFolderLoadTask.h | 2 +- launcher/modplatform/ModIndex.h | 4 +- launcher/modplatform/ResourceAPI.h | 18 ++--- launcher/modplatform/flame/FlameAPI.h | 10 +-- .../modplatform/helpers/NetworkResourceAPI.h | 8 +-- launcher/modplatform/modrinth/ModrinthAPI.h | 12 ++-- launcher/net/HttpMetaCache.h | 2 +- launcher/tasks/ConcurrentTask.h | 2 +- launcher/ui/dialogs/ResourceDownloadDialog.h | 28 ++++---- launcher/ui/pages/instance/ManagedPackPage.h | 22 +++--- launcher/ui/pages/modplatform/DataPackPage.h | 8 +-- launcher/ui/pages/modplatform/ModPage.h | 8 +-- .../modplatform/ModpackProviderBasePage.h | 2 +- launcher/ui/pages/modplatform/ResourceModel.h | 27 ++++--- .../ui/pages/modplatform/ResourcePackPage.h | 10 +-- launcher/ui/pages/modplatform/ResourcePage.h | 32 ++++----- .../ui/pages/modplatform/ShaderPackPage.h | 10 +-- .../ui/pages/modplatform/TexturePackModel.h | 2 +- .../ui/pages/modplatform/TexturePackPage.h | 4 +- .../ui/pages/modplatform/atlauncher/AtlPage.h | 2 +- .../ui/pages/modplatform/flame/FlameModel.h | 4 +- .../ui/pages/modplatform/flame/FlamePage.h | 2 +- .../modplatform/flame/FlameResourceModels.h | 20 +++--- .../modplatform/flame/FlameResourcePages.h | 70 +++++++++---------- .../modplatform/import_ftb/ImportFTBPage.h | 2 +- .../ui/pages/modplatform/legacy_ftb/Page.h | 2 +- .../modplatform/modrinth/ModrinthModel.h | 4 +- .../pages/modplatform/modrinth/ModrinthPage.h | 2 +- .../modrinth/ModrinthResourceModels.h | 20 +++--- .../modrinth/ModrinthResourcePages.h | 70 +++++++++---------- .../pages/modplatform/technic/TechnicModel.h | 4 +- .../pages/modplatform/technic/TechnicPage.h | 2 +- launcher/ui/widgets/WideBar.cpp | 2 +- launcher/ui/widgets/WideBar.h | 6 +- tests/DummyResourceAPI.h | 4 +- tests/ResourceModel_test.cpp | 2 +- 64 files changed, 325 insertions(+), 328 deletions(-) diff --git a/launcher/InstanceCopyPrefs.h b/launcher/InstanceCopyPrefs.h index 61c51b3b7..1c3c0c984 100644 --- a/launcher/InstanceCopyPrefs.h +++ b/launcher/InstanceCopyPrefs.h @@ -8,23 +8,23 @@ struct InstanceCopyPrefs { public: - [[nodiscard]] bool allTrue() const; - [[nodiscard]] QString getSelectedFiltersAsRegex() const; - [[nodiscard]] QString getSelectedFiltersAsRegex(const QStringList& additionalFilters) const; + bool allTrue() const; + QString getSelectedFiltersAsRegex() const; + QString getSelectedFiltersAsRegex(const QStringList& additionalFilters) const; // Getters - [[nodiscard]] bool isCopySavesEnabled() const; - [[nodiscard]] bool isKeepPlaytimeEnabled() const; - [[nodiscard]] bool isCopyGameOptionsEnabled() const; - [[nodiscard]] bool isCopyResourcePacksEnabled() const; - [[nodiscard]] bool isCopyShaderPacksEnabled() const; - [[nodiscard]] bool isCopyServersEnabled() const; - [[nodiscard]] bool isCopyModsEnabled() const; - [[nodiscard]] bool isCopyScreenshotsEnabled() const; - [[nodiscard]] bool isUseSymLinksEnabled() const; - [[nodiscard]] bool isLinkRecursivelyEnabled() const; - [[nodiscard]] bool isUseHardLinksEnabled() const; - [[nodiscard]] bool isDontLinkSavesEnabled() const; - [[nodiscard]] bool isUseCloneEnabled() const; + bool isCopySavesEnabled() const; + bool isKeepPlaytimeEnabled() const; + bool isCopyGameOptionsEnabled() const; + bool isCopyResourcePacksEnabled() const; + bool isCopyShaderPacksEnabled() const; + bool isCopyServersEnabled() const; + bool isCopyModsEnabled() const; + bool isCopyScreenshotsEnabled() const; + bool isUseSymLinksEnabled() const; + bool isLinkRecursivelyEnabled() const; + bool isUseHardLinksEnabled() const; + bool isDontLinkSavesEnabled() const; + bool isUseCloneEnabled() const; // Setters void enableCopySaves(bool b); void enableKeepPlaytime(bool b); diff --git a/launcher/InstanceTask.h b/launcher/InstanceTask.h index 7c02160a7..86b4cee68 100644 --- a/launcher/InstanceTask.h +++ b/launcher/InstanceTask.h @@ -14,10 +14,10 @@ struct InstanceName { InstanceName() = default; InstanceName(QString name, QString version) : m_original_name(std::move(name)), m_original_version(std::move(version)) {} - [[nodiscard]] QString modifiedName() const; - [[nodiscard]] QString originalName() const; - [[nodiscard]] QString name() const; - [[nodiscard]] QString version() const; + QString modifiedName() const; + QString originalName() const; + QString name() const; + QString version() const; void setName(QString name) { m_modified_name = name; } void setName(InstanceName& other); @@ -44,12 +44,12 @@ class InstanceTask : public Task, public InstanceName { void setGroup(const QString& group) { m_instGroup = group; } QString group() const { return m_instGroup; } - [[nodiscard]] bool shouldConfirmUpdate() const { return m_confirm_update; } + bool shouldConfirmUpdate() const { return m_confirm_update; } void setConfirmUpdate(bool confirm) { m_confirm_update = confirm; } bool shouldOverride() const { return m_override_existing; } - [[nodiscard]] QString originalInstanceID() const { return m_original_instance_id; }; + QString originalInstanceID() const { return m_original_instance_id; }; protected: void setOverride(bool override, QString instance_id_to_override = {}) diff --git a/launcher/Version.h b/launcher/Version.h index 12e7f0832..4b5ea7119 100644 --- a/launcher/Version.h +++ b/launcher/Version.h @@ -96,8 +96,8 @@ class Version { QString m_fullString; - [[nodiscard]] inline bool isAppendix() const { return m_stringPart.startsWith('+'); } - [[nodiscard]] inline bool isPreRelease() const { return m_stringPart.startsWith('-') && m_stringPart.length() > 1; } + inline bool isAppendix() const { return m_stringPart.startsWith('+'); } + inline bool isPreRelease() const { return m_stringPart.startsWith('-') && m_stringPart.length() > 1; } inline bool operator==(const Section& other) const { diff --git a/launcher/java/JavaInstallList.h b/launcher/java/JavaInstallList.h index b77f17b28..c68c2a3be 100644 --- a/launcher/java/JavaInstallList.h +++ b/launcher/java/JavaInstallList.h @@ -35,7 +35,7 @@ class JavaInstallList : public BaseVersionList { public: explicit JavaInstallList(QObject* parent = 0, bool onlyManagedVersions = false); - [[nodiscard]] Task::Ptr getLoadTask() override; + Task::Ptr getLoadTask() override; bool isLoaded() override; const BaseVersion::Ptr at(int i) const override; int count() const override; diff --git a/launcher/java/download/ArchiveDownloadTask.h b/launcher/java/download/ArchiveDownloadTask.h index 1db33763a..4cd919543 100644 --- a/launcher/java/download/ArchiveDownloadTask.h +++ b/launcher/java/download/ArchiveDownloadTask.h @@ -28,7 +28,7 @@ class ArchiveDownloadTask : public Task { ArchiveDownloadTask(QUrl url, QString final_path, QString checksumType = "", QString checksumHash = ""); virtual ~ArchiveDownloadTask() = default; - [[nodiscard]] bool canAbort() const override { return true; } + bool canAbort() const override { return true; } void executeTask() override; virtual bool abort() override; diff --git a/launcher/java/download/ManifestDownloadTask.h b/launcher/java/download/ManifestDownloadTask.h index ae9e0d0ed..0f65b343c 100644 --- a/launcher/java/download/ManifestDownloadTask.h +++ b/launcher/java/download/ManifestDownloadTask.h @@ -29,7 +29,7 @@ class ManifestDownloadTask : public Task { ManifestDownloadTask(QUrl url, QString final_path, QString checksumType = "", QString checksumHash = ""); virtual ~ManifestDownloadTask() = default; - [[nodiscard]] bool canAbort() const override { return true; } + bool canAbort() const override { return true; } void executeTask() override; virtual bool abort() override; diff --git a/launcher/meta/Version.h b/launcher/meta/Version.h index 2327879a1..6bddbf473 100644 --- a/launcher/meta/Version.h +++ b/launcher/meta/Version.h @@ -60,7 +60,7 @@ class Version : public QObject, public BaseVersion, public BaseEntity { QString localFilename() const override; - [[nodiscard]] ::Version toComparableVersion() const; + ::Version toComparableVersion() const; public: // for usage by format parsers only void setType(const QString& type); diff --git a/launcher/meta/VersionList.h b/launcher/meta/VersionList.h index 21c86b751..18681b8ed 100644 --- a/launcher/meta/VersionList.h +++ b/launcher/meta/VersionList.h @@ -37,7 +37,7 @@ class VersionList : public BaseVersionList, public BaseEntity { enum Roles { UidRole = Qt::UserRole + 100, TimeRole, RequiresRole, VersionPtrRole }; bool isLoaded() override; - [[nodiscard]] Task::Ptr getLoadTask() override; + Task::Ptr getLoadTask() override; const BaseVersion::Ptr at(int i) const override; int count() const override; void sortVersions() override; diff --git a/launcher/minecraft/MinecraftInstance.h b/launcher/minecraft/MinecraftInstance.h index a37164169..c6e519dd2 100644 --- a/launcher/minecraft/MinecraftInstance.h +++ b/launcher/minecraft/MinecraftInstance.h @@ -104,7 +104,7 @@ class MinecraftInstance : public BaseInstance { QString getLocalLibraryPath() const; /** Returns whether the instance, with its version, has support for demo mode. */ - [[nodiscard]] bool supportsDemo() const; + bool supportsDemo() const; void updateRuntimeContext() override; diff --git a/launcher/minecraft/World.h b/launcher/minecraft/World.h index 34d418e79..cca931826 100644 --- a/launcher/minecraft/World.h +++ b/launcher/minecraft/World.h @@ -59,7 +59,7 @@ class World { // WEAK compare operator - used for replacing worlds bool operator==(const World& other) const; - [[nodiscard]] auto isSymLink() const -> bool { return m_containerFile.isSymLink(); } + auto isSymLink() const -> bool { return m_containerFile.isSymLink(); } /** * @brief Take a instance path, checks if the file pointed to by the resource is a symlink or under a symlink in that instance @@ -68,9 +68,9 @@ class World { * @return true * @return false */ - [[nodiscard]] bool isSymLinkUnder(const QString& instPath) const; + bool isSymLinkUnder(const QString& instPath) const; - [[nodiscard]] bool isMoreThanOneHardLink() const; + bool isMoreThanOneHardLink() const; QString canonicalFilePath() const { return m_containerFile.canonicalFilePath(); } diff --git a/launcher/minecraft/auth/MinecraftAccount.h b/launcher/minecraft/auth/MinecraftAccount.h index f6fcfada2..a82d3f134 100644 --- a/launcher/minecraft/auth/MinecraftAccount.h +++ b/launcher/minecraft/auth/MinecraftAccount.h @@ -114,7 +114,7 @@ class MinecraftAccount : public QObject, public Usable { bool isActive() const; - [[nodiscard]] AccountType accountType() const noexcept { return data.type; } + AccountType accountType() const noexcept { return data.type; } bool ownsMinecraft() const { return data.type != AccountType::Offline && data.minecraftEntitlement.ownsMinecraft; } diff --git a/launcher/minecraft/mod/DataPack.h b/launcher/minecraft/mod/DataPack.h index 4b56cb9d8..ac6408bde 100644 --- a/launcher/minecraft/mod/DataPack.h +++ b/launcher/minecraft/mod/DataPack.h @@ -40,15 +40,15 @@ class DataPack : public Resource { DataPack(QFileInfo file_info) : Resource(file_info) {} /** Gets the numerical ID of the pack format. */ - [[nodiscard]] int packFormat() const { return m_pack_format; } + int packFormat() const { return m_pack_format; } /** Gets, respectively, the lower and upper versions supported by the set pack format. */ - [[nodiscard]] virtual std::pair compatibleVersions() const; + virtual std::pair compatibleVersions() const; /** Gets the description of the data pack. */ - [[nodiscard]] QString description() const { return m_description; } + QString description() const { return m_description; } /** Gets the image of the data pack, converted to a QPixmap for drawing, and scaled to size. */ - [[nodiscard]] QPixmap image(QSize size, Qt::AspectRatioMode mode = Qt::AspectRatioMode::IgnoreAspectRatio) const; + QPixmap image(QSize size, Qt::AspectRatioMode mode = Qt::AspectRatioMode::IgnoreAspectRatio) const; /** Thread-safe. */ void setPackFormat(int new_format_id); diff --git a/launcher/minecraft/mod/DataPackFolderModel.h b/launcher/minecraft/mod/DataPackFolderModel.h index 026ae8b76..2b90e1a2a 100644 --- a/launcher/minecraft/mod/DataPackFolderModel.h +++ b/launcher/minecraft/mod/DataPackFolderModel.h @@ -50,10 +50,10 @@ class DataPackFolderModel : public ResourceFolderModel { virtual QString id() const override { return "datapacks"; } - [[nodiscard]] QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; - [[nodiscard]] QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; - [[nodiscard]] int columnCount(const QModelIndex& parent) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + int columnCount(const QModelIndex& parent) const override; [[nodiscard]] Resource* createResource(const QFileInfo& file) override; [[nodiscard]] Task* createParseTask(Resource&) override; diff --git a/launcher/minecraft/mod/Mod.h b/launcher/minecraft/mod/Mod.h index 553af92f3..eceb8c256 100644 --- a/launcher/minecraft/mod/Mod.h +++ b/launcher/minecraft/mod/Mod.h @@ -76,7 +76,7 @@ class Mod : public Resource { /** Get the intneral path to the mod's icon file*/ QString iconPath() const { return m_local_details.icon_file; } /** Gets the icon of the mod, converted to a QPixmap for drawing, and scaled to size. */ - [[nodiscard]] QPixmap icon(QSize size, Qt::AspectRatioMode mode = Qt::AspectRatioMode::IgnoreAspectRatio) const; + QPixmap icon(QSize size, Qt::AspectRatioMode mode = Qt::AspectRatioMode::IgnoreAspectRatio) const; /** Thread-safe. */ QPixmap setIcon(QImage new_image) const; diff --git a/launcher/minecraft/mod/Resource.h b/launcher/minecraft/mod/Resource.h index 16d8c2a89..87bfd4345 100644 --- a/launcher/minecraft/mod/Resource.h +++ b/launcher/minecraft/mod/Resource.h @@ -83,23 +83,23 @@ class Resource : public QObject { void setFile(QFileInfo file_info); void parseFile(); - [[nodiscard]] auto fileinfo() const -> QFileInfo { return m_file_info; } - [[nodiscard]] auto dateTimeChanged() const -> QDateTime { return m_changed_date_time; } - [[nodiscard]] auto internal_id() const -> QString { return m_internal_id; } - [[nodiscard]] auto type() const -> ResourceType { return m_type; } - [[nodiscard]] bool enabled() const { return m_enabled; } - [[nodiscard]] auto getOriginalFileName() const -> QString; - [[nodiscard]] QString sizeStr() const { return m_size_str; } - [[nodiscard]] qint64 sizeInfo() const { return m_size_info; } - - [[nodiscard]] virtual auto name() const -> QString; - [[nodiscard]] virtual bool valid() const { return m_type != ResourceType::UNKNOWN; } - - [[nodiscard]] auto status() const -> ResourceStatus { return m_status; }; - [[nodiscard]] auto metadata() -> std::shared_ptr { return m_metadata; } - [[nodiscard]] auto metadata() const -> std::shared_ptr { return m_metadata; } - [[nodiscard]] auto provider() const -> QString; - [[nodiscard]] virtual auto homepage() const -> QString; + auto fileinfo() const -> QFileInfo { return m_file_info; } + auto dateTimeChanged() const -> QDateTime { return m_changed_date_time; } + auto internal_id() const -> QString { return m_internal_id; } + auto type() const -> ResourceType { return m_type; } + bool enabled() const { return m_enabled; } + auto getOriginalFileName() const -> QString; + QString sizeStr() const { return m_size_str; } + qint64 sizeInfo() const { return m_size_info; } + + virtual auto name() const -> QString; + virtual bool valid() const { return m_type != ResourceType::UNKNOWN; } + + auto status() const -> ResourceStatus { return m_status; }; + auto metadata() -> std::shared_ptr { return m_metadata; } + auto metadata() const -> std::shared_ptr { return m_metadata; } + auto provider() const -> QString; + virtual auto homepage() const -> QString; void setStatus(ResourceStatus status) { m_status = status; } void setMetadata(std::shared_ptr&& metadata); @@ -110,12 +110,12 @@ class Resource : public QObject { * = 0: 'this' is equal to 'other' * < 0: 'this' comes before 'other' */ - [[nodiscard]] virtual int compare(Resource const& other, SortType type = SortType::NAME) const; + virtual int compare(Resource const& other, SortType type = SortType::NAME) const; /** Returns whether the given filter should filter out 'this' (false), * or if such filter includes the Resource (true). */ - [[nodiscard]] virtual bool applyFilter(QRegularExpression filter) const; + virtual bool applyFilter(QRegularExpression filter) const; /** Changes the enabled property, according to 'action'. * @@ -123,10 +123,10 @@ class Resource : public QObject { */ bool enable(EnableAction action); - [[nodiscard]] auto shouldResolve() const -> bool { return !m_is_resolving && !m_is_resolved; } - [[nodiscard]] auto isResolving() const -> bool { return m_is_resolving; } - [[nodiscard]] auto isResolved() const -> bool { return m_is_resolved; } - [[nodiscard]] auto resolutionTicket() const -> int { return m_resolution_ticket; } + auto shouldResolve() const -> bool { return !m_is_resolving && !m_is_resolved; } + auto isResolving() const -> bool { return m_is_resolving; } + auto isResolved() const -> bool { return m_is_resolved; } + auto resolutionTicket() const -> int { return m_resolution_ticket; } void setResolving(bool resolving, int resolutionTicket) { @@ -139,7 +139,7 @@ class Resource : public QObject { // Delete the metadata only. auto destroyMetadata(const QDir& index_dir) -> void; - [[nodiscard]] auto isSymLink() const -> bool { return m_file_info.isSymLink(); } + auto isSymLink() const -> bool { return m_file_info.isSymLink(); } /** * @brief Take a instance path, checks if the file pointed to by the resource is a symlink or under a symlink in that instance @@ -148,11 +148,11 @@ class Resource : public QObject { * @return true * @return false */ - [[nodiscard]] bool isSymLinkUnder(const QString& instPath) const; + bool isSymLinkUnder(const QString& instPath) const; - [[nodiscard]] bool isMoreThanOneHardLink() const; + bool isMoreThanOneHardLink() const; - [[nodiscard]] auto mod_id() const -> QString { return m_mod_id; } + auto mod_id() const -> QString { return m_mod_id; } void setModId(const QString& modId) { m_mod_id = modId; } protected: diff --git a/launcher/minecraft/mod/ResourceFolderModel.cpp b/launcher/minecraft/mod/ResourceFolderModel.cpp index fbcd1b0bc..f93002f06 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.cpp +++ b/launcher/minecraft/mod/ResourceFolderModel.cpp @@ -709,7 +709,7 @@ SortType ResourceFolderModel::columnToSortKey(size_t column) const } /* Standard Proxy Model for createFilterProxyModel */ -[[nodiscard]] bool ResourceFolderModel::ProxyModel::filterAcceptsRow(int source_row, +bool ResourceFolderModel::ProxyModel::filterAcceptsRow(int source_row, [[maybe_unused]] const QModelIndex& source_parent) const { auto* model = qobject_cast(sourceModel()); @@ -721,7 +721,7 @@ SortType ResourceFolderModel::columnToSortKey(size_t column) const return resource.applyFilter(filterRegularExpression()); } -[[nodiscard]] bool ResourceFolderModel::ProxyModel::lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const +bool ResourceFolderModel::ProxyModel::lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const { auto* model = qobject_cast(sourceModel()); if (!model || !source_left.isValid() || !source_right.isValid() || source_left.column() != source_right.column()) { diff --git a/launcher/minecraft/mod/ResourceFolderModel.h b/launcher/minecraft/mod/ResourceFolderModel.h index 759861e14..0dea3c7f1 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.h +++ b/launcher/minecraft/mod/ResourceFolderModel.h @@ -21,11 +21,11 @@ class QSortFilterProxyModel; /* A macro to define useful functions to handle Resource* -> T* more easily on derived classes */ #define RESOURCE_HELPERS(T) \ - [[nodiscard]] T& at(int index) \ + T& at(int index) \ { \ return *static_cast(m_resources[index].get()); \ } \ - [[nodiscard]] const T& at(int index) const \ + const T& at(int index) const \ { \ return *static_cast(m_resources.at(index).get()); \ } \ @@ -115,24 +115,24 @@ class ResourceFolderModel : public QAbstractListModel { /** Creates a new parse task, if needed, for 'res' and start it.*/ virtual void resolveResource(Resource::Ptr res); - [[nodiscard]] qsizetype size() const { return m_resources.size(); } + qsizetype size() const { return m_resources.size(); } [[nodiscard]] bool empty() const { return size() == 0; } - [[nodiscard]] Resource& at(int index) { return *m_resources[index].get(); } - [[nodiscard]] const Resource& at(int index) const { return *m_resources.at(index).get(); } + Resource& at(int index) { return *m_resources[index].get(); } + const Resource& at(int index) const { return *m_resources.at(index).get(); } QList selectedResources(const QModelIndexList& indexes); QList allResources(); - [[nodiscard]] Resource::Ptr find(QString id); + Resource::Ptr find(QString id); - [[nodiscard]] QDir const& dir() const { return m_dir; } + QDir const& dir() const { return m_dir; } /** Checks whether there's any parse tasks being done. * * Since they can be quite expensive, and are usually done in a separate thread, if we were to destroy the model while having * such tasks would introduce an undefined behavior, most likely resulting in a crash. */ - [[nodiscard]] bool hasPendingParseTasks() const; + bool hasPendingParseTasks() const; /* Qt behavior */ @@ -141,22 +141,22 @@ class ResourceFolderModel : public QAbstractListModel { QStringList columnNames(bool translated = true) const { return translated ? m_column_names_translated : m_column_names; } - [[nodiscard]] int rowCount(const QModelIndex& parent = {}) const override { return parent.isValid() ? 0 : static_cast(size()); } - [[nodiscard]] int columnCount(const QModelIndex& parent = {}) const override { return parent.isValid() ? 0 : NUM_COLUMNS; } + int rowCount(const QModelIndex& parent = {}) const override { return parent.isValid() ? 0 : static_cast(size()); } + int columnCount(const QModelIndex& parent = {}) const override { return parent.isValid() ? 0 : NUM_COLUMNS; } - [[nodiscard]] Qt::DropActions supportedDropActions() const override; + Qt::DropActions supportedDropActions() const override; /// flags, mostly to support drag&drop - [[nodiscard]] Qt::ItemFlags flags(const QModelIndex& index) const override; - [[nodiscard]] QStringList mimeTypes() const override; - bool dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) override; + Qt::ItemFlags flags(const QModelIndex& index) const override; + QStringList mimeTypes() const override; + [[nodiscard]] bool dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) override; [[nodiscard]] bool validateIndex(const QModelIndex& index) const; - [[nodiscard]] QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override; - [[nodiscard]] QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; void setupHeaderAction(QAction* act, int column); void saveColumns(QTreeView* tree); @@ -169,16 +169,16 @@ class ResourceFolderModel : public QAbstractListModel { */ QSortFilterProxyModel* createFilterProxyModel(QObject* parent = nullptr); - [[nodiscard]] SortType columnToSortKey(size_t column) const; - [[nodiscard]] QList columnResizeModes() const { return m_column_resize_modes; } + SortType columnToSortKey(size_t column) const; + QList columnResizeModes() const { return m_column_resize_modes; } class ProxyModel : public QSortFilterProxyModel { public: explicit ProxyModel(QObject* parent = nullptr) : QSortFilterProxyModel(parent) {} protected: - [[nodiscard]] bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override; - [[nodiscard]] bool lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const override; + bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override; + bool lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const override; }; QString instDirPath() const; diff --git a/launcher/minecraft/mod/ResourcePack.h b/launcher/minecraft/mod/ResourcePack.h index bd161df87..494bdee97 100644 --- a/launcher/minecraft/mod/ResourcePack.h +++ b/launcher/minecraft/mod/ResourcePack.h @@ -22,7 +22,7 @@ class ResourcePack : public DataPack { ResourcePack(QFileInfo file_info) : DataPack(file_info) {} /** Gets, respectively, the lower and upper versions supported by the set pack format. */ - [[nodiscard]] std::pair compatibleVersions() const override; + std::pair compatibleVersions() const override; QString directory() override { return "/assets"; } }; diff --git a/launcher/minecraft/mod/ResourcePackFolderModel.h b/launcher/minecraft/mod/ResourcePackFolderModel.h index 9dbf41b85..b552c324e 100644 --- a/launcher/minecraft/mod/ResourcePackFolderModel.h +++ b/launcher/minecraft/mod/ResourcePackFolderModel.h @@ -13,10 +13,10 @@ class ResourcePackFolderModel : public ResourceFolderModel { QString id() const override { return "resourcepacks"; } - [[nodiscard]] QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; - [[nodiscard]] QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; - [[nodiscard]] int columnCount(const QModelIndex& parent) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + int columnCount(const QModelIndex& parent) const override; [[nodiscard]] Resource* createResource(const QFileInfo& file) override { return new ResourcePack(file); } [[nodiscard]] Task* createParseTask(Resource&) override; diff --git a/launcher/minecraft/mod/ShaderPack.h b/launcher/minecraft/mod/ShaderPack.h index ec0f9404e..9275ebed8 100644 --- a/launcher/minecraft/mod/ShaderPack.h +++ b/launcher/minecraft/mod/ShaderPack.h @@ -45,7 +45,7 @@ class ShaderPack : public Resource { public: using Ptr = shared_qobject_ptr; - [[nodiscard]] ShaderPackFormat packFormat() const { return m_pack_format; } + ShaderPackFormat packFormat() const { return m_pack_format; } ShaderPack(QObject* parent = nullptr) : Resource(parent) {} ShaderPack(QFileInfo file_info) : Resource(file_info) {} diff --git a/launcher/minecraft/mod/TexturePack.h b/launcher/minecraft/mod/TexturePack.h index bf4b5b6b4..1327e2f61 100644 --- a/launcher/minecraft/mod/TexturePack.h +++ b/launcher/minecraft/mod/TexturePack.h @@ -37,10 +37,10 @@ class TexturePack : public Resource { TexturePack(QFileInfo file_info) : Resource(file_info) {} /** Gets the description of the texture pack. */ - [[nodiscard]] QString description() const { return m_description; } + QString description() const { return m_description; } /** Gets the image of the texture pack, converted to a QPixmap for drawing, and scaled to size. */ - [[nodiscard]] QPixmap image(QSize size, Qt::AspectRatioMode mode = Qt::AspectRatioMode::IgnoreAspectRatio) const; + QPixmap image(QSize size, Qt::AspectRatioMode mode = Qt::AspectRatioMode::IgnoreAspectRatio) const; /** Thread-safe. */ void setDescription(QString new_description); diff --git a/launcher/minecraft/mod/TexturePackFolderModel.h b/launcher/minecraft/mod/TexturePackFolderModel.h index 7a9264e8f..37f78d8d7 100644 --- a/launcher/minecraft/mod/TexturePackFolderModel.h +++ b/launcher/minecraft/mod/TexturePackFolderModel.h @@ -50,10 +50,10 @@ class TexturePackFolderModel : public ResourceFolderModel { virtual QString id() const override { return "texturepacks"; } - [[nodiscard]] QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; - [[nodiscard]] QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; - [[nodiscard]] int columnCount(const QModelIndex& parent) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + int columnCount(const QModelIndex& parent) const override; [[nodiscard]] Resource* createResource(const QFileInfo& file) override { return new TexturePack(file); } [[nodiscard]] Task* createParseTask(Resource&) override; diff --git a/launcher/minecraft/mod/WorldSave.h b/launcher/minecraft/mod/WorldSave.h index 5985fc8ad..702a3edf6 100644 --- a/launcher/minecraft/mod/WorldSave.h +++ b/launcher/minecraft/mod/WorldSave.h @@ -38,9 +38,9 @@ class WorldSave : public Resource { WorldSave(QFileInfo file_info) : Resource(file_info) {} /** Gets the format of the save. */ - [[nodiscard]] WorldSaveFormat saveFormat() const { return m_save_format; } + WorldSaveFormat saveFormat() const { return m_save_format; } /** Gets the name of the save dir (first found in multi mode). */ - [[nodiscard]] QString saveDirName() const { return m_save_dir_name; } + QString saveDirName() const { return m_save_dir_name; } /** Thread-safe. */ void setSaveFormat(WorldSaveFormat new_save_format); diff --git a/launcher/minecraft/mod/tasks/LocalDataPackParseTask.h b/launcher/minecraft/mod/tasks/LocalDataPackParseTask.h index 57591a0f4..6bdf55880 100644 --- a/launcher/minecraft/mod/tasks/LocalDataPackParseTask.h +++ b/launcher/minecraft/mod/tasks/LocalDataPackParseTask.h @@ -61,7 +61,7 @@ class LocalDataPackParseTask : public Task { void executeTask() override; - [[nodiscard]] int token() const { return m_token; } + int token() const { return m_token; } private: int m_token; diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.h b/launcher/minecraft/mod/tasks/LocalModParseTask.h index 7ce5a84d2..cbe009376 100644 --- a/launcher/minecraft/mod/tasks/LocalModParseTask.h +++ b/launcher/minecraft/mod/tasks/LocalModParseTask.h @@ -39,13 +39,13 @@ class LocalModParseTask : public Task { using ResultPtr = std::shared_ptr; ResultPtr result() const { return m_result; } - [[nodiscard]] bool canAbort() const override { return true; } + bool canAbort() const override { return true; } bool abort() override; LocalModParseTask(int token, ResourceType type, const QFileInfo& modFile); void executeTask() override; - [[nodiscard]] int token() const { return m_token; } + int token() const { return m_token; } private: int m_token; diff --git a/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.h b/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.h index 6be2183cd..55d77f33b 100644 --- a/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.h +++ b/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.h @@ -46,12 +46,12 @@ class LocalShaderPackParseTask : public Task { public: LocalShaderPackParseTask(int token, ShaderPack& sp); - [[nodiscard]] bool canAbort() const override { return true; } + bool canAbort() const override { return true; } bool abort() override; void executeTask() override; - [[nodiscard]] int token() const { return m_token; } + int token() const { return m_token; } private: int m_token; diff --git a/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.h b/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.h index 1341590f2..b9cc1ea54 100644 --- a/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.h +++ b/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.h @@ -50,12 +50,12 @@ class LocalTexturePackParseTask : public Task { public: LocalTexturePackParseTask(int token, TexturePack& rp); - [[nodiscard]] bool canAbort() const override { return true; } + bool canAbort() const override { return true; } bool abort() override; void executeTask() override; - [[nodiscard]] int token() const { return m_token; } + int token() const { return m_token; } private: int m_token; diff --git a/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.h b/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.h index 12f677b02..42faf51c5 100644 --- a/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.h +++ b/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.h @@ -46,12 +46,12 @@ class LocalWorldSaveParseTask : public Task { public: LocalWorldSaveParseTask(int token, WorldSave& save); - [[nodiscard]] bool canAbort() const override { return true; } + bool canAbort() const override { return true; } bool abort() override; void executeTask() override; - [[nodiscard]] int token() const { return m_token; } + int token() const { return m_token; } private: int m_token; diff --git a/launcher/minecraft/mod/tasks/ResourceFolderLoadTask.h b/launcher/minecraft/mod/tasks/ResourceFolderLoadTask.h index 9950345ef..7c872c13d 100644 --- a/launcher/minecraft/mod/tasks/ResourceFolderLoadTask.h +++ b/launcher/minecraft/mod/tasks/ResourceFolderLoadTask.h @@ -60,7 +60,7 @@ class ResourceFolderLoadTask : public Task { bool clean_orphan, std::function create_function); - [[nodiscard]] bool canAbort() const override { return true; } + bool canAbort() const override { return true; } bool abort() override { m_aborted.store(true); diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h index ad2503ea7..88a951c34 100644 --- a/launcher/modplatform/ModIndex.h +++ b/launcher/modplatform/ModIndex.h @@ -155,14 +155,14 @@ struct IndexedPack { ExtraPackData extraData; // For internal use, not provided by APIs - [[nodiscard]] bool isVersionSelected(int index) const + bool isVersionSelected(int index) const { if (!versionsLoaded) return false; return versions.at(index).is_currently_selected; } - [[nodiscard]] bool isAnyVersionSelected() const + bool isAnyVersionSelected() const { if (!versionsLoaded) return false; diff --git a/launcher/modplatform/ResourceAPI.h b/launcher/modplatform/ResourceAPI.h index 62a1ff199..6eed33acf 100644 --- a/launcher/modplatform/ResourceAPI.h +++ b/launcher/modplatform/ResourceAPI.h @@ -128,7 +128,7 @@ class ResourceAPI { public: /** Gets a list of available sorting methods for this API. */ - [[nodiscard]] virtual auto getSortingMethods() const -> QList = 0; + virtual auto getSortingMethods() const -> QList = 0; public slots: [[nodiscard]] virtual Task::Ptr searchProjects(SearchArgs&&, SearchCallbacks&&) const @@ -136,40 +136,40 @@ class ResourceAPI { qWarning() << "TODO: ResourceAPI::searchProjects"; return nullptr; } - [[nodiscard]] virtual Task::Ptr getProject([[maybe_unused]] QString addonId, + virtual Task::Ptr getProject([[maybe_unused]] QString addonId, [[maybe_unused]] std::shared_ptr response) const { qWarning() << "TODO: ResourceAPI::getProject"; return nullptr; } - [[nodiscard]] virtual Task::Ptr getProjects([[maybe_unused]] QStringList addonIds, + virtual Task::Ptr getProjects([[maybe_unused]] QStringList addonIds, [[maybe_unused]] std::shared_ptr response) const { qWarning() << "TODO: ResourceAPI::getProjects"; return nullptr; } - [[nodiscard]] virtual Task::Ptr getProjectInfo(ProjectInfoArgs&&, ProjectInfoCallbacks&&) const + virtual Task::Ptr getProjectInfo(ProjectInfoArgs&&, ProjectInfoCallbacks&&) const { qWarning() << "TODO: ResourceAPI::getProjectInfo"; return nullptr; } - [[nodiscard]] virtual Task::Ptr getProjectVersions(VersionSearchArgs&&, VersionSearchCallbacks&&) const + virtual Task::Ptr getProjectVersions(VersionSearchArgs&&, VersionSearchCallbacks&&) const { qWarning() << "TODO: ResourceAPI::getProjectVersions"; return nullptr; } - [[nodiscard]] virtual Task::Ptr getDependencyVersion(DependencySearchArgs&&, DependencySearchCallbacks&&) const + virtual Task::Ptr getDependencyVersion(DependencySearchArgs&&, DependencySearchCallbacks&&) const { qWarning() << "TODO"; return nullptr; } protected: - [[nodiscard]] inline QString debugName() const { return "External resource API"; } + inline QString debugName() const { return "External resource API"; } - [[nodiscard]] inline QString mapMCVersionToModrinth(Version v) const + inline QString mapMCVersionToModrinth(Version v) const { static const QString preString = " Pre-Release "; auto verStr = v.toString(); @@ -181,7 +181,7 @@ class ResourceAPI { return verStr; } - [[nodiscard]] inline QString getGameVersionsString(std::list mcVersions) const + inline QString getGameVersionsString(std::list mcVersions) const { QString s; for (auto& ver : mcVersions) { diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index 316d2e9c9..8d681aeb5 100644 --- a/launcher/modplatform/flame/FlameAPI.h +++ b/launcher/modplatform/flame/FlameAPI.h @@ -29,7 +29,7 @@ class FlameAPI : public NetworkResourceAPI { static Task::Ptr getModCategories(std::shared_ptr response); static QList loadModCategories(std::shared_ptr response); - [[nodiscard]] QList getSortingMethods() const override; + QList getSortingMethods() const override; static inline bool validateModLoaders(ModPlatform::ModLoaderTypes loaders) { @@ -90,7 +90,7 @@ class FlameAPI : public NetworkResourceAPI { static const QString getModLoaderFilters(ModPlatform::ModLoaderTypes types) { return "[" + getModLoaderStrings(types).join(',') + "]"; } public: - [[nodiscard]] std::optional getSearchURL(SearchArgs const& args) const override + std::optional getSearchURL(SearchArgs const& args) const override { QStringList get_arguments; get_arguments.append(QString("classId=%1").arg(getClassId(args.type))); @@ -116,7 +116,7 @@ class FlameAPI : public NetworkResourceAPI { return BuildConfig.FLAME_BASE_URL + "/mods/search?gameId=432&" + get_arguments.join('&'); } - [[nodiscard]] std::optional getVersionsURL(VersionSearchArgs const& args) const override + std::optional getVersionsURL(VersionSearchArgs const& args) const override { auto addonId = args.pack.addonId.toString(); QString url = QString(BuildConfig.FLAME_BASE_URL + "/mods/%1/files?pageSize=10000").arg(addonId); @@ -133,11 +133,11 @@ class FlameAPI : public NetworkResourceAPI { } private: - [[nodiscard]] std::optional getInfoURL(QString const& id) const override + std::optional getInfoURL(QString const& id) const override { return QString(BuildConfig.FLAME_BASE_URL + "/mods/%1").arg(id); } - [[nodiscard]] std::optional getDependencyURL(DependencySearchArgs const& args) const override + std::optional getDependencyURL(DependencySearchArgs const& args) const override { auto addonId = args.dependency.addonId.toString(); auto url = diff --git a/launcher/modplatform/helpers/NetworkResourceAPI.h b/launcher/modplatform/helpers/NetworkResourceAPI.h index b72e82533..d89014a38 100644 --- a/launcher/modplatform/helpers/NetworkResourceAPI.h +++ b/launcher/modplatform/helpers/NetworkResourceAPI.h @@ -18,8 +18,8 @@ class NetworkResourceAPI : public ResourceAPI { Task::Ptr getDependencyVersion(DependencySearchArgs&&, DependencySearchCallbacks&&) const override; protected: - [[nodiscard]] virtual auto getSearchURL(SearchArgs const& args) const -> std::optional = 0; - [[nodiscard]] virtual auto getInfoURL(QString const& id) const -> std::optional = 0; - [[nodiscard]] virtual auto getVersionsURL(VersionSearchArgs const& args) const -> std::optional = 0; - [[nodiscard]] virtual auto getDependencyURL(DependencySearchArgs const& args) const -> std::optional = 0; + virtual auto getSearchURL(SearchArgs const& args) const -> std::optional = 0; + virtual auto getInfoURL(QString const& id) const -> std::optional = 0; + virtual auto getVersionsURL(VersionSearchArgs const& args) const -> std::optional = 0; + virtual auto getDependencyURL(DependencySearchArgs const& args) const -> std::optional = 0; }; diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index 17b23723b..9c48ca4fe 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -35,7 +35,7 @@ class ModrinthAPI : public NetworkResourceAPI { static QList loadModCategories(std::shared_ptr response); public: - [[nodiscard]] auto getSortingMethods() const -> QList override; + auto getSortingMethods() const -> QList override; inline auto getAuthorURL(const QString& name) const -> QString { return "https://modrinth.com/user/" + name; }; @@ -83,7 +83,7 @@ class ModrinthAPI : public NetworkResourceAPI { return {}; } - [[nodiscard]] static inline QString mapMCVersionFromModrinth(QString v) + static inline QString mapMCVersionFromModrinth(QString v) { static const QString preString = " Pre-Release "; bool pre = false; @@ -99,7 +99,7 @@ class ModrinthAPI : public NetworkResourceAPI { } private: - [[nodiscard]] static QString resourceTypeParameter(ModPlatform::ResourceType type) + static QString resourceTypeParameter(ModPlatform::ResourceType type) { switch (type) { case ModPlatform::ResourceType::MOD: @@ -120,7 +120,7 @@ class ModrinthAPI : public NetworkResourceAPI { return ""; } - [[nodiscard]] QString createFacets(SearchArgs const& args) const + QString createFacets(SearchArgs const& args) const { QStringList facets_list; @@ -144,7 +144,7 @@ class ModrinthAPI : public NetworkResourceAPI { } public: - [[nodiscard]] inline auto getSearchURL(SearchArgs const& args) const -> std::optional override + inline auto getSearchURL(SearchArgs const& args) const -> std::optional override { if (args.loaders.has_value() && args.loaders.value() != 0) { if (!validateModLoaders(args.loaders.value())) { @@ -203,7 +203,7 @@ class ModrinthAPI : public NetworkResourceAPI { ModPlatform::DataPack); } - [[nodiscard]] std::optional getDependencyURL(DependencySearchArgs const& args) const override + std::optional getDependencyURL(DependencySearchArgs const& args) const override { return args.dependency.version.length() != 0 ? QString("%1/version/%2").arg(BuildConfig.MODRINTH_PROD_URL, args.dependency.version) : QString("%1/project/%2/version?game_versions=[\"%3\"]&loaders=[\"%4\"]") diff --git a/launcher/net/HttpMetaCache.h b/launcher/net/HttpMetaCache.h index 144012ae5..c8b02dae4 100644 --- a/launcher/net/HttpMetaCache.h +++ b/launcher/net/HttpMetaCache.h @@ -66,7 +66,7 @@ class MetaEntry { /* Whether the entry expires after some time (false) or not (true). */ void makeEternal(bool eternal) { m_is_eternal = eternal; } - [[nodiscard]] bool isEternal() const { return m_is_eternal; } + bool isEternal() const { return m_is_eternal; } auto getCurrentAge() -> qint64 { return m_current_age; } void setCurrentAge(qint64 age) { m_current_age = age; } diff --git a/launcher/tasks/ConcurrentTask.h b/launcher/tasks/ConcurrentTask.h index cc6256cf8..a65613bf2 100644 --- a/launcher/tasks/ConcurrentTask.h +++ b/launcher/tasks/ConcurrentTask.h @@ -88,7 +88,7 @@ class ConcurrentTask : public Task { protected: // NOTE: This is not thread-safe. - [[nodiscard]] unsigned int totalSize() const { return static_cast(m_queue.size() + m_doing.size() + m_done.size()); } + unsigned int totalSize() const { return static_cast(m_queue.size() + m_doing.size() + m_done.size()); } virtual void updateState(); diff --git a/launcher/ui/dialogs/ResourceDownloadDialog.h b/launcher/ui/dialogs/ResourceDownloadDialog.h index 181086d82..a83f3c536 100644 --- a/launcher/ui/dialogs/ResourceDownloadDialog.h +++ b/launcher/ui/dialogs/ResourceDownloadDialog.h @@ -57,7 +57,7 @@ class ResourceDownloadDialog : public QDialog, public BasePageProvider { void connectButtons(); //: String that gets appended to the download dialog title ("Download " + resourcesString()) - [[nodiscard]] virtual QString resourcesString() const { return tr("resources"); } + virtual QString resourcesString() const { return tr("resources"); } QString dialogTitle() override { return tr("Download %1").arg(resourcesString()); }; @@ -68,7 +68,7 @@ class ResourceDownloadDialog : public QDialog, public BasePageProvider { void removeResource(const QString&); const QList getTasks(); - [[nodiscard]] const std::shared_ptr getBaseModel() const { return m_base_model; } + const std::shared_ptr getBaseModel() const { return m_base_model; } void setResourceMetadata(const std::shared_ptr& meta); @@ -82,10 +82,10 @@ class ResourceDownloadDialog : public QDialog, public BasePageProvider { virtual void confirm(); protected: - [[nodiscard]] virtual QString geometrySaveKey() const { return ""; } + virtual QString geometrySaveKey() const { return ""; } void setButtonStatus(); - [[nodiscard]] virtual GetModDependenciesTask::Ptr getModDependenciesTask() { return nullptr; } + virtual GetModDependenciesTask::Ptr getModDependenciesTask() { return nullptr; } protected: const std::shared_ptr m_base_model; @@ -104,8 +104,8 @@ class ModDownloadDialog final : public ResourceDownloadDialog { ~ModDownloadDialog() override = default; //: String that gets appended to the mod download dialog title ("Download " + resourcesString()) - [[nodiscard]] QString resourcesString() const override { return tr("mods"); } - [[nodiscard]] QString geometrySaveKey() const override { return "ModDownloadGeometry"; } + QString resourcesString() const override { return tr("mods"); } + QString geometrySaveKey() const override { return "ModDownloadGeometry"; } QList getPages() override; GetModDependenciesTask::Ptr getModDependenciesTask() override; @@ -124,8 +124,8 @@ class ResourcePackDownloadDialog final : public ResourceDownloadDialog { ~ResourcePackDownloadDialog() override = default; //: String that gets appended to the resource pack download dialog title ("Download " + resourcesString()) - [[nodiscard]] QString resourcesString() const override { return tr("resource packs"); } - [[nodiscard]] QString geometrySaveKey() const override { return "RPDownloadGeometry"; } + QString resourcesString() const override { return tr("resource packs"); } + QString geometrySaveKey() const override { return "RPDownloadGeometry"; } QList getPages() override; @@ -143,8 +143,8 @@ class TexturePackDownloadDialog final : public ResourceDownloadDialog { ~TexturePackDownloadDialog() override = default; //: String that gets appended to the texture pack download dialog title ("Download " + resourcesString()) - [[nodiscard]] QString resourcesString() const override { return tr("texture packs"); } - [[nodiscard]] QString geometrySaveKey() const override { return "TPDownloadGeometry"; } + QString resourcesString() const override { return tr("texture packs"); } + QString geometrySaveKey() const override { return "TPDownloadGeometry"; } QList getPages() override; @@ -160,8 +160,8 @@ class ShaderPackDownloadDialog final : public ResourceDownloadDialog { ~ShaderPackDownloadDialog() override = default; //: String that gets appended to the shader pack download dialog title ("Download " + resourcesString()) - [[nodiscard]] QString resourcesString() const override { return tr("shader packs"); } - [[nodiscard]] QString geometrySaveKey() const override { return "ShaderDownloadGeometry"; } + QString resourcesString() const override { return tr("shader packs"); } + QString geometrySaveKey() const override { return "ShaderDownloadGeometry"; } QList getPages() override; @@ -177,8 +177,8 @@ class DataPackDownloadDialog final : public ResourceDownloadDialog { ~DataPackDownloadDialog() override = default; //: String that gets appended to the data pack download dialog title ("Download " + resourcesString()) - [[nodiscard]] QString resourcesString() const override { return tr("data packs"); } - [[nodiscard]] QString geometrySaveKey() const override { return "DataPackDownloadGeometry"; } + QString resourcesString() const override { return tr("data packs"); } + QString geometrySaveKey() const override { return "DataPackDownloadGeometry"; } QList getPages() override; diff --git a/launcher/ui/pages/instance/ManagedPackPage.h b/launcher/ui/pages/instance/ManagedPackPage.h index e8d304c6b..966c57768 100644 --- a/launcher/ui/pages/instance/ManagedPackPage.h +++ b/launcher/ui/pages/instance/ManagedPackPage.h @@ -37,11 +37,11 @@ class ManagedPackPage : public QWidget, public BasePage { static ManagedPackPage* createPage(BaseInstance* inst, QString type, QWidget* parent = nullptr); ~ManagedPackPage() override; - [[nodiscard]] QString displayName() const override; - [[nodiscard]] QIcon icon() const override; - [[nodiscard]] QString helpPage() const override; - [[nodiscard]] QString id() const override { return "managed_pack"; } - [[nodiscard]] bool shouldDisplay() const override; + QString displayName() const override; + QIcon icon() const override; + QString helpPage() const override; + QString id() const override { return "managed_pack"; } + bool shouldDisplay() const override; void openedImpl() override; @@ -55,7 +55,7 @@ class ManagedPackPage : public QWidget, public BasePage { /** URL of the managed pack. * Not the version-specific one. */ - [[nodiscard]] virtual QString url() const { return {}; }; + virtual QString url() const { return {}; }; void setInstanceWindow(InstanceWindow* window) { m_instance_window = window; } @@ -109,7 +109,7 @@ class GenericManagedPackPage final : public ManagedPackPage { ~GenericManagedPackPage() override = default; // TODO: We may want to show this page with some useful info at some point. - [[nodiscard]] bool shouldDisplay() const override { return false; }; + bool shouldDisplay() const override { return false; }; }; class ModrinthManagedPackPage final : public ManagedPackPage { @@ -120,8 +120,8 @@ class ModrinthManagedPackPage final : public ManagedPackPage { ~ModrinthManagedPackPage() override = default; void parseManagedPack() override; - [[nodiscard]] QString url() const override; - [[nodiscard]] QString helpPage() const override { return "modrinth-managed-pack"; } + QString url() const override; + QString helpPage() const override { return "modrinth-managed-pack"; } public slots: void suggestVersion() override; @@ -144,8 +144,8 @@ class FlameManagedPackPage final : public ManagedPackPage { ~FlameManagedPackPage() override = default; void parseManagedPack() override; - [[nodiscard]] QString url() const override; - [[nodiscard]] QString helpPage() const override { return "curseforge-managed-pack"; } + QString url() const override; + QString helpPage() const override { return "curseforge-managed-pack"; } public slots: void suggestVersion() override; diff --git a/launcher/ui/pages/modplatform/DataPackPage.h b/launcher/ui/pages/modplatform/DataPackPage.h index cf78df96c..431fc9a05 100644 --- a/launcher/ui/pages/modplatform/DataPackPage.h +++ b/launcher/ui/pages/modplatform/DataPackPage.h @@ -34,13 +34,13 @@ class DataPackResourcePage : public ResourcePage { } //: The plural version of 'data pack' - [[nodiscard]] inline QString resourcesString() const override { return tr("data packs"); } + inline QString resourcesString() const override { return tr("data packs"); } //: The singular version of 'data packs' - [[nodiscard]] inline QString resourceString() const override { return tr("data pack"); } + inline QString resourceString() const override { return tr("data pack"); } - [[nodiscard]] bool supportsFiltering() const override { return false; }; + bool supportsFiltering() const override { return false; }; - [[nodiscard]] QMap urlHandlers() const override; + QMap urlHandlers() const override; protected: DataPackResourcePage(DataPackDownloadDialog* dialog, BaseInstance& instance); diff --git a/launcher/ui/pages/modplatform/ModPage.h b/launcher/ui/pages/modplatform/ModPage.h index fb9f3f9d3..d3b08cbd9 100644 --- a/launcher/ui/pages/modplatform/ModPage.h +++ b/launcher/ui/pages/modplatform/ModPage.h @@ -43,17 +43,17 @@ class ModPage : public ResourcePage { } //: The plural version of 'mod' - [[nodiscard]] inline QString resourcesString() const override { return tr("mods"); } + inline QString resourcesString() const override { return tr("mods"); } //: The singular version of 'mods' - [[nodiscard]] inline QString resourceString() const override { return tr("mod"); } + inline QString resourceString() const override { return tr("mod"); } - [[nodiscard]] QMap urlHandlers() const override; + QMap urlHandlers() const override; void addResourceToPage(ModPlatform::IndexedPack::Ptr, ModPlatform::IndexedVersion&, std::shared_ptr) override; virtual std::unique_ptr createFilterWidget() = 0; - [[nodiscard]] bool supportsFiltering() const override { return true; }; + bool supportsFiltering() const override { return true; }; auto getFilter() const -> const std::shared_ptr { return m_filter; } void setFilterWidget(std::unique_ptr&); diff --git a/launcher/ui/pages/modplatform/ModpackProviderBasePage.h b/launcher/ui/pages/modplatform/ModpackProviderBasePage.h index a3daa9a81..6cc0b8e99 100644 --- a/launcher/ui/pages/modplatform/ModpackProviderBasePage.h +++ b/launcher/ui/pages/modplatform/ModpackProviderBasePage.h @@ -25,5 +25,5 @@ class ModpackProviderBasePage : public BasePage { /** Programatically set the term in the search bar. */ virtual void setSearchTerm(QString) = 0; /** Get the current term in the search bar. */ - [[nodiscard]] virtual QString getSerachTerm() const = 0; + virtual QString getSerachTerm() const = 0; }; \ No newline at end of file diff --git a/launcher/ui/pages/modplatform/ResourceModel.h b/launcher/ui/pages/modplatform/ResourceModel.h index 3f1e633ec..cae1d6581 100644 --- a/launcher/ui/pages/modplatform/ResourceModel.h +++ b/launcher/ui/pages/modplatform/ResourceModel.h @@ -36,25 +36,25 @@ class ResourceModel : public QAbstractListModel { ResourceModel(ResourceAPI* api); ~ResourceModel() override; - [[nodiscard]] auto data(const QModelIndex&, int role) const -> QVariant override; - [[nodiscard]] auto roleNames() const -> QHash override; + auto data(const QModelIndex&, int role) const -> QVariant override; + auto roleNames() const -> QHash override; bool setData(const QModelIndex& index, const QVariant& value, int role) override; - [[nodiscard]] virtual auto debugName() const -> QString; - [[nodiscard]] virtual auto metaEntryBase() const -> QString = 0; + virtual auto debugName() const -> QString; + virtual auto metaEntryBase() const -> QString = 0; - [[nodiscard]] inline int rowCount(const QModelIndex& parent) const override + inline int rowCount(const QModelIndex& parent) const override { return parent.isValid() ? 0 : static_cast(m_packs.size()); } - [[nodiscard]] inline int columnCount(const QModelIndex& parent) const override { return parent.isValid() ? 0 : 1; } - [[nodiscard]] inline auto flags(const QModelIndex& index) const -> Qt::ItemFlags override { return QAbstractListModel::flags(index); } + inline int columnCount(const QModelIndex& parent) const override { return parent.isValid() ? 0 : 1; } + inline auto flags(const QModelIndex& index) const -> Qt::ItemFlags override { return QAbstractListModel::flags(index); } - [[nodiscard]] bool hasActiveSearchJob() const { return m_current_search_job && m_current_search_job->isRunning(); } - [[nodiscard]] bool hasActiveInfoJob() const { return m_current_info_job.isRunning(); } - [[nodiscard]] Task::Ptr activeSearchJob() { return hasActiveSearchJob() ? m_current_search_job : nullptr; } + bool hasActiveSearchJob() const { return m_current_search_job && m_current_search_job->isRunning(); } + bool hasActiveInfoJob() const { return m_current_info_job.isRunning(); } + Task::Ptr activeSearchJob() { return hasActiveSearchJob() ? m_current_search_job : nullptr; } - [[nodiscard]] auto getSortingMethods() const { return m_api->getSortingMethods(); } + auto getSortingMethods() const { return m_api->getSortingMethods(); } virtual QVariant getInstalledPackVersion(ModPlatform::IndexedPack::Ptr) const { return {}; } /** Whether the version is opted out or not. Currently only makes sense in CF. */ @@ -69,7 +69,6 @@ class ResourceModel : public QAbstractListModel { public slots: void fetchMore(const QModelIndex& parent) override; - // NOTE: Can't use [[nodiscard]] here because of https://bugreports.qt.io/browse/QTBUG-58628 on Qt 5.12 inline bool canFetchMore(const QModelIndex& parent) const override { return parent.isValid() ? false : m_search_state == SearchState::CanFetchMore; @@ -113,14 +112,14 @@ class ResourceModel : public QAbstractListModel { void runSearchJob(Task::Ptr); void runInfoJob(Task::Ptr); - [[nodiscard]] auto getCurrentSortingMethodByIndex() const -> std::optional; + auto getCurrentSortingMethodByIndex() const -> std::optional; /** Converts a JSON document to a common array format. * * This is needed so that different providers, with different JSON structures, can be parsed * uniformally. You NEED to re-implement this if you intend on using the default callbacks. */ - [[nodiscard]] virtual auto documentToArray(QJsonDocument&) const -> QJsonArray; + virtual auto documentToArray(QJsonDocument&) const -> QJsonArray; /** Functions to load data into a pack. * diff --git a/launcher/ui/pages/modplatform/ResourcePackPage.h b/launcher/ui/pages/modplatform/ResourcePackPage.h index 8d967f73a..f8d4d5bf9 100644 --- a/launcher/ui/pages/modplatform/ResourcePackPage.h +++ b/launcher/ui/pages/modplatform/ResourcePackPage.h @@ -33,15 +33,15 @@ class ResourcePackResourcePage : public ResourcePage { } //: The plural version of 'resource pack' - [[nodiscard]] inline QString resourcesString() const override { return tr("resource packs"); } + inline QString resourcesString() const override { return tr("resource packs"); } //: The singular version of 'resource packs' - [[nodiscard]] inline QString resourceString() const override { return tr("resource pack"); } + inline QString resourceString() const override { return tr("resource pack"); } - [[nodiscard]] bool supportsFiltering() const override { return false; }; + bool supportsFiltering() const override { return false; }; - [[nodiscard]] QMap urlHandlers() const override; + QMap urlHandlers() const override; - [[nodiscard]] inline auto helpPage() const -> QString override { return "resourcepack-platform"; } + inline auto helpPage() const -> QString override { return "resourcepack-platform"; } protected: ResourcePackResourcePage(ResourceDownloadDialog* dialog, BaseInstance& instance); diff --git a/launcher/ui/pages/modplatform/ResourcePage.h b/launcher/ui/pages/modplatform/ResourcePage.h index 055db441a..8f4d2c496 100644 --- a/launcher/ui/pages/modplatform/ResourcePage.h +++ b/launcher/ui/pages/modplatform/ResourcePage.h @@ -33,37 +33,37 @@ class ResourcePage : public QWidget, public BasePage { ~ResourcePage() override; /* Affects what the user sees */ - [[nodiscard]] auto displayName() const -> QString override = 0; - [[nodiscard]] auto icon() const -> QIcon override = 0; - [[nodiscard]] auto id() const -> QString override = 0; - [[nodiscard]] auto helpPage() const -> QString override = 0; - [[nodiscard]] bool shouldDisplay() const override = 0; + auto displayName() const -> QString override = 0; + auto icon() const -> QIcon override = 0; + auto id() const -> QString override = 0; + auto helpPage() const -> QString override = 0; + bool shouldDisplay() const override = 0; /* Used internally */ - [[nodiscard]] virtual auto metaEntryBase() const -> QString = 0; - [[nodiscard]] virtual auto debugName() const -> QString = 0; + virtual auto metaEntryBase() const -> QString = 0; + virtual auto debugName() const -> QString = 0; //: The plural version of 'resource' - [[nodiscard]] virtual inline QString resourcesString() const { return tr("resources"); } + virtual inline QString resourcesString() const { return tr("resources"); } //: The singular version of 'resources' - [[nodiscard]] virtual inline QString resourceString() const { return tr("resource"); } + virtual inline QString resourceString() const { return tr("resource"); } /* Features this resource's page supports */ - [[nodiscard]] virtual bool supportsFiltering() const = 0; + virtual bool supportsFiltering() const = 0; void retranslate() override; void openedImpl() override; auto eventFilter(QObject* watched, QEvent* event) -> bool override; /** Get the current term in the search bar. */ - [[nodiscard]] auto getSearchTerm() const -> QString; + auto getSearchTerm() const -> QString; /** Programatically set the term in the search bar. */ void setSearchTerm(QString); - [[nodiscard]] bool setCurrentPack(ModPlatform::IndexedPack::Ptr); - [[nodiscard]] auto getCurrentPack() const -> ModPlatform::IndexedPack::Ptr; - [[nodiscard]] auto getDialog() const -> const ResourceDownloadDialog* { return m_parentDialog; } - [[nodiscard]] auto getModel() const -> ResourceModel* { return m_model; } + bool setCurrentPack(ModPlatform::IndexedPack::Ptr); + auto getCurrentPack() const -> ModPlatform::IndexedPack::Ptr; + auto getDialog() const -> const ResourceDownloadDialog* { return m_parentDialog; } + auto getModel() const -> ResourceModel* { return m_model; } protected: ResourcePage(ResourceDownloadDialog* parent, BaseInstance&); @@ -95,8 +95,6 @@ class ResourcePage : public QWidget, public BasePage { void onResourceSelected(); void onResourceToggle(const QModelIndex& index); - // NOTE: Can't use [[nodiscard]] here because of https://bugreports.qt.io/browse/QTBUG-58628 on Qt 5.12 - /** Associates regex expressions to pages in the order they're given in the map. */ virtual QMap urlHandlers() const = 0; virtual void openUrl(const QUrl&); diff --git a/launcher/ui/pages/modplatform/ShaderPackPage.h b/launcher/ui/pages/modplatform/ShaderPackPage.h index d436e218a..85d2b16e6 100644 --- a/launcher/ui/pages/modplatform/ShaderPackPage.h +++ b/launcher/ui/pages/modplatform/ShaderPackPage.h @@ -33,17 +33,17 @@ class ShaderPackResourcePage : public ResourcePage { } //: The plural version of 'shader pack' - [[nodiscard]] inline QString resourcesString() const override { return tr("shader packs"); } + inline QString resourcesString() const override { return tr("shader packs"); } //: The singular version of 'shader packs' - [[nodiscard]] inline QString resourceString() const override { return tr("shader pack"); } + inline QString resourceString() const override { return tr("shader pack"); } - [[nodiscard]] bool supportsFiltering() const override { return false; }; + bool supportsFiltering() const override { return false; }; void addResourceToPage(ModPlatform::IndexedPack::Ptr, ModPlatform::IndexedVersion&, std::shared_ptr) override; - [[nodiscard]] QMap urlHandlers() const override; + QMap urlHandlers() const override; - [[nodiscard]] inline auto helpPage() const -> QString override { return "shaderpack-platform"; } + inline auto helpPage() const -> QString override { return "shaderpack-platform"; } protected: ShaderPackResourcePage(ShaderPackDownloadDialog* dialog, BaseInstance& instance); diff --git a/launcher/ui/pages/modplatform/TexturePackModel.h b/launcher/ui/pages/modplatform/TexturePackModel.h index 45b5734ee..885bbced8 100644 --- a/launcher/ui/pages/modplatform/TexturePackModel.h +++ b/launcher/ui/pages/modplatform/TexturePackModel.h @@ -15,7 +15,7 @@ class TexturePackResourceModel : public ResourcePackResourceModel { public: TexturePackResourceModel(BaseInstance const& inst, ResourceAPI* api); - [[nodiscard]] inline ::Version maximumTexturePackVersion() const { return { "1.6" }; } + inline ::Version maximumTexturePackVersion() const { return { "1.6" }; } ResourceAPI::SearchArgs createSearchArguments() override; ResourceAPI::VersionSearchArgs createVersionsArguments(const QModelIndex&) override; diff --git a/launcher/ui/pages/modplatform/TexturePackPage.h b/launcher/ui/pages/modplatform/TexturePackPage.h index 27fd8bcfc..262004dfd 100644 --- a/launcher/ui/pages/modplatform/TexturePackPage.h +++ b/launcher/ui/pages/modplatform/TexturePackPage.h @@ -35,9 +35,9 @@ class TexturePackResourcePage : public ResourcePackResourcePage { } //: The plural version of 'texture pack' - [[nodiscard]] inline QString resourcesString() const override { return tr("texture packs"); } + inline QString resourcesString() const override { return tr("texture packs"); } //: The singular version of 'texture packs' - [[nodiscard]] inline QString resourceString() const override { return tr("texture pack"); } + inline QString resourceString() const override { return tr("texture pack"); } protected: TexturePackResourcePage(TexturePackDownloadDialog* dialog, BaseInstance& instance) : ResourcePackResourcePage(dialog, instance) {} diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.h b/launcher/ui/pages/modplatform/atlauncher/AtlPage.h index 73460232b..556e90b1d 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlPage.h +++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.h @@ -68,7 +68,7 @@ class AtlPage : public QWidget, public ModpackProviderBasePage { /** Programatically set the term in the search bar. */ virtual void setSearchTerm(QString) override; /** Get the current term in the search bar. */ - [[nodiscard]] virtual QString getSerachTerm() const override; + virtual QString getSerachTerm() const override; private: void suggestCurrent(); diff --git a/launcher/ui/pages/modplatform/flame/FlameModel.h b/launcher/ui/pages/modplatform/flame/FlameModel.h index 026f6d1ee..bfdd81810 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModel.h +++ b/launcher/ui/pages/modplatform/flame/FlameModel.h @@ -41,8 +41,8 @@ class ListModel : public QAbstractListModel { void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback); void searchWithTerm(const QString& term, int sort, std::shared_ptr filter, bool filterChanged); - [[nodiscard]] bool hasActiveSearchJob() const { return jobPtr && jobPtr->isRunning(); } - [[nodiscard]] Task::Ptr activeSearchJob() { return hasActiveSearchJob() ? jobPtr : nullptr; } + bool hasActiveSearchJob() const { return jobPtr && jobPtr->isRunning(); } + Task::Ptr activeSearchJob() { return hasActiveSearchJob() ? jobPtr : nullptr; } private slots: void performPaginatedSearch(); diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.h b/launcher/ui/pages/modplatform/flame/FlamePage.h index 32b752bbe..19c8d4dbc 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.h +++ b/launcher/ui/pages/modplatform/flame/FlamePage.h @@ -76,7 +76,7 @@ class FlamePage : public QWidget, public ModpackProviderBasePage { /** Programatically set the term in the search bar. */ virtual void setSearchTerm(QString) override; /** Get the current term in the search bar. */ - [[nodiscard]] virtual QString getSerachTerm() const override; + virtual QString getSerachTerm() const override; private: void suggestCurrent(); diff --git a/launcher/ui/pages/modplatform/flame/FlameResourceModels.h b/launcher/ui/pages/modplatform/flame/FlameResourceModels.h index 5fffe6361..2cdc2910d 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourceModels.h +++ b/launcher/ui/pages/modplatform/flame/FlameResourceModels.h @@ -20,8 +20,8 @@ class FlameModModel : public ModModel { bool optedOut(const ModPlatform::IndexedVersion& ver) const override; private: - [[nodiscard]] QString debugName() const override { return Flame::debugName() + " (Model)"; } - [[nodiscard]] QString metaEntryBase() const override { return Flame::metaEntryBase(); } + QString debugName() const override { return Flame::debugName() + " (Model)"; } + QString metaEntryBase() const override { return Flame::metaEntryBase(); } void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override; @@ -41,8 +41,8 @@ class FlameResourcePackModel : public ResourcePackResourceModel { bool optedOut(const ModPlatform::IndexedVersion& ver) const override; private: - [[nodiscard]] QString debugName() const override { return Flame::debugName() + " (Model)"; } - [[nodiscard]] QString metaEntryBase() const override { return Flame::metaEntryBase(); } + QString debugName() const override { return Flame::debugName() + " (Model)"; } + QString metaEntryBase() const override { return Flame::metaEntryBase(); } void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override; @@ -61,8 +61,8 @@ class FlameTexturePackModel : public TexturePackResourceModel { bool optedOut(const ModPlatform::IndexedVersion& ver) const override; private: - [[nodiscard]] QString debugName() const override { return Flame::debugName() + " (Model)"; } - [[nodiscard]] QString metaEntryBase() const override { return Flame::metaEntryBase(); } + QString debugName() const override { return Flame::debugName() + " (Model)"; } + QString metaEntryBase() const override { return Flame::metaEntryBase(); } void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override; @@ -84,8 +84,8 @@ class FlameShaderPackModel : public ShaderPackResourceModel { bool optedOut(const ModPlatform::IndexedVersion& ver) const override; private: - [[nodiscard]] QString debugName() const override { return Flame::debugName() + " (Model)"; } - [[nodiscard]] QString metaEntryBase() const override { return Flame::metaEntryBase(); } + QString debugName() const override { return Flame::debugName() + " (Model)"; } + QString metaEntryBase() const override { return Flame::metaEntryBase(); } void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override; @@ -103,8 +103,8 @@ class FlameDataPackModel : public DataPackResourceModel { bool optedOut(const ModPlatform::IndexedVersion& ver) const override; private: - [[nodiscard]] QString debugName() const override { return Flame::debugName() + " (Model)"; } - [[nodiscard]] QString metaEntryBase() const override { return Flame::metaEntryBase(); } + QString debugName() const override { return Flame::debugName() + " (Model)"; } + QString metaEntryBase() const override { return Flame::metaEntryBase(); } void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override; diff --git a/launcher/ui/pages/modplatform/flame/FlameResourcePages.h b/launcher/ui/pages/modplatform/flame/FlameResourcePages.h index 309e1e019..19f3731c7 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourcePages.h +++ b/launcher/ui/pages/modplatform/flame/FlameResourcePages.h @@ -86,15 +86,15 @@ class FlameModPage : public ModPage { FlameModPage(ModDownloadDialog* dialog, BaseInstance& instance); ~FlameModPage() override = default; - [[nodiscard]] bool shouldDisplay() const override; + bool shouldDisplay() const override; - [[nodiscard]] inline auto displayName() const -> QString override { return Flame::displayName(); } - [[nodiscard]] inline auto icon() const -> QIcon override { return Flame::icon(); } - [[nodiscard]] inline auto id() const -> QString override { return Flame::id(); } - [[nodiscard]] inline auto debugName() const -> QString override { return Flame::debugName(); } - [[nodiscard]] inline auto metaEntryBase() const -> QString override { return Flame::metaEntryBase(); } + inline auto displayName() const -> QString override { return Flame::displayName(); } + inline auto icon() const -> QIcon override { return Flame::icon(); } + inline auto id() const -> QString override { return Flame::id(); } + inline auto debugName() const -> QString override { return Flame::debugName(); } + inline auto metaEntryBase() const -> QString override { return Flame::metaEntryBase(); } - [[nodiscard]] inline auto helpPage() const -> QString override { return "Mod-platform"; } + inline auto helpPage() const -> QString override { return "Mod-platform"; } void openUrl(const QUrl& url) override; std::unique_ptr createFilterWidget() override; @@ -118,15 +118,15 @@ class FlameResourcePackPage : public ResourcePackResourcePage { FlameResourcePackPage(ResourcePackDownloadDialog* dialog, BaseInstance& instance); ~FlameResourcePackPage() override = default; - [[nodiscard]] bool shouldDisplay() const override; + bool shouldDisplay() const override; - [[nodiscard]] inline auto displayName() const -> QString override { return Flame::displayName(); } - [[nodiscard]] inline auto icon() const -> QIcon override { return Flame::icon(); } - [[nodiscard]] inline auto id() const -> QString override { return Flame::id(); } - [[nodiscard]] inline auto debugName() const -> QString override { return Flame::debugName(); } - [[nodiscard]] inline auto metaEntryBase() const -> QString override { return Flame::metaEntryBase(); } + inline auto displayName() const -> QString override { return Flame::displayName(); } + inline auto icon() const -> QIcon override { return Flame::icon(); } + inline auto id() const -> QString override { return Flame::id(); } + inline auto debugName() const -> QString override { return Flame::debugName(); } + inline auto metaEntryBase() const -> QString override { return Flame::metaEntryBase(); } - [[nodiscard]] inline auto helpPage() const -> QString override { return ""; } + inline auto helpPage() const -> QString override { return ""; } void openUrl(const QUrl& url) override; }; @@ -143,15 +143,15 @@ class FlameTexturePackPage : public TexturePackResourcePage { FlameTexturePackPage(TexturePackDownloadDialog* dialog, BaseInstance& instance); ~FlameTexturePackPage() override = default; - [[nodiscard]] bool shouldDisplay() const override; + bool shouldDisplay() const override; - [[nodiscard]] inline auto displayName() const -> QString override { return Flame::displayName(); } - [[nodiscard]] inline auto icon() const -> QIcon override { return Flame::icon(); } - [[nodiscard]] inline auto id() const -> QString override { return Flame::id(); } - [[nodiscard]] inline auto debugName() const -> QString override { return Flame::debugName(); } - [[nodiscard]] inline auto metaEntryBase() const -> QString override { return Flame::metaEntryBase(); } + inline auto displayName() const -> QString override { return Flame::displayName(); } + inline auto icon() const -> QIcon override { return Flame::icon(); } + inline auto id() const -> QString override { return Flame::id(); } + inline auto debugName() const -> QString override { return Flame::debugName(); } + inline auto metaEntryBase() const -> QString override { return Flame::metaEntryBase(); } - [[nodiscard]] inline auto helpPage() const -> QString override { return ""; } + inline auto helpPage() const -> QString override { return ""; } void openUrl(const QUrl& url) override; }; @@ -168,15 +168,15 @@ class FlameShaderPackPage : public ShaderPackResourcePage { FlameShaderPackPage(ShaderPackDownloadDialog* dialog, BaseInstance& instance); ~FlameShaderPackPage() override = default; - [[nodiscard]] bool shouldDisplay() const override; + bool shouldDisplay() const override; - [[nodiscard]] inline auto displayName() const -> QString override { return Flame::displayName(); } - [[nodiscard]] inline auto icon() const -> QIcon override { return Flame::icon(); } - [[nodiscard]] inline auto id() const -> QString override { return Flame::id(); } - [[nodiscard]] inline auto debugName() const -> QString override { return Flame::debugName(); } - [[nodiscard]] inline auto metaEntryBase() const -> QString override { return Flame::metaEntryBase(); } + inline auto displayName() const -> QString override { return Flame::displayName(); } + inline auto icon() const -> QIcon override { return Flame::icon(); } + inline auto id() const -> QString override { return Flame::id(); } + inline auto debugName() const -> QString override { return Flame::debugName(); } + inline auto metaEntryBase() const -> QString override { return Flame::metaEntryBase(); } - [[nodiscard]] inline auto helpPage() const -> QString override { return ""; } + inline auto helpPage() const -> QString override { return ""; } void openUrl(const QUrl& url) override; }; @@ -195,15 +195,15 @@ class FlameDataPackPage : public DataPackResourcePage { FlameDataPackPage(DataPackDownloadDialog* dialog, BaseInstance& instance); ~FlameDataPackPage() override = default; - [[nodiscard]] bool shouldDisplay() const override; + bool shouldDisplay() const override; - [[nodiscard]] inline auto displayName() const -> QString override { return Flame::displayName(); } - [[nodiscard]] inline auto icon() const -> QIcon override { return Flame::icon(); } - [[nodiscard]] inline auto id() const -> QString override { return Flame::id(); } - [[nodiscard]] inline auto debugName() const -> QString override { return Flame::debugName(); } - [[nodiscard]] inline auto metaEntryBase() const -> QString override { return Flame::metaEntryBase(); } + inline auto displayName() const -> QString override { return Flame::displayName(); } + inline auto icon() const -> QIcon override { return Flame::icon(); } + inline auto id() const -> QString override { return Flame::id(); } + inline auto debugName() const -> QString override { return Flame::debugName(); } + inline auto metaEntryBase() const -> QString override { return Flame::metaEntryBase(); } - [[nodiscard]] inline auto helpPage() const -> QString override { return ""; } + inline auto helpPage() const -> QString override { return ""; } void openUrl(const QUrl& url) override; }; diff --git a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.h b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.h index 7afff5a9d..c00a93dfa 100644 --- a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.h +++ b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.h @@ -52,7 +52,7 @@ class ImportFTBPage : public QWidget, public ModpackProviderBasePage { /** Programatically set the term in the search bar. */ virtual void setSearchTerm(QString) override; /** Get the current term in the search bar. */ - [[nodiscard]] virtual QString getSerachTerm() const override; + virtual QString getSerachTerm() const override; private: void suggestCurrent(); diff --git a/launcher/ui/pages/modplatform/legacy_ftb/Page.h b/launcher/ui/pages/modplatform/legacy_ftb/Page.h index 818000c05..fc789971f 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/Page.h +++ b/launcher/ui/pages/modplatform/legacy_ftb/Page.h @@ -74,7 +74,7 @@ class Page : public QWidget, public ModpackProviderBasePage { /** Programatically set the term in the search bar. */ virtual void setSearchTerm(QString) override; /** Get the current term in the search bar. */ - [[nodiscard]] virtual QString getSerachTerm() const override; + virtual QString getSerachTerm() const override; private: void suggestCurrent(); diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h index 640ddf688..cdd5c4e79 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h @@ -73,8 +73,8 @@ class ModpackListModel : public QAbstractListModel { void refresh(); void searchWithTerm(const QString& term, int sort, std::shared_ptr filter, bool filterChanged); - [[nodiscard]] bool hasActiveSearchJob() const { return jobPtr && jobPtr->isRunning(); } - [[nodiscard]] Task::Ptr activeSearchJob() { return hasActiveSearchJob() ? jobPtr : nullptr; } + bool hasActiveSearchJob() const { return jobPtr && jobPtr->isRunning(); } + Task::Ptr activeSearchJob() { return hasActiveSearchJob() ? jobPtr : nullptr; } void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback); diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h index d22a72e4e..77cc173dd 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h @@ -82,7 +82,7 @@ class ModrinthPage : public QWidget, public ModpackProviderBasePage { /** Programatically set the term in the search bar. */ virtual void setSearchTerm(QString) override; /** Get the current term in the search bar. */ - [[nodiscard]] virtual QString getSerachTerm() const override; + virtual QString getSerachTerm() const override; private slots: void onSelectionChanged(QModelIndex first, QModelIndex second); diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h index 6a5ba0382..7f68ed47d 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h @@ -35,8 +35,8 @@ class ModrinthModModel : public ModModel { ~ModrinthModModel() override = default; private: - [[nodiscard]] QString debugName() const override { return Modrinth::debugName() + " (Model)"; } - [[nodiscard]] QString metaEntryBase() const override { return Modrinth::metaEntryBase(); } + QString debugName() const override { return Modrinth::debugName() + " (Model)"; } + QString metaEntryBase() const override { return Modrinth::metaEntryBase(); } void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override; @@ -54,8 +54,8 @@ class ModrinthResourcePackModel : public ResourcePackResourceModel { ~ModrinthResourcePackModel() override = default; private: - [[nodiscard]] QString debugName() const override { return Modrinth::debugName() + " (Model)"; } - [[nodiscard]] QString metaEntryBase() const override { return Modrinth::metaEntryBase(); } + QString debugName() const override { return Modrinth::debugName() + " (Model)"; } + QString metaEntryBase() const override { return Modrinth::metaEntryBase(); } void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override; @@ -72,8 +72,8 @@ class ModrinthTexturePackModel : public TexturePackResourceModel { ~ModrinthTexturePackModel() override = default; private: - [[nodiscard]] QString debugName() const override { return Modrinth::debugName() + " (Model)"; } - [[nodiscard]] QString metaEntryBase() const override { return Modrinth::metaEntryBase(); } + QString debugName() const override { return Modrinth::debugName() + " (Model)"; } + QString metaEntryBase() const override { return Modrinth::metaEntryBase(); } void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override; @@ -90,8 +90,8 @@ class ModrinthShaderPackModel : public ShaderPackResourceModel { ~ModrinthShaderPackModel() override = default; private: - [[nodiscard]] QString debugName() const override { return Modrinth::debugName() + " (Model)"; } - [[nodiscard]] QString metaEntryBase() const override { return Modrinth::metaEntryBase(); } + QString debugName() const override { return Modrinth::debugName() + " (Model)"; } + QString metaEntryBase() const override { return Modrinth::metaEntryBase(); } void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override; @@ -108,8 +108,8 @@ class ModrinthDataPackModel : public DataPackResourceModel { ~ModrinthDataPackModel() override = default; private: - [[nodiscard]] QString debugName() const override { return Modrinth::debugName() + " (Model)"; } - [[nodiscard]] QString metaEntryBase() const override { return Modrinth::metaEntryBase(); } + QString debugName() const override { return Modrinth::debugName() + " (Model)"; } + QString metaEntryBase() const override { return Modrinth::metaEntryBase(); } void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h index a4c7344b5..cb0f4d85c 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h @@ -85,15 +85,15 @@ class ModrinthModPage : public ModPage { ModrinthModPage(ModDownloadDialog* dialog, BaseInstance& instance); ~ModrinthModPage() override = default; - [[nodiscard]] bool shouldDisplay() const override; + bool shouldDisplay() const override; - [[nodiscard]] inline auto displayName() const -> QString override { return Modrinth::displayName(); } - [[nodiscard]] inline auto icon() const -> QIcon override { return Modrinth::icon(); } - [[nodiscard]] inline auto id() const -> QString override { return Modrinth::id(); } - [[nodiscard]] inline auto debugName() const -> QString override { return Modrinth::debugName(); } - [[nodiscard]] inline auto metaEntryBase() const -> QString override { return Modrinth::metaEntryBase(); } + inline auto displayName() const -> QString override { return Modrinth::displayName(); } + inline auto icon() const -> QIcon override { return Modrinth::icon(); } + inline auto id() const -> QString override { return Modrinth::id(); } + inline auto debugName() const -> QString override { return Modrinth::debugName(); } + inline auto metaEntryBase() const -> QString override { return Modrinth::metaEntryBase(); } - [[nodiscard]] inline auto helpPage() const -> QString override { return "Mod-platform"; } + inline auto helpPage() const -> QString override { return "Mod-platform"; } std::unique_ptr createFilterWidget() override; @@ -114,15 +114,15 @@ class ModrinthResourcePackPage : public ResourcePackResourcePage { ModrinthResourcePackPage(ResourcePackDownloadDialog* dialog, BaseInstance& instance); ~ModrinthResourcePackPage() override = default; - [[nodiscard]] bool shouldDisplay() const override; + bool shouldDisplay() const override; - [[nodiscard]] inline auto displayName() const -> QString override { return Modrinth::displayName(); } - [[nodiscard]] inline auto icon() const -> QIcon override { return Modrinth::icon(); } - [[nodiscard]] inline auto id() const -> QString override { return Modrinth::id(); } - [[nodiscard]] inline auto debugName() const -> QString override { return Modrinth::debugName(); } - [[nodiscard]] inline auto metaEntryBase() const -> QString override { return Modrinth::metaEntryBase(); } + inline auto displayName() const -> QString override { return Modrinth::displayName(); } + inline auto icon() const -> QIcon override { return Modrinth::icon(); } + inline auto id() const -> QString override { return Modrinth::id(); } + inline auto debugName() const -> QString override { return Modrinth::debugName(); } + inline auto metaEntryBase() const -> QString override { return Modrinth::metaEntryBase(); } - [[nodiscard]] inline auto helpPage() const -> QString override { return ""; } + inline auto helpPage() const -> QString override { return ""; } }; class ModrinthTexturePackPage : public TexturePackResourcePage { @@ -137,15 +137,15 @@ class ModrinthTexturePackPage : public TexturePackResourcePage { ModrinthTexturePackPage(TexturePackDownloadDialog* dialog, BaseInstance& instance); ~ModrinthTexturePackPage() override = default; - [[nodiscard]] bool shouldDisplay() const override; + bool shouldDisplay() const override; - [[nodiscard]] inline auto displayName() const -> QString override { return Modrinth::displayName(); } - [[nodiscard]] inline auto icon() const -> QIcon override { return Modrinth::icon(); } - [[nodiscard]] inline auto id() const -> QString override { return Modrinth::id(); } - [[nodiscard]] inline auto debugName() const -> QString override { return Modrinth::debugName(); } - [[nodiscard]] inline auto metaEntryBase() const -> QString override { return Modrinth::metaEntryBase(); } + inline auto displayName() const -> QString override { return Modrinth::displayName(); } + inline auto icon() const -> QIcon override { return Modrinth::icon(); } + inline auto id() const -> QString override { return Modrinth::id(); } + inline auto debugName() const -> QString override { return Modrinth::debugName(); } + inline auto metaEntryBase() const -> QString override { return Modrinth::metaEntryBase(); } - [[nodiscard]] inline auto helpPage() const -> QString override { return ""; } + inline auto helpPage() const -> QString override { return ""; } }; class ModrinthShaderPackPage : public ShaderPackResourcePage { @@ -160,15 +160,15 @@ class ModrinthShaderPackPage : public ShaderPackResourcePage { ModrinthShaderPackPage(ShaderPackDownloadDialog* dialog, BaseInstance& instance); ~ModrinthShaderPackPage() override = default; - [[nodiscard]] bool shouldDisplay() const override; + bool shouldDisplay() const override; - [[nodiscard]] inline auto displayName() const -> QString override { return Modrinth::displayName(); } - [[nodiscard]] inline auto icon() const -> QIcon override { return Modrinth::icon(); } - [[nodiscard]] inline auto id() const -> QString override { return Modrinth::id(); } - [[nodiscard]] inline auto debugName() const -> QString override { return Modrinth::debugName(); } - [[nodiscard]] inline auto metaEntryBase() const -> QString override { return Modrinth::metaEntryBase(); } + inline auto displayName() const -> QString override { return Modrinth::displayName(); } + inline auto icon() const -> QIcon override { return Modrinth::icon(); } + inline auto id() const -> QString override { return Modrinth::id(); } + inline auto debugName() const -> QString override { return Modrinth::debugName(); } + inline auto metaEntryBase() const -> QString override { return Modrinth::metaEntryBase(); } - [[nodiscard]] inline auto helpPage() const -> QString override { return ""; } + inline auto helpPage() const -> QString override { return ""; } }; class ModrinthDataPackPage : public DataPackResourcePage { @@ -183,15 +183,15 @@ class ModrinthDataPackPage : public DataPackResourcePage { ModrinthDataPackPage(DataPackDownloadDialog* dialog, BaseInstance& instance); ~ModrinthDataPackPage() override = default; - [[nodiscard]] bool shouldDisplay() const override; + bool shouldDisplay() const override; - [[nodiscard]] inline auto displayName() const -> QString override { return Modrinth::displayName(); } - [[nodiscard]] inline auto icon() const -> QIcon override { return Modrinth::icon(); } - [[nodiscard]] inline auto id() const -> QString override { return Modrinth::id(); } - [[nodiscard]] inline auto debugName() const -> QString override { return Modrinth::debugName(); } - [[nodiscard]] inline auto metaEntryBase() const -> QString override { return Modrinth::metaEntryBase(); } + inline auto displayName() const -> QString override { return Modrinth::displayName(); } + inline auto icon() const -> QIcon override { return Modrinth::icon(); } + inline auto id() const -> QString override { return Modrinth::id(); } + inline auto debugName() const -> QString override { return Modrinth::debugName(); } + inline auto metaEntryBase() const -> QString override { return Modrinth::metaEntryBase(); } - [[nodiscard]] inline auto helpPage() const -> QString override { return ""; } + inline auto helpPage() const -> QString override { return ""; } }; } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/technic/TechnicModel.h b/launcher/ui/pages/modplatform/technic/TechnicModel.h index 09e9294bb..4979000e9 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicModel.h +++ b/launcher/ui/pages/modplatform/technic/TechnicModel.h @@ -58,8 +58,8 @@ class ListModel : public QAbstractListModel { void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback); void searchWithTerm(const QString& term); - [[nodiscard]] bool hasActiveSearchJob() const { return jobPtr && jobPtr->isRunning(); } - [[nodiscard]] Task::Ptr activeSearchJob() { return hasActiveSearchJob() ? jobPtr : nullptr; } + bool hasActiveSearchJob() const { return jobPtr && jobPtr->isRunning(); } + Task::Ptr activeSearchJob() { return hasActiveSearchJob() ? jobPtr : nullptr; } private slots: void searchRequestFinished(); diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.h b/launcher/ui/pages/modplatform/technic/TechnicPage.h index d1f691b22..71b6390ef 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.h +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.h @@ -74,7 +74,7 @@ class TechnicPage : public QWidget, public ModpackProviderBasePage { /** Programatically set the term in the search bar. */ virtual void setSearchTerm(QString) override; /** Get the current term in the search bar. */ - [[nodiscard]] virtual QString getSerachTerm() const override; + virtual QString getSerachTerm() const override; private: void suggestCurrent(); diff --git a/launcher/ui/widgets/WideBar.cpp b/launcher/ui/widgets/WideBar.cpp index 2940d7ce7..e87c8b4c1 100644 --- a/launcher/ui/widgets/WideBar.cpp +++ b/launcher/ui/widgets/WideBar.cpp @@ -250,7 +250,7 @@ void WideBar::addContextMenuAction(QAction* action) m_context_menu_actions.append(action); } -[[nodiscard]] QByteArray WideBar::getVisibilityState() const +QByteArray WideBar::getVisibilityState() const { QByteArray state; diff --git a/launcher/ui/widgets/WideBar.h b/launcher/ui/widgets/WideBar.h index f4877a89a..68a052a23 100644 --- a/launcher/ui/widgets/WideBar.h +++ b/launcher/ui/widgets/WideBar.h @@ -35,7 +35,7 @@ class WideBar : public QToolBar { // Ideally we would use a QBitArray for this, but it doesn't support string conversion, // so using it in settings is very messy. - [[nodiscard]] QByteArray getVisibilityState() const; + QByteArray getVisibilityState() const; void setVisibilityState(QByteArray&&); void removeAction(QAction* action); @@ -50,8 +50,8 @@ class WideBar : public QToolBar { auto getMatching(QAction* act) -> QList::iterator; /** Used to distinguish between versions of the WideBar with different actions */ - [[nodiscard]] QByteArray getHash() const; - [[nodiscard]] bool checkHash(QByteArray const&) const; + QByteArray getHash() const; + bool checkHash(QByteArray const&) const; private: QList m_entries; diff --git a/tests/DummyResourceAPI.h b/tests/DummyResourceAPI.h index f8ab71e59..d5ae1392d 100644 --- a/tests/DummyResourceAPI.h +++ b/tests/DummyResourceAPI.h @@ -32,9 +32,9 @@ class DummyResourceAPI : public ResourceAPI { } DummyResourceAPI() : ResourceAPI() {} - [[nodiscard]] auto getSortingMethods() const -> QList override { return {}; } + auto getSortingMethods() const -> QList override { return {}; } - [[nodiscard]] Task::Ptr searchProjects(SearchArgs&&, SearchCallbacks&& callbacks) const override + Task::Ptr searchProjects(SearchArgs&&, SearchCallbacks&& callbacks) const override { auto task = makeShared(); QObject::connect(task.get(), &Task::succeeded, [callbacks] { diff --git a/tests/ResourceModel_test.cpp b/tests/ResourceModel_test.cpp index 30bb99fb8..c4ea1a20f 100644 --- a/tests/ResourceModel_test.cpp +++ b/tests/ResourceModel_test.cpp @@ -40,7 +40,7 @@ class DummyResourceModel : public ResourceModel { DummyResourceModel() : ResourceModel(new DummyResourceAPI) {} ~DummyResourceModel() {} - [[nodiscard]] auto metaEntryBase() const -> QString override { return ""; } + auto metaEntryBase() const -> QString override { return ""; } ResourceAPI::SearchArgs createSearchArguments() override { return {}; } ResourceAPI::VersionSearchArgs createVersionsArguments(const QModelIndex&) override { return {}; } From 6d19984873817f9520fdecb110211cc92b1496ec Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Mon, 7 Jul 2025 20:56:36 +0100 Subject: [PATCH 349/695] Add [[nodiscard]] guidelines Signed-off-by: TheKodeToad --- CONTRIBUTING.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5965f4d8e..fdc79faf2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributions Guidelines -## Code formatting +## Code style All files are formatted with `clang-format` using the configuration in `.clang-format`. Ensure it is run on changed files before committing! @@ -15,6 +15,11 @@ Please also follow the project's conventions for C++: - Global functions and non-`const` global variables should be formatted as `camelCase` without a prefix: `globalData`. - `const` global variables, macros, and enum constants should be formatted as `SCREAMING_SNAKE_CASE`: `LIGHT_GRAY`. - Avoid inventing acronyms or abbreviations especially for a name of multiple words - like `tp` for `texturePack`. +- Avoid using `[[nodiscard]]` unless ignoring the return value is likely to cause a bug in cases such as: + - A function allocates memory or another resource and the caller needs to clean it up. + - A function has side effects and an error status is returned. + - A function is likely be mistaken for having side effects. +- A plain getter is unlikely to cause confusion and adding `[[nodiscard]]` can create clutter and inconsistency. Most of these rules are included in the `.clang-tidy` file, so you can run `clang-tidy` to check for any violations. From 91abebbb596e5c57cc3f3beb946a3e00bb2145db Mon Sep 17 00:00:00 2001 From: Seth Flynn Date: Fri, 4 Jul 2025 02:13:47 -0400 Subject: [PATCH 350/695] build: let cmake know when we're cross compiling on msvc This (unsurprisingly) makes some things actually work as they're expected to when cross compiling, like windeployqt Signed-off-by: Seth Flynn --- cmake/windowsMSVCPreset.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cmake/windowsMSVCPreset.json b/cmake/windowsMSVCPreset.json index 278f09b94..2cb996b81 100644 --- a/cmake/windowsMSVCPreset.json +++ b/cmake/windowsMSVCPreset.json @@ -20,7 +20,10 @@ "hidden": true, "inherits": [ "windows_msvc_base" - ] + ], + "cacheVariables": { + "CMAKE_SYSTEM_NAME": "${hostSystemName}" + } }, { "name": "windows_msvc_debug", From 1688db055ece8bc1430241e7b2ff68eddfb3ea6d Mon Sep 17 00:00:00 2001 From: seth Date: Fri, 2 May 2025 05:43:59 -0400 Subject: [PATCH 351/695] build: modernize launcher bundling Replaces fixup_bundle with Qt's deployment scripts and CMake's newer RUNTIME_DEPENDENCY_SET target, making it a bit easier to find and include linked dependencies with less code on our end Signed-off-by: seth --- CMakeLists.txt | 18 --- launcher/CMakeLists.txt | 212 ++++++------------------------ launcher/install_prereqs.cmake.in | 26 ---- 3 files changed, 43 insertions(+), 213 deletions(-) delete mode 100644 launcher/install_prereqs.cmake.in diff --git a/CMakeLists.txt b/CMakeLists.txt index a4d6df243..9ddf6b71a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -385,9 +385,6 @@ if(UNIX AND APPLE) set(RESOURCES_DEST_DIR "${Launcher_Name}.app/Contents/Resources") set(JARS_DEST_DIR "${Launcher_Name}.app/Contents/MacOS/jars") - # Apps to bundle - set(APPS "\${CMAKE_INSTALL_PREFIX}/${Launcher_Name}.app") - # Mac bundle settings set(MACOSX_BUNDLE_BUNDLE_NAME "${Launcher_DisplayName}") set(MACOSX_BUNDLE_INFO_STRING "${Launcher_DisplayName}: A custom launcher for Minecraft that allows you to easily manage multiple installations of Minecraft at once.") @@ -404,9 +401,6 @@ if(UNIX AND APPLE) set(MACOSX_SPARKLE_SHA256 "50612a06038abc931f16011d7903b8326a362c1074dabccb718404ce8e585f0b" CACHE STRING "SHA256 checksum for Sparkle release archive") set(MACOSX_SPARKLE_DIR "${CMAKE_BINARY_DIR}/frameworks/Sparkle") - # directories to look for dependencies - set(DIRS ${QT_LIBS_DIR} ${QT_LIBEXECS_DIR} ${CMAKE_LIBRARY_OUTPUT_DIRECTORY} ${CMAKE_RUNTIME_OUTPUT_DIRECTORY} ${MACOSX_SPARKLE_DIR}) - if(NOT MACOSX_SPARKLE_UPDATE_PUBLIC_KEY STREQUAL "" AND NOT MACOSX_SPARKLE_UPDATE_FEED_URL STREQUAL "") set(Launcher_ENABLE_UPDATER YES) endif() @@ -441,12 +435,6 @@ elseif(UNIX) set(PLUGIN_DEST_DIR "plugins") set(BUNDLE_DEST_DIR ".") set(RESOURCES_DEST_DIR ".") - - # Apps to bundle - set(APPS "\${CMAKE_INSTALL_PREFIX}/bin/${Launcher_APP_BINARY_NAME}") - - # directories to look for dependencies - set(DIRS ${QT_LIBS_DIR} ${QT_LIBEXECS_DIR} ${CMAKE_LIBRARY_OUTPUT_DIRECTORY} ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}) endif() if(Launcher_ManPage) @@ -464,12 +452,6 @@ elseif(WIN32) set(RESOURCES_DEST_DIR ".") set(JARS_DEST_DIR "jars") - # Apps to bundle - set(APPS "\${CMAKE_INSTALL_PREFIX}/${Launcher_Name}.exe") - - # directories to look for dependencies - set(DIRS ${QT_LIBS_DIR} ${QT_LIBEXECS_DIR} ${CMAKE_LIBRARY_OUTPUT_DIRECTORY} ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}) - # install as bundle set(INSTALL_BUNDLE "full" CACHE STRING "Use fixup_bundle to bundle dependencies") else() diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 6204acabd..4555d6194 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -1385,6 +1385,7 @@ if(DEFINED Launcher_APP_BINARY_DEFS) endif() install(TARGETS ${Launcher_Name} + RUNTIME_DEPENDENCY_SET LAUNCHER_DEPENDENCY_SET BUNDLE DESTINATION "." COMPONENT Runtime LIBRARY DESTINATION ${LIBRARY_DEST_DIR} COMPONENT Runtime RUNTIME DESTINATION ${BINARY_DEST_DIR} COMPONENT Runtime @@ -1498,183 +1499,56 @@ endif() # Bundle utilities are used to complete the portable packages - they add all the libraries that would otherwise be missing on the target system. # NOTE: it seems that this absolutely has to be here, and nowhere else. if(INSTALL_BUNDLE STREQUAL "full") + if(WIN32) + set(QT_DEPLOY_TOOL_OPTIONS "--no-opengl-sw --no-quick-import --no-system-d3d-compiler --no-system-dxc-compiler --skip-plugin-types generic,networkinformation") + endif() + + qt_generate_deploy_script( + TARGET ${Launcher_Name} + OUTPUT_SCRIPT QT_DEPLOY_SCRIPT + CONTENT " + qt_deploy_runtime_dependencies( + EXECUTABLE ${BINARY_DEST_DIR}/$ + BIN_DIR ${BINARY_DEST_DIR} + LIBEXEC_DIR ${LIBRARY_DEST_DIR} + LIB_DIR ${LIBRARY_DEST_DIR} + PLUGINS_DIR ${PLUGIN_DEST_DIR} + NO_OVERWRITE + NO_TRANSLATIONS + NO_COMPILER_RUNTIME + DEPLOY_TOOL_OPTIONS ${QT_DEPLOY_TOOL_OPTIONS} + )" + ) + + # Bundle our linked dependencies + install(RUNTIME_DEPENDENCY_SET LAUNCHER_DEPENDENCY_SET + DIRECTORIES + ${CMAKE_SYSTEM_LIBRARY_PATH} + ${QT_LIBS_DIR} + ${QT_LIBEXECS_DIR} + PRE_EXCLUDE_REGEXES + "^(api-ms-win|ext-ms)-.*\\.dll$" + # FIXME: Why aren't these caught by the below regex??? + "^azure.*\\.dll$" + "^vcruntime.*\\.dll$" + POST_EXCLUDE_REGEXES + "system32" + LIBRARY DESTINATION ${LIBRARY_DEST_DIR} + RUNTIME DESTINATION ${BINARY_DEST_DIR} + FRAMEWORK DESTINATION ${FRAMEWORK_DEST_DIR} + ) + # Deploy Qt plugins + install(SCRIPT ${QT_DEPLOY_SCRIPT}) + # Add qt.conf - this makes Qt stop looking for things outside the bundle install( CODE "file(WRITE \"\${CMAKE_INSTALL_PREFIX}/${RESOURCES_DEST_DIR}/qt.conf\" \" \")" COMPONENT Runtime ) - # add qtlogging.ini as a config file + # Add qtlogging.ini as a config file install( FILES "qtlogging.ini" DESTINATION ${CMAKE_INSTALL_PREFIX}/${RESOURCES_DEST_DIR} COMPONENT Runtime ) - # Bundle plugins - # Image formats - install( - DIRECTORY "${QT_PLUGINS_DIR}/imageformats" - CONFIGURATIONS Debug RelWithDebInfo "" - DESTINATION ${PLUGIN_DEST_DIR} - COMPONENT Runtime - REGEX "tga|tiff|mng" EXCLUDE - ) - install( - DIRECTORY "${QT_PLUGINS_DIR}/imageformats" - CONFIGURATIONS Release MinSizeRel - DESTINATION ${PLUGIN_DEST_DIR} - COMPONENT Runtime - REGEX "tga|tiff|mng" EXCLUDE - REGEX "d\\." EXCLUDE - REGEX "_debug\\." EXCLUDE - REGEX "\\.dSYM" EXCLUDE - ) - # Icon engines - install( - DIRECTORY "${QT_PLUGINS_DIR}/iconengines" - CONFIGURATIONS Debug RelWithDebInfo "" - DESTINATION ${PLUGIN_DEST_DIR} - COMPONENT Runtime - REGEX "fontawesome" EXCLUDE - ) - install( - DIRECTORY "${QT_PLUGINS_DIR}/iconengines" - CONFIGURATIONS Release MinSizeRel - DESTINATION ${PLUGIN_DEST_DIR} - COMPONENT Runtime - REGEX "fontawesome" EXCLUDE - REGEX "d\\." EXCLUDE - REGEX "_debug\\." EXCLUDE - REGEX "\\.dSYM" EXCLUDE - ) - # Platform plugins - install( - DIRECTORY "${QT_PLUGINS_DIR}/platforms" - CONFIGURATIONS Debug RelWithDebInfo "" - DESTINATION ${PLUGIN_DEST_DIR} - COMPONENT Runtime - REGEX "minimal|linuxfb|offscreen" EXCLUDE - ) - install( - DIRECTORY "${QT_PLUGINS_DIR}/platforms" - CONFIGURATIONS Release MinSizeRel - DESTINATION ${PLUGIN_DEST_DIR} - COMPONENT Runtime - REGEX "minimal|linuxfb|offscreen" EXCLUDE - REGEX "[^2]d\\." EXCLUDE - REGEX "_debug\\." EXCLUDE - REGEX "\\.dSYM" EXCLUDE - ) - # Style plugins - if(EXISTS "${QT_PLUGINS_DIR}/styles") - install( - DIRECTORY "${QT_PLUGINS_DIR}/styles" - CONFIGURATIONS Debug RelWithDebInfo "" - DESTINATION ${PLUGIN_DEST_DIR} - COMPONENT Runtime - ) - install( - DIRECTORY "${QT_PLUGINS_DIR}/styles" - CONFIGURATIONS Release MinSizeRel - DESTINATION ${PLUGIN_DEST_DIR} - COMPONENT Runtime - REGEX "d\\." EXCLUDE - REGEX "_debug\\." EXCLUDE - REGEX "\\.dSYM" EXCLUDE - ) - endif() - # TLS plugins (Qt 6 only) - if(EXISTS "${QT_PLUGINS_DIR}/tls") - install( - DIRECTORY "${QT_PLUGINS_DIR}/tls" - CONFIGURATIONS Debug RelWithDebInfo "" - DESTINATION ${PLUGIN_DEST_DIR} - COMPONENT Runtime - PATTERN "*qcertonlybackend*" EXCLUDE - ) - install( - DIRECTORY "${QT_PLUGINS_DIR}/tls" - CONFIGURATIONS Release MinSizeRel - DESTINATION ${PLUGIN_DEST_DIR} - COMPONENT Runtime - REGEX "dd\\." EXCLUDE - REGEX "_debug\\." EXCLUDE - REGEX "\\.dSYM" EXCLUDE - PATTERN "*qcertonlybackend*" EXCLUDE - ) - endif() - # Wayland support - if(EXISTS "${QT_PLUGINS_DIR}/wayland-graphics-integration-client") - install( - DIRECTORY "${QT_PLUGINS_DIR}/wayland-graphics-integration-client" - CONFIGURATIONS Debug RelWithDebInfo "" - DESTINATION ${PLUGIN_DEST_DIR} - COMPONENT Runtime - ) - install( - DIRECTORY "${QT_PLUGINS_DIR}/wayland-graphics-integration-client" - CONFIGURATIONS Release MinSizeRel - DESTINATION ${PLUGIN_DEST_DIR} - COMPONENT Runtime - REGEX "dd\\." EXCLUDE - REGEX "_debug\\." EXCLUDE - REGEX "\\.dSYM" EXCLUDE - ) - endif() - if(EXISTS "${QT_PLUGINS_DIR}/wayland-graphics-integration-server") - install( - DIRECTORY "${QT_PLUGINS_DIR}/wayland-graphics-integration-server" - CONFIGURATIONS Debug RelWithDebInfo "" - DESTINATION ${PLUGIN_DEST_DIR} - COMPONENT Runtime - ) - install( - DIRECTORY "${QT_PLUGINS_DIR}/wayland-graphics-integration-server" - CONFIGURATIONS Release MinSizeRel - DESTINATION ${PLUGIN_DEST_DIR} - COMPONENT Runtime - REGEX "dd\\." EXCLUDE - REGEX "_debug\\." EXCLUDE - REGEX "\\.dSYM" EXCLUDE - ) - endif() - if(EXISTS "${QT_PLUGINS_DIR}/wayland-decoration-client") - install( - DIRECTORY "${QT_PLUGINS_DIR}/wayland-decoration-client" - CONFIGURATIONS Debug RelWithDebInfo "" - DESTINATION ${PLUGIN_DEST_DIR} - COMPONENT Runtime - ) - install( - DIRECTORY "${QT_PLUGINS_DIR}/wayland-decoration-client" - CONFIGURATIONS Release MinSizeRel - DESTINATION ${PLUGIN_DEST_DIR} - COMPONENT Runtime - REGEX "dd\\." EXCLUDE - REGEX "_debug\\." EXCLUDE - REGEX "\\.dSYM" EXCLUDE - ) - endif() - if(EXISTS "${QT_PLUGINS_DIR}/wayland-shell-integration") - install( - DIRECTORY "${QT_PLUGINS_DIR}/wayland-shell-integration" - CONFIGURATIONS Debug RelWithDebInfo "" - DESTINATION ${PLUGIN_DEST_DIR} - COMPONENT Runtime - ) - install( - DIRECTORY "${QT_PLUGINS_DIR}/wayland-shell-integration" - CONFIGURATIONS Release MinSizeRel - DESTINATION ${PLUGIN_DEST_DIR} - COMPONENT Runtime - REGEX "dd\\." EXCLUDE - REGEX "_debug\\." EXCLUDE - REGEX "\\.dSYM" EXCLUDE - ) - endif() - configure_file( - "${CMAKE_CURRENT_SOURCE_DIR}/install_prereqs.cmake.in" - "${CMAKE_CURRENT_BINARY_DIR}/install_prereqs.cmake" - @ONLY - ) - install(SCRIPT "${CMAKE_CURRENT_BINARY_DIR}/install_prereqs.cmake" COMPONENT Runtime) endif() diff --git a/launcher/install_prereqs.cmake.in b/launcher/install_prereqs.cmake.in deleted file mode 100644 index acbce9650..000000000 --- a/launcher/install_prereqs.cmake.in +++ /dev/null @@ -1,26 +0,0 @@ -set(CMAKE_MODULE_PATH "@CMAKE_MODULE_PATH@") -file(GLOB_RECURSE QTPLUGINS "${CMAKE_INSTALL_PREFIX}/@PLUGIN_DEST_DIR@/*@CMAKE_SHARED_LIBRARY_SUFFIX@") -function(gp_resolved_file_type_override resolved_file type_var) - if(resolved_file MATCHES "^/(usr/)?lib/libQt") - set(${type_var} other PARENT_SCOPE) - elseif(resolved_file MATCHES "^/(usr/)?lib(.+)?/libxcb-") - set(${type_var} other PARENT_SCOPE) - elseif(resolved_file MATCHES "^/(usr/)?lib(.+)?/libicu") - set(${type_var} other PARENT_SCOPE) - elseif(resolved_file MATCHES "^/(usr/)?lib(.+)?/libpng") - set(${type_var} other PARENT_SCOPE) - elseif(resolved_file MATCHES "^/(usr/)?lib(.+)?/libproxy") - set(${type_var} other PARENT_SCOPE) - elseif((resolved_file MATCHES "^/(usr/)?lib(.+)?/libstdc\\+\\+") AND (UNIX AND NOT APPLE)) - set(${type_var} other PARENT_SCOPE) - endif() -endfunction() - -set(gp_tool "@CMAKE_GP_TOOL@") -set(gp_cmd_paths ${gp_cmd_paths} - "@CMAKE_GP_CMD_PATHS@" -) - -include(BundleUtilities) -fixup_bundle("@APPS@" "${QTPLUGINS}" "@DIRS@") - From f3b778342e99ff4d842ee06ac730873365ec7b74 Mon Sep 17 00:00:00 2001 From: Seth Flynn Date: Tue, 8 Jul 2025 11:01:50 -0400 Subject: [PATCH 352/695] build(cmake): replace INSTALL_BUNDLE with install component Considering this doesn't affect the build, it has never made much sense for it to be a build option or require rereconfiguration of the project to change Signed-off-by: Seth Flynn --- CMakeLists.txt | 17 +++-------------- launcher/CMakeLists.txt | 17 +++++++++++------ 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9ddf6b71a..73f1710b7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -405,9 +405,6 @@ if(UNIX AND APPLE) set(Launcher_ENABLE_UPDATER YES) endif() - # install as bundle - set(INSTALL_BUNDLE "full" CACHE STRING "Use fixup_bundle to bundle dependencies") - # Add the icon install(FILES ${Launcher_Branding_ICNS} DESTINATION ${RESOURCES_DEST_DIR} RENAME ${Launcher_Name}.icns) @@ -418,9 +415,6 @@ elseif(UNIX) set(LIBRARY_DEST_DIR "lib${LIB_SUFFIX}") set(JARS_DEST_DIR "share/${Launcher_Name}") - # install as bundle with no dependencies included - set(INSTALL_BUNDLE "nodeps" CACHE STRING "Use fixup_bundle to bundle dependencies") - # Set RPATH SET(Launcher_BINARY_RPATH "$ORIGIN/") @@ -431,11 +425,9 @@ elseif(UNIX) install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/launcher/qtlogging.ini" DESTINATION "share/${Launcher_Name}") - if (INSTALL_BUNDLE STREQUAL full) - set(PLUGIN_DEST_DIR "plugins") - set(BUNDLE_DEST_DIR ".") - set(RESOURCES_DEST_DIR ".") - endif() + set(PLUGIN_DEST_DIR "plugins") + set(BUNDLE_DEST_DIR ".") + set(RESOURCES_DEST_DIR ".") if(Launcher_ManPage) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${Launcher_ManPage} DESTINATION "${KDE_INSTALL_MANDIR}/man6") @@ -451,9 +443,6 @@ elseif(WIN32) set(PLUGIN_DEST_DIR ".") set(RESOURCES_DEST_DIR ".") set(JARS_DEST_DIR "jars") - - # install as bundle - set(INSTALL_BUNDLE "full" CACHE STRING "Use fixup_bundle to bundle dependencies") else() message(FATAL_ERROR "Platform not supported") endif() diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 4555d6194..c1bc83b59 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -1496,9 +1496,9 @@ if (UNIX AND APPLE AND Launcher_ENABLE_UPDATER) endif() #### The bundle mess! #### -# Bundle utilities are used to complete the portable packages - they add all the libraries that would otherwise be missing on the target system. +# Bundle utilities are used to complete packages for different platforms - they add all the libraries that would otherwise be missing on the target system. # NOTE: it seems that this absolutely has to be here, and nowhere else. -if(INSTALL_BUNDLE STREQUAL "full") +if(WIN32 OR (UNIX AND APPLE)) if(WIN32) set(QT_DEPLOY_TOOL_OPTIONS "--no-opengl-sw --no-quick-import --no-system-d3d-compiler --no-system-dxc-compiler --skip-plugin-types generic,networkinformation") endif() @@ -1521,7 +1521,9 @@ if(INSTALL_BUNDLE STREQUAL "full") ) # Bundle our linked dependencies - install(RUNTIME_DEPENDENCY_SET LAUNCHER_DEPENDENCY_SET + install( + RUNTIME_DEPENDENCY_SET LAUNCHER_DEPENDENCY_SET + COMPONENT bundle DIRECTORIES ${CMAKE_SYSTEM_LIBRARY_PATH} ${QT_LIBS_DIR} @@ -1538,17 +1540,20 @@ if(INSTALL_BUNDLE STREQUAL "full") FRAMEWORK DESTINATION ${FRAMEWORK_DEST_DIR} ) # Deploy Qt plugins - install(SCRIPT ${QT_DEPLOY_SCRIPT}) + install( + SCRIPT ${QT_DEPLOY_SCRIPT} + COMPONENT bundle + ) # Add qt.conf - this makes Qt stop looking for things outside the bundle install( CODE "file(WRITE \"\${CMAKE_INSTALL_PREFIX}/${RESOURCES_DEST_DIR}/qt.conf\" \" \")" - COMPONENT Runtime + COMPONENT bundle ) # Add qtlogging.ini as a config file install( FILES "qtlogging.ini" DESTINATION ${CMAKE_INSTALL_PREFIX}/${RESOURCES_DEST_DIR} - COMPONENT Runtime + COMPONENT bundle ) endif() From a5f5d14538fcfa3d077b039ecc993aba0c07d710 Mon Sep 17 00:00:00 2001 From: seth Date: Mon, 5 May 2025 11:54:20 -0400 Subject: [PATCH 353/695] build(cmake): fallback to pkg-config discovery for tomlplusplus Some distributions of it (like in vcpkg *wink*) won't contain CMake files Signed-off-by: seth --- CMakeLists.txt | 8 ++++++++ launcher/CMakeLists.txt | 6 +++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a4d6df243..10b0fcd08 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -348,6 +348,14 @@ endif() if(NOT Launcher_FORCE_BUNDLED_LIBS) # Find toml++ find_package(tomlplusplus 3.2.0 QUIET) + # Fallback to pkg-config (if available) if CMake files aren't found + if(NOT tomlplusplus_FOUND) + find_package(PkgConfig) + if(PkgConfig_FOUND) + pkg_check_modules(tomlplusplus IMPORTED_TARGET tomlplusplus>=3.2.0) + endif() + endif() + # Find cmark find_package(cmark QUIET) diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 6204acabd..7d96ffc38 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -1309,12 +1309,16 @@ target_link_libraries(Launcher_logic Launcher_murmur2 nbt++ ${ZLIB_LIBRARIES} - tomlplusplus::tomlplusplus qdcss BuildConfig Qt${QT_VERSION_MAJOR}::Widgets qrcodegenerator ) +if(TARGET PkgConfig::tomlplusplus) + target_link_libraries(Launcher_logic PkgConfig::tomlplusplus) +else() + target_link_libraries(Launcher_logic tomlplusplus::tomlplusplus) +endif() if (UNIX AND NOT CYGWIN AND NOT APPLE) target_link_libraries(Launcher_logic From 7f78f6b85fd2621b3abb36c081d5b07e0b8c81fd Mon Sep 17 00:00:00 2001 From: seth Date: Thu, 1 May 2025 21:04:37 -0400 Subject: [PATCH 354/695] build: add support for vcpkg Signed-off-by: seth --- vcpkg-configuration.json | 14 ++++++++++++++ vcpkg.json | 10 ++++++++++ 2 files changed, 24 insertions(+) create mode 100644 vcpkg-configuration.json create mode 100644 vcpkg.json diff --git a/vcpkg-configuration.json b/vcpkg-configuration.json new file mode 100644 index 000000000..610f6b31d --- /dev/null +++ b/vcpkg-configuration.json @@ -0,0 +1,14 @@ +{ + "default-registry": { + "kind": "git", + "baseline": "0c4cf19224a049cf82f4521e29e39f7bd680440c", + "repository": "https://github.com/microsoft/vcpkg" + }, + "registries": [ + { + "kind": "artifact", + "location": "https://github.com/microsoft/vcpkg-ce-catalog/archive/refs/heads/main.zip", + "name": "microsoft" + } + ] +} diff --git a/vcpkg.json b/vcpkg.json new file mode 100644 index 000000000..4abf8d1b7 --- /dev/null +++ b/vcpkg.json @@ -0,0 +1,10 @@ +{ + "dependencies": [ + "bzip2", + "cmark", + { "name": "ecm", "host": true }, + { "name": "pkgconf", "host": true }, + "tomlplusplus", + "zlib" + ] +} From 463cf431610a04a3c7ec4a90f4d83389fdbae2c1 Mon Sep 17 00:00:00 2001 From: Seth Flynn Date: Mon, 5 May 2025 13:00:20 -0400 Subject: [PATCH 355/695] ci(setup-deps/windows): use vcpkg for msvc Signed-off-by: Seth Flynn --- .../setup-dependencies/windows/action.yml | 25 +++++++++++++++++++ .github/workflows/build.yml | 4 +++ 2 files changed, 29 insertions(+) diff --git a/.github/actions/setup-dependencies/windows/action.yml b/.github/actions/setup-dependencies/windows/action.yml index 0a643f583..e899c36d6 100644 --- a/.github/actions/setup-dependencies/windows/action.yml +++ b/.github/actions/setup-dependencies/windows/action.yml @@ -25,6 +25,31 @@ runs: arch: ${{ inputs.vcvars-arch }} vsversion: 2022 + - name: Setup vcpkg cache (MSVC) + if: ${{ inputs.msystem == '' && inputs.build-type == 'Debug' }} + shell: pwsh + env: + USERNAME: ${{ github.repository_owner }} + FEED_URL: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + run: | + .$(vcpkg fetch nuget) ` + sources add ` + -Source "$env:FEED_URL" ` + -StorePasswordInClearText ` + -Name GitHubPackages ` + -UserName "$env:USERNAME" ` + -Password "$env:GITHUB_TOKEN" + .$(vcpkg fetch nuget) ` + setapikey "$env:GITHUB_TOKEN" ` + -Source "$env:FEED_URL" + Write-Output "VCPKG_BINARY_SOURCES=clear;nuget,$env:FEED_URL,readwrite" >> "$GITHUB_ENV" + + - name: Setup vcpkg environment (MSVC) + if: ${{ inputs.msystem == '' }} + shell: bash + run: | + echo "CMAKE_TOOLCHAIN_FILE=$VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake" >> "$GITHUB_ENV" + - name: Setup MSYS2 (MinGW) if: ${{ inputs.msystem != '' }} uses: msys2/setup-msys2@v2 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 15de6f70f..b8ba57a45 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -69,6 +69,10 @@ jobs: build: name: Build (${{ matrix.artifact-name }}) + permissions: + # Required for vcpkg binary cache + packages: write + strategy: fail-fast: false matrix: From 20a833e1b952ed75480ca2ad8cdecdce3ed0ab4c Mon Sep 17 00:00:00 2001 From: matthewperiut Date: Thu, 10 Jul 2025 11:07:17 -0400 Subject: [PATCH 356/695] Add "Babric" and "Babric (BTA)" as shown on Modrinth. Add "Show More" button to allow for space efficiency and readiness to add all other Modrinth modloader types. Signed-off-by: matthewperiut --- launcher/modplatform/ModIndex.cpp | 10 +++- launcher/modplatform/ModIndex.h | 4 +- launcher/modplatform/flame/FlameAPI.h | 2 + .../import_ftb/PackInstallTask.cpp | 4 ++ launcher/modplatform/modrinth/ModrinthAPI.h | 4 +- launcher/ui/widgets/ModFilterWidget.cpp | 24 ++++++++-- launcher/ui/widgets/ModFilterWidget.h | 1 + launcher/ui/widgets/ModFilterWidget.ui | 46 ++++++++++++++++++- 8 files changed, 85 insertions(+), 10 deletions(-) diff --git a/launcher/modplatform/ModIndex.cpp b/launcher/modplatform/ModIndex.cpp index e18ccaefa..edb5e5aa1 100644 --- a/launcher/modplatform/ModIndex.cpp +++ b/launcher/modplatform/ModIndex.cpp @@ -31,7 +31,7 @@ static const QMap s_indexed_version_ty { "alpha", IndexedVersionType::VersionType::Alpha } }; -static const QList loaderList = { NeoForge, Forge, Cauldron, LiteLoader, Quilt, Fabric }; +static const QList loaderList = { NeoForge, Forge, Cauldron, LiteLoader, Quilt, Fabric, Babric, BTA }; QList modLoaderTypesToList(ModLoaderTypes flags) { @@ -129,6 +129,10 @@ auto getModLoaderAsString(ModLoaderType type) -> const QString return "quilt"; case DataPack: return "datapack"; + case Babric: + return "babric"; + case BTA: + return "bta-babric"; default: break; } @@ -149,6 +153,10 @@ auto getModLoaderFromString(QString type) -> ModLoaderType return Fabric; if (type == "quilt") return Quilt; + if (type == "babric") + return Babric; + if (type == "bta-babric") + return BTA; return {}; } diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h index cfe4eba75..7c4d1c885 100644 --- a/launcher/modplatform/ModIndex.h +++ b/launcher/modplatform/ModIndex.h @@ -36,7 +36,9 @@ enum ModLoaderType { LiteLoader = 1 << 3, Fabric = 1 << 4, Quilt = 1 << 5, - DataPack = 1 << 6 + DataPack = 1 << 6, + Babric = 1 << 7, + BTA = 1 << 8 }; Q_DECLARE_FLAGS(ModLoaderTypes, ModLoaderType) QList modLoaderTypesToList(ModLoaderTypes flags); diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index 316d2e9c9..c578f7ae6 100644 --- a/launcher/modplatform/flame/FlameAPI.h +++ b/launcher/modplatform/flame/FlameAPI.h @@ -71,6 +71,8 @@ class FlameAPI : public NetworkResourceAPI { case ModPlatform::NeoForge: return 6; case ModPlatform::DataPack: + case ModPlatform::Babric: + case ModPlatform::BTA: break; // not supported } return 0; diff --git a/launcher/modplatform/import_ftb/PackInstallTask.cpp b/launcher/modplatform/import_ftb/PackInstallTask.cpp index 7cb8b6ebc..9ddca008d 100644 --- a/launcher/modplatform/import_ftb/PackInstallTask.cpp +++ b/launcher/modplatform/import_ftb/PackInstallTask.cpp @@ -91,6 +91,10 @@ void PackInstallTask::copySettings() break; case ModPlatform::DataPack: break; + case ModPlatform::Babric: + break; + case ModPlatform::BTA: + break; } components->saveNow(); diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index 7c2592256..3c2da0651 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -43,7 +43,7 @@ class ModrinthAPI : public NetworkResourceAPI { { QStringList l; for (auto loader : { ModPlatform::NeoForge, ModPlatform::Forge, ModPlatform::Fabric, ModPlatform::Quilt, ModPlatform::LiteLoader, - ModPlatform::DataPack }) { + ModPlatform::DataPack, ModPlatform::Babric, ModPlatform::BTA }) { if (types & loader) { l << getModLoaderAsString(loader); } @@ -202,7 +202,7 @@ class ModrinthAPI : public NetworkResourceAPI { static inline auto validateModLoaders(ModPlatform::ModLoaderTypes loaders) -> bool { return loaders & (ModPlatform::NeoForge | ModPlatform::Forge | ModPlatform::Fabric | ModPlatform::Quilt | ModPlatform::LiteLoader | - ModPlatform::DataPack); + ModPlatform::DataPack | ModPlatform::Babric | ModPlatform::BTA); } [[nodiscard]] std::optional getDependencyURL(DependencySearchArgs const& args) const override diff --git a/launcher/ui/widgets/ModFilterWidget.cpp b/launcher/ui/widgets/ModFilterWidget.cpp index 3c6a2db07..654eb75b1 100644 --- a/launcher/ui/widgets/ModFilterWidget.cpp +++ b/launcher/ui/widgets/ModFilterWidget.cpp @@ -149,10 +149,16 @@ ModFilterWidget::ModFilterWidget(MinecraftInstance* instance, bool extended) connect(ui->forge, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged); connect(ui->fabric, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged); connect(ui->quilt, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged); - if (extended) - connect(ui->liteLoader, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged); - else - ui->liteLoader->setVisible(false); + connect(ui->liteLoader, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged); + connect(ui->babric, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged); + connect(ui->btaBabric, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged); + + connect(ui->showMoreButton, &QPushButton::clicked, this, &ModFilterWidget::onShowMoreClicked); + + if (!extended) { + ui->showMoreButton->setVisible(false); + ui->extendedModLoadersWidget->setVisible(false); + } if (extended) { connect(ui->clientSide, &QCheckBox::stateChanged, this, &ModFilterWidget::onSideFilterChanged); @@ -279,6 +285,10 @@ void ModFilterWidget::onLoadersFilterChanged() loaders |= ModPlatform::Quilt; if (ui->liteLoader->isChecked()) loaders |= ModPlatform::LiteLoader; + if (ui->babric->isChecked()) + loaders |= ModPlatform::Babric; + if (ui->btaBabric->isChecked()) + loaders |= ModPlatform::BTA; m_filter_changed = loaders != m_filter->loaders; m_filter->loaders = loaders; if (m_filter_changed) @@ -381,4 +391,10 @@ void ModFilterWidget::onReleaseFilterChanged() emit filterChanged(); } +void ModFilterWidget::onShowMoreClicked() +{ + ui->extendedModLoadersWidget->setVisible(true); + ui->showMoreButton->setVisible(false); +} + #include "ModFilterWidget.moc" diff --git a/launcher/ui/widgets/ModFilterWidget.h b/launcher/ui/widgets/ModFilterWidget.h index be60ba70a..8a858fd30 100644 --- a/launcher/ui/widgets/ModFilterWidget.h +++ b/launcher/ui/widgets/ModFilterWidget.h @@ -110,6 +110,7 @@ class ModFilterWidget : public QTabWidget { void onShowAllVersionsChanged(); void onOpenSourceFilterChanged(); void onReleaseFilterChanged(); + void onShowMoreClicked(); private: Ui::ModFilterWidget* ui; diff --git a/launcher/ui/widgets/ModFilterWidget.ui b/launcher/ui/widgets/ModFilterWidget.ui index 788202714..87d9af2e3 100644 --- a/launcher/ui/widgets/ModFilterWidget.ui +++ b/launcher/ui/widgets/ModFilterWidget.ui @@ -122,12 +122,54 @@
    - + - LiteLoader + Show More + + + + false + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + LiteLoader + + + + + + + Babric + + + + + + + BTA (Babric) + + + + + + From 9ce6d3571e8924f7a27daaf8c2e301e9d60d47a1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 11 Jul 2025 22:00:35 +0000 Subject: [PATCH 357/695] chore(deps): update cachix/install-nix-action digest to cebd211 --- .github/workflows/update-flake.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/update-flake.yml b/.github/workflows/update-flake.yml index 123028ff2..ce86ff798 100644 --- a/.github/workflows/update-flake.yml +++ b/.github/workflows/update-flake.yml @@ -17,7 +17,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31 + - uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31 - uses: DeterminateSystems/update-flake-lock@v26 with: From 5fb6022b472f454a573be97d0f03272b643badc2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 12 Jul 2025 13:50:35 +0000 Subject: [PATCH 358/695] chore(deps): update cachix/install-nix-action digest to f0fe604 --- .github/workflows/update-flake.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/update-flake.yml b/.github/workflows/update-flake.yml index ce86ff798..123028ff2 100644 --- a/.github/workflows/update-flake.yml +++ b/.github/workflows/update-flake.yml @@ -17,7 +17,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: cachix/install-nix-action@cebd211ec2008b83bda8fb0b21c3c072f004fe04 # v31 + - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31 - uses: DeterminateSystems/update-flake-lock@v26 with: From f5fffd27ab466514fd41ba757f9bb7525de775cc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 13 Jul 2025 00:31:57 +0000 Subject: [PATCH 359/695] chore(nix): update lockfile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'nixpkgs': 'github:NixOS/nixpkgs/5c724ed1388e53cc231ed98330a60eb2f7be4be3?narHash=sha256-xVNy/XopSfIG9c46nRmPaKfH1Gn/56vQ8%2B%2BxWA8itO4%3D' (2025-07-04) → 'github:NixOS/nixpkgs/9807714d6944a957c2e036f84b0ff8caf9930bc0?narHash=sha256-LwWRsENAZJKUdD3SpLluwDmdXY9F45ZEgCb0X%2BxgOL0%3D' (2025-07-08) --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 17b77e22b..b5f6258e1 100644 --- a/flake.lock +++ b/flake.lock @@ -18,11 +18,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1751637120, - "narHash": "sha256-xVNy/XopSfIG9c46nRmPaKfH1Gn/56vQ8++xWA8itO4=", + "lastModified": 1751984180, + "narHash": "sha256-LwWRsENAZJKUdD3SpLluwDmdXY9F45ZEgCb0X+xgOL0=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "5c724ed1388e53cc231ed98330a60eb2f7be4be3", + "rev": "9807714d6944a957c2e036f84b0ff8caf9930bc0", "type": "github" }, "original": { From 9210d68ed1df8f6fe84d3bb51682a983154b440e Mon Sep 17 00:00:00 2001 From: Seth Flynn Date: Sun, 13 Jul 2025 15:24:01 -0400 Subject: [PATCH 360/695] ci(setup-deps/windows): try to fix vcpkg binary cache auth Signed-off-by: Seth Flynn --- .github/actions/setup-dependencies/windows/action.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/actions/setup-dependencies/windows/action.yml b/.github/actions/setup-dependencies/windows/action.yml index e899c36d6..97033747e 100644 --- a/.github/actions/setup-dependencies/windows/action.yml +++ b/.github/actions/setup-dependencies/windows/action.yml @@ -30,6 +30,7 @@ runs: shell: pwsh env: USERNAME: ${{ github.repository_owner }} + GITHUB_TOKEN: ${{ github.token }} FEED_URL: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json run: | .$(vcpkg fetch nuget) ` @@ -42,7 +43,7 @@ runs: .$(vcpkg fetch nuget) ` setapikey "$env:GITHUB_TOKEN" ` -Source "$env:FEED_URL" - Write-Output "VCPKG_BINARY_SOURCES=clear;nuget,$env:FEED_URL,readwrite" >> "$GITHUB_ENV" + "VCPKG_BINARY_SOURCES=clear;nuget,$env:FEED_URL,readwrite" | Out-File -Append $env:GITHUB_ENV - name: Setup vcpkg environment (MSVC) if: ${{ inputs.msystem == '' }} From 4614d683b3593022c4d07c97fea2434cd33c3451 Mon Sep 17 00:00:00 2001 From: Seth Flynn Date: Sun, 13 Jul 2025 15:30:55 -0400 Subject: [PATCH 361/695] ci(macos): use vcpkg Signed-off-by: Seth Flynn --- .github/actions/setup-dependencies/action.yml | 2 ++ .../setup-dependencies/macos/action.yml | 32 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/.github/actions/setup-dependencies/action.yml b/.github/actions/setup-dependencies/action.yml index e61509c44..6c718f9e3 100644 --- a/.github/actions/setup-dependencies/action.yml +++ b/.github/actions/setup-dependencies/action.yml @@ -42,6 +42,8 @@ runs: - name: Setup macOS dependencies if: ${{ runner.os == 'macOS' }} uses: ./.github/actions/setup-dependencies/macos + with: + build-type: ${{ inputs.build-type }} - name: Setup Windows dependencies if: ${{ runner.os == 'Windows' }} diff --git a/.github/actions/setup-dependencies/macos/action.yml b/.github/actions/setup-dependencies/macos/action.yml index dcbb308c2..6fc3ed3bf 100644 --- a/.github/actions/setup-dependencies/macos/action.yml +++ b/.github/actions/setup-dependencies/macos/action.yml @@ -1,5 +1,11 @@ name: Setup macOS dependencies +inputs: + build-type: + description: Type for the build + required: true + default: Debug + runs: using: composite @@ -14,3 +20,29 @@ runs: shell: bash run: | echo "JAVA_HOME=$(/usr/libexec/java_home -v 17)" >> "$GITHUB_ENV" + + - name: Setup vcpkg cache + if: ${{ inputs.build-type == 'Debug' }} + shell: bash + env: + USERNAME: ${{ github.repository_owner }} + GITHUB_TOKEN: ${{ github.token }} + FEED_URL: https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json + run: | + mono `vcpkg fetch nuget | tail -n 1` \ + sources add \ + -Source "$FEED_URL" \ + -StorePasswordInClearText \ + -Name GitHubPackages \ + -UserName "$USERNAME" \ + -Password "$GITHUB_TOKEN" + mono `vcpkg fetch nuget | tail -n 1` \ + setapikey "$GITHUB_TOKEN" \ + -Source "$FEED_URL" + echo "VCPKG_BINARY_SOURCES=clear;nuget,$FEED_URL,readwrite" >> "$GITHUB_ENV" + + - name: Setup vcpkg environment + if: ${{ inputs.build-type == 'Debug' }} + shell: bash + run: | + echo "CMAKE_TOOLCHAIN_FILE=$VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake" >> "$GITHUB_ENV" From 5c8ce8db664507c5b1aa7d6e38e98f0ea646ede3 Mon Sep 17 00:00:00 2001 From: Seth Flynn Date: Sun, 13 Jul 2025 15:49:26 -0400 Subject: [PATCH 362/695] build(vcpkg): add univesal-osx triplet vcpkg doesn't officially support universal binaries, but this should function as a workaround until it does one day Signed-off-by: Seth Flynn --- cmake/macosPreset.json | 3 ++- cmake/vcpkg-triplets/universal-osx.cmake | 8 ++++++++ vcpkg-configuration.json | 3 +++ 3 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 cmake/vcpkg-triplets/universal-osx.cmake diff --git a/cmake/macosPreset.json b/cmake/macosPreset.json index de503d7a2..9098f9a9a 100644 --- a/cmake/macosPreset.json +++ b/cmake/macosPreset.json @@ -22,7 +22,8 @@ "macos_base" ], "cacheVariables": { - "CMAKE_OSX_ARCHITECTURES": "x86_64;arm64" + "CMAKE_OSX_ARCHITECTURES": "x86_64;arm64", + "VCPKG_TARGET_TRIPLET": "universal-osx" } }, { diff --git a/cmake/vcpkg-triplets/universal-osx.cmake b/cmake/vcpkg-triplets/universal-osx.cmake new file mode 100644 index 000000000..1c91a5650 --- /dev/null +++ b/cmake/vcpkg-triplets/universal-osx.cmake @@ -0,0 +1,8 @@ +# See https://github.com/microsoft/vcpkg/discussions/19454 +# NOTE: Try to keep in sync with default arm64-osx definition +set(VCPKG_TARGET_ARCHITECTURE x64) +set(VCPKG_CRT_LINKAGE dynamic) +set(VCPKG_LIBRARY_LINKAGE static) + +set(VCPKG_CMAKE_SYSTEM_NAME Darwin) +set(VCPKG_OSX_ARCHITECTURES "arm64;x86_64") diff --git a/vcpkg-configuration.json b/vcpkg-configuration.json index 610f6b31d..3a59b2658 100644 --- a/vcpkg-configuration.json +++ b/vcpkg-configuration.json @@ -10,5 +10,8 @@ "location": "https://github.com/microsoft/vcpkg-ce-catalog/archive/refs/heads/main.zip", "name": "microsoft" } + ], + "overlay-triplets": [ + "./cmake/vcpkg-triplets" ] } From 3cc83626629becc5e9a1d95440c20df292374b42 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 15 Jul 2025 03:24:25 +0000 Subject: [PATCH 363/695] chore(deps): update determinatesystems/nix-installer-action action to v19 --- .github/workflows/nix.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index ecd71977e..6c7b2dac2 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -110,7 +110,7 @@ jobs: ref: ${{ steps.merge-commit.outputs.merge-commit-sha || github.sha }} - name: Install Nix - uses: DeterminateSystems/nix-installer-action@v18 + uses: DeterminateSystems/nix-installer-action@v19 with: determinate: ${{ env.USE_DETERMINATE }} From 49daf6211417a13fd2989998c626b07bc1277d1c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 15 Jul 2025 03:24:29 +0000 Subject: [PATCH 364/695] chore(deps): update determinatesystems/update-flake-lock action to v27 --- .github/workflows/update-flake.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/update-flake.yml b/.github/workflows/update-flake.yml index 123028ff2..de341b517 100644 --- a/.github/workflows/update-flake.yml +++ b/.github/workflows/update-flake.yml @@ -19,7 +19,7 @@ jobs: - uses: actions/checkout@v4 - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31 - - uses: DeterminateSystems/update-flake-lock@v26 + - uses: DeterminateSystems/update-flake-lock@v27 with: commit-msg: "chore(nix): update lockfile" pr-title: "chore(nix): update lockfile" From 3e65d3a9b5edcc5c1353686fe00b56d266db4e6e Mon Sep 17 00:00:00 2001 From: Kenneth Chew <79120643+kthchew@users.noreply.github.com> Date: Wed, 16 Jul 2025 00:28:28 -0400 Subject: [PATCH 365/695] Apply selected style to window elements on macOS Qt doesn't apply the proper style to elements such as the title bar or text shadows, so this must be done in native code. Signed-off-by: Kenneth Chew <79120643+kthchew@users.noreply.github.com> --- launcher/CMakeLists.txt | 7 +++ launcher/ui/themes/ThemeManager.cpp | 8 ++++ launcher/ui/themes/ThemeManager.h | 9 ++++ launcher/ui/themes/ThemeManager.mm | 67 +++++++++++++++++++++++++++++ 4 files changed, 91 insertions(+) create mode 100644 launcher/ui/themes/ThemeManager.mm diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index ff6a9ab2a..5c23aae0d 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -1178,6 +1178,13 @@ SET(LAUNCHER_SOURCES ui/instanceview/VisualGroup.h ) +if (APPLE) + set(LAUNCHER_SOURCES + ${LAUNCHER_SOURCES} + ui/themes/ThemeManager.mm + ) +endif() + if (NOT Apple) set(LAUNCHER_SOURCES ${LAUNCHER_SOURCES} diff --git a/launcher/ui/themes/ThemeManager.cpp b/launcher/ui/themes/ThemeManager.cpp index 30a1fe7be..c1af63dc8 100644 --- a/launcher/ui/themes/ThemeManager.cpp +++ b/launcher/ui/themes/ThemeManager.cpp @@ -174,6 +174,13 @@ void ThemeManager::initializeWidgets() themeDebugLog() << "<> Widget themes initialized."; } +#ifndef Q_OS_MACOS +void ThemeManager::setTitlebarColorOnMac(WId windowId, QColor color) +{} +void ThemeManager::setTitlebarColorOfAllWindowsOnMac(QColor color) +{} +#endif + QList ThemeManager::getValidIconThemes() { QList ret; @@ -247,6 +254,7 @@ void ThemeManager::setApplicationTheme(const QString& name, bool initial) auto& theme = themeIter->second; themeDebugLog() << "applying theme" << theme->name(); theme->apply(initial); + setTitlebarColorOfAllWindowsOnMac(qApp->palette().window().color()); m_logColors = theme->logColorScheme(); } else { diff --git a/launcher/ui/themes/ThemeManager.h b/launcher/ui/themes/ThemeManager.h index 8de7562d1..dd33523d8 100644 --- a/launcher/ui/themes/ThemeManager.h +++ b/launcher/ui/themes/ThemeManager.h @@ -81,6 +81,15 @@ class ThemeManager { void initializeIcons(); void initializeWidgets(); + // On non-Mac systems, this is a no-op. + void setTitlebarColorOnMac(WId windowId, QColor color); + // This also will set the titlebar color of newly opened windows after this method is called. + // On non-Mac systems, this is a no-op. + void setTitlebarColorOfAllWindowsOnMac(QColor color); +#ifdef Q_OS_MACOS + NSObject* m_windowTitlebarObserver = nullptr; +#endif + const QStringList builtinIcons{ "pe_colored", "pe_light", "pe_dark", "pe_blue", "breeze_light", "breeze_dark", "OSX", "iOS", "flat", "flat_white", "multimc" }; }; diff --git a/launcher/ui/themes/ThemeManager.mm b/launcher/ui/themes/ThemeManager.mm new file mode 100644 index 000000000..21b3d8e35 --- /dev/null +++ b/launcher/ui/themes/ThemeManager.mm @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2025 Kenneth Chew <79120643+kthchew@users.noreply.github.com> + * + * 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 "ThemeManager.h" + +#include + +void ThemeManager::setTitlebarColorOnMac(WId windowId, QColor color) +{ + if (windowId == 0) { + return; + } + + NSView* view = (NSView*)windowId; + NSWindow* window = [view window]; + window.titlebarAppearsTransparent = YES; + window.backgroundColor = [NSColor colorWithRed:color.redF() green:color.greenF() blue:color.blueF() alpha:color.alphaF()]; + + // Unfortunately there seems to be no easy way to set the titlebar text color. + // The closest we can do without dubious hacks is set the dark/light mode state based on the brightness of the + // background color, which should at least make the text readable even if we can't use the theme's text color. + // It's a good idea to set this anyway since it also affects some other UI elements like text shadows (PrismLauncher#3825). + if (color.lightnessF() < 0.5) { + window.appearance = [NSAppearance appearanceNamed:NSAppearanceNameDarkAqua]; + } else { + window.appearance = [NSAppearance appearanceNamed:NSAppearanceNameAqua]; + } +} + +void ThemeManager::setTitlebarColorOfAllWindowsOnMac(QColor color) +{ + NSArray* windows = [NSApp windows]; + for (NSWindow* window : windows) { + setTitlebarColorOnMac((WId)window.contentView, color); + } + + // We want to change the titlebar color of newly opened windows as well. + // There's no notification for when a new window is opened, but we can set the color when a window switches + // from occluded to visible, which also fires on open. + NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; + if (m_windowTitlebarObserver) { + [center removeObserver:m_windowTitlebarObserver]; + m_windowTitlebarObserver = nil; + } + m_windowTitlebarObserver = [center addObserverForName:NSWindowDidChangeOcclusionStateNotification + object:nil + queue:[NSOperationQueue mainQueue] + usingBlock:^(NSNotification* notification) { + NSWindow* window = notification.object; + setTitlebarColorOnMac((WId)window.contentView, color); + }]; +} From 677a7d7a052f2f200eac28ddf822da493d1e2ac1 Mon Sep 17 00:00:00 2001 From: Kenneth Chew <79120643+kthchew@users.noreply.github.com> Date: Wed, 16 Jul 2025 01:10:05 -0400 Subject: [PATCH 366/695] Unregister window observer before theme manager is deallocated Technically this probably isn't actually necessary since ThemeManager looks like it should remain allocated until the program quits, but... Signed-off-by: Kenneth Chew <79120643+kthchew@users.noreply.github.com> --- launcher/ui/themes/ThemeManager.cpp | 7 +++++++ launcher/ui/themes/ThemeManager.h | 3 +++ launcher/ui/themes/ThemeManager.mm | 14 ++++++++++---- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/launcher/ui/themes/ThemeManager.cpp b/launcher/ui/themes/ThemeManager.cpp index c1af63dc8..a7076da5a 100644 --- a/launcher/ui/themes/ThemeManager.cpp +++ b/launcher/ui/themes/ThemeManager.cpp @@ -50,6 +50,11 @@ ThemeManager::ThemeManager() initializeCatPacks(); } +ThemeManager::~ThemeManager() +{ + stopSettingNewWindowColorsOnMac(); +} + /// @brief Adds the Theme to the list of themes /// @param theme The Theme to add /// @return Theme ID @@ -179,6 +184,8 @@ void ThemeManager::setTitlebarColorOnMac(WId windowId, QColor color) {} void ThemeManager::setTitlebarColorOfAllWindowsOnMac(QColor color) {} +void ThemeManager::stopSettingNewWindowColorsOnMac() +{} #endif QList ThemeManager::getValidIconThemes() diff --git a/launcher/ui/themes/ThemeManager.h b/launcher/ui/themes/ThemeManager.h index dd33523d8..8baa88627 100644 --- a/launcher/ui/themes/ThemeManager.h +++ b/launcher/ui/themes/ThemeManager.h @@ -39,6 +39,7 @@ inline auto themeWarningLog() class ThemeManager { public: ThemeManager(); + ~ThemeManager(); QList getValidIconThemes(); QList getValidApplicationThemes(); @@ -86,6 +87,8 @@ class ThemeManager { // This also will set the titlebar color of newly opened windows after this method is called. // On non-Mac systems, this is a no-op. void setTitlebarColorOfAllWindowsOnMac(QColor color); + // On non-Mac systems, this is a no-op. + void stopSettingNewWindowColorsOnMac(); #ifdef Q_OS_MACOS NSObject* m_windowTitlebarObserver = nullptr; #endif diff --git a/launcher/ui/themes/ThemeManager.mm b/launcher/ui/themes/ThemeManager.mm index 21b3d8e35..d9fc291b6 100644 --- a/launcher/ui/themes/ThemeManager.mm +++ b/launcher/ui/themes/ThemeManager.mm @@ -53,10 +53,7 @@ // There's no notification for when a new window is opened, but we can set the color when a window switches // from occluded to visible, which also fires on open. NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; - if (m_windowTitlebarObserver) { - [center removeObserver:m_windowTitlebarObserver]; - m_windowTitlebarObserver = nil; - } + stopSettingNewWindowColorsOnMac(); m_windowTitlebarObserver = [center addObserverForName:NSWindowDidChangeOcclusionStateNotification object:nil queue:[NSOperationQueue mainQueue] @@ -65,3 +62,12 @@ setTitlebarColorOnMac((WId)window.contentView, color); }]; } + +void ThemeManager::stopSettingNewWindowColorsOnMac() +{ + if (m_windowTitlebarObserver) { + NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; + [center removeObserver:m_windowTitlebarObserver]; + m_windowTitlebarObserver = nil; + } +} From 78dc42f4dd3ba76dcf2e3cdbd55f8cbb7e7811c6 Mon Sep 17 00:00:00 2001 From: clague <93119153+clague@users.noreply.github.com> Date: Wed, 16 Jul 2025 14:56:44 +0800 Subject: [PATCH 367/695] fix grammar Co-authored-by: Seth Flynn Signed-off-by: clague <93119153+clague@users.noreply.github.com> --- launcher/ui/pages/global/APIPage.ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/pages/global/APIPage.ui b/launcher/ui/pages/global/APIPage.ui index cc36ff7b2..b918ae29e 100644 --- a/launcher/ui/pages/global/APIPage.ui +++ b/launcher/ui/pages/global/APIPage.ui @@ -138,7 +138,7 @@ - You can set this to another server if you have problem in downloading assets. + You can set this to another server if you have problems with downloading assets. Qt::RichText From 9a51cd55df95c0d99423f7e358775b7f82dc4e76 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Mon, 24 Mar 2025 23:06:53 +0200 Subject: [PATCH 368/695] make universal resource type Signed-off-by: Trial97 --- launcher/CMakeLists.txt | 2 + .../mod/tasks/LocalResourceParse.cpp | 30 ++++---------- .../minecraft/mod/tasks/LocalResourceParse.h | 12 +----- launcher/modplatform/ModIndex.h | 2 - launcher/modplatform/ResourceAPI.h | 1 + launcher/modplatform/ResourceType.cpp | 41 +++++++++++++++++++ launcher/modplatform/ResourceType.h | 39 ++++++++++++++++++ .../modplatform/flame/FileResolvingTask.cpp | 14 +++---- launcher/modplatform/flame/FlameAPI.cpp | 2 +- launcher/modplatform/flame/FlameAPI.h | 10 ++--- .../flame/FlameInstanceCreationTask.cpp | 16 ++++---- launcher/modplatform/flame/PackManifest.h | 4 +- launcher/modplatform/modrinth/ModrinthAPI.h | 10 ++--- launcher/ui/MainWindow.cpp | 16 ++++---- launcher/ui/dialogs/ImportResourceDialog.cpp | 5 ++- launcher/ui/dialogs/ImportResourceDialog.h | 6 +-- .../ui/pages/modplatform/DataPackModel.cpp | 2 +- launcher/ui/pages/modplatform/ModModel.cpp | 2 +- .../pages/modplatform/ResourcePackModel.cpp | 2 +- .../ui/pages/modplatform/ShaderPackModel.cpp | 2 +- .../ui/pages/modplatform/flame/FlameModel.cpp | 2 +- .../ui/pages/modplatform/flame/FlamePage.cpp | 2 +- .../modplatform/modrinth/ModrinthModel.cpp | 2 +- 23 files changed, 143 insertions(+), 81 deletions(-) create mode 100644 launcher/modplatform/ResourceType.cpp create mode 100644 launcher/modplatform/ResourceType.h diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index ff6a9ab2a..79cbd90fc 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -494,6 +494,8 @@ set(META_SOURCES set(API_SOURCES modplatform/ModIndex.h modplatform/ModIndex.cpp + modplatform/ResourceType.h + modplatform/ResourceType.cpp modplatform/ResourceAPI.h diff --git a/launcher/minecraft/mod/tasks/LocalResourceParse.cpp b/launcher/minecraft/mod/tasks/LocalResourceParse.cpp index e309b2105..39e8a321b 100644 --- a/launcher/minecraft/mod/tasks/LocalResourceParse.cpp +++ b/launcher/minecraft/mod/tasks/LocalResourceParse.cpp @@ -28,50 +28,38 @@ #include "LocalShaderPackParseTask.h" #include "LocalTexturePackParseTask.h" #include "LocalWorldSaveParseTask.h" - -static const QMap s_packed_type_names = { { PackedResourceType::ResourcePack, QObject::tr("resource pack") }, - { PackedResourceType::TexturePack, QObject::tr("texture pack") }, - { PackedResourceType::DataPack, QObject::tr("data pack") }, - { PackedResourceType::ShaderPack, QObject::tr("shader pack") }, - { PackedResourceType::WorldSave, QObject::tr("world save") }, - { PackedResourceType::Mod, QObject::tr("mod") }, - { PackedResourceType::UNKNOWN, QObject::tr("unknown") } }; +#include "modplatform/ResourceType.h" namespace ResourceUtils { -PackedResourceType identify(QFileInfo file) +ModPlatform::ResourceType identify(QFileInfo file) { if (file.exists() && file.isFile()) { if (ModUtils::validate(file)) { // mods can contain resource and data packs so they must be tested first qDebug() << file.fileName() << "is a mod"; - return PackedResourceType::Mod; + return ModPlatform::ResourceType::Mod; } else if (DataPackUtils::validateResourcePack(file)) { qDebug() << file.fileName() << "is a resource pack"; - return PackedResourceType::ResourcePack; + return ModPlatform::ResourceType::ResourcePack; } else if (TexturePackUtils::validate(file)) { qDebug() << file.fileName() << "is a pre 1.6 texture pack"; - return PackedResourceType::TexturePack; + return ModPlatform::ResourceType::TexturePack; } else if (DataPackUtils::validate(file)) { qDebug() << file.fileName() << "is a data pack"; - return PackedResourceType::DataPack; + return ModPlatform::ResourceType::DataPack; } else if (WorldSaveUtils::validate(file)) { qDebug() << file.fileName() << "is a world save"; - return PackedResourceType::WorldSave; + return ModPlatform::ResourceType::World; } else if (ShaderPackUtils::validate(file)) { qDebug() << file.fileName() << "is a shader pack"; - return PackedResourceType::ShaderPack; + return ModPlatform::ResourceType::ShaderPack; } else { qDebug() << "Can't Identify" << file.fileName(); } } else { qDebug() << "Can't find" << file.absolutePath(); } - return PackedResourceType::UNKNOWN; -} - -QString getPackedTypeName(PackedResourceType type) -{ - return s_packed_type_names.constFind(type).value(); + return ModPlatform::ResourceType::Unknown; } } // namespace ResourceUtils diff --git a/launcher/minecraft/mod/tasks/LocalResourceParse.h b/launcher/minecraft/mod/tasks/LocalResourceParse.h index 7385d24b0..dc3aeb025 100644 --- a/launcher/minecraft/mod/tasks/LocalResourceParse.h +++ b/launcher/minecraft/mod/tasks/LocalResourceParse.h @@ -21,17 +21,9 @@ #pragma once -#include - -#include #include -#include +#include "modplatform/ResourceType.h" -enum class PackedResourceType { DataPack, ResourcePack, TexturePack, ShaderPack, WorldSave, Mod, UNKNOWN }; namespace ResourceUtils { -static const std::set ValidResourceTypes = { PackedResourceType::DataPack, PackedResourceType::ResourcePack, - PackedResourceType::TexturePack, PackedResourceType::ShaderPack, - PackedResourceType::WorldSave, PackedResourceType::Mod }; -PackedResourceType identify(QFileInfo file); -QString getPackedTypeName(PackedResourceType type); +ModPlatform::ResourceType identify(QFileInfo file); } // namespace ResourceUtils diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h index cfe4eba75..6a316af9f 100644 --- a/launcher/modplatform/ModIndex.h +++ b/launcher/modplatform/ModIndex.h @@ -43,8 +43,6 @@ QList modLoaderTypesToList(ModLoaderTypes flags); enum class ResourceProvider { MODRINTH, FLAME }; -enum class ResourceType { MOD, RESOURCE_PACK, SHADER_PACK, MODPACK, DATA_PACK }; - enum class DependencyType { REQUIRED, OPTIONAL, INCOMPATIBLE, EMBEDDED, TOOL, INCLUDE, UNKNOWN }; enum class Side { NoSide = 0, ClientSide = 1 << 0, ServerSide = 1 << 1, UniversalSide = ClientSide | ServerSide }; diff --git a/launcher/modplatform/ResourceAPI.h b/launcher/modplatform/ResourceAPI.h index bd6b90227..0799d1450 100644 --- a/launcher/modplatform/ResourceAPI.h +++ b/launcher/modplatform/ResourceAPI.h @@ -48,6 +48,7 @@ #include "../Version.h" #include "modplatform/ModIndex.h" +#include "modplatform/ResourceType.h" #include "tasks/Task.h" /* Simple class with a common interface for interacting with APIs */ diff --git a/launcher/modplatform/ResourceType.cpp b/launcher/modplatform/ResourceType.cpp new file mode 100644 index 000000000..2758f113f --- /dev/null +++ b/launcher/modplatform/ResourceType.cpp @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * 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 "ResourceType.h" + +namespace ModPlatform { +static const QMap s_packedTypeNames = { { ResourceType::ResourcePack, QObject::tr("resource pack") }, + { ResourceType::TexturePack, QObject::tr("texture pack") }, + { ResourceType::DataPack, QObject::tr("data pack") }, + { ResourceType::ShaderPack, QObject::tr("shader pack") }, + { ResourceType::World, QObject::tr("world save") }, + { ResourceType::Mod, QObject::tr("mod") }, + { ResourceType::Unknown, QObject::tr("unknown") } }; + +namespace ResourceTypeUtils { + +QString getName(ResourceType type) +{ + return s_packedTypeNames.constFind(type).value(); +} + +} // namespace ResourceTypeUtils +} // namespace ModPlatform diff --git a/launcher/modplatform/ResourceType.h b/launcher/modplatform/ResourceType.h new file mode 100644 index 000000000..4acc384d1 --- /dev/null +++ b/launcher/modplatform/ResourceType.h @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * 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 +#include + +namespace ModPlatform { + +enum class ResourceType { Mod, ResourcePack, ShaderPack, Modpack, DataPack, World, Screenshots, TexturePack, Unknown }; + +namespace ResourceTypeUtils { +static const std::set ValidResources = { ResourceType::DataPack, ResourceType::ResourcePack, ResourceType::TexturePack, + ResourceType::ShaderPack, ResourceType::World, ResourceType::Mod }; +QString getName(ResourceType type); +} // namespace ResourceTypeUtils +} // namespace ModPlatform \ No newline at end of file diff --git a/launcher/modplatform/flame/FileResolvingTask.cpp b/launcher/modplatform/flame/FileResolvingTask.cpp index 5f812d219..6dacb43de 100644 --- a/launcher/modplatform/flame/FileResolvingTask.cpp +++ b/launcher/modplatform/flame/FileResolvingTask.cpp @@ -84,18 +84,18 @@ void Flame::FileResolvingTask::executeTask() m_task->start(); } -PackedResourceType getResourceType(int classId) +ModPlatform::ResourceType getResourceType(int classId) { switch (classId) { case 17: // Worlds - return PackedResourceType::WorldSave; + return ModPlatform::ResourceType::World; case 6: // Mods - return PackedResourceType::Mod; + return ModPlatform::ResourceType::Mod; case 12: // Resource Packs - // return PackedResourceType::ResourcePack; // not really a resourcepack + // return ModPlatform::ResourceType::ResourcePack; // not really a resourcepack /* fallthrough */ case 4546: // Customization - // return PackedResourceType::ShaderPack; // not really a shaderPack + // return ModPlatform::ResourceType::ShaderPack; // not really a shaderPack /* fallthrough */ case 4471: // Modpacks /* fallthrough */ @@ -104,7 +104,7 @@ PackedResourceType getResourceType(int classId) case 4559: // Addons /* fallthrough */ default: - return PackedResourceType::UNKNOWN; + return ModPlatform::ResourceType::Unknown; } } @@ -256,7 +256,7 @@ void Flame::FileResolvingTask::getFlameProjects() setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(file->version.fileName)); FlameMod::loadIndexedPack(file->pack, entry_obj); file->resourceType = getResourceType(Json::requireInteger(entry_obj, "classId", "modClassId")); - if (file->resourceType == PackedResourceType::WorldSave) { + if (file->resourceType == ModPlatform::ResourceType::World) { file->targetFolder = "saves"; } } diff --git a/launcher/modplatform/flame/FlameAPI.cpp b/launcher/modplatform/flame/FlameAPI.cpp index 0a5997ed9..d1facfd23 100644 --- a/launcher/modplatform/flame/FlameAPI.cpp +++ b/launcher/modplatform/flame/FlameAPI.cpp @@ -182,7 +182,7 @@ Task::Ptr FlameAPI::getCategories(std::shared_ptr response, ModPlatf Task::Ptr FlameAPI::getModCategories(std::shared_ptr response) { - return getCategories(response, ModPlatform::ResourceType::MOD); + return getCategories(response, ModPlatform::ResourceType::Mod); } QList FlameAPI::loadModCategories(std::shared_ptr response) diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index 316d2e9c9..88b108910 100644 --- a/launcher/modplatform/flame/FlameAPI.h +++ b/launcher/modplatform/flame/FlameAPI.h @@ -41,15 +41,15 @@ class FlameAPI : public NetworkResourceAPI { { switch (type) { default: - case ModPlatform::ResourceType::MOD: + case ModPlatform::ResourceType::Mod: return 6; - case ModPlatform::ResourceType::RESOURCE_PACK: + case ModPlatform::ResourceType::ResourcePack: return 12; - case ModPlatform::ResourceType::SHADER_PACK: + case ModPlatform::ResourceType::ShaderPack: return 6552; - case ModPlatform::ResourceType::MODPACK: + case ModPlatform::ResourceType::Modpack: return 4471; - case ModPlatform::ResourceType::DATA_PACK: + case ModPlatform::ResourceType::DataPack: return 6945; } } diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp index adf4c1065..caf75fe6c 100644 --- a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp +++ b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp @@ -517,7 +517,7 @@ void FlameCreationTask::idResolverSucceeded(QEventLoop& loop) QList blocked_mods; auto anyBlocked = false; for (const auto& result : results.values()) { - if (result.resourceType != PackedResourceType::Mod) { + if (result.resourceType != ModPlatform::ResourceType::Mod) { m_otherResources.append(std::make_pair(result.version.fileName, result.targetFolder)); } @@ -687,29 +687,29 @@ void FlameCreationTask::validateOtherResources(QEventLoop& loop) QString worldPath; switch (type) { - case PackedResourceType::Mod: + case ModPlatform::ResourceType::Mod: validatePath(fileName, targetFolder, "mods"); zipMods.push_back(fileName); break; - case PackedResourceType::ResourcePack: + case ModPlatform::ResourceType::ResourcePack: validatePath(fileName, targetFolder, "resourcepacks"); break; - case PackedResourceType::TexturePack: + case ModPlatform::ResourceType::TexturePack: validatePath(fileName, targetFolder, "texturepacks"); break; - case PackedResourceType::DataPack: + case ModPlatform::ResourceType::DataPack: validatePath(fileName, targetFolder, "datapacks"); break; - case PackedResourceType::ShaderPack: + case ModPlatform::ResourceType::ShaderPack: // in theory flame API can't do this but who knows, that *may* change ? // better to handle it if it *does* occur in the future validatePath(fileName, targetFolder, "shaderpacks"); break; - case PackedResourceType::WorldSave: + case ModPlatform::ResourceType::World: worldPath = validatePath(fileName, targetFolder, "saves"); installWorld(worldPath); break; - case PackedResourceType::UNKNOWN: + case ModPlatform::ResourceType::Unknown: /* fallthrough */ default: qDebug() << "Can't Identify" << fileName << "at" << localPath << ", leaving it where it is."; diff --git a/launcher/modplatform/flame/PackManifest.h b/launcher/modplatform/flame/PackManifest.h index 6b911ffb4..049a99871 100644 --- a/launcher/modplatform/flame/PackManifest.h +++ b/launcher/modplatform/flame/PackManifest.h @@ -40,8 +40,8 @@ #include #include #include -#include "minecraft/mod/tasks/LocalResourceParse.h" #include "modplatform/ModIndex.h" +#include "modplatform/ResourceType.h" namespace Flame { struct File { @@ -55,7 +55,7 @@ struct File { // our QString targetFolder = QStringLiteral("mods"); - PackedResourceType resourceType; + ModPlatform::ResourceType resourceType; }; struct Modloader { diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index 7c2592256..4fa70f425 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -104,15 +104,15 @@ class ModrinthAPI : public NetworkResourceAPI { [[nodiscard]] static QString resourceTypeParameter(ModPlatform::ResourceType type) { switch (type) { - case ModPlatform::ResourceType::MOD: + case ModPlatform::ResourceType::Mod: return "mod"; - case ModPlatform::ResourceType::RESOURCE_PACK: + case ModPlatform::ResourceType::ResourcePack: return "resourcepack"; - case ModPlatform::ResourceType::SHADER_PACK: + case ModPlatform::ResourceType::ShaderPack: return "shader"; - case ModPlatform::ResourceType::DATA_PACK: + case ModPlatform::ResourceType::DataPack: return "datapack"; - case ModPlatform::ResourceType::MODPACK: + case ModPlatform::ResourceType::Modpack: return "modpack"; default: qWarning() << "Invalid resource type for Modrinth API!"; diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 9edbda605..3209f080f 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -1041,7 +1041,7 @@ void MainWindow::processURLs(QList urls) auto type = ResourceUtils::identify(localFileInfo); - if (ResourceUtils::ValidResourceTypes.count(type) == 0) { // probably instance/modpack + if (ModPlatform::ResourceTypeUtils::ValidResources.count(type) == 0) { // probably instance/modpack addInstance(localFileName, extra_info); continue; } @@ -1065,25 +1065,25 @@ void MainWindow::processURLs(QList urls) auto minecraftInst = std::dynamic_pointer_cast(inst); switch (type) { - case PackedResourceType::ResourcePack: + case ModPlatform::ResourceType::ResourcePack: minecraftInst->resourcePackList()->installResourceWithFlameMetadata(localFileName, version); break; - case PackedResourceType::TexturePack: + case ModPlatform::ResourceType::TexturePack: minecraftInst->texturePackList()->installResourceWithFlameMetadata(localFileName, version); break; - case PackedResourceType::DataPack: + case ModPlatform::ResourceType::DataPack: qWarning() << "Importing of Data Packs not supported at this time. Ignoring" << localFileName; break; - case PackedResourceType::Mod: + case ModPlatform::ResourceType::Mod: minecraftInst->loaderModList()->installResourceWithFlameMetadata(localFileName, version); break; - case PackedResourceType::ShaderPack: + case ModPlatform::ResourceType::ShaderPack: minecraftInst->shaderPackList()->installResourceWithFlameMetadata(localFileName, version); break; - case PackedResourceType::WorldSave: + case ModPlatform::ResourceType::World: minecraftInst->worldList()->installWorld(localFileInfo); break; - case PackedResourceType::UNKNOWN: + case ModPlatform::ResourceType::Unknown: default: qDebug() << "Can't Identify" << localFileName << "Ignoring it."; break; diff --git a/launcher/ui/dialogs/ImportResourceDialog.cpp b/launcher/ui/dialogs/ImportResourceDialog.cpp index 97c8f22c5..7cd178130 100644 --- a/launcher/ui/dialogs/ImportResourceDialog.cpp +++ b/launcher/ui/dialogs/ImportResourceDialog.cpp @@ -8,10 +8,11 @@ #include "InstanceList.h" #include +#include "modplatform/ResourceType.h" #include "ui/instanceview/InstanceDelegate.h" #include "ui/instanceview/InstanceProxyModel.h" -ImportResourceDialog::ImportResourceDialog(QString file_path, PackedResourceType type, QWidget* parent) +ImportResourceDialog::ImportResourceDialog(QString file_path, ModPlatform::ResourceType type, QWidget* parent) : QDialog(parent), ui(new Ui::ImportResourceDialog), m_resource_type(type), m_file_path(file_path) { ui->setupUi(this); @@ -42,7 +43,7 @@ ImportResourceDialog::ImportResourceDialog(QString file_path, PackedResourceType connect(contentsWidget->selectionModel(), &QItemSelectionModel::selectionChanged, this, &ImportResourceDialog::selectionChanged); ui->label->setText( - tr("Choose the instance you would like to import this %1 to.").arg(ResourceUtils::getPackedTypeName(m_resource_type))); + tr("Choose the instance you would like to import this %1 to.").arg(ModPlatform::ResourceTypeUtils::getName(m_resource_type))); ui->label_file_path->setText(tr("File: %1").arg(m_file_path)); ui->buttonBox->button(QDialogButtonBox::Cancel)->setText(tr("Cancel")); diff --git a/launcher/ui/dialogs/ImportResourceDialog.h b/launcher/ui/dialogs/ImportResourceDialog.h index bbde1ba7b..d96099661 100644 --- a/launcher/ui/dialogs/ImportResourceDialog.h +++ b/launcher/ui/dialogs/ImportResourceDialog.h @@ -3,7 +3,7 @@ #include #include -#include "minecraft/mod/tasks/LocalResourceParse.h" +#include "modplatform/ResourceType.h" #include "ui/instanceview/InstanceProxyModel.h" namespace Ui { @@ -14,13 +14,13 @@ class ImportResourceDialog : public QDialog { Q_OBJECT public: - explicit ImportResourceDialog(QString file_path, PackedResourceType type, QWidget* parent = nullptr); + explicit ImportResourceDialog(QString file_path, ModPlatform::ResourceType type, QWidget* parent = nullptr); ~ImportResourceDialog() override; QString selectedInstanceKey; private: Ui::ImportResourceDialog* ui; - PackedResourceType m_resource_type; + ModPlatform::ResourceType m_resource_type; QString m_file_path; InstanceProxyModel* proxyModel; diff --git a/launcher/ui/pages/modplatform/DataPackModel.cpp b/launcher/ui/pages/modplatform/DataPackModel.cpp index 085bd2d53..547f0a363 100644 --- a/launcher/ui/pages/modplatform/DataPackModel.cpp +++ b/launcher/ui/pages/modplatform/DataPackModel.cpp @@ -18,7 +18,7 @@ DataPackResourceModel::DataPackResourceModel(BaseInstance const& base_inst, Reso ResourceAPI::SearchArgs DataPackResourceModel::createSearchArguments() { auto sort = getCurrentSortingMethodByIndex(); - return { ModPlatform::ResourceType::DATA_PACK, m_next_search_offset, m_search_term, sort, ModPlatform::ModLoaderType::DataPack }; + return { ModPlatform::ResourceType::DataPack, m_next_search_offset, m_search_term, sort, ModPlatform::ModLoaderType::DataPack }; } ResourceAPI::VersionSearchArgs DataPackResourceModel::createVersionsArguments(const QModelIndex& entry) diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp index 32e6f2146..feaa4cfa7 100644 --- a/launcher/ui/pages/modplatform/ModModel.cpp +++ b/launcher/ui/pages/modplatform/ModModel.cpp @@ -41,7 +41,7 @@ ResourceAPI::SearchArgs ModModel::createSearchArguments() auto sort = getCurrentSortingMethodByIndex(); return { - ModPlatform::ResourceType::MOD, m_next_search_offset, m_search_term, sort, loaders, versions, side, categories, m_filter->openSource + ModPlatform::ResourceType::Mod, m_next_search_offset, m_search_term, sort, loaders, versions, side, categories, m_filter->openSource }; } diff --git a/launcher/ui/pages/modplatform/ResourcePackModel.cpp b/launcher/ui/pages/modplatform/ResourcePackModel.cpp index 0de980ed8..986cb56a6 100644 --- a/launcher/ui/pages/modplatform/ResourcePackModel.cpp +++ b/launcher/ui/pages/modplatform/ResourcePackModel.cpp @@ -17,7 +17,7 @@ ResourcePackResourceModel::ResourcePackResourceModel(BaseInstance const& base_in ResourceAPI::SearchArgs ResourcePackResourceModel::createSearchArguments() { auto sort = getCurrentSortingMethodByIndex(); - return { ModPlatform::ResourceType::RESOURCE_PACK, m_next_search_offset, m_search_term, sort }; + return { ModPlatform::ResourceType::ResourcePack, m_next_search_offset, m_search_term, sort }; } ResourceAPI::VersionSearchArgs ResourcePackResourceModel::createVersionsArguments(const QModelIndex& entry) diff --git a/launcher/ui/pages/modplatform/ShaderPackModel.cpp b/launcher/ui/pages/modplatform/ShaderPackModel.cpp index efc6bfaf9..b59bf182b 100644 --- a/launcher/ui/pages/modplatform/ShaderPackModel.cpp +++ b/launcher/ui/pages/modplatform/ShaderPackModel.cpp @@ -17,7 +17,7 @@ ShaderPackResourceModel::ShaderPackResourceModel(BaseInstance const& base_inst, ResourceAPI::SearchArgs ShaderPackResourceModel::createSearchArguments() { auto sort = getCurrentSortingMethodByIndex(); - return { ModPlatform::ResourceType::SHADER_PACK, m_next_search_offset, m_search_term, sort }; + return { ModPlatform::ResourceType::ShaderPack, m_next_search_offset, m_search_term, sort }; } ResourceAPI::VersionSearchArgs ShaderPackResourceModel::createVersionsArguments(const QModelIndex& entry) diff --git a/launcher/ui/pages/modplatform/flame/FlameModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModel.cpp index e67e4fb38..f8fca6570 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModel.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModel.cpp @@ -188,7 +188,7 @@ void ListModel::performPaginatedSearch() auto netJob = makeShared("Flame::Search", APPLICATION->network()); auto searchUrl = - FlameAPI().getSearchURL({ ModPlatform::ResourceType::MODPACK, nextSearchOffset, currentSearchTerm, sort, m_filter->loaders, + FlameAPI().getSearchURL({ ModPlatform::ResourceType::Modpack, nextSearchOffset, currentSearchTerm, sort, m_filter->loaders, m_filter->versions, ModPlatform::Side::NoSide, m_filter->categoryIds, m_filter->openSource }); netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(searchUrl.value()), response)); diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.cpp b/launcher/ui/pages/modplatform/flame/FlamePage.cpp index 1a2fc7aa4..9578eb73e 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlamePage.cpp @@ -350,7 +350,7 @@ void FlamePage::createFilterWidget() connect(m_filterWidget.get(), &ModFilterWidget::filterChanged, this, &FlamePage::triggerSearch); auto response = std::make_shared(); - m_categoriesTask = FlameAPI::getCategories(response, ModPlatform::ResourceType::MODPACK); + m_categoriesTask = FlameAPI::getCategories(response, ModPlatform::ResourceType::Modpack); connect(m_categoriesTask.get(), &Task::succeeded, [this, response]() { auto categories = FlameAPI::loadModCategories(response); m_filterWidget->setCategories(categories); diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index 870b47beb..c66fb5655 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -154,7 +154,7 @@ void ModpackListModel::performPaginatedSearch() ResourceAPI::SortingMethod sort{}; sort.name = currentSort; auto searchUrl = - ModrinthAPI().getSearchURL({ ModPlatform::ResourceType::MODPACK, nextSearchOffset, currentSearchTerm, sort, m_filter->loaders, + ModrinthAPI().getSearchURL({ ModPlatform::ResourceType::Modpack, nextSearchOffset, currentSearchTerm, sort, m_filter->loaders, m_filter->versions, ModPlatform::Side::NoSide, m_filter->categoryIds, m_filter->openSource }); auto netJob = makeShared("Modrinth::SearchModpack", APPLICATION->network()); From 6f23c6ea416868684717c8c98e97ee36751909c0 Mon Sep 17 00:00:00 2001 From: Alexandru Ionut Tripon Date: Fri, 18 Jul 2025 18:22:46 +0300 Subject: [PATCH 369/695] Update launcher/modplatform/ResourceType.h Co-authored-by: TheKodeToad Signed-off-by: Alexandru Ionut Tripon --- launcher/modplatform/ResourceType.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/modplatform/ResourceType.h b/launcher/modplatform/ResourceType.h index 4acc384d1..b9073aa17 100644 --- a/launcher/modplatform/ResourceType.h +++ b/launcher/modplatform/ResourceType.h @@ -32,7 +32,7 @@ namespace ModPlatform { enum class ResourceType { Mod, ResourcePack, ShaderPack, Modpack, DataPack, World, Screenshots, TexturePack, Unknown }; namespace ResourceTypeUtils { -static const std::set ValidResources = { ResourceType::DataPack, ResourceType::ResourcePack, ResourceType::TexturePack, +static const std::set VALID_RESOURCES = { ResourceType::DataPack, ResourceType::ResourcePack, ResourceType::TexturePack, ResourceType::ShaderPack, ResourceType::World, ResourceType::Mod }; QString getName(ResourceType type); } // namespace ResourceTypeUtils From b60b577d25008af0284834a420e0133b1eb17c7f Mon Sep 17 00:00:00 2001 From: Trial97 Date: Fri, 18 Jul 2025 18:26:25 +0300 Subject: [PATCH 370/695] rename VALID_RESOURCES static variable Signed-off-by: Trial97 --- launcher/ui/MainWindow.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index 3209f080f..d89224504 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -1041,7 +1041,7 @@ void MainWindow::processURLs(QList urls) auto type = ResourceUtils::identify(localFileInfo); - if (ModPlatform::ResourceTypeUtils::ValidResources.count(type) == 0) { // probably instance/modpack + if (ModPlatform::ResourceTypeUtils::VALID_RESOURCES.count(type) == 0) { // probably instance/modpack addInstance(localFileName, extra_info); continue; } From 43b2b07e0ec55cc0f8d2d6175ac467c3042adf86 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 20 Jul 2025 00:32:26 +0000 Subject: [PATCH 371/695] chore(nix): update lockfile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'nixpkgs': 'github:NixOS/nixpkgs/9807714d6944a957c2e036f84b0ff8caf9930bc0?narHash=sha256-LwWRsENAZJKUdD3SpLluwDmdXY9F45ZEgCb0X%2BxgOL0%3D' (2025-07-08) → 'github:NixOS/nixpkgs/6e987485eb2c77e5dcc5af4e3c70843711ef9251?narHash=sha256-RKwfXA4OZROjBTQAl9WOZQFm7L8Bo93FQwSJpAiSRvo%3D' (2025-07-16) --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index b5f6258e1..162ad5baa 100644 --- a/flake.lock +++ b/flake.lock @@ -18,11 +18,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1751984180, - "narHash": "sha256-LwWRsENAZJKUdD3SpLluwDmdXY9F45ZEgCb0X+xgOL0=", + "lastModified": 1752687322, + "narHash": "sha256-RKwfXA4OZROjBTQAl9WOZQFm7L8Bo93FQwSJpAiSRvo=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "9807714d6944a957c2e036f84b0ff8caf9930bc0", + "rev": "6e987485eb2c77e5dcc5af4e3c70843711ef9251", "type": "github" }, "original": { From a75713897d96c674fc1fc9b2d0ac751bf0a6bfbe Mon Sep 17 00:00:00 2001 From: Trial97 Date: Thu, 19 Dec 2024 11:18:22 +0200 Subject: [PATCH 372/695] chore:renamed variables to camelCase Signed-off-by: Trial97 --- launcher/modplatform/CheckUpdateTask.h | 14 ++-- .../modplatform/flame/FlameCheckUpdate.cpp | 10 +-- .../modrinth/ModrinthCheckUpdate.cpp | 20 ++--- .../modrinth/ModrinthCheckUpdate.h | 6 +- launcher/ui/dialogs/ResourceUpdateDialog.cpp | 80 +++++++++---------- launcher/ui/dialogs/ResourceUpdateDialog.h | 32 ++++---- 6 files changed, 80 insertions(+), 82 deletions(-) diff --git a/launcher/modplatform/CheckUpdateTask.h b/launcher/modplatform/CheckUpdateTask.h index 1ee820a63..c5beff26c 100644 --- a/launcher/modplatform/CheckUpdateTask.h +++ b/launcher/modplatform/CheckUpdateTask.h @@ -1,9 +1,7 @@ #pragma once -#include "minecraft/mod/Mod.h" #include "minecraft/mod/tasks/GetModDependenciesTask.h" #include "modplatform/ModIndex.h" -#include "modplatform/ResourceAPI.h" #include "tasks/Task.h" class ResourceDownloadTask; @@ -19,9 +17,9 @@ class CheckUpdateTask : public Task { std::shared_ptr resourceModel) : Task() , m_resources(resources) - , m_game_versions(mcVersions) - , m_loaders_list(std::move(loadersList)) - , m_resource_model(std::move(resourceModel)) + , m_gameVersions(mcVersions) + , m_loadersList(std::move(loadersList)) + , m_resourceModel(std::move(resourceModel)) {} struct Update { @@ -71,9 +69,9 @@ class CheckUpdateTask : public Task { protected: QList& m_resources; - std::list& m_game_versions; - QList m_loaders_list; - std::shared_ptr m_resource_model; + std::list& m_gameVersions; + QList m_loadersList; + std::shared_ptr m_resourceModel; std::vector m_updates; QList> m_deps; diff --git a/launcher/modplatform/flame/FlameCheckUpdate.cpp b/launcher/modplatform/flame/FlameCheckUpdate.cpp index 047813675..8f54ee0c6 100644 --- a/launcher/modplatform/flame/FlameCheckUpdate.cpp +++ b/launcher/modplatform/flame/FlameCheckUpdate.cpp @@ -46,12 +46,12 @@ void FlameCheckUpdate::executeTask() connect(netJob, &Task::stepProgress, this, &FlameCheckUpdate::propagateStepProgress); connect(netJob, &Task::details, this, &FlameCheckUpdate::setDetails); for (auto* resource : m_resources) { - auto versions_url_optional = api.getVersionsURL({ { resource->metadata()->project_id.toString() }, m_game_versions }); - if (!versions_url_optional.has_value()) + auto versionsUrlOptional = api.getVersionsURL({ { resource->metadata()->project_id.toString() }, m_gameVersions }); + if (!versionsUrlOptional.has_value()) continue; auto response = std::make_shared(); - auto task = Net::ApiDownload::makeByteArray(versions_url_optional.value(), response); + auto task = Net::ApiDownload::makeByteArray(versionsUrlOptional.value(), response); connect(task.get(), &Task::succeeded, this, [this, resource, response] { getLatestVersionCallback(resource, response); }); netJob->addNetAction(task); @@ -87,7 +87,7 @@ void FlameCheckUpdate::getLatestVersionCallback(Resource* resource, std::shared_ qCritical() << e.what(); qDebug() << doc; } - auto latest_ver = api.getLatestVersion(pack->versions, m_loaders_list, resource->metadata()->loaders); + auto latest_ver = api.getLatestVersion(pack->versions, m_loadersList, resource->metadata()->loaders); setStatus(tr("Parsing the API response from CurseForge for '%1'...").arg(resource->name())); @@ -119,7 +119,7 @@ void FlameCheckUpdate::getLatestVersionCallback(Resource* resource, std::shared_ old_version = tr("Unknown"); } - auto download_task = makeShared(pack, latest_ver.value(), m_resource_model); + auto download_task = makeShared(pack, latest_ver.value(), m_resourceModel); m_updates.emplace_back(pack->name, resource->metadata()->hash, old_version, latest_ver->version, latest_ver->version_type, api.getModFileChangelog(latest_ver->addonId.toInt(), latest_ver->fileId.toInt()), ModPlatform::ResourceProvider::FLAME, download_task, resource->enabled()); diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp index aa371f280..041ffddb7 100644 --- a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp +++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp @@ -30,7 +30,7 @@ bool ModrinthCheckUpdate::abort() void ModrinthCheckUpdate::executeTask() { setStatus(tr("Preparing resources for Modrinth...")); - setProgress(0, (m_loaders_list.isEmpty() ? 1 : m_loaders_list.length()) * 2 + 1); + setProgress(0, (m_loadersList.isEmpty() ? 1 : m_loadersList.length()) * 2 + 1); auto hashing_task = makeShared("MakeModrinthHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt()); @@ -40,7 +40,7 @@ void ModrinthCheckUpdate::executeTask() // Sadly the API can only handle one hash type per call, se we // need to generate a new hash if the current one is innadequate // (though it will rarely happen, if at all) - if (resource->metadata()->hash_format != m_hash_type) { + if (resource->metadata()->hash_format != m_hashType) { auto hash_task = Hashing::createHasher(resource->fileinfo().absoluteFilePath(), ModPlatform::ResourceProvider::MODRINTH); connect(hash_task.get(), &Hashing::Hasher::resultsReady, [this, resource](QString hash) { m_mappings.insert(hash, resource); }); connect(hash_task.get(), &Task::failed, [this] { failed("Failed to generate hash"); }); @@ -62,7 +62,7 @@ void ModrinthCheckUpdate::getUpdateModsForLoader(std::optional(); QStringList hashes = m_mappings.keys(); - auto job = api.latestVersions(hashes, m_hash_type, m_game_versions, loader, response); + auto job = api.latestVersions(hashes, m_hashType, m_gameVersions, loader, response); connect(job.get(), &Task::succeeded, this, [this, response, loader] { checkVersionsResponse(response, loader); }); @@ -121,7 +121,7 @@ void ModrinthCheckUpdate::checkVersionsResponse(std::shared_ptr resp // - The version reported by the JAR is different from the version reported by the indexed version (it's usually the case) // Such is the pain of having arbitrary files for a given version .-. - auto project_ver = Modrinth::loadIndexedPackVersion(project_obj, m_hash_type, loader_filter); + auto project_ver = Modrinth::loadIndexedPackVersion(project_obj, m_hashType, loader_filter); if (project_ver.downloadUrl.isEmpty()) { qCritical() << "Modrinth mod without download url!" << project_ver.fileName; ++iter; @@ -135,7 +135,7 @@ void ModrinthCheckUpdate::checkVersionsResponse(std::shared_ptr resp pack->addonId = resource->metadata()->project_id; pack->provider = ModPlatform::ResourceProvider::MODRINTH; if ((project_ver.hash != hash && project_ver.is_preferred) || (resource->status() == ResourceStatus::NOT_INSTALLED)) { - auto download_task = makeShared(pack, project_ver, m_resource_model); + auto download_task = makeShared(pack, project_ver, m_resourceModel); QString old_version = resource->metadata()->version_number; if (old_version.isEmpty()) { @@ -166,15 +166,15 @@ void ModrinthCheckUpdate::checkNextLoader() return; } - if (m_loaders_list.isEmpty() && m_loader_idx == 0) { + if (m_loadersList.isEmpty() && m_loaderIdx == 0) { getUpdateModsForLoader({}); - m_loader_idx++; + m_loaderIdx++; return; } - if (m_loader_idx < m_loaders_list.size()) { - getUpdateModsForLoader(m_loaders_list.at(m_loader_idx)); - m_loader_idx++; + if (m_loaderIdx < m_loadersList.size()) { + getUpdateModsForLoader(m_loadersList.at(m_loaderIdx)); + m_loaderIdx++; return; } diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.h b/launcher/modplatform/modrinth/ModrinthCheckUpdate.h index 204b24784..bde61bb23 100644 --- a/launcher/modplatform/modrinth/ModrinthCheckUpdate.h +++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.h @@ -11,7 +11,7 @@ class ModrinthCheckUpdate : public CheckUpdateTask { QList loadersList, std::shared_ptr resourceModel) : CheckUpdateTask(resources, mcVersions, std::move(loadersList), std::move(resourceModel)) - , m_hash_type(ModPlatform::ProviderCapabilities::hashType(ModPlatform::ResourceProvider::MODRINTH).first()) + , m_hashType(ModPlatform::ProviderCapabilities::hashType(ModPlatform::ResourceProvider::MODRINTH).first()) {} public slots: @@ -26,6 +26,6 @@ class ModrinthCheckUpdate : public CheckUpdateTask { private: Task::Ptr m_job = nullptr; QHash m_mappings; - QString m_hash_type; - int m_loader_idx = 0; + QString m_hashType; + int m_loaderIdx = 0; }; diff --git a/launcher/ui/dialogs/ResourceUpdateDialog.cpp b/launcher/ui/dialogs/ResourceUpdateDialog.cpp index 774d3a339..eb69a6c6b 100644 --- a/launcher/ui/dialogs/ResourceUpdateDialog.cpp +++ b/launcher/ui/dialogs/ResourceUpdateDialog.cpp @@ -34,17 +34,17 @@ static std::list mcVersions(BaseInstance* inst) ResourceUpdateDialog::ResourceUpdateDialog(QWidget* parent, BaseInstance* instance, - const std::shared_ptr resource_model, - QList& search_for, - bool include_deps, + const std::shared_ptr resourceModel, + QList& searchFor, + bool includeDeps, QList loadersList) : ReviewMessageBox(parent, tr("Confirm resources to update"), "") , m_parent(parent) - , m_resource_model(resource_model) - , m_candidates(search_for) - , m_second_try_metadata(new ConcurrentTask("Second Metadata Search", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt())) + , m_resourceModel(resourceModel) + , m_candidates(searchFor) + , m_secondTryMetadata(new ConcurrentTask("Second Metadata Search", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt())) , m_instance(instance) - , m_include_deps(include_deps) + , m_includeDeps(includeDeps) , m_loadersList(std::move(loadersList)) { ReviewMessageBox::setGeometry(0, 0, 800, 600); @@ -63,9 +63,9 @@ void ResourceUpdateDialog::checkCandidates() } // Report failed metadata generation - if (!m_failed_metadata.empty()) { + if (!m_failedMetadata.empty()) { QString text; - for (const auto& failed : m_failed_metadata) { + for (const auto& failed : m_failedMetadata) { const auto& mod = std::get<0>(failed); const auto& reason = std::get<1>(failed); text += tr("Mod name: %1
    File name: %2
    Reason: %3

    ").arg(mod->name(), mod->fileinfo().fileName(), reason); @@ -84,24 +84,24 @@ void ResourceUpdateDialog::checkCandidates() } auto versions = mcVersions(m_instance); + SequentialTask check_task(tr("Checking for updates")); - if (!m_modrinth_to_update.empty()) { - m_modrinth_check_task.reset(new ModrinthCheckUpdate(m_modrinth_to_update, versions, m_loadersList, m_resource_model)); - connect(m_modrinth_check_task.get(), &CheckUpdateTask::checkFailed, this, + if (!m_modrinthToUpdate.empty()) { + m_modrinthCheckTask.reset(new ModrinthCheckUpdate(m_modrinthToUpdate, versions, m_loadersList, m_resourceModel)); + connect(m_modrinthCheckTask.get(), &CheckUpdateTask::checkFailed, this, [this](Resource* resource, QString reason, QUrl recover_url) { - m_failed_check_update.append({ resource, reason, recover_url }); + m_failedCheckUpdate.append({ resource, reason, recover_url }); }); - check_task.addTask(m_modrinth_check_task); + check_task.addTask(m_modrinthCheckTask); } - if (!m_flame_to_update.empty()) { - m_flame_check_task.reset(new FlameCheckUpdate(m_flame_to_update, versions, m_loadersList, m_resource_model)); - connect(m_flame_check_task.get(), &CheckUpdateTask::checkFailed, this, - [this](Resource* resource, QString reason, QUrl recover_url) { - m_failed_check_update.append({ resource, reason, recover_url }); - }); - check_task.addTask(m_flame_check_task); + if (!m_flameToUpdate.empty()) { + m_flameCheckTask.reset(new FlameCheckUpdate(m_flameToUpdate, versions, m_loadersList, m_resourceModel)); + connect(m_flameCheckTask.get(), &CheckUpdateTask::checkFailed, this, [this](Resource* resource, QString reason, QUrl recover_url) { + m_failedCheckUpdate.append({ resource, reason, recover_url }); + }); + check_task.addTask(m_flameCheckTask); } connect(&check_task, &Task::failed, this, @@ -130,33 +130,33 @@ void ResourceUpdateDialog::checkCandidates() QList> selectedVers; // Add found updates for Modrinth - if (m_modrinth_check_task) { - auto modrinth_updates = m_modrinth_check_task->getUpdates(); + if (m_modrinthCheckTask) { + auto modrinth_updates = m_modrinthCheckTask->getUpdates(); for (auto& updatable : modrinth_updates) { qDebug() << QString("Mod %1 has an update available!").arg(updatable.name); appendResource(updatable); m_tasks.insert(updatable.name, updatable.download); } - selectedVers.append(m_modrinth_check_task->getDependencies()); + selectedVers.append(m_modrinthCheckTask->getDependencies()); } // Add found updated for Flame - if (m_flame_check_task) { - auto flame_updates = m_flame_check_task->getUpdates(); + if (m_flameCheckTask) { + auto flame_updates = m_flameCheckTask->getUpdates(); for (auto& updatable : flame_updates) { qDebug() << QString("Mod %1 has an update available!").arg(updatable.name); appendResource(updatable); m_tasks.insert(updatable.name, updatable.download); } - selectedVers.append(m_flame_check_task->getDependencies()); + selectedVers.append(m_flameCheckTask->getDependencies()); } // Report failed update checking - if (!m_failed_check_update.empty()) { + if (!m_failedCheckUpdate.empty()) { QString text; - for (const auto& failed : m_failed_check_update) { + for (const auto& failed : m_failedCheckUpdate) { const auto& mod = std::get<0>(failed); const auto& reason = std::get<1>(failed); const auto& recover_url = std::get<2>(failed); @@ -185,8 +185,8 @@ void ResourceUpdateDialog::checkCandidates() } } - if (m_include_deps && !APPLICATION->settings()->get("ModDependenciesDisabled").toBool()) { // dependencies - auto* mod_model = dynamic_cast(m_resource_model.get()); + if (m_includeDeps && !APPLICATION->settings()->get("ModDependenciesDisabled").toBool()) { // dependencies + auto* mod_model = dynamic_cast(m_resourceModel.get()); if (mod_model != nullptr) { auto depTask = makeShared(m_instance, mod_model, selectedVers); @@ -224,7 +224,7 @@ void ResourceUpdateDialog::checkCandidates() auto changelog = dep->version.changelog; if (dep->pack->provider == ModPlatform::ResourceProvider::FLAME) changelog = api.getModFileChangelog(dep->version.addonId.toInt(), dep->version.fileId.toInt()); - auto download_task = makeShared(dep->pack, dep->version, m_resource_model); + auto download_task = makeShared(dep->pack, dep->version, m_resourceModel); auto extraInfo = dependencyExtraInfo.value(dep->version.addonId.toString()); CheckUpdateTask::Update updatable = { dep->pack->name, dep->version.hash, tr("Not installed"), dep->version.version, dep->version.version_type, @@ -239,7 +239,7 @@ void ResourceUpdateDialog::checkCandidates() // If there's no resource to be updated if (ui->modTreeWidget->topLevelItemCount() == 0) { - m_no_updates = true; + m_noUpdates = true; } else { // FIXME: Find a more efficient way of doing this! @@ -254,7 +254,7 @@ void ResourceUpdateDialog::checkCandidates() } } - if (m_aborted || m_no_updates) + if (m_aborted || m_noUpdates) QMetaObject::invokeMethod(this, "reject", Qt::QueuedConnection); } @@ -362,7 +362,7 @@ auto ResourceUpdateDialog::ensureMetadata() -> bool seq.addTask(flame_task); } - seq.addTask(m_second_try_metadata); + seq.addTask(m_secondTryMetadata); // execute all the tasks ProgressDialog checking_dialog(m_parent); @@ -381,10 +381,10 @@ void ResourceUpdateDialog::onMetadataEnsured(Resource* resource) switch (resource->metadata()->provider) { case ModPlatform::ResourceProvider::MODRINTH: - m_modrinth_to_update.push_back(resource); + m_modrinthToUpdate.push_back(resource); break; case ModPlatform::ResourceProvider::FLAME: - m_flame_to_update.push_back(resource); + m_flameToUpdate.push_back(resource); break; } } @@ -415,14 +415,14 @@ void ResourceUpdateDialog::onMetadataFailed(Resource* resource, bool try_others, auto seq = makeShared(); seq->addTask(task->getHashingTask()); seq->addTask(task); - m_second_try_metadata->addTask(seq); + m_secondTryMetadata->addTask(seq); } else { - m_second_try_metadata->addTask(task); + m_secondTryMetadata->addTask(task); } } else { QString reason{ tr("Couldn't find a valid version on the selected mod provider(s)") }; - m_failed_metadata.append({ resource, reason }); + m_failedMetadata.append({ resource, reason }); } } diff --git a/launcher/ui/dialogs/ResourceUpdateDialog.h b/launcher/ui/dialogs/ResourceUpdateDialog.h index aef11c90f..be3c19dcc 100644 --- a/launcher/ui/dialogs/ResourceUpdateDialog.h +++ b/launcher/ui/dialogs/ResourceUpdateDialog.h @@ -18,9 +18,9 @@ class ResourceUpdateDialog final : public ReviewMessageBox { public: explicit ResourceUpdateDialog(QWidget* parent, BaseInstance* instance, - std::shared_ptr resource_model, - QList& search_for, - bool include_deps, + std::shared_ptr resourceModel, + QList& searchFor, + bool includeDeps, QList loadersList = {}); void checkCandidates(); @@ -28,9 +28,9 @@ class ResourceUpdateDialog final : public ReviewMessageBox { void appendResource(const CheckUpdateTask::Update& info, QStringList requiredBy = {}); const QList getTasks(); - auto indexDir() const -> QDir { return m_resource_model->indexDir(); } + auto indexDir() const -> QDir { return m_resourceModel->indexDir(); } - auto noUpdates() const -> bool { return m_no_updates; }; + auto noUpdates() const -> bool { return m_noUpdates; }; auto aborted() const -> bool { return m_aborted; }; private: @@ -40,29 +40,29 @@ class ResourceUpdateDialog final : public ReviewMessageBox { void onMetadataEnsured(Resource* resource); void onMetadataFailed(Resource* resource, bool try_others = false, - ModPlatform::ResourceProvider first_choice = ModPlatform::ResourceProvider::MODRINTH); + ModPlatform::ResourceProvider firstChoice = ModPlatform::ResourceProvider::MODRINTH); private: QWidget* m_parent; - shared_qobject_ptr m_modrinth_check_task; - shared_qobject_ptr m_flame_check_task; + shared_qobject_ptr m_modrinthCheckTask; + shared_qobject_ptr m_flameCheckTask; - const std::shared_ptr m_resource_model; + const std::shared_ptr m_resourceModel; QList& m_candidates; - QList m_modrinth_to_update; - QList m_flame_to_update; + QList m_modrinthToUpdate; + QList m_flameToUpdate; - ConcurrentTask::Ptr m_second_try_metadata; - QList> m_failed_metadata; - QList> m_failed_check_update; + ConcurrentTask::Ptr m_secondTryMetadata; + QList> m_failedMetadata; + QList> m_failedCheckUpdate; QHash m_tasks; BaseInstance* m_instance; - bool m_no_updates = false; + bool m_noUpdates = false; bool m_aborted = false; - bool m_include_deps = false; + bool m_includeDeps = false; QList m_loadersList; }; From bd570aa5d1d371675ecedcf8df1437bf155853e7 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Sat, 16 Nov 2024 00:54:36 +0200 Subject: [PATCH 373/695] fix:resource update with loaders Signed-off-by: Trial97 --- launcher/modplatform/flame/FlameAPI.cpp | 12 +++- launcher/modplatform/flame/FlameAPI.h | 3 +- .../modplatform/flame/FlameCheckUpdate.cpp | 2 +- .../modrinth/ModrinthCheckUpdate.cpp | 61 ++++++++++++++----- .../modrinth/ModrinthCheckUpdate.h | 8 +-- 5 files changed, 63 insertions(+), 23 deletions(-) diff --git a/launcher/modplatform/flame/FlameAPI.cpp b/launcher/modplatform/flame/FlameAPI.cpp index d1facfd23..b0d9af804 100644 --- a/launcher/modplatform/flame/FlameAPI.cpp +++ b/launcher/modplatform/flame/FlameAPI.cpp @@ -218,9 +218,19 @@ QList FlameAPI::loadModCategories(std::shared_ptr FlameAPI::getLatestVersion(QList versions, QList instanceLoaders, - ModPlatform::ModLoaderTypes modLoaders) + ModPlatform::ModLoaderTypes modLoaders, + bool checkLoaders) { static const auto noLoader = ModPlatform::ModLoaderType(0); + if (!checkLoaders) { + std::optional ver; + for (auto file_tmp : versions) { + if (!ver.has_value() || file_tmp.date > ver->date) { + ver = file_tmp; + } + } + return ver; + } QHash bestMatch; auto checkVersion = [&bestMatch](const ModPlatform::IndexedVersion& version, const ModPlatform::ModLoaderType& loader) { if (bestMatch.contains(loader)) { diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index f72bdb624..5b8f794e6 100644 --- a/launcher/modplatform/flame/FlameAPI.h +++ b/launcher/modplatform/flame/FlameAPI.h @@ -18,7 +18,8 @@ class FlameAPI : public NetworkResourceAPI { std::optional getLatestVersion(QList versions, QList instanceLoaders, - ModPlatform::ModLoaderTypes fallback); + ModPlatform::ModLoaderTypes fallback, + bool checkLoaders); Task::Ptr getProjects(QStringList addonIds, std::shared_ptr response) const override; Task::Ptr matchFingerprints(const QList& fingerprints, std::shared_ptr response); diff --git a/launcher/modplatform/flame/FlameCheckUpdate.cpp b/launcher/modplatform/flame/FlameCheckUpdate.cpp index 8f54ee0c6..17d13deda 100644 --- a/launcher/modplatform/flame/FlameCheckUpdate.cpp +++ b/launcher/modplatform/flame/FlameCheckUpdate.cpp @@ -87,7 +87,7 @@ void FlameCheckUpdate::getLatestVersionCallback(Resource* resource, std::shared_ qCritical() << e.what(); qDebug() << doc; } - auto latest_ver = api.getLatestVersion(pack->versions, m_loadersList, resource->metadata()->loaders); + auto latest_ver = api.getLatestVersion(pack->versions, m_loadersList, resource->metadata()->loaders, !m_loadersList.isEmpty()); setStatus(tr("Parsing the API response from CurseForge for '%1'...").arg(resource->name())); diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp index 041ffddb7..6683a0ed5 100644 --- a/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp +++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.cpp @@ -15,6 +15,26 @@ static ModrinthAPI api; +ModrinthCheckUpdate::ModrinthCheckUpdate(QList& resources, + std::list& mcVersions, + QList loadersList, + std::shared_ptr resourceModel) + : CheckUpdateTask(resources, mcVersions, std::move(loadersList), std::move(resourceModel)) + , m_hashType(ModPlatform::ProviderCapabilities::hashType(ModPlatform::ResourceProvider::MODRINTH).first()) +{ + if (!m_loadersList.isEmpty()) { // this is for mods so append all the other posible loaders to the initial list + m_initialSize = m_loadersList.length(); + ModPlatform::ModLoaderTypes modLoaders; + for (auto m : resources) { + modLoaders |= m->metadata()->loaders; + } + for (auto l : m_loadersList) { + modLoaders &= ~l; + } + m_loadersList.append(ModPlatform::modLoaderTypesToList(modLoaders)); + } +} + bool ModrinthCheckUpdate::abort() { if (m_job) @@ -34,6 +54,7 @@ void ModrinthCheckUpdate::executeTask() auto hashing_task = makeShared("MakeModrinthHashesTask", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt()); + bool startHasing = false; for (auto* resource : m_resources) { auto hash = resource->metadata()->hash; @@ -45,23 +66,37 @@ void ModrinthCheckUpdate::executeTask() connect(hash_task.get(), &Hashing::Hasher::resultsReady, [this, resource](QString hash) { m_mappings.insert(hash, resource); }); connect(hash_task.get(), &Task::failed, [this] { failed("Failed to generate hash"); }); hashing_task->addTask(hash_task); + startHasing = true; } else { m_mappings.insert(hash, resource); } } - connect(hashing_task.get(), &Task::finished, this, &ModrinthCheckUpdate::checkNextLoader); - m_job = hashing_task; - hashing_task->start(); + if (startHasing) { + connect(hashing_task.get(), &Task::finished, this, &ModrinthCheckUpdate::checkNextLoader); + m_job = hashing_task; + hashing_task->start(); + } else { + checkNextLoader(); + } } -void ModrinthCheckUpdate::getUpdateModsForLoader(std::optional loader) +void ModrinthCheckUpdate::getUpdateModsForLoader(std::optional loader, bool forceModLoaderCheck) { setStatus(tr("Waiting for the API response from Modrinth...")); setProgress(m_progress + 1, m_progressTotal); auto response = std::make_shared(); - QStringList hashes = m_mappings.keys(); + QStringList hashes; + if (forceModLoaderCheck && loader.has_value()) { + for (auto hash : m_mappings.keys()) { + if (m_mappings[hash]->metadata()->loaders & loader.value()) { + hashes.append(hash); + } + } + } else { + hashes = m_mappings.keys(); + } auto job = api.latestVersions(hashes, m_hashType, m_gameVersions, loader, response); connect(job.get(), &Task::succeeded, this, [this, response, loader] { checkVersionsResponse(response, loader); }); @@ -69,6 +104,7 @@ void ModrinthCheckUpdate::getUpdateModsForLoader(std::optionalstart(); } @@ -165,16 +201,11 @@ void ModrinthCheckUpdate::checkNextLoader() emitSucceeded(); return; } - - if (m_loadersList.isEmpty() && m_loaderIdx == 0) { - getUpdateModsForLoader({}); - m_loaderIdx++; + if (m_loaderIdx < m_loadersList.size()) { // this are mods so check with loades + getUpdateModsForLoader(m_loadersList.at(m_loaderIdx), m_loaderIdx > m_initialSize); return; - } - - if (m_loaderIdx < m_loadersList.size()) { - getUpdateModsForLoader(m_loadersList.at(m_loaderIdx)); - m_loaderIdx++; + } else if (m_loadersList.isEmpty() && m_loaderIdx == 0) { // this are other resources no need to check more than once with empty loader + getUpdateModsForLoader(); return; } @@ -192,4 +223,4 @@ void ModrinthCheckUpdate::checkNextLoader() } emitSucceeded(); -} +} \ No newline at end of file diff --git a/launcher/modplatform/modrinth/ModrinthCheckUpdate.h b/launcher/modplatform/modrinth/ModrinthCheckUpdate.h index bde61bb23..eb8057694 100644 --- a/launcher/modplatform/modrinth/ModrinthCheckUpdate.h +++ b/launcher/modplatform/modrinth/ModrinthCheckUpdate.h @@ -9,17 +9,14 @@ class ModrinthCheckUpdate : public CheckUpdateTask { ModrinthCheckUpdate(QList& resources, std::list& mcVersions, QList loadersList, - std::shared_ptr resourceModel) - : CheckUpdateTask(resources, mcVersions, std::move(loadersList), std::move(resourceModel)) - , m_hashType(ModPlatform::ProviderCapabilities::hashType(ModPlatform::ResourceProvider::MODRINTH).first()) - {} + std::shared_ptr resourceModel); public slots: bool abort() override; protected slots: void executeTask() override; - void getUpdateModsForLoader(std::optional loader); + void getUpdateModsForLoader(std::optional loader = {}, bool forceModLoaderCheck = false); void checkVersionsResponse(std::shared_ptr response, std::optional loader); void checkNextLoader(); @@ -28,4 +25,5 @@ class ModrinthCheckUpdate : public CheckUpdateTask { QHash m_mappings; QString m_hashType; int m_loaderIdx = 0; + int m_initialSize = 0; }; From 0ae0996adc7b50566965be68363a9a3fb8a19987 Mon Sep 17 00:00:00 2001 From: Seth Flynn Date: Sun, 13 Jul 2025 16:28:56 -0400 Subject: [PATCH 374/695] build(vcpkg): patch meson to support universal binaries tomlplusplus uses Meson as a build system, which makes us come across a small bug when building Universal Binaries with our custom triplet I hate vendoring this Signed-off-by: Seth Flynn --- COPYING.md | 23 + cmake/vcpkg-ports/vcpkg-tool-meson/README.md | 3 + .../vcpkg-tool-meson/adjust-args.patch | 13 + .../vcpkg-tool-meson/adjust-python-dep.patch | 45 ++ .../fix-libcpp-enable-assertions.patch | 52 ++ .../vcpkg-tool-meson/install.cmake | 5 + .../vcpkg-tool-meson/meson-intl.patch | 13 + .../vcpkg-tool-meson/meson.template.in | 43 ++ .../vcpkg-tool-meson/portfile.cmake | 45 ++ ...remove-freebsd-pcfile-specialization.patch | 23 + .../vcpkg-tool-meson/universal-osx.patch | 16 + .../vcpkg-tool-meson/vcpkg-port-config.cmake | 62 +++ cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg.json | 11 + .../vcpkg_configure_meson.cmake | 480 ++++++++++++++++++ .../vcpkg_install_meson.cmake | 71 +++ vcpkg-configuration.json | 3 + 16 files changed, 908 insertions(+) create mode 100644 cmake/vcpkg-ports/vcpkg-tool-meson/README.md create mode 100644 cmake/vcpkg-ports/vcpkg-tool-meson/adjust-args.patch create mode 100644 cmake/vcpkg-ports/vcpkg-tool-meson/adjust-python-dep.patch create mode 100644 cmake/vcpkg-ports/vcpkg-tool-meson/fix-libcpp-enable-assertions.patch create mode 100644 cmake/vcpkg-ports/vcpkg-tool-meson/install.cmake create mode 100644 cmake/vcpkg-ports/vcpkg-tool-meson/meson-intl.patch create mode 100644 cmake/vcpkg-ports/vcpkg-tool-meson/meson.template.in create mode 100644 cmake/vcpkg-ports/vcpkg-tool-meson/portfile.cmake create mode 100644 cmake/vcpkg-ports/vcpkg-tool-meson/remove-freebsd-pcfile-specialization.patch create mode 100644 cmake/vcpkg-ports/vcpkg-tool-meson/universal-osx.patch create mode 100644 cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg-port-config.cmake create mode 100644 cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg.json create mode 100644 cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg_configure_meson.cmake create mode 100644 cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg_install_meson.cmake diff --git a/COPYING.md b/COPYING.md index f9b905351..e64bb8760 100644 --- a/COPYING.md +++ b/COPYING.md @@ -412,3 +412,26 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose and noninfringement. In no event shall the authors or copyright holders be liable for any claim, damages or other liability, whether in an action of contract, tort or otherwise, arising from, out of or in connection with the Software or the use or other dealings in the Software. + +## vcpkg (`cmake/vcpkg-ports`) + + MIT License + + Copyright (c) Microsoft Corporation + + Permission is hereby granted, free of charge, to any person obtaining a copy of this + software and associated documentation files (the "Software"), to deal in the Software + without restriction, including without limitation the rights to use, copy, modify, + merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be included in all copies + or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A + PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE + OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/cmake/vcpkg-ports/vcpkg-tool-meson/README.md b/cmake/vcpkg-ports/vcpkg-tool-meson/README.md new file mode 100644 index 000000000..9047c8037 --- /dev/null +++ b/cmake/vcpkg-ports/vcpkg-tool-meson/README.md @@ -0,0 +1,3 @@ +The only difference between this and the upstream vcpkg port is the addition of `universal-osx.patch`. It's very annoying we need to bundle this entire tree to do that. + +-@getchoo diff --git a/cmake/vcpkg-ports/vcpkg-tool-meson/adjust-args.patch b/cmake/vcpkg-ports/vcpkg-tool-meson/adjust-args.patch new file mode 100644 index 000000000..ad800aa66 --- /dev/null +++ b/cmake/vcpkg-ports/vcpkg-tool-meson/adjust-args.patch @@ -0,0 +1,13 @@ +diff --git a/mesonbuild/cmake/toolchain.py b/mesonbuild/cmake/toolchain.py +index 11a00be5d..89ae490ff 100644 +--- a/mesonbuild/cmake/toolchain.py ++++ b/mesonbuild/cmake/toolchain.py +@@ -202,7 +202,7 @@ class CMakeToolchain: + @staticmethod + def is_cmdline_option(compiler: 'Compiler', arg: str) -> bool: + if compiler.get_argument_syntax() == 'msvc': +- return arg.startswith('/') ++ return arg.startswith(('/','-')) + else: + if os.path.basename(compiler.get_exe()) == 'zig' and arg in {'ar', 'cc', 'c++', 'dlltool', 'lib', 'ranlib', 'objcopy', 'rc'}: + return True diff --git a/cmake/vcpkg-ports/vcpkg-tool-meson/adjust-python-dep.patch b/cmake/vcpkg-ports/vcpkg-tool-meson/adjust-python-dep.patch new file mode 100644 index 000000000..0cbfe717d --- /dev/null +++ b/cmake/vcpkg-ports/vcpkg-tool-meson/adjust-python-dep.patch @@ -0,0 +1,45 @@ +diff --git a/mesonbuild/dependencies/python.py b/mesonbuild/dependencies/python.py +index 883a29a..d9a82af 100644 +--- a/mesonbuild/dependencies/python.py ++++ b/mesonbuild/dependencies/python.py +@@ -232,8 +232,10 @@ class _PythonDependencyBase(_Base): + else: + if self.is_freethreaded: + libpath = Path('libs') / f'python{vernum}t.lib' ++ libpath = Path('libs') / f'..' / f'..' / f'..' / f'lib' / f'python{vernum}t.lib' + else: + libpath = Path('libs') / f'python{vernum}.lib' ++ libpath = Path('libs') / f'..' / f'..' / f'..' / f'lib' / f'python{vernum}.lib' + # For a debug build, pyconfig.h may force linking with + # pythonX_d.lib (see meson#10776). This cannot be avoided + # and won't work unless we also have a debug build of +@@ -250,6 +252,8 @@ class _PythonDependencyBase(_Base): + vscrt = self.env.coredata.optstore.get_value('b_vscrt') + if vscrt in {'mdd', 'mtd', 'from_buildtype', 'static_from_buildtype'}: + vscrt_debug = True ++ if is_debug_build: ++ libpath = Path('libs') / f'..' / f'..' / f'..' / f'debug/lib' / f'python{vernum}_d.lib' + if is_debug_build and vscrt_debug and not self.variables.get('Py_DEBUG'): + mlog.warning(textwrap.dedent('''\ + Using a debug build type with MSVC or an MSVC-compatible compiler +@@ -350,9 +354,10 @@ class PythonSystemDependency(SystemDependency, _PythonDependencyBase): + self.is_found = True + + # compile args ++ verdot = self.variables.get('py_version_short') + inc_paths = mesonlib.OrderedSet([ + self.variables.get('INCLUDEPY'), +- self.paths.get('include'), ++ self.paths.get('include') + f'/../../../include/python${verdot}', + self.paths.get('platinclude')]) + + self.compile_args += ['-I' + path for path in inc_paths if path] +@@ -416,7 +421,7 @@ def python_factory(env: 'Environment', for_machine: 'MachineChoice', + candidates.append(functools.partial(wrap_in_pythons_pc_dir, pkg_name, env, kwargs, installation)) + # We only need to check both, if a python install has a LIBPC. It might point to the wrong location, + # e.g. relocated / cross compilation, but the presence of LIBPC indicates we should definitely look for something. +- if pkg_libdir is not None: ++ if True or pkg_libdir is not None: + candidates.append(functools.partial(PythonPkgConfigDependency, pkg_name, env, kwargs, installation)) + else: + candidates.append(functools.partial(PkgConfigDependency, 'python3', env, kwargs)) diff --git a/cmake/vcpkg-ports/vcpkg-tool-meson/fix-libcpp-enable-assertions.patch b/cmake/vcpkg-ports/vcpkg-tool-meson/fix-libcpp-enable-assertions.patch new file mode 100644 index 000000000..394b064dc --- /dev/null +++ b/cmake/vcpkg-ports/vcpkg-tool-meson/fix-libcpp-enable-assertions.patch @@ -0,0 +1,52 @@ +From a16ec8b0fb6d7035b669a13edd4d97ff0c307a0b Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Martin=20D=C3=B8rum?= +Date: Fri, 2 May 2025 10:56:28 +0200 +Subject: [PATCH] cpp: fix _LIBCPP_ENABLE_ASSERTIONS warning + +libc++ deprecated _LIBCPP_ENABLE_ASSERTIONS from version 18. +However, the libc++ shipped with Apple Clang backported that +deprecation in version 17 already, +which is the version which Apple currently ships for macOS. +This PR changes the _LIBCPP_ENABLE_ASSERTIONS deprecation check +to use version ">=17" on Apple Clang. +--- + mesonbuild/compilers/cpp.py | 12 ++++++++++-- + 1 file changed, 10 insertions(+), 2 deletions(-) + +diff --git a/mesonbuild/compilers/cpp.py b/mesonbuild/compilers/cpp.py +index 01b9bb9fa34f..f7dc150e8608 100644 +--- a/mesonbuild/compilers/cpp.py ++++ b/mesonbuild/compilers/cpp.py +@@ -311,6 +311,9 @@ def get_option_link_args(self, target: 'BuildTarget', env: 'Environment', subpro + return libs + return [] + ++ def is_libcpp_enable_assertions_deprecated(self) -> bool: ++ return version_compare(self.version, ">=18") ++ + def get_assert_args(self, disable: bool, env: 'Environment') -> T.List[str]: + if disable: + return ['-DNDEBUG'] +@@ -323,7 +326,7 @@ def get_assert_args(self, disable: bool, env: 'Environment') -> T.List[str]: + if self.language_stdlib_provider(env) == 'stdc++': + return ['-D_GLIBCXX_ASSERTIONS=1'] + else: +- if version_compare(self.version, '>=18'): ++ if self.is_libcpp_enable_assertions_deprecated(): + return ['-D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_FAST'] + elif version_compare(self.version, '>=15'): + return ['-D_LIBCPP_ENABLE_ASSERTIONS=1'] +@@ -343,7 +346,12 @@ class ArmLtdClangCPPCompiler(ClangCPPCompiler): + + + class AppleClangCPPCompiler(AppleCompilerMixin, AppleCPPStdsMixin, ClangCPPCompiler): +- pass ++ def is_libcpp_enable_assertions_deprecated(self) -> bool: ++ # Upstream libc++ deprecated _LIBCPP_ENABLE_ASSERTIONS ++ # in favor of _LIBCPP_HARDENING_MODE from version 18 onwards, ++ # but Apple Clang 17's libc++ has back-ported that change. ++ # See: https://github.com/mesonbuild/meson/issues/14440 ++ return version_compare(self.version, ">=17") + + + class EmscriptenCPPCompiler(EmscriptenMixin, ClangCPPCompiler): diff --git a/cmake/vcpkg-ports/vcpkg-tool-meson/install.cmake b/cmake/vcpkg-ports/vcpkg-tool-meson/install.cmake new file mode 100644 index 000000000..84201aa1a --- /dev/null +++ b/cmake/vcpkg-ports/vcpkg-tool-meson/install.cmake @@ -0,0 +1,5 @@ +file(MAKE_DIRECTORY "${CURRENT_PACKAGES_DIR}/tools/meson") +file(INSTALL "${SOURCE_PATH}/meson.py" + "${SOURCE_PATH}/mesonbuild" + DESTINATION "${CURRENT_PACKAGES_DIR}/tools/meson" +) diff --git a/cmake/vcpkg-ports/vcpkg-tool-meson/meson-intl.patch b/cmake/vcpkg-ports/vcpkg-tool-meson/meson-intl.patch new file mode 100644 index 000000000..8f2a029de --- /dev/null +++ b/cmake/vcpkg-ports/vcpkg-tool-meson/meson-intl.patch @@ -0,0 +1,13 @@ +diff --git a/mesonbuild/dependencies/misc.py b/mesonbuild/dependencies/misc.py +--- a/mesonbuild/dependencies/misc.py ++++ b/mesonbuild/dependencies/misc.py +@@ -593,7 +593,8 @@ iconv_factory = DependencyFactory( + + packages['intl'] = intl_factory = DependencyFactory( + 'intl', ++ [DependencyMethods.BUILTIN, DependencyMethods.SYSTEM, DependencyMethods.CMAKE], ++ cmake_name='Intl', +- [DependencyMethods.BUILTIN, DependencyMethods.SYSTEM], + builtin_class=IntlBuiltinDependency, + system_class=IntlSystemDependency, + ) diff --git a/cmake/vcpkg-ports/vcpkg-tool-meson/meson.template.in b/cmake/vcpkg-ports/vcpkg-tool-meson/meson.template.in new file mode 100644 index 000000000..df21b753b --- /dev/null +++ b/cmake/vcpkg-ports/vcpkg-tool-meson/meson.template.in @@ -0,0 +1,43 @@ +[binaries] +cmake = ['@CMAKE_COMMAND@'] +ninja = ['@NINJA@'] +pkg-config = ['@PKGCONFIG@'] +@MESON_MT@ +@MESON_AR@ +@MESON_RC@ +@MESON_C@ +@MESON_C_LD@ +@MESON_CXX@ +@MESON_CXX_LD@ +@MESON_OBJC@ +@MESON_OBJC_LD@ +@MESON_OBJCPP@ +@MESON_OBJCPP_LD@ +@MESON_FC@ +@MESON_FC_LD@ +@MESON_WINDRES@ +@MESON_ADDITIONAL_BINARIES@ +[properties] +cmake_toolchain_file = '@SCRIPTS@/buildsystems/vcpkg.cmake' +@MESON_ADDITIONAL_PROPERTIES@ +[cmake] +CMAKE_BUILD_TYPE = '@MESON_CMAKE_BUILD_TYPE@' +VCPKG_TARGET_TRIPLET = '@TARGET_TRIPLET@' +VCPKG_HOST_TRIPLET = '@_HOST_TRIPLET@' +VCPKG_CHAINLOAD_TOOLCHAIN_FILE = '@VCPKG_CHAINLOAD_TOOLCHAIN_FILE@' +VCPKG_CRT_LINKAGE = '@VCPKG_CRT_LINKAGE@' +_VCPKG_INSTALLED_DIR = '@_VCPKG_INSTALLED_DIR@' +@MESON_HOST_MACHINE@ +@MESON_BUILD_MACHINE@ +[built-in options] +default_library = '@MESON_DEFAULT_LIBRARY@' +werror = false +@MESON_CFLAGS@ +@MESON_CXXFLAGS@ +@MESON_FCFLAGS@ +@MESON_OBJCFLAGS@ +@MESON_OBJCPPFLAGS@ +# b_vscrt +@MESON_VSCRT_LINKAGE@ +# c_winlibs/cpp_winlibs +@MESON_WINLIBS@ \ No newline at end of file diff --git a/cmake/vcpkg-ports/vcpkg-tool-meson/portfile.cmake b/cmake/vcpkg-ports/vcpkg-tool-meson/portfile.cmake new file mode 100644 index 000000000..fdea886a7 --- /dev/null +++ b/cmake/vcpkg-ports/vcpkg-tool-meson/portfile.cmake @@ -0,0 +1,45 @@ +# This port represents a dependency on the Meson build system. +# In the future, it is expected that this port acquires and installs Meson. +# Currently is used in ports that call vcpkg_find_acquire_program(MESON) in order to force rebuilds. + +set(VCPKG_POLICY_CMAKE_HELPER_PORT enabled) + +set(patches + meson-intl.patch + adjust-python-dep.patch + adjust-args.patch + remove-freebsd-pcfile-specialization.patch + fix-libcpp-enable-assertions.patch # https://github.com/mesonbuild/meson/pull/14548, Remove in 1.8.3 + universal-osx.patch # NOTE(@getchoo): THIS IS THE ONLY CHANGE NEEDED FOR PRISM +) +set(scripts + vcpkg-port-config.cmake + vcpkg_configure_meson.cmake + vcpkg_install_meson.cmake + meson.template.in +) +set(to_hash + "${CMAKE_CURRENT_LIST_DIR}/vcpkg.json" + "${CMAKE_CURRENT_LIST_DIR}/portfile.cmake" +) +foreach(file IN LISTS patches scripts) + set(filepath "${CMAKE_CURRENT_LIST_DIR}/${file}") + list(APPEND to_hash "${filepath}") + file(COPY "${filepath}" DESTINATION "${CURRENT_PACKAGES_DIR}/share/${PORT}") +endforeach() + +set(meson_path_hash "") +foreach(filepath IN LISTS to_hash) + file(SHA1 "${filepath}" to_append) + string(APPEND meson_path_hash "${to_append}") +endforeach() +string(SHA512 meson_path_hash "${meson_path_hash}") + +string(SUBSTRING "${meson_path_hash}" 0 6 MESON_SHORT_HASH) +list(TRANSFORM patches REPLACE [[^(..*)$]] [["${CMAKE_CURRENT_LIST_DIR}/\0"]]) +list(JOIN patches "\n " PATCHES) +configure_file("${CMAKE_CURRENT_LIST_DIR}/vcpkg-port-config.cmake" "${CURRENT_PACKAGES_DIR}/share/${PORT}/vcpkg-port-config.cmake" @ONLY) + +vcpkg_install_copyright(FILE_LIST "${VCPKG_ROOT_DIR}/LICENSE.txt") + +include("${CURRENT_PACKAGES_DIR}/share/${PORT}/vcpkg-port-config.cmake") diff --git a/cmake/vcpkg-ports/vcpkg-tool-meson/remove-freebsd-pcfile-specialization.patch b/cmake/vcpkg-ports/vcpkg-tool-meson/remove-freebsd-pcfile-specialization.patch new file mode 100644 index 000000000..947345ccf --- /dev/null +++ b/cmake/vcpkg-ports/vcpkg-tool-meson/remove-freebsd-pcfile-specialization.patch @@ -0,0 +1,23 @@ +diff --git a/mesonbuild/modules/pkgconfig.py b/mesonbuild/modules/pkgconfig.py +index cc0450a52..13501466d 100644 +--- a/mesonbuild/modules/pkgconfig.py ++++ b/mesonbuild/modules/pkgconfig.py +@@ -701,16 +701,8 @@ class PkgConfigModule(NewExtensionModule): + pcfile = filebase + '.pc' + pkgroot = pkgroot_name = kwargs['install_dir'] or default_install_dir + if pkgroot is None: +- m = state.environment.machines.host +- if m.is_freebsd(): +- pkgroot = os.path.join(_as_str(state.environment.coredata.optstore.get_value_for(OptionKey('prefix'))), 'libdata', 'pkgconfig') +- pkgroot_name = os.path.join('{prefix}', 'libdata', 'pkgconfig') +- elif m.is_haiku(): +- pkgroot = os.path.join(_as_str(state.environment.coredata.optstore.get_value_for(OptionKey('prefix'))), 'develop', 'lib', 'pkgconfig') +- pkgroot_name = os.path.join('{prefix}', 'develop', 'lib', 'pkgconfig') +- else: +- pkgroot = os.path.join(_as_str(state.environment.coredata.optstore.get_value_for(OptionKey('libdir'))), 'pkgconfig') +- pkgroot_name = os.path.join('{libdir}', 'pkgconfig') ++ pkgroot = os.path.join(_as_str(state.environment.coredata.optstore.get_value_for(OptionKey('libdir'))), 'pkgconfig') ++ pkgroot_name = os.path.join('{libdir}', 'pkgconfig') + relocatable = state.get_option('pkgconfig.relocatable') + self._generate_pkgconfig_file(state, deps, subdirs, name, description, url, + version, pcfile, conflicts, variables, diff --git a/cmake/vcpkg-ports/vcpkg-tool-meson/universal-osx.patch b/cmake/vcpkg-ports/vcpkg-tool-meson/universal-osx.patch new file mode 100644 index 000000000..58b96d5ce --- /dev/null +++ b/cmake/vcpkg-ports/vcpkg-tool-meson/universal-osx.patch @@ -0,0 +1,16 @@ +diff --git a/mesonbuild/compilers/detect.py b/mesonbuild/compilers/detect.py +index f57957f0b..a72e72a0b 100644 +--- a/mesonbuild/compilers/detect.py ++++ b/mesonbuild/compilers/detect.py +@@ -1472,6 +1472,11 @@ def _get_clang_compiler_defines(compiler: T.List[str], lang: str) -> T.Dict[str, + """ + from .mixins.clang import clang_lang_map + ++ # Filter out `-arch` flags passed to the compiler for Universal Binaries ++ # https://github.com/mesonbuild/meson/issues/5290 ++ # https://github.com/mesonbuild/meson/issues/8206 ++ compiler = [arg for i, arg in enumerate(compiler) if not (i > 0 and compiler[i - 1] == "-arch") and not arg == "-arch"] ++ + def _try_obtain_compiler_defines(args: T.List[str]) -> str: + mlog.debug(f'Running command: {join_args(args)}') + p, output, error = Popen_safe(compiler + args, write='', stdin=subprocess.PIPE) diff --git a/cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg-port-config.cmake b/cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg-port-config.cmake new file mode 100644 index 000000000..c0dee3a38 --- /dev/null +++ b/cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg-port-config.cmake @@ -0,0 +1,62 @@ +include("${CURRENT_HOST_INSTALLED_DIR}/share/vcpkg-cmake-get-vars/vcpkg-port-config.cmake") +# Overwrite builtin scripts +include("${CMAKE_CURRENT_LIST_DIR}/vcpkg_configure_meson.cmake") +include("${CMAKE_CURRENT_LIST_DIR}/vcpkg_install_meson.cmake") + +set(meson_short_hash @MESON_SHORT_HASH@) + +# Setup meson: +set(program MESON) +set(program_version @VERSION@) +set(program_name meson) +set(search_names meson meson.py) +set(ref "${program_version}") +set(path_to_search "${DOWNLOADS}/tools/meson-${program_version}-${meson_short_hash}") +set(download_urls "https://github.com/mesonbuild/meson/archive/${ref}.tar.gz") +set(download_filename "meson-${ref}.tar.gz") +set(download_sha512 bd2e65f0863d9cb974e659ff502d773e937b8a60aaddfd7d81e34cd2c296c8e82bf214d790ac089ba441543059dfc2677ba95ed51f676df9da420859f404a907) + +find_program(SCRIPT_MESON NAMES ${search_names} PATHS "${path_to_search}" NO_DEFAULT_PATH) # NO_DEFAULT_PATH due top patching + +if(NOT SCRIPT_MESON) + vcpkg_download_distfile(archive_path + URLS ${download_urls} + SHA512 "${download_sha512}" + FILENAME "${download_filename}" + ) + file(REMOVE_RECURSE "${path_to_search}") + file(REMOVE_RECURSE "${path_to_search}-tmp") + file(MAKE_DIRECTORY "${path_to_search}-tmp") + file(ARCHIVE_EXTRACT INPUT "${archive_path}" + DESTINATION "${path_to_search}-tmp" + #PATTERNS "**/mesonbuild/*" "**/*.py" + ) + z_vcpkg_apply_patches( + SOURCE_PATH "${path_to_search}-tmp/meson-${ref}" + PATCHES + @PATCHES@ + ) + file(MAKE_DIRECTORY "${path_to_search}") + file(RENAME "${path_to_search}-tmp/meson-${ref}/meson.py" "${path_to_search}/meson.py") + file(RENAME "${path_to_search}-tmp/meson-${ref}/mesonbuild" "${path_to_search}/mesonbuild") + file(REMOVE_RECURSE "${path_to_search}-tmp") + set(SCRIPT_MESON "${path_to_search}/meson.py") +endif() + +# Check required python version +vcpkg_find_acquire_program(PYTHON3) +vcpkg_execute_in_download_mode( + COMMAND "${PYTHON3}" --version + OUTPUT_VARIABLE version_contents + WORKING_DIRECTORY "${CURRENT_BUILDTREES_DIR}" +) +string(REGEX MATCH [[[0-9]+\.[0-9]+\.[0-9]+]] python_ver "${version_contents}") + +set(min_required 3.7) +if(python_ver VERSION_LESS "${min_required}") + message(FATAL_ERROR "Found Python version '${python_ver} at ${PYTHON3}' is insufficient for meson. meson requires at least version '${min_required}'") +else() + message(STATUS "Found Python version '${python_ver} at ${PYTHON3}'") +endif() + +message(STATUS "Using meson: ${SCRIPT_MESON}") diff --git a/cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg.json b/cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg.json new file mode 100644 index 000000000..04a0cbbec --- /dev/null +++ b/cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg.json @@ -0,0 +1,11 @@ +{ + "name": "vcpkg-tool-meson", + "version": "1.8.2", + "description": "Meson build system", + "homepage": "https://github.com/mesonbuild/meson", + "license": "Apache-2.0", + "supports": "native", + "dependencies": [ + "vcpkg-cmake-get-vars" + ] +} diff --git a/cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg_configure_meson.cmake b/cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg_configure_meson.cmake new file mode 100644 index 000000000..6b00200d1 --- /dev/null +++ b/cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg_configure_meson.cmake @@ -0,0 +1,480 @@ +function(z_vcpkg_meson_set_proglist_variables config_type) + if(VCPKG_TARGET_IS_WINDOWS) + set(proglist MT AR) + else() + set(proglist AR RANLIB STRIP NM OBJDUMP DLLTOOL MT) + endif() + foreach(prog IN LISTS proglist) + if(VCPKG_DETECTED_CMAKE_${prog}) + if(meson_${prog}) + string(TOUPPER "MESON_${meson_${prog}}" var_to_set) + set("${var_to_set}" "${meson_${prog}} = ['${VCPKG_DETECTED_CMAKE_${prog}}']" PARENT_SCOPE) + elseif(${prog} STREQUAL AR AND VCPKG_COMBINED_STATIC_LINKER_FLAGS_${config_type}) + # Probably need to move AR somewhere else + string(TOLOWER "${prog}" proglower) + z_vcpkg_meson_convert_compiler_flags_to_list(ar_flags "${VCPKG_COMBINED_STATIC_LINKER_FLAGS_${config_type}}") + list(PREPEND ar_flags "${VCPKG_DETECTED_CMAKE_${prog}}") + z_vcpkg_meson_convert_list_to_python_array(ar_flags ${ar_flags}) + set("MESON_AR" "${proglower} = ${ar_flags}" PARENT_SCOPE) + else() + string(TOUPPER "MESON_${prog}" var_to_set) + string(TOLOWER "${prog}" proglower) + set("${var_to_set}" "${proglower} = ['${VCPKG_DETECTED_CMAKE_${prog}}']" PARENT_SCOPE) + endif() + endif() + endforeach() + set(compilers "${arg_LANGUAGES}") + if(VCPKG_TARGET_IS_WINDOWS) + list(APPEND compilers RC) + endif() + set(meson_RC windres) + set(meson_Fortran fortran) + set(meson_CXX cpp) + foreach(prog IN LISTS compilers) + if(VCPKG_DETECTED_CMAKE_${prog}_COMPILER) + string(TOUPPER "MESON_${prog}" var_to_set) + if(meson_${prog}) + if(VCPKG_COMBINED_${prog}_FLAGS_${config_type}) + # Need compiler flags in prog vars for sanity check. + z_vcpkg_meson_convert_compiler_flags_to_list(${prog}flags "${VCPKG_COMBINED_${prog}_FLAGS_${config_type}}") + endif() + list(PREPEND ${prog}flags "${VCPKG_DETECTED_CMAKE_${prog}_COMPILER}") + list(FILTER ${prog}flags EXCLUDE REGEX "(-|/)nologo") # Breaks compiler detection otherwise + z_vcpkg_meson_convert_list_to_python_array(${prog}flags ${${prog}flags}) + set("${var_to_set}" "${meson_${prog}} = ${${prog}flags}" PARENT_SCOPE) + if (DEFINED VCPKG_DETECTED_CMAKE_${prog}_COMPILER_ID + AND NOT VCPKG_DETECTED_CMAKE_${prog}_COMPILER_ID MATCHES "^(GNU|Intel)$" + AND VCPKG_DETECTED_CMAKE_LINKER) + string(TOUPPER "MESON_${prog}_LD" var_to_set) + set(${var_to_set} "${meson_${prog}}_ld = ['${VCPKG_DETECTED_CMAKE_LINKER}']" PARENT_SCOPE) + endif() + else() + if(VCPKG_COMBINED_${prog}_FLAGS_${config_type}) + # Need compiler flags in prog vars for sanity check. + z_vcpkg_meson_convert_compiler_flags_to_list(${prog}flags "${VCPKG_COMBINED_${prog}_FLAGS_${config_type}}") + endif() + list(PREPEND ${prog}flags "${VCPKG_DETECTED_CMAKE_${prog}_COMPILER}") + list(FILTER ${prog}flags EXCLUDE REGEX "(-|/)nologo") # Breaks compiler detection otherwise + z_vcpkg_meson_convert_list_to_python_array(${prog}flags ${${prog}flags}) + string(TOLOWER "${prog}" proglower) + set("${var_to_set}" "${proglower} = ${${prog}flags}" PARENT_SCOPE) + if (DEFINED VCPKG_DETECTED_CMAKE_${prog}_COMPILER_ID + AND NOT VCPKG_DETECTED_CMAKE_${prog}_COMPILER_ID MATCHES "^(GNU|Intel)$" + AND VCPKG_DETECTED_CMAKE_LINKER) + string(TOUPPER "MESON_${prog}_LD" var_to_set) + set(${var_to_set} "${proglower}_ld = ['${VCPKG_DETECTED_CMAKE_LINKER}']" PARENT_SCOPE) + endif() + endif() + endif() + endforeach() +endfunction() + +function(z_vcpkg_meson_convert_compiler_flags_to_list out_var compiler_flags) + separate_arguments(cmake_list NATIVE_COMMAND "${compiler_flags}") + list(TRANSFORM cmake_list REPLACE ";" [[\\;]]) + set("${out_var}" "${cmake_list}" PARENT_SCOPE) +endfunction() + +function(z_vcpkg_meson_convert_list_to_python_array out_var) + z_vcpkg_function_arguments(flag_list 1) + vcpkg_list(REMOVE_ITEM flag_list "") # remove empty elements if any + vcpkg_list(JOIN flag_list "', '" flag_list) + set("${out_var}" "['${flag_list}']" PARENT_SCOPE) +endfunction() + +# Generates the required compiler properties for meson +function(z_vcpkg_meson_set_flags_variables config_type) + if(VCPKG_TARGET_IS_WINDOWS AND NOT VCPKG_TARGET_IS_MINGW) + set(libpath_flag /LIBPATH:) + else() + set(libpath_flag -L) + endif() + if(config_type STREQUAL "DEBUG") + set(path_suffix "/debug") + else() + set(path_suffix "") + endif() + + set(includepath "-I${CURRENT_INSTALLED_DIR}/include") + set(libpath "${libpath_flag}${CURRENT_INSTALLED_DIR}${path_suffix}/lib") + + foreach(lang IN LISTS arg_LANGUAGES) + z_vcpkg_meson_convert_compiler_flags_to_list(${lang}flags "${VCPKG_COMBINED_${lang}_FLAGS_${config_type}}") + if(lang MATCHES "^(C|CXX)$") + vcpkg_list(APPEND ${lang}flags "${includepath}") + endif() + z_vcpkg_meson_convert_list_to_python_array(${lang}flags ${${lang}flags}) + set(lang_mapping "${lang}") + if(lang STREQUAL "Fortran") + set(lang_mapping "FC") + endif() + string(TOLOWER "${lang_mapping}" langlower) + if(lang STREQUAL "CXX") + set(langlower cpp) + endif() + set(MESON_${lang_mapping}FLAGS "${langlower}_args = ${${lang}flags}\n") + set(linker_flags "${VCPKG_COMBINED_SHARED_LINKER_FLAGS_${config_type}}") + z_vcpkg_meson_convert_compiler_flags_to_list(linker_flags "${linker_flags}") + vcpkg_list(APPEND linker_flags "${libpath}") + z_vcpkg_meson_convert_list_to_python_array(linker_flags ${linker_flags}) + string(APPEND MESON_${lang_mapping}FLAGS "${langlower}_link_args = ${linker_flags}\n") + set(MESON_${lang_mapping}FLAGS "${MESON_${lang_mapping}FLAGS}" PARENT_SCOPE) + endforeach() +endfunction() + +function(z_vcpkg_get_build_and_host_system build_system host_system is_cross) #https://mesonbuild.com/Cross-compilation.html + set(build_unknown FALSE) + if(CMAKE_HOST_WIN32) + if(DEFINED ENV{PROCESSOR_ARCHITEW6432}) + set(build_arch $ENV{PROCESSOR_ARCHITEW6432}) + else() + set(build_arch $ENV{PROCESSOR_ARCHITECTURE}) + endif() + if(build_arch MATCHES "(amd|AMD)64") + set(build_cpu_fam x86_64) + set(build_cpu x86_64) + elseif(build_arch MATCHES "(x|X)86") + set(build_cpu_fam x86) + set(build_cpu i686) + elseif(build_arch MATCHES "^(ARM|arm)64$") + set(build_cpu_fam aarch64) + set(build_cpu armv8) + elseif(build_arch MATCHES "^(ARM|arm)$") + set(build_cpu_fam arm) + set(build_cpu armv7hl) + else() + if(NOT DEFINED VCPKG_MESON_CROSS_FILE OR NOT DEFINED VCPKG_MESON_NATIVE_FILE) + message(WARNING "Unsupported build architecture ${build_arch}! Please set VCPKG_MESON_(CROSS|NATIVE)_FILE to a meson file containing the build_machine entry!") + endif() + set(build_unknown TRUE) + endif() + elseif(CMAKE_HOST_UNIX) + # at this stage, CMAKE_HOST_SYSTEM_PROCESSOR is not defined + execute_process( + COMMAND uname -m + OUTPUT_VARIABLE MACHINE + OUTPUT_STRIP_TRAILING_WHITESPACE + COMMAND_ERROR_IS_FATAL ANY) + + # Show real machine architecture to visually understand whether we are in a native Apple Silicon terminal or running under Rosetta emulation + debug_message("Machine: ${MACHINE}") + + if(MACHINE MATCHES "arm64|aarch64") + set(build_cpu_fam aarch64) + set(build_cpu armv8) + elseif(MACHINE MATCHES "armv7h?l") + set(build_cpu_fam arm) + set(build_cpu ${MACHINE}) + elseif(MACHINE MATCHES "x86_64|amd64") + set(build_cpu_fam x86_64) + set(build_cpu x86_64) + elseif(MACHINE MATCHES "x86|i686") + set(build_cpu_fam x86) + set(build_cpu i686) + elseif(MACHINE MATCHES "i386") + set(build_cpu_fam x86) + set(build_cpu i386) + elseif(MACHINE MATCHES "loongarch64") + set(build_cpu_fam loongarch64) + set(build_cpu loongarch64) + else() + # https://github.com/mesonbuild/meson/blob/master/docs/markdown/Reference-tables.md#cpu-families + if(NOT DEFINED VCPKG_MESON_CROSS_FILE OR NOT DEFINED VCPKG_MESON_NATIVE_FILE) + message(WARNING "Unhandled machine: ${MACHINE}! Please set VCPKG_MESON_(CROSS|NATIVE)_FILE to a meson file containing the build_machine entry!") + endif() + set(build_unknown TRUE) + endif() + else() + if(NOT DEFINED VCPKG_MESON_CROSS_FILE OR NOT DEFINED VCPKG_MESON_NATIVE_FILE) + message(WARNING "Failed to detect the build architecture! Please set VCPKG_MESON_(CROSS|NATIVE)_FILE to a meson file containing the build_machine entry!") + endif() + set(build_unknown TRUE) + endif() + + set(build "[build_machine]\n") # Machine the build is performed on + string(APPEND build "endian = 'little'\n") + if(CMAKE_HOST_WIN32) + string(APPEND build "system = 'windows'\n") + elseif(CMAKE_HOST_APPLE) + string(APPEND build "system = 'darwin'\n") + elseif(CYGWIN) + string(APPEND build "system = 'cygwin'\n") + elseif(CMAKE_HOST_UNIX) + string(APPEND build "system = 'linux'\n") + else() + set(build_unknown TRUE) + endif() + + if(DEFINED build_cpu_fam) + string(APPEND build "cpu_family = '${build_cpu_fam}'\n") + endif() + if(DEFINED build_cpu) + string(APPEND build "cpu = '${build_cpu}'") + endif() + if(NOT build_unknown) + set(${build_system} "${build}" PARENT_SCOPE) + endif() + + set(host_unkown FALSE) + if(VCPKG_TARGET_ARCHITECTURE MATCHES "(amd|AMD|x|X)64") + set(host_cpu_fam x86_64) + set(host_cpu x86_64) + elseif(VCPKG_TARGET_ARCHITECTURE MATCHES "(x|X)86") + set(host_cpu_fam x86) + set(host_cpu i686) + elseif(VCPKG_TARGET_ARCHITECTURE MATCHES "^(ARM|arm)64$") + set(host_cpu_fam aarch64) + set(host_cpu armv8) + elseif(VCPKG_TARGET_ARCHITECTURE MATCHES "^(ARM|arm)$") + set(host_cpu_fam arm) + set(host_cpu armv7hl) + elseif(VCPKG_TARGET_ARCHITECTURE MATCHES "loongarch64") + set(host_cpu_fam loongarch64) + set(host_cpu loongarch64) + elseif(VCPKG_TARGET_ARCHITECTURE MATCHES "wasm32") + set(host_cpu_fam wasm32) + set(host_cpu wasm32) + else() + if(NOT DEFINED VCPKG_MESON_CROSS_FILE OR NOT DEFINED VCPKG_MESON_NATIVE_FILE) + message(WARNING "Unsupported target architecture ${VCPKG_TARGET_ARCHITECTURE}! Please set VCPKG_MESON_(CROSS|NATIVE)_FILE to a meson file containing the host_machine entry!" ) + endif() + set(host_unkown TRUE) + endif() + + set(host "[host_machine]\n") # host=target in vcpkg. + string(APPEND host "endian = 'little'\n") + if(NOT VCPKG_CMAKE_SYSTEM_NAME OR VCPKG_TARGET_IS_MINGW OR VCPKG_TARGET_IS_UWP) + set(meson_system_name "windows") + else() + string(TOLOWER "${VCPKG_CMAKE_SYSTEM_NAME}" meson_system_name) + endif() + string(APPEND host "system = '${meson_system_name}'\n") + string(APPEND host "cpu_family = '${host_cpu_fam}'\n") + string(APPEND host "cpu = '${host_cpu}'") + if(NOT host_unkown) + set(${host_system} "${host}" PARENT_SCOPE) + endif() + + if(NOT build_cpu_fam MATCHES "${host_cpu_fam}" + OR VCPKG_TARGET_IS_ANDROID OR VCPKG_TARGET_IS_IOS OR VCPKG_TARGET_IS_UWP + OR (VCPKG_TARGET_IS_MINGW AND NOT CMAKE_HOST_WIN32)) + set(${is_cross} TRUE PARENT_SCOPE) + endif() +endfunction() + +function(z_vcpkg_meson_setup_extra_windows_variables config_type) + ## b_vscrt + if(VCPKG_CRT_LINKAGE STREQUAL "static") + set(crt_type "mt") + else() + set(crt_type "md") + endif() + if(config_type STREQUAL "DEBUG") + set(crt_type "${crt_type}d") + endif() + set(MESON_VSCRT_LINKAGE "b_vscrt = '${crt_type}'" PARENT_SCOPE) + ## winlibs + separate_arguments(c_winlibs NATIVE_COMMAND "${VCPKG_DETECTED_CMAKE_C_STANDARD_LIBRARIES}") + separate_arguments(cpp_winlibs NATIVE_COMMAND "${VCPKG_DETECTED_CMAKE_CXX_STANDARD_LIBRARIES}") + z_vcpkg_meson_convert_list_to_python_array(c_winlibs ${c_winlibs}) + z_vcpkg_meson_convert_list_to_python_array(cpp_winlibs ${cpp_winlibs}) + set(MESON_WINLIBS "c_winlibs = ${c_winlibs}\n") + string(APPEND MESON_WINLIBS "cpp_winlibs = ${cpp_winlibs}") + set(MESON_WINLIBS "${MESON_WINLIBS}" PARENT_SCOPE) +endfunction() + +function(z_vcpkg_meson_setup_variables config_type) + set(meson_var_list VSCRT_LINKAGE WINLIBS MT AR RC C C_LD CXX CXX_LD OBJC OBJC_LD OBJCXX OBJCXX_LD FC FC_LD WINDRES CFLAGS CXXFLAGS OBJCFLAGS OBJCXXFLAGS FCFLAGS SHARED_LINKER_FLAGS) + foreach(var IN LISTS meson_var_list) + set(MESON_${var} "") + endforeach() + + if(VCPKG_TARGET_IS_WINDOWS) + z_vcpkg_meson_setup_extra_windows_variables("${config_type}") + endif() + + z_vcpkg_meson_set_proglist_variables("${config_type}") + z_vcpkg_meson_set_flags_variables("${config_type}") + + foreach(var IN LISTS meson_var_list) + set(MESON_${var} "${MESON_${var}}" PARENT_SCOPE) + endforeach() +endfunction() + +function(vcpkg_generate_meson_cmd_args) + cmake_parse_arguments(PARSE_ARGV 0 arg + "" + "OUTPUT;CONFIG" + "OPTIONS;LANGUAGES;ADDITIONAL_BINARIES;ADDITIONAL_PROPERTIES" + ) + + if(NOT arg_LANGUAGES) + set(arg_LANGUAGES C CXX) + endif() + + vcpkg_list(JOIN arg_ADDITIONAL_BINARIES "\n" MESON_ADDITIONAL_BINARIES) + vcpkg_list(JOIN arg_ADDITIONAL_PROPERTIES "\n" MESON_ADDITIONAL_PROPERTIES) + + set(buildtype "${arg_CONFIG}") + + if(NOT VCPKG_CHAINLOAD_TOOLCHAIN_FILE) + z_vcpkg_select_default_vcpkg_chainload_toolchain() + endif() + vcpkg_list(APPEND VCPKG_CMAKE_CONFIGURE_OPTIONS "-DVCPKG_LANGUAGES=${arg_LANGUAGES}") + vcpkg_cmake_get_vars(cmake_vars_file) + debug_message("Including cmake vars from: ${cmake_vars_file}") + include("${cmake_vars_file}") + + vcpkg_list(APPEND arg_OPTIONS --backend ninja --wrap-mode nodownload -Doptimization=plain) + + z_vcpkg_get_build_and_host_system(MESON_HOST_MACHINE MESON_BUILD_MACHINE IS_CROSS) + + if(arg_CONFIG STREQUAL "DEBUG") + set(suffix "dbg") + else() + string(SUBSTRING "${arg_CONFIG}" 0 3 suffix) + string(TOLOWER "${suffix}" suffix) + endif() + set(meson_input_file_${buildtype} "${CURRENT_BUILDTREES_DIR}/meson-${TARGET_TRIPLET}-${suffix}.log") + + if(IS_CROSS) + # VCPKG_CROSSCOMPILING is not used since it regresses a lot of ports in x64-windows-x triplets + # For consistency this should proably be changed in the future? + vcpkg_list(APPEND arg_OPTIONS --native "${SCRIPTS}/buildsystems/meson/none.txt") + vcpkg_list(APPEND arg_OPTIONS --cross "${meson_input_file_${buildtype}}") + else() + vcpkg_list(APPEND arg_OPTIONS --native "${meson_input_file_${buildtype}}") + endif() + + # User provided cross/native files + if(VCPKG_MESON_NATIVE_FILE) + vcpkg_list(APPEND arg_OPTIONS --native "${VCPKG_MESON_NATIVE_FILE}") + endif() + if(VCPKG_MESON_NATIVE_FILE_${buildtype}) + vcpkg_list(APPEND arg_OPTIONS --native "${VCPKG_MESON_NATIVE_FILE_${buildtype}}") + endif() + if(VCPKG_MESON_CROSS_FILE) + vcpkg_list(APPEND arg_OPTIONS --cross "${VCPKG_MESON_CROSS_FILE}") + endif() + if(VCPKG_MESON_CROSS_FILE_${buildtype}) + vcpkg_list(APPEND arg_OPTIONS --cross "${VCPKG_MESON_CROSS_FILE_${buildtype}}") + endif() + + vcpkg_list(APPEND arg_OPTIONS --libdir lib) # else meson install into an architecture describing folder + vcpkg_list(APPEND arg_OPTIONS --pkgconfig.relocatable) + + if(arg_CONFIG STREQUAL "RELEASE") + vcpkg_list(APPEND arg_OPTIONS -Ddebug=false --prefix "${CURRENT_PACKAGES_DIR}") + vcpkg_list(APPEND arg_OPTIONS "--pkg-config-path;['${CURRENT_INSTALLED_DIR}/lib/pkgconfig','${CURRENT_INSTALLED_DIR}/share/pkgconfig']") + if(VCPKG_TARGET_IS_WINDOWS) + vcpkg_list(APPEND arg_OPTIONS "-Dcmake_prefix_path=['${CURRENT_INSTALLED_DIR}','${CURRENT_INSTALLED_DIR}/debug','${CURRENT_INSTALLED_DIR}/share']") + else() + vcpkg_list(APPEND arg_OPTIONS "-Dcmake_prefix_path=['${CURRENT_INSTALLED_DIR}','${CURRENT_INSTALLED_DIR}/debug']") + endif() + elseif(arg_CONFIG STREQUAL "DEBUG") + vcpkg_list(APPEND arg_OPTIONS -Ddebug=true --prefix "${CURRENT_PACKAGES_DIR}/debug" --includedir ../include) + vcpkg_list(APPEND arg_OPTIONS "--pkg-config-path;['${CURRENT_INSTALLED_DIR}/debug/lib/pkgconfig','${CURRENT_INSTALLED_DIR}/share/pkgconfig']") + if(VCPKG_TARGET_IS_WINDOWS) + vcpkg_list(APPEND arg_OPTIONS "-Dcmake_prefix_path=['${CURRENT_INSTALLED_DIR}/debug','${CURRENT_INSTALLED_DIR}','${CURRENT_INSTALLED_DIR}/share']") + else() + vcpkg_list(APPEND arg_OPTIONS "-Dcmake_prefix_path=['${CURRENT_INSTALLED_DIR}/debug','${CURRENT_INSTALLED_DIR}']") + endif() + else() + message(FATAL_ERROR "Unknown configuration. Only DEBUG and RELEASE are valid values.") + endif() + + # Allow overrides / additional configuration variables from triplets + if(DEFINED VCPKG_MESON_CONFIGURE_OPTIONS) + vcpkg_list(APPEND arg_OPTIONS ${VCPKG_MESON_CONFIGURE_OPTIONS}) + endif() + if(DEFINED VCPKG_MESON_CONFIGURE_OPTIONS_${buildtype}) + vcpkg_list(APPEND arg_OPTIONS ${VCPKG_MESON_CONFIGURE_OPTIONS_${buildtype}}) + endif() + + if(VCPKG_LIBRARY_LINKAGE STREQUAL "dynamic") + set(MESON_DEFAULT_LIBRARY shared) + else() + set(MESON_DEFAULT_LIBRARY static) + endif() + set(MESON_CMAKE_BUILD_TYPE "${cmake_build_type_${buildtype}}") + z_vcpkg_meson_setup_variables(${buildtype}) + configure_file("${CMAKE_CURRENT_FUNCTION_LIST_DIR}/meson.template.in" "${meson_input_file_${buildtype}}" @ONLY) + set("${arg_OUTPUT}" ${arg_OPTIONS} PARENT_SCOPE) +endfunction() + +function(vcpkg_configure_meson) + # parse parameters such that semicolons in options arguments to COMMAND don't get erased + cmake_parse_arguments(PARSE_ARGV 0 arg + "NO_PKG_CONFIG" + "SOURCE_PATH" + "OPTIONS;OPTIONS_DEBUG;OPTIONS_RELEASE;LANGUAGES;ADDITIONAL_BINARIES;ADDITIONAL_NATIVE_BINARIES;ADDITIONAL_CROSS_BINARIES;ADDITIONAL_PROPERTIES" + ) + + if(DEFINED arg_ADDITIONAL_NATIVE_BINARIES OR DEFINED arg_ADDITIONAL_CROSS_BINARIES) + message(WARNING "Options ADDITIONAL_(NATIVE|CROSS)_BINARIES have been deprecated. Only use ADDITIONAL_BINARIES!") + endif() + vcpkg_list(APPEND arg_ADDITIONAL_BINARIES ${arg_ADDITIONAL_NATIVE_BINARIES} ${arg_ADDITIONAL_CROSS_BINARIES}) + vcpkg_list(REMOVE_DUPLICATES arg_ADDITIONAL_BINARIES) + + file(REMOVE_RECURSE "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-rel") + file(REMOVE_RECURSE "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-dbg") + + vcpkg_find_acquire_program(MESON) + + get_filename_component(CMAKE_PATH "${CMAKE_COMMAND}" DIRECTORY) + vcpkg_add_to_path("${CMAKE_PATH}") # Make CMake invokeable for Meson + + vcpkg_find_acquire_program(NINJA) + + if(NOT arg_NO_PKG_CONFIG) + vcpkg_find_acquire_program(PKGCONFIG) + set(ENV{PKG_CONFIG} "${PKGCONFIG}") + endif() + + vcpkg_find_acquire_program(PYTHON3) + get_filename_component(PYTHON3_DIR "${PYTHON3}" DIRECTORY) + vcpkg_add_to_path(PREPEND "${PYTHON3_DIR}") + + set(buildtypes "") + if(NOT DEFINED VCPKG_BUILD_TYPE OR VCPKG_BUILD_TYPE STREQUAL "debug") + set(buildname "DEBUG") + set(cmake_build_type_${buildname} "Debug") + vcpkg_list(APPEND buildtypes "${buildname}") + set(path_suffix_${buildname} "debug/") + set(suffix_${buildname} "dbg") + endif() + if(NOT DEFINED VCPKG_BUILD_TYPE OR VCPKG_BUILD_TYPE STREQUAL "release") + set(buildname "RELEASE") + set(cmake_build_type_${buildname} "Release") + vcpkg_list(APPEND buildtypes "${buildname}") + set(path_suffix_${buildname} "") + set(suffix_${buildname} "rel") + endif() + + # configure build + foreach(buildtype IN LISTS buildtypes) + message(STATUS "Configuring ${TARGET_TRIPLET}-${suffix_${buildtype}}") + file(MAKE_DIRECTORY "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-${suffix_${buildtype}}") + + vcpkg_generate_meson_cmd_args( + OUTPUT cmd_args + CONFIG ${buildtype} + LANGUAGES ${arg_LANGUAGES} + OPTIONS ${arg_OPTIONS} ${arg_OPTIONS_${buildtype}} + ADDITIONAL_BINARIES ${arg_ADDITIONAL_BINARIES} + ADDITIONAL_PROPERTIES ${arg_ADDITIONAL_PROPERTIES} + ) + + vcpkg_execute_required_process( + COMMAND ${MESON} setup ${cmd_args} ${arg_SOURCE_PATH} + WORKING_DIRECTORY "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-${suffix_${buildtype}}" + LOGNAME config-${TARGET_TRIPLET}-${suffix_${buildtype}} + SAVE_LOG_FILES + meson-logs/meson-log.txt + meson-info/intro-dependencies.json + meson-logs/install-log.txt + ) + + message(STATUS "Configuring ${TARGET_TRIPLET}-${suffix_${buildtype}} done") + endforeach() +endfunction() diff --git a/cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg_install_meson.cmake b/cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg_install_meson.cmake new file mode 100644 index 000000000..0351f271a --- /dev/null +++ b/cmake/vcpkg-ports/vcpkg-tool-meson/vcpkg_install_meson.cmake @@ -0,0 +1,71 @@ +function(vcpkg_install_meson) + cmake_parse_arguments(PARSE_ARGV 0 arg "ADD_BIN_TO_PATH" "" "") + + vcpkg_find_acquire_program(NINJA) + unset(ENV{DESTDIR}) # installation directory was already specified with '--prefix' option + + if(VCPKG_TARGET_IS_OSX) + vcpkg_backup_env_variables(VARS SDKROOT MACOSX_DEPLOYMENT_TARGET) + set(ENV{SDKROOT} "${VCPKG_DETECTED_CMAKE_OSX_SYSROOT}") + set(ENV{MACOSX_DEPLOYMENT_TARGET} "${VCPKG_DETECTED_CMAKE_OSX_DEPLOYMENT_TARGET}") + endif() + + foreach(buildtype IN ITEMS "debug" "release") + if(DEFINED VCPKG_BUILD_TYPE AND NOT VCPKG_BUILD_TYPE STREQUAL buildtype) + continue() + endif() + + if(buildtype STREQUAL "debug") + set(short_buildtype "dbg") + else() + set(short_buildtype "rel") + endif() + + message(STATUS "Package ${TARGET_TRIPLET}-${short_buildtype}") + if(arg_ADD_BIN_TO_PATH) + vcpkg_backup_env_variables(VARS PATH) + if(buildtype STREQUAL "debug") + vcpkg_add_to_path(PREPEND "${CURRENT_INSTALLED_DIR}/debug/bin") + else() + vcpkg_add_to_path(PREPEND "${CURRENT_INSTALLED_DIR}/bin") + endif() + endif() + vcpkg_execute_required_process( + COMMAND "${NINJA}" install -v + WORKING_DIRECTORY "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-${short_buildtype}" + LOGNAME package-${TARGET_TRIPLET}-${short_buildtype} + ) + if(arg_ADD_BIN_TO_PATH) + vcpkg_restore_env_variables(VARS PATH) + endif() + endforeach() + + vcpkg_list(SET renamed_libs) + if(VCPKG_TARGET_IS_WINDOWS AND VCPKG_LIBRARY_LINKAGE STREQUAL static AND NOT VCPKG_TARGET_IS_MINGW) + # Meson names all static libraries lib.a which basically breaks the world + file(GLOB_RECURSE gen_libraries "${CURRENT_PACKAGES_DIR}*/**/lib*.a") + foreach(gen_library IN LISTS gen_libraries) + get_filename_component(libdir "${gen_library}" DIRECTORY) + get_filename_component(libname "${gen_library}" NAME) + string(REGEX REPLACE ".a$" ".lib" fixed_librawname "${libname}") + string(REGEX REPLACE "^lib" "" fixed_librawname "${fixed_librawname}") + file(RENAME "${gen_library}" "${libdir}/${fixed_librawname}") + # For cmake fixes. + string(REGEX REPLACE ".a$" "" origin_librawname "${libname}") + string(REGEX REPLACE ".lib$" "" fixed_librawname "${fixed_librawname}") + vcpkg_list(APPEND renamed_libs ${fixed_librawname}) + set(${librawname}_old ${origin_librawname}) + set(${librawname}_new ${fixed_librawname}) + endforeach() + file(GLOB_RECURSE cmake_files "${CURRENT_PACKAGES_DIR}*/*.cmake") + foreach(cmake_file IN LISTS cmake_files) + foreach(current_lib IN LISTS renamed_libs) + vcpkg_replace_string("${cmake_file}" "${${current_lib}_old}" "${${current_lib}_new}" IGNORE_UNCHANGED) + endforeach() + endforeach() + endif() + + if(VCPKG_TARGET_IS_OSX) + vcpkg_restore_env_variables(VARS SDKROOT MACOSX_DEPLOYMENT_TARGET) + endif() +endfunction() diff --git a/vcpkg-configuration.json b/vcpkg-configuration.json index 3a59b2658..71db4bcca 100644 --- a/vcpkg-configuration.json +++ b/vcpkg-configuration.json @@ -11,6 +11,9 @@ "name": "microsoft" } ], + "overlay-ports": [ + "./cmake/vcpkg-ports" + ], "overlay-triplets": [ "./cmake/vcpkg-triplets" ] From a8eea411e97475aa4bdf135d272db571ca9e4e53 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 23 Jul 2025 23:28:11 +0000 Subject: [PATCH 375/695] chore(deps): update cachix/install-nix-action digest to c134e4c --- .github/workflows/update-flake.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/update-flake.yml b/.github/workflows/update-flake.yml index de341b517..5822c411b 100644 --- a/.github/workflows/update-flake.yml +++ b/.github/workflows/update-flake.yml @@ -17,7 +17,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: cachix/install-nix-action@f0fe604f8a612776892427721526b4c7cfb23aba # v31 + - uses: cachix/install-nix-action@c134e4c9e34bac6cab09cf239815f9339aaaf84e # v31 - uses: DeterminateSystems/update-flake-lock@v27 with: From 2e428330f4933b128f45717b3b13eaa2463ed5ae Mon Sep 17 00:00:00 2001 From: Trial97 Date: Sat, 18 Jan 2025 15:39:25 +0200 Subject: [PATCH 376/695] support gif catpacks Signed-off-by: Trial97 --- launcher/ui/themes/CatPainter.cpp | 17 ++++++++++++++++- launcher/ui/themes/CatPainter.h | 1 + 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/launcher/ui/themes/CatPainter.cpp b/launcher/ui/themes/CatPainter.cpp index 7ff24932b..7c152fdc9 100644 --- a/launcher/ui/themes/CatPainter.cpp +++ b/launcher/ui/themes/CatPainter.cpp @@ -22,12 +22,27 @@ CatPainter::CatPainter(const QString& path, QObject* parent) : QObject(parent) { - m_image = QPixmap(path); + // Attempt to load as a movie + m_movie = new QMovie(path, QByteArray(), this); + if (m_movie->isValid()) { + // Start the animation if it's a valid movie file + connect(m_movie, &QMovie::frameChanged, this, &CatPainter::updateFrame); + m_movie->start(); + } else { + // Otherwise, load it as a static image + delete m_movie; + m_movie = nullptr; + + m_image = QPixmap(path); + } } void CatPainter::paint(QPainter* painter, const QRect& viewport) { QPixmap frame = m_image; + if (m_movie && m_movie->isValid()) { + frame = m_movie->currentPixmap(); + } auto fit = APPLICATION->settings()->get("CatFit").toString(); painter->setOpacity(APPLICATION->settings()->get("CatOpacity").toFloat() / 100); diff --git a/launcher/ui/themes/CatPainter.h b/launcher/ui/themes/CatPainter.h index 3b790c640..c36cb7617 100644 --- a/launcher/ui/themes/CatPainter.h +++ b/launcher/ui/themes/CatPainter.h @@ -34,5 +34,6 @@ class CatPainter : public QObject { void updateFrame(); private: + QMovie* m_movie = nullptr; QPixmap m_image; }; From 597309ceeb7aff9f429f2567a8e97929cf316cdd Mon Sep 17 00:00:00 2001 From: Trial97 Date: Thu, 24 Jul 2025 18:44:46 +0300 Subject: [PATCH 377/695] ensure that both cape and skin are downloaded via https Signed-off-by: Trial97 --- launcher/minecraft/auth/AccountData.cpp | 2 ++ launcher/minecraft/auth/Parsers.cpp | 3 +++ 2 files changed, 5 insertions(+) diff --git a/launcher/minecraft/auth/AccountData.cpp b/launcher/minecraft/auth/AccountData.cpp index 161fd968c..29a65e275 100644 --- a/launcher/minecraft/auth/AccountData.cpp +++ b/launcher/minecraft/auth/AccountData.cpp @@ -180,6 +180,7 @@ MinecraftProfile profileFromJSONV3(const QJsonObject& parent, const char* tokenN } out.skin.id = idV.toString(); out.skin.url = urlV.toString(); + out.skin.url.replace("http://textures.minecraft.net", "https://textures.minecraft.net"); out.skin.variant = variantV.toString(); // data for skin is optional @@ -216,6 +217,7 @@ MinecraftProfile profileFromJSONV3(const QJsonObject& parent, const char* tokenN Cape cape; cape.id = idV.toString(); cape.url = urlV.toString(); + cape.url.replace("http://textures.minecraft.net", "https://textures.minecraft.net"); cape.alias = aliasV.toString(); // data for cape is optional. diff --git a/launcher/minecraft/auth/Parsers.cpp b/launcher/minecraft/auth/Parsers.cpp index de1ffda86..ba77d3e31 100644 --- a/launcher/minecraft/auth/Parsers.cpp +++ b/launcher/minecraft/auth/Parsers.cpp @@ -207,6 +207,7 @@ bool parseMinecraftProfile(QByteArray& data, MinecraftProfile& output) if (!getString(capeObj.value("url"), capeOut.url)) { continue; } + capeOut.url.replace("http://textures.minecraft.net", "https://textures.minecraft.net"); if (!getString(capeObj.value("alias"), capeOut.alias)) { continue; } @@ -358,6 +359,7 @@ bool parseMinecraftProfileMojang(QByteArray& data, MinecraftProfile& output) qWarning() << "Skin url is not a string"; return false; } + skinOut.url.replace("http://textures.minecraft.net", "https://textures.minecraft.net"); auto maybeMeta = skin.find("metadata"); if (maybeMeta != skin.end() && maybeMeta->isObject()) { @@ -371,6 +373,7 @@ bool parseMinecraftProfileMojang(QByteArray& data, MinecraftProfile& output) qWarning() << "Cape url is not a string"; return false; } + capeOut.url.replace("http://textures.minecraft.net", "https://textures.minecraft.net"); // 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 :( From eefe0375af0b8be810781952cffe9368db1cec53 Mon Sep 17 00:00:00 2001 From: Octol1ttle Date: Sat, 26 Jul 2025 18:41:34 +0500 Subject: [PATCH 378/695] fix(mojang api): use new endpoint for Username->UUID resolution Signed-off-by: Octol1ttle --- launcher/ui/dialogs/skins/SkinManageDialog.cpp | 2 +- .../legacy/org/prismlauncher/legacy/utils/api/MojangApi.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/launcher/ui/dialogs/skins/SkinManageDialog.cpp b/launcher/ui/dialogs/skins/SkinManageDialog.cpp index e7c06d048..5967281c4 100644 --- a/launcher/ui/dialogs/skins/SkinManageDialog.cpp +++ b/launcher/ui/dialogs/skins/SkinManageDialog.cpp @@ -446,7 +446,7 @@ void SkinManageDialog::on_userBtn_clicked() auto uuidLoop = makeShared(); auto profileLoop = makeShared(); - auto getUUID = Net::Download::makeByteArray("https://api.mojang.com/users/profiles/minecraft/" + user, uuidOut); + auto getUUID = Net::Download::makeByteArray("https://api.minecraftservices.com/minecraft/profile/lookup/name/" + user, uuidOut); auto getProfile = Net::Download::makeByteArray(QUrl(), profileOut); auto downloadSkin = Net::Download::makeFile(QUrl(), path); diff --git a/libraries/launcher/legacy/org/prismlauncher/legacy/utils/api/MojangApi.java b/libraries/launcher/legacy/org/prismlauncher/legacy/utils/api/MojangApi.java index 41f7f9114..34313e91a 100644 --- a/libraries/launcher/legacy/org/prismlauncher/legacy/utils/api/MojangApi.java +++ b/libraries/launcher/legacy/org/prismlauncher/legacy/utils/api/MojangApi.java @@ -49,7 +49,7 @@ @SuppressWarnings("unchecked") public final class MojangApi { public static String getUuid(String username) throws IOException { - try (InputStream in = new URL("https://api.mojang.com/users/profiles/minecraft/" + username).openStream()) { + try (InputStream in = new URL("https://api.minecraftservices.com/minecraft/profile/lookup/name/" + username).openStream()) { Map map = (Map) JsonParser.parse(in); return (String) map.get("id"); } From f40117b4318a1c4d9fd1b94b67124210d1f92d49 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Sun, 27 Jul 2025 12:58:59 +0300 Subject: [PATCH 379/695] update ftb import to consider meta folder Signed-off-by: Trial97 --- .../modplatform/import_ftb/PackHelpers.cpp | 39 ++++++++++++++++++- .../modplatform/import_ftb/ImportFTBPage.cpp | 38 +++++++++++++++++- .../modplatform/import_ftb/ListModel.cpp | 1 - 3 files changed, 75 insertions(+), 3 deletions(-) diff --git a/launcher/modplatform/import_ftb/PackHelpers.cpp b/launcher/modplatform/import_ftb/PackHelpers.cpp index e523b9d20..089b5fff2 100644 --- a/launcher/modplatform/import_ftb/PackHelpers.cpp +++ b/launcher/modplatform/import_ftb/PackHelpers.cpp @@ -19,6 +19,7 @@ #include "modplatform/import_ftb/PackHelpers.h" #include +#include #include #include @@ -27,6 +28,35 @@ namespace FTBImportAPP { +QIcon loadFTBIcon(const QString& imagePath) +{ + // Map of type byte to image type string + static const QHash imageTypeMap = { { 0x00, "png" }, { 0x01, "jpg" }, { 0x02, "gif" }, { 0x03, "webp" } }; + QFile file(imagePath); + if (!file.exists() || !file.open(QIODevice::ReadOnly)) { + return QIcon(); + } + char type; + if (!file.getChar(&type)) { + qDebug() << "Missing FTB image type header at" << imagePath; + return QIcon(); + } + if (!imageTypeMap.contains(type)) { + qDebug().nospace().noquote() << "Don't recognize FTB image type 0x" << QString::number(type, 16); + return QIcon(); + } + + auto imageType = imageTypeMap[type]; + // Extract actual image data beyond the first byte + QImageReader reader(&file, imageType); + auto pixmap = QPixmap::fromImageReader(&reader); + if (pixmap.isNull()) { + qDebug() << "The FTB image at" << imagePath << "is not valid"; + return QIcon(); + } + return QIcon(pixmap); +} + Modpack parseDirectory(QString path) { Modpack modpack{ path }; @@ -48,9 +78,14 @@ Modpack parseDirectory(QString path) qDebug() << "Couldn't load ftb instance json: " << e.cause(); return {}; } + auto versionsFile = QFileInfo(FS::PathCombine(path, "version.json")); - if (!versionsFile.exists() || !versionsFile.isFile()) + if (!versionsFile.exists() || !versionsFile.isFile()) { + versionsFile = QFileInfo(FS::PathCombine(path, ".ftbapp", "version.json")); + } + if (!versionsFile.exists() || !versionsFile.isFile()) { return {}; + } try { auto doc = Json::requireDocument(versionsFile.absoluteFilePath(), "FTB_APP version JSON file"); const auto root = doc.object(); @@ -85,6 +120,8 @@ Modpack parseDirectory(QString path) auto iconFile = QFileInfo(FS::PathCombine(path, "folder.jpg")); if (iconFile.exists() && iconFile.isFile()) { modpack.icon = QIcon(iconFile.absoluteFilePath()); + } else { // the logo is a file that the first bit denotes the image tipe followed by the actual image data + modpack.icon = loadFTBIcon(FS::PathCombine(path, ".ftbapp", "logo")); } return modpack; } diff --git a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.cpp b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.cpp index 35e1dc110..9a2b31768 100644 --- a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.cpp +++ b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.cpp @@ -22,6 +22,7 @@ #include #include +#include #include #include "FileSystem.h" #include "ListModel.h" @@ -88,6 +89,34 @@ void ImportFTBPage::retranslate() ui->retranslateUi(this); } +QString saveIconToTempFile(const QIcon& icon) +{ + if (icon.isNull()) { + return QString(); + } + + QPixmap pixmap = icon.pixmap(icon.availableSizes().last()); + if (pixmap.isNull()) { + return QString(); + } + + QTemporaryFile tempFile(QDir::tempPath() + "/iconXXXXXX.png"); + tempFile.setAutoRemove(false); + if (!tempFile.open()) { + return QString(); + } + + QString tempPath = tempFile.fileName(); + tempFile.close(); + + if (!pixmap.save(tempPath, "PNG")) { + QFile::remove(tempPath); + return QString(); + } + + return tempPath; // Success +} + void ImportFTBPage::suggestCurrent() { if (!isOpened) @@ -100,7 +129,14 @@ void ImportFTBPage::suggestCurrent() dialog->setSuggestedPack(selected.name, new PackInstallTask(selected)); QString editedLogoName = QString("ftb_%1_%2.jpg").arg(selected.name, QString::number(selected.id)); - dialog->setSuggestedIconFromFile(FS::PathCombine(selected.path, "folder.jpg"), editedLogoName); + auto iconPath = FS::PathCombine(selected.path, "folder.jpg"); + if (!QFileInfo::exists(iconPath)) { + // need to save the icon as that actual logo is not a image on the disk + iconPath = saveIconToTempFile(selected.icon); + } + if (!iconPath.isEmpty() && QFileInfo::exists(iconPath)) { + dialog->setSuggestedIconFromFile(iconPath, editedLogoName); + } } void ImportFTBPage::onPublicPackSelectionChanged(QModelIndex now, QModelIndex) diff --git a/launcher/ui/pages/modplatform/import_ftb/ListModel.cpp b/launcher/ui/pages/modplatform/import_ftb/ListModel.cpp index 8d3beea01..daa2189ad 100644 --- a/launcher/ui/pages/modplatform/import_ftb/ListModel.cpp +++ b/launcher/ui/pages/modplatform/import_ftb/ListModel.cpp @@ -17,7 +17,6 @@ */ #include "ListModel.h" -#include #include #include #include From 29231e2038f57b72cc71fb1c20cd8f052cb35377 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Wed, 9 Jul 2025 18:43:24 +0300 Subject: [PATCH 380/695] chore: update to cxx20 Signed-off-by: Trial97 --- CMakeLists.txt | 2 +- launcher/java/JavaVersion.cpp | 6 +++--- launcher/java/JavaVersion.h | 6 +++--- launcher/modplatform/ResourceAPI.h | 11 ----------- 4 files changed, 7 insertions(+), 18 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 929e1b394..1360b82ba 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,7 +24,7 @@ set(CMAKE_JAVA_TARGET_OUTPUT_DIR ${PROJECT_BINARY_DIR}/jars) ######## Set compiler flags ######## set(CMAKE_CXX_STANDARD_REQUIRED true) set(CMAKE_C_STANDARD_REQUIRED true) -set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD 20) set(CMAKE_C_STANDARD 11) include(GenerateExportHeader) if(MSVC) diff --git a/launcher/java/JavaVersion.cpp b/launcher/java/JavaVersion.cpp index e9a160ea7..fef573c16 100644 --- a/launcher/java/JavaVersion.cpp +++ b/launcher/java/JavaVersion.cpp @@ -63,7 +63,7 @@ bool JavaVersion::isModular() const return m_parseable && m_major >= 9; } -bool JavaVersion::operator<(const JavaVersion& rhs) +bool JavaVersion::operator<(const JavaVersion& rhs) const { if (m_parseable && rhs.m_parseable) { auto major = m_major; @@ -101,7 +101,7 @@ bool JavaVersion::operator<(const JavaVersion& rhs) return StringUtils::naturalCompare(m_string, rhs.m_string, Qt::CaseSensitive) < 0; } -bool JavaVersion::operator==(const JavaVersion& rhs) +bool JavaVersion::operator==(const JavaVersion& rhs) const { if (m_parseable && rhs.m_parseable) { return m_major == rhs.m_major && m_minor == rhs.m_minor && m_security == rhs.m_security && m_prerelease == rhs.m_prerelease; @@ -109,7 +109,7 @@ bool JavaVersion::operator==(const JavaVersion& rhs) return m_string == rhs.m_string; } -bool JavaVersion::operator>(const JavaVersion& rhs) +bool JavaVersion::operator>(const JavaVersion& rhs) const { return (!operator<(rhs)) && (!operator==(rhs)); } diff --git a/launcher/java/JavaVersion.h b/launcher/java/JavaVersion.h index c070bdeec..143ddd262 100644 --- a/launcher/java/JavaVersion.h +++ b/launcher/java/JavaVersion.h @@ -20,9 +20,9 @@ class JavaVersion { JavaVersion& operator=(const QString& rhs); - bool operator<(const JavaVersion& rhs); - bool operator==(const JavaVersion& rhs); - bool operator>(const JavaVersion& rhs); + bool operator<(const JavaVersion& rhs) const; + bool operator==(const JavaVersion& rhs) const; + bool operator>(const JavaVersion& rhs) const; bool requiresPermGen() const; bool defaultsToUtf8() const; diff --git a/launcher/modplatform/ResourceAPI.h b/launcher/modplatform/ResourceAPI.h index 6641628f6..4d40432ee 100644 --- a/launcher/modplatform/ResourceAPI.h +++ b/launcher/modplatform/ResourceAPI.h @@ -90,14 +90,6 @@ class ResourceAPI { std::optional> mcVersions; std::optional loaders; - - VersionSearchArgs(VersionSearchArgs const&) = default; - void operator=(VersionSearchArgs other) - { - pack = other.pack; - mcVersions = other.mcVersions; - loaders = other.loaders; - } }; struct VersionSearchCallbacks { std::function on_succeed; @@ -106,9 +98,6 @@ class ResourceAPI { struct ProjectInfoArgs { ModPlatform::IndexedPack pack; - - ProjectInfoArgs(ProjectInfoArgs const&) = default; - void operator=(ProjectInfoArgs other) { pack = other.pack; } }; struct ProjectInfoCallbacks { std::function on_succeed; From b196c5d59fde3cac29f93fe5a8351629e95abf9c Mon Sep 17 00:00:00 2001 From: Seth Flynn Date: Thu, 31 Jul 2025 20:11:16 -0400 Subject: [PATCH 381/695] ci(setup-deps/windows): install java Signed-off-by: Seth Flynn --- .github/actions/setup-dependencies/windows/action.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/actions/setup-dependencies/windows/action.yml b/.github/actions/setup-dependencies/windows/action.yml index 97033747e..9b045610e 100644 --- a/.github/actions/setup-dependencies/windows/action.yml +++ b/.github/actions/setup-dependencies/windows/action.yml @@ -25,6 +25,14 @@ runs: arch: ${{ inputs.vcvars-arch }} vsversion: 2022 + - name: Setup Java (MSVC) + uses: actions/setup-java@v4 + with: + # NOTE(@getchoo): We should probably stay on Zulu. + # Temurin doesn't have Java 17 builds for WoA + distribution: zulu + java-version: 17 + - name: Setup vcpkg cache (MSVC) if: ${{ inputs.msystem == '' && inputs.build-type == 'Debug' }} shell: pwsh From 6ab1a246cb63e3d2e9c177f63c16eef000a29754 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Sun, 3 Aug 2025 12:07:57 +0100 Subject: [PATCH 382/695] Use radio buttons for Instance Renaming Mode Signed-off-by: TheKodeToad --- launcher/ui/pages/global/LauncherPage.cpp | 39 ++---- launcher/ui/pages/global/LauncherPage.ui | 137 ++++++++++++++-------- 2 files changed, 98 insertions(+), 78 deletions(-) diff --git a/launcher/ui/pages/global/LauncherPage.cpp b/launcher/ui/pages/global/LauncherPage.cpp index 132a2c320..7a0f11c83 100644 --- a/launcher/ui/pages/global/LauncherPage.cpp +++ b/launcher/ui/pages/global/LauncherPage.cpp @@ -65,15 +65,6 @@ enum InstSortMode { Sort_LastLaunch }; -enum InstRenamingMode { - // Rename metadata only. - Rename_Always, - // Ask everytime. - Rename_Ask, - // Rename physical directory too. - Rename_Never -}; - LauncherPage::LauncherPage(QWidget* parent) : QWidget(parent), ui(new Ui::LauncherPage) { ui->setupUi(this); @@ -242,18 +233,12 @@ void LauncherPage::applySettings() break; } - auto renamingMode = (InstRenamingMode)ui->renamingBehaviorComboBox->currentIndex(); - switch (renamingMode) { - case Rename_Always: - s->set("InstRenamingMode", "MetadataOnly"); - break; - case Rename_Never: - s->set("InstRenamingMode", "PhysicalDir"); - break; - case Rename_Ask: - default: - s->set("InstRenamingMode", "AskEverytime"); - break; + if (ui->askToRenameDirBtn->isChecked()) { + s->set("InstRenamingMode", "AskEverytime"); + } else if (ui->alwaysRenameDirBtn->isChecked()) { + s->set("InstRenamingMode", "PhysicalDir"); + } else if (ui->neverRenameDirBtn->isChecked()) { + s->set("InstRenamingMode", "MetadataOnly"); } // Mods @@ -300,15 +285,9 @@ void LauncherPage::loadSettings() } QString renamingMode = s->get("InstRenamingMode").toString(); - InstRenamingMode renamingModeEnum; - if (renamingMode == "MetadataOnly") { - renamingModeEnum = Rename_Always; - } else if (renamingMode == "PhysicalDir") { - renamingModeEnum = Rename_Never; - } else { - renamingModeEnum = Rename_Ask; - } - ui->renamingBehaviorComboBox->setCurrentIndex(renamingModeEnum); + ui->askToRenameDirBtn->setChecked(renamingMode == "AskEverytime"); + ui->alwaysRenameDirBtn->setChecked(renamingMode == "PhysicalDir"); + ui->neverRenameDirBtn->setChecked(renamingMode == "MetadataOnly"); // Mods ui->metadataEnableBtn->setChecked(!s->get("ModMetadataDisabled").toBool()); diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index 55478e6a0..c04d904cf 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -32,7 +32,7 @@ - Qt::ScrollBarAsNeeded + Qt::ScrollBarPolicy::ScrollBarAsNeeded true @@ -41,9 +41,9 @@ 0 - -356 - 742 - 1148 + 0 + 746 + 1202 @@ -64,32 +64,56 @@ - - - By &name + + + - - sortingModeGroup - - - - - - - &By last launched + + true - - sortingModeGroup - + + + 0 + + + 0 + + + 0 + + + 0 + + + + + By &name + + + sortingModeGroup + + + + + + + &By last launched + + + sortingModeGroup + + + + - Qt::Vertical + Qt::Orientation::Vertical - QSizePolicy::Fixed + QSizePolicy::Policy::Fixed @@ -107,37 +131,57 @@
    - - - - 0 - 0 - + + + - - - Ask what to do with the folder + + true + + + + 0 - - - - Always rename the folder + + 0 - - - - Never rename the folder—only the displayed name + + 0 - + + 0 + + + + + Ask what to do + + + + + + + Always rename the folder + + + + + + + Never rename the folder + + + + - Qt::Vertical + Qt::Orientation::Vertical - QSizePolicy::Fixed + QSizePolicy::Policy::Fixed @@ -206,7 +250,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal @@ -433,7 +477,7 @@ - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignVCenter @@ -602,7 +646,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal @@ -618,7 +662,7 @@ - Qt::Vertical + Qt::Orientation::Vertical @@ -636,9 +680,6 @@ scrollArea - sortByNameBtn - sortLastLaunchedBtn - renamingBehaviorComboBox preferMenuBarCheckBox autoUpdateCheckBox updateIntervalSpinBox From 99f6a02a141f702048c9faee1b4c297f871149ae Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Sun, 3 Aug 2025 12:14:03 +0100 Subject: [PATCH 383/695] Cat Fit -> Cat Scaling Signed-off-by: TheKodeToad --- launcher/ui/widgets/AppearanceWidget.ui | 94 ++++++++++++------------- 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/launcher/ui/widgets/AppearanceWidget.ui b/launcher/ui/widgets/AppearanceWidget.ui index 99bf4a500..cfe464dd6 100644 --- a/launcher/ui/widgets/AppearanceWidget.ui +++ b/launcher/ui/widgets/AppearanceWidget.ui @@ -203,53 +203,6 @@ - - - - - 0 - 0 - - - - Fit - - - - - - - - 0 - 0 - - - - - 77 - 30 - - - - 0 - - - - Fit - - - - - Fill - - - - - Stretch - - - - @@ -370,6 +323,53 @@ + + + + + 0 + 0 + + + + Cat Scaling + + + + + + + + 0 + 0 + + + + + 77 + 30 + + + + 0 + + + + Fit + + + + + Fill + + + + + Stretch + + + + From 6abd7ac673ed42d70491c64225477a6e4487354d Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Mon, 4 Aug 2025 10:16:26 +0100 Subject: [PATCH 384/695] Remove invisible GroupBoxes (they appear on Fusion) Signed-off-by: TheKodeToad --- launcher/ui/pages/global/LauncherPage.ui | 122 ++++++++--------------- 1 file changed, 42 insertions(+), 80 deletions(-) diff --git a/launcher/ui/pages/global/LauncherPage.ui b/launcher/ui/pages/global/LauncherPage.ui index c04d904cf..0debe3f4d 100644 --- a/launcher/ui/pages/global/LauncherPage.ui +++ b/launcher/ui/pages/global/LauncherPage.ui @@ -43,7 +43,7 @@ 0 0 746 - 1202 + 1194 @@ -64,47 +64,23 @@ - - - + + + By &name - - true + + sortingModeGroup + + + + + + + &By last launched - - - 0 - - - 0 - - - 0 - - - 0 - - - - - By &name - - - sortingModeGroup - - - - - - - &By last launched - - - sortingModeGroup - - - - + + sortingModeGroup + @@ -131,48 +107,33 @@ - - - + + + Ask what to do - - true + + renamingBehaviorGroup + + + + + + + Always rename the folder + + + renamingBehaviorGroup + + + + + + + Never rename the folder - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Ask what to do - - - - - - - Always rename the folder - - - - - - - Never rename the folder - - - - + + renamingBehaviorGroup + @@ -711,5 +672,6 @@ + From d41db5253e682b71e9cc6f87af250e4445f25c4d Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Mon, 4 Aug 2025 14:02:54 +0100 Subject: [PATCH 385/695] Refactor Filter Signed-off-by: TheKodeToad --- launcher/CMakeLists.txt | 1 - launcher/Filter.cpp | 37 -------- launcher/Filter.h | 89 ++++++++------------ launcher/MMCZip.cpp | 2 +- launcher/MMCZip.h | 6 +- launcher/VersionProxyModel.cpp | 6 +- launcher/VersionProxyModel.h | 6 +- launcher/ui/java/InstallJavaDialog.cpp | 4 +- launcher/ui/pages/modplatform/CustomPage.cpp | 2 +- launcher/ui/widgets/ModFilterWidget.cpp | 8 +- launcher/ui/widgets/VersionSelectWidget.cpp | 8 +- launcher/ui/widgets/VersionSelectWidget.h | 4 +- 12 files changed, 56 insertions(+), 117 deletions(-) delete mode 100644 launcher/Filter.cpp diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 194694d7f..6af7d32a2 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -56,7 +56,6 @@ set(CORE_SOURCES # String filters Filter.h - Filter.cpp # JSON parsing helpers Json.h diff --git a/launcher/Filter.cpp b/launcher/Filter.cpp deleted file mode 100644 index adeb2209e..000000000 --- a/launcher/Filter.cpp +++ /dev/null @@ -1,37 +0,0 @@ -#include "Filter.h" - -ContainsFilter::ContainsFilter(const QString& pattern) : pattern(pattern) {} -bool ContainsFilter::accepts(const QString& value) -{ - return value.contains(pattern); -} - -ExactFilter::ExactFilter(const QString& pattern) : pattern(pattern) {} -bool ExactFilter::accepts(const QString& value) -{ - return value == pattern; -} - -ExactIfPresentFilter::ExactIfPresentFilter(const QString& pattern) : pattern(pattern) {} -bool ExactIfPresentFilter::accepts(const QString& value) -{ - return value.isEmpty() || value == pattern; -} - -RegexpFilter::RegexpFilter(const QString& regexp, bool invert) : invert(invert) -{ - pattern.setPattern(regexp); - pattern.optimize(); -} -bool RegexpFilter::accepts(const QString& value) -{ - auto match = pattern.match(value); - bool matched = match.hasMatch(); - return invert ? (!matched) : (matched); -} - -ExactListFilter::ExactListFilter(const QStringList& pattern) : m_pattern(pattern) {} -bool ExactListFilter::accepts(const QString& value) -{ - return m_pattern.isEmpty() || m_pattern.contains(value); -} \ No newline at end of file diff --git a/launcher/Filter.h b/launcher/Filter.h index ae835e724..d94a45fea 100644 --- a/launcher/Filter.h +++ b/launcher/Filter.h @@ -3,59 +3,36 @@ #include #include -class Filter { - public: - virtual ~Filter() = default; - virtual bool accepts(const QString& value) = 0; -}; - -class ContainsFilter : public Filter { - public: - ContainsFilter(const QString& pattern); - virtual ~ContainsFilter() = default; - bool accepts(const QString& value) override; - - private: - QString pattern; -}; - -class ExactFilter : public Filter { - public: - ExactFilter(const QString& pattern); - virtual ~ExactFilter() = default; - bool accepts(const QString& value) override; - - private: - QString pattern; -}; - -class ExactIfPresentFilter : public Filter { - public: - ExactIfPresentFilter(const QString& pattern); - virtual ~ExactIfPresentFilter() override = default; - bool accepts(const QString& value) override; - - private: - QString pattern; -}; - -class RegexpFilter : public Filter { - public: - RegexpFilter(const QString& regexp, bool invert); - virtual ~RegexpFilter() = default; - bool accepts(const QString& value) override; - - private: - QRegularExpression pattern; - bool invert = false; -}; - -class ExactListFilter : public Filter { - public: - ExactListFilter(const QStringList& pattern = {}); - virtual ~ExactListFilter() = default; - bool accepts(const QString& value) override; - - private: - QStringList m_pattern; -}; +using Filter = std::function; + +namespace Filters { +inline Filter inverse(Filter filter) +{ + return [filter = std::move(filter)](const QString& src) { return !filter(src); }; +} + +inline Filter contains(QString pattern) +{ + return [pattern = std::move(pattern)](const QString& src) { return src.contains(pattern); }; +} + +inline Filter equals(QString pattern) +{ + return [pattern = std::move(pattern)](const QString& src) { return src == pattern; }; +} + +inline Filter equalsAny(QStringList patterns = {}) +{ + return [patterns = std::move(patterns)](const QString& src) { return patterns.isEmpty() || patterns.contains(src); }; +} + +inline Filter equalsOrEmpty(QString pattern) +{ + return [pattern = std::move(pattern)](const QString& src) { return src.isEmpty() || src == pattern; }; +} + +inline Filter regexp(QRegularExpression pattern) +{ + return [pattern = std::move(pattern)](const QString& src) { return pattern.match(src).hasMatch(); }; +} +} // namespace Filters diff --git a/launcher/MMCZip.cpp b/launcher/MMCZip.cpp index 0b1a2b39e..dfe397930 100644 --- a/launcher/MMCZip.cpp +++ b/launcher/MMCZip.cpp @@ -51,7 +51,7 @@ namespace MMCZip { // ours -bool mergeZipFiles(QuaZip* into, QFileInfo from, QSet& contained, const FilterFunction& filter) +bool mergeZipFiles(QuaZip* into, QFileInfo from, QSet& contained, const Filter& filter) { QuaZip modZip(from.filePath()); modZip.open(QuaZip::mdUnzip); diff --git a/launcher/MMCZip.h b/launcher/MMCZip.h index fe0c79de2..aafcbd194 100644 --- a/launcher/MMCZip.h +++ b/launcher/MMCZip.h @@ -52,16 +52,16 @@ #if defined(LAUNCHER_APPLICATION) #include "minecraft/mod/Mod.h" #endif +#include "Filter.h" #include "tasks/Task.h" namespace MMCZip { -using FilterFunction = std::function; using FilterFileFunction = std::function; /** * Merge two zip files, using a filter function */ -bool mergeZipFiles(QuaZip* into, QFileInfo from, QSet& contained, const FilterFunction& filter = nullptr); +bool mergeZipFiles(QuaZip* into, QFileInfo from, QSet& contained, const Filter& filter = nullptr); /** * Compress directory, by providing a list of files to compress @@ -178,7 +178,7 @@ class ExportToZipTask : public Task { QString destinationPrefix = "", bool followSymlinks = false, bool utf8Enabled = false) - : ExportToZipTask(outputPath, QDir(dir), files, destinationPrefix, followSymlinks, utf8Enabled) {}; + : ExportToZipTask(outputPath, QDir(dir), files, destinationPrefix, followSymlinks, utf8Enabled){}; virtual ~ExportToZipTask() = default; diff --git a/launcher/VersionProxyModel.cpp b/launcher/VersionProxyModel.cpp index 165dd4cb7..950b2276a 100644 --- a/launcher/VersionProxyModel.cpp +++ b/launcher/VersionProxyModel.cpp @@ -63,7 +63,7 @@ class VersionFilterModel : public QSortFilterProxyModel { for (auto it = filters.begin(); it != filters.end(); ++it) { auto data = sourceModel()->data(idx, it.key()); auto match = data.toString(); - if (!it.value()->accepts(match)) { + if (!it.value()(match)) { return false; } } @@ -380,9 +380,9 @@ void VersionProxyModel::clearFilters() filterModel->filterChanged(); } -void VersionProxyModel::setFilter(const BaseVersionList::ModelRoles column, Filter* f) +void VersionProxyModel::setFilter(const BaseVersionList::ModelRoles column, Filter f) { - m_filters[column].reset(f); + m_filters[column] = std::move(f); filterModel->filterChanged(); } diff --git a/launcher/VersionProxyModel.h b/launcher/VersionProxyModel.h index 7965af0ad..0fcb380e4 100644 --- a/launcher/VersionProxyModel.h +++ b/launcher/VersionProxyModel.h @@ -10,11 +10,11 @@ class VersionProxyModel : public QAbstractProxyModel { Q_OBJECT public: enum Column { Name, ParentVersion, Branch, Type, CPUArchitecture, Path, Time, JavaName, JavaMajor }; - using FilterMap = QHash>; + using FilterMap = QHash; public: VersionProxyModel(QObject* parent = 0); - virtual ~VersionProxyModel() {}; + virtual ~VersionProxyModel(){}; virtual int columnCount(const QModelIndex& parent = QModelIndex()) const override; virtual int rowCount(const QModelIndex& parent = QModelIndex()) const override; @@ -28,7 +28,7 @@ class VersionProxyModel : public QAbstractProxyModel { const FilterMap& filters() const; const QString& search() const; - void setFilter(BaseVersionList::ModelRoles column, Filter* filter); + void setFilter(BaseVersionList::ModelRoles column, Filter filter); void setSearch(const QString& search); void clearFilters(); QModelIndex getRecommended() const; diff --git a/launcher/ui/java/InstallJavaDialog.cpp b/launcher/ui/java/InstallJavaDialog.cpp index 5f69b9d46..4a628b003 100644 --- a/launcher/ui/java/InstallJavaDialog.cpp +++ b/launcher/ui/java/InstallJavaDialog.cpp @@ -140,9 +140,9 @@ class InstallJavaPage : public QWidget, public BasePage { void recommendedFilterChanged() { if (m_recommend) { - majorVersionSelect->setFilter(BaseVersionList::ModelRoles::JavaMajorRole, new ExactListFilter(m_recommended_majors)); + majorVersionSelect->setFilter(BaseVersionList::ModelRoles::JavaMajorRole, Filters::equalsAny(m_recommended_majors)); } else { - majorVersionSelect->setFilter(BaseVersionList::ModelRoles::JavaMajorRole, new ExactListFilter()); + majorVersionSelect->setFilter(BaseVersionList::ModelRoles::JavaMajorRole, Filters::equalsAny()); } } diff --git a/launcher/ui/pages/modplatform/CustomPage.cpp b/launcher/ui/pages/modplatform/CustomPage.cpp index ba22bd2e6..87e126fd7 100644 --- a/launcher/ui/pages/modplatform/CustomPage.cpp +++ b/launcher/ui/pages/modplatform/CustomPage.cpp @@ -104,7 +104,7 @@ void CustomPage::filterChanged() if (ui->experimentsFilter->isChecked()) out << "(experiment)"; auto regexp = out.join('|'); - ui->versionList->setFilter(BaseVersionList::TypeRole, new RegexpFilter(regexp, false)); + ui->versionList->setFilter(BaseVersionList::TypeRole, Filters::regexp(QRegularExpression(regexp))); } void CustomPage::loaderFilterChanged() diff --git a/launcher/ui/widgets/ModFilterWidget.cpp b/launcher/ui/widgets/ModFilterWidget.cpp index 654eb75b1..522829fe0 100644 --- a/launcher/ui/widgets/ModFilterWidget.cpp +++ b/launcher/ui/widgets/ModFilterWidget.cpp @@ -114,7 +114,7 @@ ModFilterWidget::ModFilterWidget(MinecraftInstance* instance, bool extended) ui->setupUi(this); m_versions_proxy = new VersionProxyModel(this); - m_versions_proxy->setFilter(BaseVersionList::TypeRole, new ExactFilter("release")); + m_versions_proxy->setFilter(BaseVersionList::TypeRole, Filters::equals("release")); QAbstractProxyModel* proxy = new VersionBasicModel(this); proxy->setSourceModel(m_versions_proxy); @@ -152,9 +152,9 @@ ModFilterWidget::ModFilterWidget(MinecraftInstance* instance, bool extended) connect(ui->liteLoader, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged); connect(ui->babric, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged); connect(ui->btaBabric, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged); - + connect(ui->showMoreButton, &QPushButton::clicked, this, &ModFilterWidget::onShowMoreClicked); - + if (!extended) { ui->showMoreButton->setVisible(false); ui->extendedModLoadersWidget->setVisible(false); @@ -253,7 +253,7 @@ void ModFilterWidget::onShowAllVersionsChanged() if (ui->showAllVersions->isChecked()) m_versions_proxy->clearFilters(); else - m_versions_proxy->setFilter(BaseVersionList::TypeRole, new ExactFilter("release")); + m_versions_proxy->setFilter(BaseVersionList::TypeRole, Filters::equals("release")); } void ModFilterWidget::onVersionFilterChanged(int) diff --git a/launcher/ui/widgets/VersionSelectWidget.cpp b/launcher/ui/widgets/VersionSelectWidget.cpp index 2d735d18f..040355f4b 100644 --- a/launcher/ui/widgets/VersionSelectWidget.cpp +++ b/launcher/ui/widgets/VersionSelectWidget.cpp @@ -224,20 +224,20 @@ BaseVersion::Ptr VersionSelectWidget::selectedVersion() const void VersionSelectWidget::setFuzzyFilter(BaseVersionList::ModelRoles role, QString filter) { - m_proxyModel->setFilter(role, new ContainsFilter(filter)); + m_proxyModel->setFilter(role, Filters::contains(filter)); } void VersionSelectWidget::setExactFilter(BaseVersionList::ModelRoles role, QString filter) { - m_proxyModel->setFilter(role, new ExactFilter(filter)); + m_proxyModel->setFilter(role, Filters::equals(filter)); } void VersionSelectWidget::setExactIfPresentFilter(BaseVersionList::ModelRoles role, QString filter) { - m_proxyModel->setFilter(role, new ExactIfPresentFilter(filter)); + m_proxyModel->setFilter(role, Filters::equalsOrEmpty(filter)); } -void VersionSelectWidget::setFilter(BaseVersionList::ModelRoles role, Filter* filter) +void VersionSelectWidget::setFilter(BaseVersionList::ModelRoles role, Filter filter) { m_proxyModel->setFilter(role, filter); } diff --git a/launcher/ui/widgets/VersionSelectWidget.h b/launcher/ui/widgets/VersionSelectWidget.h index c16d4c0dd..c66d7e98e 100644 --- a/launcher/ui/widgets/VersionSelectWidget.h +++ b/launcher/ui/widgets/VersionSelectWidget.h @@ -39,13 +39,13 @@ #include #include #include "BaseVersionList.h" +#include "Filter.h" #include "VersionListView.h" class VersionProxyModel; class VersionListView; class QVBoxLayout; class QProgressBar; -class Filter; class VersionSelectWidget : public QWidget { Q_OBJECT @@ -70,7 +70,7 @@ class VersionSelectWidget : public QWidget { void setFuzzyFilter(BaseVersionList::ModelRoles role, QString filter); void setExactFilter(BaseVersionList::ModelRoles role, QString filter); void setExactIfPresentFilter(BaseVersionList::ModelRoles role, QString filter); - void setFilter(BaseVersionList::ModelRoles role, Filter* filter); + void setFilter(BaseVersionList::ModelRoles role, Filter filter); void setEmptyString(QString emptyString); void setEmptyErrorString(QString emptyErrorString); void setEmptyMode(VersionListView::EmptyMode mode); From d7eddd37739ec507786e8399b20d9c6b750a3add Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Mon, 4 Aug 2025 14:28:45 +0100 Subject: [PATCH 386/695] Replace IPathMatcher stuff with Filter Signed-off-by: TheKodeToad --- launcher/Application.cpp | 31 ++++++++--------- launcher/BaseInstance.h | 1 - launcher/CMakeLists.txt | 10 ------ launcher/DataMigrationTask.cpp | 2 +- launcher/DataMigrationTask.h | 6 ++-- launcher/FileSystem.cpp | 6 ++-- launcher/FileSystem.h | 20 +++++------ launcher/Filter.h | 20 +++++++++-- launcher/InstanceCopyTask.cpp | 7 ++-- launcher/InstanceCopyTask.h | 3 +- launcher/MMCZip.h | 2 +- launcher/RecursiveFileSystemWatcher.cpp | 2 +- launcher/RecursiveFileSystemWatcher.h | 6 ++-- launcher/VersionProxyModel.h | 2 +- launcher/minecraft/MinecraftInstance.cpp | 1 - launcher/pathmatcher/FSTreeMatcher.h | 14 -------- launcher/pathmatcher/IPathMatcher.h | 12 ------- launcher/pathmatcher/MultiMatcher.h | 27 --------------- launcher/pathmatcher/RegexpMatcher.h | 40 ---------------------- launcher/pathmatcher/SimplePrefixMatcher.h | 24 ------------- launcher/ui/pages/instance/OtherLogsPage.h | 1 - tests/FileSystem_test.cpp | 12 +++---- 22 files changed, 66 insertions(+), 183 deletions(-) delete mode 100644 launcher/pathmatcher/FSTreeMatcher.h delete mode 100644 launcher/pathmatcher/IPathMatcher.h delete mode 100644 launcher/pathmatcher/MultiMatcher.h delete mode 100644 launcher/pathmatcher/RegexpMatcher.h delete mode 100644 launcher/pathmatcher/SimplePrefixMatcher.h diff --git a/launcher/Application.cpp b/launcher/Application.cpp index d7182c48d..a8bfd52e3 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -46,8 +46,6 @@ #include "DataMigrationTask.h" #include "java/JavaInstallList.h" #include "net/PasteUpload.h" -#include "pathmatcher/MultiMatcher.h" -#include "pathmatcher/SimplePrefixMatcher.h" #include "tasks/Task.h" #include "tools/GenericProfiler.h" #include "ui/InstanceWindow.h" @@ -1985,22 +1983,23 @@ bool Application::handleDataMigration(const QString& currentData, if (!currentExists) { // Migrate! - auto matcher = std::make_shared(); - matcher->add(std::make_shared(configFile)); - matcher->add(std::make_shared( - BuildConfig.LAUNCHER_CONFIGFILE)); // it's possible that we already used that directory before - matcher->add(std::make_shared("logs/")); - matcher->add(std::make_shared("accounts.json")); - matcher->add(std::make_shared("accounts/")); - matcher->add(std::make_shared("assets/")); - matcher->add(std::make_shared("icons/")); - matcher->add(std::make_shared("instances/")); - matcher->add(std::make_shared("libraries/")); - matcher->add(std::make_shared("mods/")); - matcher->add(std::make_shared("themes/")); + using namespace Filters; + + QList filters; + filters.append(equals(configFile)); + filters.append(equals(BuildConfig.LAUNCHER_CONFIGFILE)); // it's possible that we already used that directory before + filters.append(startsWith("logs/")); + filters.append(equals("accounts.json")); + filters.append(startsWith("accounts/")); + filters.append(startsWith("assets/")); + filters.append(startsWith("icons/")); + filters.append(startsWith("instances/")); + filters.append(startsWith("libraries/")); + filters.append(startsWith("mods/")); + filters.append(startsWith("themes/")); ProgressDialog diag; - DataMigrationTask task(oldData, currentData, matcher); + DataMigrationTask task(oldData, currentData, any(std::move(filters))); if (diag.execWithTask(&task)) { qDebug() << "<> Migration succeeded"; setDoNotMigrate(); diff --git a/launcher/BaseInstance.h b/launcher/BaseInstance.h index 6baac4ce8..a542b76eb 100644 --- a/launcher/BaseInstance.h +++ b/launcher/BaseInstance.h @@ -52,7 +52,6 @@ #include "BaseVersionList.h" #include "MessageLevel.h" #include "minecraft/auth/MinecraftAccount.h" -#include "pathmatcher/IPathMatcher.h" #include "settings/INIFile.h" #include "net/Mode.h" diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 6af7d32a2..2d1c62269 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -107,15 +107,6 @@ if (UNIX AND NOT CYGWIN AND NOT APPLE) ) endif() -set(PATHMATCHER_SOURCES - # Path matchers - pathmatcher/FSTreeMatcher.h - pathmatcher/IPathMatcher.h - pathmatcher/MultiMatcher.h - pathmatcher/RegexpMatcher.h - pathmatcher/SimplePrefixMatcher.h -) - set(NET_SOURCES # network stuffs net/ByteArraySink.h @@ -759,7 +750,6 @@ endif() set(LOGIC_SOURCES ${CORE_SOURCES} - ${PATHMATCHER_SOURCES} ${NET_SOURCES} ${LAUNCH_SOURCES} ${UPDATE_SOURCES} diff --git a/launcher/DataMigrationTask.cpp b/launcher/DataMigrationTask.cpp index 18decc7c3..9677f868e 100644 --- a/launcher/DataMigrationTask.cpp +++ b/launcher/DataMigrationTask.cpp @@ -12,7 +12,7 @@ #include -DataMigrationTask::DataMigrationTask(const QString& sourcePath, const QString& targetPath, const IPathMatcher::Ptr pathMatcher) +DataMigrationTask::DataMigrationTask(const QString& sourcePath, const QString& targetPath, Filter pathMatcher) : Task(), m_sourcePath(sourcePath), m_targetPath(targetPath), m_pathMatcher(pathMatcher), m_copy(sourcePath, targetPath) { m_copy.matcher(m_pathMatcher).whitelist(true); diff --git a/launcher/DataMigrationTask.h b/launcher/DataMigrationTask.h index fc613cd5e..9a2b0adb8 100644 --- a/launcher/DataMigrationTask.h +++ b/launcher/DataMigrationTask.h @@ -5,7 +5,7 @@ #pragma once #include "FileSystem.h" -#include "pathmatcher/IPathMatcher.h" +#include "Filter.h" #include "tasks/Task.h" #include @@ -18,7 +18,7 @@ class DataMigrationTask : public Task { Q_OBJECT public: - explicit DataMigrationTask(const QString& sourcePath, const QString& targetPath, IPathMatcher::Ptr pathmatcher); + explicit DataMigrationTask(const QString& sourcePath, const QString& targetPath, Filter pathmatcher); ~DataMigrationTask() override = default; protected: @@ -33,7 +33,7 @@ class DataMigrationTask : public Task { private: const QString& m_sourcePath; const QString& m_targetPath; - const IPathMatcher::Ptr m_pathMatcher; + const Filter m_pathMatcher; FS::copy m_copy; int m_toCopy = 0; diff --git a/launcher/FileSystem.cpp b/launcher/FileSystem.cpp index 308f8620e..e987fa59a 100644 --- a/launcher/FileSystem.cpp +++ b/launcher/FileSystem.cpp @@ -331,7 +331,7 @@ bool copy::operator()(const QString& offset, bool dryRun) // Function that'll do the actual copying auto copy_file = [this, dryRun, src, dst, opt, &err](QString src_path, QString relative_dst_path) { - if (m_matcher && (m_matcher->matches(relative_dst_path) != m_whitelist)) + if (m_matcher && (m_matcher(relative_dst_path) != m_whitelist)) return; auto dst_path = PathCombine(dst, relative_dst_path); @@ -418,7 +418,7 @@ void create_link::make_link_list(const QString& offset) // Function that'll do the actual linking auto link_file = [this, dst](QString src_path, QString relative_dst_path) { - if (m_matcher && (m_matcher->matches(relative_dst_path) != m_whitelist)) { + if (m_matcher && (m_matcher(relative_dst_path) != m_whitelist)) { qDebug() << "path" << relative_dst_path << "in black list or not in whitelist"; return; } @@ -1277,7 +1277,7 @@ bool clone::operator()(const QString& offset, bool dryRun) // Function that'll do the actual cloneing auto cloneFile = [this, dryRun, dst, &err](QString src_path, QString relative_dst_path) { - if (m_matcher && (m_matcher->matches(relative_dst_path) != m_whitelist)) + if (m_matcher && (m_matcher(relative_dst_path) != m_whitelist)) return; auto dst_path = PathCombine(dst, relative_dst_path); diff --git a/launcher/FileSystem.h b/launcher/FileSystem.h index 0e573a09e..b0d9ae2e8 100644 --- a/launcher/FileSystem.h +++ b/launcher/FileSystem.h @@ -38,7 +38,7 @@ #pragma once #include "Exception.h" -#include "pathmatcher/IPathMatcher.h" +#include "Filter.h" #include @@ -115,9 +115,9 @@ class copy : public QObject { m_followSymlinks = follow; return *this; } - copy& matcher(IPathMatcher::Ptr filter) + copy& matcher(Filter filter) { - m_matcher = filter; + m_matcher = std::move(filter); return *this; } copy& whitelist(bool whitelist) @@ -147,7 +147,7 @@ class copy : public QObject { private: bool m_followSymlinks = true; - IPathMatcher::Ptr m_matcher = nullptr; + Filter m_matcher = nullptr; bool m_whitelist = false; bool m_overwrite = false; QDir m_src; @@ -209,9 +209,9 @@ class create_link : public QObject { m_useHardLinks = useHard; return *this; } - create_link& matcher(IPathMatcher::Ptr filter) + create_link& matcher(Filter filter) { - m_matcher = filter; + m_matcher = std::move(filter); return *this; } create_link& whitelist(bool whitelist) @@ -260,7 +260,7 @@ class create_link : public QObject { private: bool m_useHardLinks = false; - IPathMatcher::Ptr m_matcher = nullptr; + Filter m_matcher = nullptr; bool m_whitelist = false; bool m_recursive = true; @@ -492,9 +492,9 @@ class clone : public QObject { m_src.setPath(src); m_dst.setPath(dst); } - clone& matcher(IPathMatcher::Ptr filter) + clone& matcher(Filter filter) { - m_matcher = filter; + m_matcher = std::move(filter); return *this; } clone& whitelist(bool whitelist) @@ -518,7 +518,7 @@ class clone : public QObject { bool operator()(const QString& offset, bool dryRun = false); private: - IPathMatcher::Ptr m_matcher = nullptr; + Filter m_matcher = nullptr; bool m_whitelist = false; QDir m_src; QDir m_dst; diff --git a/launcher/Filter.h b/launcher/Filter.h index d94a45fea..317f5b067 100644 --- a/launcher/Filter.h +++ b/launcher/Filter.h @@ -11,9 +11,15 @@ inline Filter inverse(Filter filter) return [filter = std::move(filter)](const QString& src) { return !filter(src); }; } -inline Filter contains(QString pattern) +inline Filter any(QList filters) { - return [pattern = std::move(pattern)](const QString& src) { return src.contains(pattern); }; + return [filters = std::move(filters)](const QString& src) { + for (auto& filter : filters) + if (filter(src)) + return true; + + return false; + }; } inline Filter equals(QString pattern) @@ -31,6 +37,16 @@ inline Filter equalsOrEmpty(QString pattern) return [pattern = std::move(pattern)](const QString& src) { return src.isEmpty() || src == pattern; }; } +inline Filter contains(QString pattern) +{ + return [pattern = std::move(pattern)](const QString& src) { return src.contains(pattern); }; +} + +inline Filter startsWith(QString pattern) +{ + return [pattern = std::move(pattern)](const QString& src) { return src.startsWith(pattern); }; +} + inline Filter regexp(QRegularExpression pattern) { return [pattern = std::move(pattern)](const QString& src) { return pattern.match(src).hasMatch(); }; diff --git a/launcher/InstanceCopyTask.cpp b/launcher/InstanceCopyTask.cpp index fb5963532..eba1a1339 100644 --- a/launcher/InstanceCopyTask.cpp +++ b/launcher/InstanceCopyTask.cpp @@ -3,8 +3,8 @@ #include #include #include "FileSystem.h" +#include "Filter.h" #include "NullInstance.h" -#include "pathmatcher/RegexpMatcher.h" #include "settings/INISettingsObject.h" #include "tasks/Task.h" @@ -30,9 +30,8 @@ InstanceCopyTask::InstanceCopyTask(InstancePtr origInstance, const InstanceCopyP if (!filters.isEmpty()) { // Set regex filter: // FIXME: get this from the original instance type... - auto matcherReal = new RegexpMatcher(filters); - matcherReal->caseSensitive(false); - m_matcher.reset(matcherReal); + QRegularExpression regexp(filters, QRegularExpression::CaseInsensitiveOption); + m_matcher = Filters::regexp(regexp); } } diff --git a/launcher/InstanceCopyTask.h b/launcher/InstanceCopyTask.h index 3aba13e5c..ef4120bc6 100644 --- a/launcher/InstanceCopyTask.h +++ b/launcher/InstanceCopyTask.h @@ -5,6 +5,7 @@ #include #include "BaseInstance.h" #include "BaseVersion.h" +#include "Filter.h" #include "InstanceCopyPrefs.h" #include "InstanceTask.h" #include "net/NetJob.h" @@ -28,7 +29,7 @@ class InstanceCopyTask : public InstanceTask { InstancePtr m_origInstance; QFuture m_copyFuture; QFutureWatcher m_copyFutureWatcher; - IPathMatcher::Ptr m_matcher; + Filter m_matcher; bool m_keepPlaytime; bool m_useLinks = false; bool m_useHardLinks = false; diff --git a/launcher/MMCZip.h b/launcher/MMCZip.h index aafcbd194..e23d29d65 100644 --- a/launcher/MMCZip.h +++ b/launcher/MMCZip.h @@ -178,7 +178,7 @@ class ExportToZipTask : public Task { QString destinationPrefix = "", bool followSymlinks = false, bool utf8Enabled = false) - : ExportToZipTask(outputPath, QDir(dir), files, destinationPrefix, followSymlinks, utf8Enabled){}; + : ExportToZipTask(outputPath, QDir(dir), files, destinationPrefix, followSymlinks, utf8Enabled) {}; virtual ~ExportToZipTask() = default; diff --git a/launcher/RecursiveFileSystemWatcher.cpp b/launcher/RecursiveFileSystemWatcher.cpp index 5cb3cd0be..b0137fb5c 100644 --- a/launcher/RecursiveFileSystemWatcher.cpp +++ b/launcher/RecursiveFileSystemWatcher.cpp @@ -78,7 +78,7 @@ QStringList RecursiveFileSystemWatcher::scanRecursive(const QDir& directory) } for (const QString& file : directory.entryList(QDir::Files | QDir::Hidden)) { auto relPath = m_root.relativeFilePath(directory.absoluteFilePath(file)); - if (m_matcher->matches(relPath)) { + if (m_matcher(relPath)) { ret.append(relPath); } } diff --git a/launcher/RecursiveFileSystemWatcher.h b/launcher/RecursiveFileSystemWatcher.h index 7f96f5cd0..0a71e64c2 100644 --- a/launcher/RecursiveFileSystemWatcher.h +++ b/launcher/RecursiveFileSystemWatcher.h @@ -2,7 +2,7 @@ #include #include -#include "pathmatcher/IPathMatcher.h" +#include "Filter.h" class RecursiveFileSystemWatcher : public QObject { Q_OBJECT @@ -16,7 +16,7 @@ class RecursiveFileSystemWatcher : public QObject { void setWatchFiles(bool watchFiles); bool watchFiles() const { return m_watchFiles; } - void setMatcher(IPathMatcher::Ptr matcher) { m_matcher = matcher; } + void setMatcher(Filter matcher) { m_matcher = std::move(matcher); } QStringList files() const { return m_files; } @@ -32,7 +32,7 @@ class RecursiveFileSystemWatcher : public QObject { QDir m_root; bool m_watchFiles = false; bool m_isEnabled = false; - IPathMatcher::Ptr m_matcher; + Filter m_matcher; QFileSystemWatcher* m_watcher; diff --git a/launcher/VersionProxyModel.h b/launcher/VersionProxyModel.h index 0fcb380e4..ddd5d2458 100644 --- a/launcher/VersionProxyModel.h +++ b/launcher/VersionProxyModel.h @@ -14,7 +14,7 @@ class VersionProxyModel : public QAbstractProxyModel { public: VersionProxyModel(QObject* parent = 0); - virtual ~VersionProxyModel(){}; + virtual ~VersionProxyModel() {}; virtual int columnCount(const QModelIndex& parent = QModelIndex()) const override; virtual int rowCount(const QModelIndex& parent = QModelIndex()) const override; diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index 7749d0f6b..a58ad21bc 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -53,7 +53,6 @@ #include "FileSystem.h" #include "MMCTime.h" #include "java/JavaVersion.h" -#include "pathmatcher/MultiMatcher.h" #include "launch/LaunchTask.h" #include "launch/TaskStepWrapper.h" diff --git a/launcher/pathmatcher/FSTreeMatcher.h b/launcher/pathmatcher/FSTreeMatcher.h deleted file mode 100644 index d8d36d2c3..000000000 --- a/launcher/pathmatcher/FSTreeMatcher.h +++ /dev/null @@ -1,14 +0,0 @@ -#pragma once - -#include -#include "IPathMatcher.h" - -class FSTreeMatcher : public IPathMatcher { - public: - virtual ~FSTreeMatcher() {}; - FSTreeMatcher(SeparatorPrefixTree<'/'>& tree) : m_fsTree(tree) {} - - bool matches(const QString& string) const override { return m_fsTree.covers(string); } - - SeparatorPrefixTree<'/'>& m_fsTree; -}; diff --git a/launcher/pathmatcher/IPathMatcher.h b/launcher/pathmatcher/IPathMatcher.h deleted file mode 100644 index f3b01e8cf..000000000 --- a/launcher/pathmatcher/IPathMatcher.h +++ /dev/null @@ -1,12 +0,0 @@ -#pragma once -#include -#include - -class IPathMatcher { - public: - using Ptr = std::shared_ptr; - - public: - virtual ~IPathMatcher() {} - virtual bool matches(const QString& string) const = 0; -}; diff --git a/launcher/pathmatcher/MultiMatcher.h b/launcher/pathmatcher/MultiMatcher.h deleted file mode 100644 index 3ad07b643..000000000 --- a/launcher/pathmatcher/MultiMatcher.h +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once - -#include -#include "IPathMatcher.h" - -class MultiMatcher : public IPathMatcher { - public: - virtual ~MultiMatcher() {}; - MultiMatcher() {} - MultiMatcher& add(Ptr add) - { - m_matchers.append(add); - return *this; - } - - virtual bool matches(const QString& string) const override - { - for (auto iter : m_matchers) { - if (iter->matches(string)) { - return true; - } - } - return false; - } - - QList m_matchers; -}; diff --git a/launcher/pathmatcher/RegexpMatcher.h b/launcher/pathmatcher/RegexpMatcher.h deleted file mode 100644 index e36516386..000000000 --- a/launcher/pathmatcher/RegexpMatcher.h +++ /dev/null @@ -1,40 +0,0 @@ -#pragma once - -#include -#include "IPathMatcher.h" - -class RegexpMatcher : public IPathMatcher { - public: - virtual ~RegexpMatcher() {} - RegexpMatcher(const QString& regexp) - { - m_regexp.setPattern(regexp); - m_onlyFilenamePart = !regexp.contains('/'); - } - - RegexpMatcher(const QRegularExpression& regex) : m_regexp(regex) { m_onlyFilenamePart = !regex.pattern().contains('/'); } - - RegexpMatcher& caseSensitive(bool cs = true) - { - if (cs) { - m_regexp.setPatternOptions(QRegularExpression::CaseInsensitiveOption); - } else { - m_regexp.setPatternOptions(QRegularExpression::NoPatternOption); - } - return *this; - } - - virtual bool matches(const QString& string) const override - { - if (m_onlyFilenamePart) { - auto slash = string.lastIndexOf('/'); - if (slash != -1) { - auto part = string.mid(slash + 1); - return m_regexp.match(part).hasMatch(); - } - } - return m_regexp.match(string).hasMatch(); - } - QRegularExpression m_regexp; - bool m_onlyFilenamePart = false; -}; diff --git a/launcher/pathmatcher/SimplePrefixMatcher.h b/launcher/pathmatcher/SimplePrefixMatcher.h deleted file mode 100644 index 57bf63a30..000000000 --- a/launcher/pathmatcher/SimplePrefixMatcher.h +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Sefa Eyeoglu -// -// SPDX-License-Identifier: GPL-3.0-only - -#include "IPathMatcher.h" - -class SimplePrefixMatcher : public IPathMatcher { - public: - virtual ~SimplePrefixMatcher() {}; - SimplePrefixMatcher(const QString& prefix) - { - m_prefix = prefix; - m_isPrefix = prefix.endsWith('/'); - } - - virtual bool matches(const QString& string) const override - { - if (m_isPrefix) - return string.startsWith(m_prefix); - return string == m_prefix; - } - QString m_prefix; - bool m_isPrefix = false; -}; diff --git a/launcher/ui/pages/instance/OtherLogsPage.h b/launcher/ui/pages/instance/OtherLogsPage.h index 4104d8f3c..fbf9991e1 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.h +++ b/launcher/ui/pages/instance/OtherLogsPage.h @@ -38,7 +38,6 @@ #include #include -#include #include #include "LogPage.h" #include "ui/pages/BasePage.h" diff --git a/tests/FileSystem_test.cpp b/tests/FileSystem_test.cpp index 48da30a25..9e99a3f30 100644 --- a/tests/FileSystem_test.cpp +++ b/tests/FileSystem_test.cpp @@ -12,8 +12,6 @@ #include namespace fs = std::filesystem; -#include - class LinkTask : public Task { Q_OBJECT @@ -27,7 +25,7 @@ class LinkTask : public Task { ~LinkTask() { delete m_lnk; } - void matcher(IPathMatcher::Ptr filter) { m_lnk->matcher(filter); } + void matcher(Filter filter) { m_lnk->matcher(filter); } void linkRecursively(bool recursive) { @@ -190,7 +188,7 @@ class FileSystemTest : public QObject { qDebug() << tempDir.path(); qDebug() << target_dir.path(); FS::copy c(folder, target_dir.path()); - RegexpMatcher::Ptr re = std::make_shared("[.]?mcmeta"); + auto re = Filters::regexp(QRegularExpression("/[.]?mcmeta$")); c.matcher(re); c(); @@ -223,7 +221,7 @@ class FileSystemTest : public QObject { qDebug() << tempDir.path(); qDebug() << target_dir.path(); FS::copy c(folder, target_dir.path()); - RegexpMatcher::Ptr re = std::make_shared("[.]?mcmeta"); + auto re = Filters::regexp(QRegularExpression("/[.]?mcmeta$")); c.matcher(re); c.whitelist(true); c(); @@ -415,7 +413,7 @@ class FileSystemTest : public QObject { qDebug() << target_dir.path(); LinkTask lnk_tsk(folder, target_dir.path()); - RegexpMatcher::Ptr re = std::make_shared("[.]?mcmeta"); + auto re = Filters::regexp(QRegularExpression("/[.]?mcmeta$")); lnk_tsk.matcher(re); lnk_tsk.linkRecursively(true); connect(&lnk_tsk, &Task::finished, @@ -461,7 +459,7 @@ class FileSystemTest : public QObject { qDebug() << target_dir.path(); LinkTask lnk_tsk(folder, target_dir.path()); - RegexpMatcher::Ptr re = std::make_shared("[.]?mcmeta"); + auto re = Filters::regexp(QRegularExpression("/[.]?mcmeta$")); lnk_tsk.matcher(re); lnk_tsk.linkRecursively(true); lnk_tsk.whitelist(true); From e14b18ca71d3e24c60331829c78a2b4a57a6f5ec Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Mon, 4 Aug 2025 15:31:38 +0100 Subject: [PATCH 387/695] Fix tests (oops) Signed-off-by: TheKodeToad --- tests/FileSystem_test.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/FileSystem_test.cpp b/tests/FileSystem_test.cpp index 9e99a3f30..37e5e1201 100644 --- a/tests/FileSystem_test.cpp +++ b/tests/FileSystem_test.cpp @@ -188,7 +188,7 @@ class FileSystemTest : public QObject { qDebug() << tempDir.path(); qDebug() << target_dir.path(); FS::copy c(folder, target_dir.path()); - auto re = Filters::regexp(QRegularExpression("/[.]?mcmeta$")); + auto re = Filters::regexp(QRegularExpression("[.]?mcmeta")); c.matcher(re); c(); @@ -221,7 +221,7 @@ class FileSystemTest : public QObject { qDebug() << tempDir.path(); qDebug() << target_dir.path(); FS::copy c(folder, target_dir.path()); - auto re = Filters::regexp(QRegularExpression("/[.]?mcmeta$")); + auto re = Filters::regexp(QRegularExpression("[.]?mcmeta")); c.matcher(re); c.whitelist(true); c(); @@ -413,7 +413,7 @@ class FileSystemTest : public QObject { qDebug() << target_dir.path(); LinkTask lnk_tsk(folder, target_dir.path()); - auto re = Filters::regexp(QRegularExpression("/[.]?mcmeta$")); + auto re = Filters::regexp(QRegularExpression("[.]?mcmeta")); lnk_tsk.matcher(re); lnk_tsk.linkRecursively(true); connect(&lnk_tsk, &Task::finished, @@ -459,7 +459,7 @@ class FileSystemTest : public QObject { qDebug() << target_dir.path(); LinkTask lnk_tsk(folder, target_dir.path()); - auto re = Filters::regexp(QRegularExpression("/[.]?mcmeta$")); + auto re = Filters::regexp(QRegularExpression("[.]?mcmeta")); lnk_tsk.matcher(re); lnk_tsk.linkRecursively(true); lnk_tsk.whitelist(true); From 3ba948301119a3e760689d6f6b60d81947667ffb Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Mon, 4 Aug 2025 13:29:19 +0100 Subject: [PATCH 388/695] Simplify Rule Signed-off-by: TheKodeToad --- launcher/minecraft/Library.cpp | 8 +- launcher/minecraft/Library.h | 4 +- launcher/minecraft/MojangVersionFormat.cpp | 8 +- launcher/minecraft/Rule.cpp | 95 +++++++++------------- launcher/minecraft/Rule.h | 61 ++++---------- 5 files changed, 65 insertions(+), 111 deletions(-) diff --git a/launcher/minecraft/Library.cpp b/launcher/minecraft/Library.cpp index 0bc462474..026f9c281 100644 --- a/launcher/minecraft/Library.cpp +++ b/launcher/minecraft/Library.cpp @@ -242,13 +242,13 @@ bool Library::isActive(const RuntimeContext& runtimeContext) const if (m_rules.empty()) { result = true; } else { - RuleAction ruleResult = Disallow; + Rule::Action ruleResult = Rule::Disallow; for (auto rule : m_rules) { - RuleAction temp = rule->apply(this, runtimeContext); - if (temp != Defer) + Rule::Action temp = rule.apply(runtimeContext); + if (temp != Rule::Defer) ruleResult = temp; } - result = result && (ruleResult == Allow); + result = result && (ruleResult == Rule::Allow); } if (isNative()) { result = result && !getCompatibleNative(runtimeContext).isNull(); diff --git a/launcher/minecraft/Library.h b/launcher/minecraft/Library.h index d3019e814..d827554aa 100644 --- a/launcher/minecraft/Library.h +++ b/launcher/minecraft/Library.h @@ -129,7 +129,7 @@ class Library { void setHint(const QString& hint) { m_hint = hint; } /// Set the load rules - void setRules(QList> rules) { m_rules = rules; } + void setRules(QList rules) { m_rules = rules; } /// Returns true if the library should be loaded (or extracted, in case of natives) bool isActive(const RuntimeContext& runtimeContext) const; @@ -203,7 +203,7 @@ class Library { bool applyRules = false; /// rules associated with the library - QList> m_rules; + QList m_rules; /// MOJANG: container with Mojang style download info MojangLibraryDownloadInfo::Ptr m_mojangDownloads; diff --git a/launcher/minecraft/MojangVersionFormat.cpp b/launcher/minecraft/MojangVersionFormat.cpp index d17a3a21f..42730b12d 100644 --- a/launcher/minecraft/MojangVersionFormat.cpp +++ b/launcher/minecraft/MojangVersionFormat.cpp @@ -319,7 +319,11 @@ LibraryPtr MojangVersionFormat::libraryFromJson(ProblemContainer& problems, cons } if (libObj.contains("rules")) { out->applyRules = true; - out->m_rules = rulesFromJsonV4(libObj); + + QJsonArray rulesArray = requireArray(libObj.value("rules")); + for (auto rule : rulesArray) { + out->m_rules.append(Rule::fromJson(requireObject(rule))); + } } if (libObj.contains("downloads")) { out->m_mojangDownloads = libDownloadInfoFromJson(libObj); @@ -355,7 +359,7 @@ QJsonObject MojangVersionFormat::libraryToJson(Library* library) if (!library->m_rules.isEmpty()) { QJsonArray allRules; for (auto& rule : library->m_rules) { - QJsonObject ruleObj = rule->toJson(); + QJsonObject ruleObj = rule.toJson(); allRules.append(ruleObj); } libRoot.insert("rules", allRules); diff --git a/launcher/minecraft/Rule.cpp b/launcher/minecraft/Rule.cpp index d80aab84d..3cc655835 100644 --- a/launcher/minecraft/Rule.cpp +++ b/launcher/minecraft/Rule.cpp @@ -2,6 +2,7 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2025 TheKodeToad * * 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 @@ -38,72 +39,54 @@ #include "Rule.h" -RuleAction RuleAction_fromString(QString name) +Rule Rule::fromJson(const QJsonObject& object) { - if (name == "allow") - return Allow; - if (name == "disallow") - return Disallow; - return Defer; + Rule result; + + if (object["action"] == "allow") + result.action = Allow; + else if (object["action"] == "disallow") + result.action = Disallow; + + if (auto os = object["os"]; os.isObject()) { + if (auto name = os["name"].toString(); !name.isNull()) { + result.os = OS{ + name, + os["version"].toString(), + }; + } + } + + return result; } -QList> rulesFromJsonV4(const QJsonObject& objectWithRules) +QJsonObject Rule::toJson() { - QList> rules; - auto rulesVal = objectWithRules.value("rules"); - if (!rulesVal.isArray()) - return rules; + QJsonObject result; - QJsonArray ruleList = rulesVal.toArray(); - for (auto ruleVal : ruleList) { - std::shared_ptr rule; - if (!ruleVal.isObject()) - continue; - auto ruleObj = ruleVal.toObject(); - auto actionVal = ruleObj.value("action"); - if (!actionVal.isString()) - continue; - auto action = RuleAction_fromString(actionVal.toString()); - if (action == Defer) - continue; + if (action == Allow) + result["action"] = "allow"; + else if (action == Disallow) + result["action"] = "disallow"; - auto osVal = ruleObj.value("os"); - if (!osVal.isObject()) { - // add a new implicit action rule - rules.append(ImplicitRule::create(action)); - continue; - } + if (os.has_value()) { + QJsonObject osResult; + + osResult["name"] = os->name; - auto osObj = osVal.toObject(); - auto osNameVal = osObj.value("name"); - if (!osNameVal.isString()) - continue; - QString osName = osNameVal.toString(); - QString versionRegex = osObj.value("version").toString(); - // add a new OS rule - rules.append(OsRule::create(action, osName, versionRegex)); + if (!os->version.isEmpty()) + osResult["version"] = os->version; + + result["os"] = osResult; } - return rules; -} -QJsonObject ImplicitRule::toJson() -{ - QJsonObject ruleObj; - ruleObj.insert("action", m_result == Allow ? QString("allow") : QString("disallow")); - return ruleObj; + return result; } -QJsonObject OsRule::toJson() +Rule::Action Rule::apply(const RuntimeContext& runtimeContext) { - QJsonObject ruleObj; - ruleObj.insert("action", m_result == Allow ? QString("allow") : QString("disallow")); - QJsonObject osObj; - { - osObj.insert("name", m_system); - if (!m_version_regexp.isEmpty()) { - osObj.insert("version", m_version_regexp); - } - } - ruleObj.insert("os", osObj); - return ruleObj; + if (!runtimeContext.classifierMatches(os->name)) + return Defer; + + return action; } diff --git a/launcher/minecraft/Rule.h b/launcher/minecraft/Rule.h index c6cdbc43f..c7d5f420e 100644 --- a/launcher/minecraft/Rule.h +++ b/launcher/minecraft/Rule.h @@ -2,6 +2,7 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2025 TheKodeToad * * 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 @@ -38,59 +39,25 @@ #include #include #include -#include #include "RuntimeContext.h" class Library; -class Rule; -enum RuleAction { Allow, Disallow, Defer }; +struct Rule { + enum Action { Allow, Disallow, Defer }; -QList> rulesFromJsonV4(const QJsonObject& objectWithRules); + struct OS { + QString name; + // FIXME: unsupported + // retained to avoid information being lost from files + QString version; + }; -class Rule { - protected: - RuleAction m_result; - virtual bool applies(const Library* parent, const RuntimeContext& runtimeContext) = 0; + Action action = Defer; + std::optional os; - public: - Rule(RuleAction result) : m_result(result) {} - virtual ~Rule() {} - virtual QJsonObject toJson() = 0; - RuleAction apply(const Library* parent, const RuntimeContext& runtimeContext) - { - if (applies(parent, runtimeContext)) - return m_result; - else - return Defer; - } -}; - -class OsRule : public Rule { - private: - // the OS - QString m_system; - // the OS version regexp - QString m_version_regexp; - - protected: - virtual bool applies(const Library*, const RuntimeContext& runtimeContext) { return runtimeContext.classifierMatches(m_system); } - OsRule(RuleAction result, QString system, QString version_regexp) : Rule(result), m_system(system), m_version_regexp(version_regexp) {} - - public: - virtual QJsonObject toJson(); - static std::shared_ptr create(RuleAction result, QString system, QString version_regexp) - { - return std::shared_ptr(new OsRule(result, system, version_regexp)); - } -}; - -class ImplicitRule : public Rule { - protected: - virtual bool applies(const Library*, [[maybe_unused]] const RuntimeContext& runtimeContext) { return true; } - ImplicitRule(RuleAction result) : Rule(result) {} + static Rule fromJson(const QJsonObject& json); + QJsonObject toJson(); - public: - virtual QJsonObject toJson(); - static std::shared_ptr create(RuleAction result) { return std::shared_ptr(new ImplicitRule(result)); } + Action apply(const RuntimeContext& runtimeContext); }; From aaa1a748752fb9a0bc160822421348bb5118a1d9 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Mon, 4 Aug 2025 15:41:36 +0100 Subject: [PATCH 389/695] Reintroduce some encapulation As much as I like keeping things as simple as possible it's probably best to be consistent with other library related stuff Signed-off-by: TheKodeToad --- launcher/minecraft/Rule.cpp | 26 +++++++++++++------------- launcher/minecraft/Rule.h | 18 ++++++++++-------- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/launcher/minecraft/Rule.cpp b/launcher/minecraft/Rule.cpp index 3cc655835..1a7c7c768 100644 --- a/launcher/minecraft/Rule.cpp +++ b/launcher/minecraft/Rule.cpp @@ -44,13 +44,13 @@ Rule Rule::fromJson(const QJsonObject& object) Rule result; if (object["action"] == "allow") - result.action = Allow; + result.m_action = Allow; else if (object["action"] == "disallow") - result.action = Disallow; + result.m_action = Disallow; if (auto os = object["os"]; os.isObject()) { if (auto name = os["name"].toString(); !name.isNull()) { - result.os = OS{ + result.m_os = OS{ name, os["version"].toString(), }; @@ -64,20 +64,20 @@ QJsonObject Rule::toJson() { QJsonObject result; - if (action == Allow) + if (m_action == Allow) result["action"] = "allow"; - else if (action == Disallow) + else if (m_action == Disallow) result["action"] = "disallow"; - if (os.has_value()) { - QJsonObject osResult; + if (m_os.has_value()) { + QJsonObject os; - osResult["name"] = os->name; + os["name"] = m_os->name; - if (!os->version.isEmpty()) - osResult["version"] = os->version; + if (!m_os->version.isEmpty()) + os["version"] = m_os->version; - result["os"] = osResult; + result["os"] = os; } return result; @@ -85,8 +85,8 @@ QJsonObject Rule::toJson() Rule::Action Rule::apply(const RuntimeContext& runtimeContext) { - if (!runtimeContext.classifierMatches(os->name)) + if (!runtimeContext.classifierMatches(m_os->name)) return Defer; - return action; + return m_action; } diff --git a/launcher/minecraft/Rule.h b/launcher/minecraft/Rule.h index c7d5f420e..b0b689fd7 100644 --- a/launcher/minecraft/Rule.h +++ b/launcher/minecraft/Rule.h @@ -43,9 +43,16 @@ class Library; -struct Rule { +class Rule { + public: enum Action { Allow, Disallow, Defer }; + static Rule fromJson(const QJsonObject& json); + QJsonObject toJson(); + + Action apply(const RuntimeContext& runtimeContext); + + private: struct OS { QString name; // FIXME: unsupported @@ -53,11 +60,6 @@ struct Rule { QString version; }; - Action action = Defer; - std::optional os; - - static Rule fromJson(const QJsonObject& json); - QJsonObject toJson(); - - Action apply(const RuntimeContext& runtimeContext); + Action m_action = Defer; + std::optional m_os; }; From 100b209043b9cf288d5e5a4566ee3fd408d2c3ff Mon Sep 17 00:00:00 2001 From: moehreag Date: Tue, 5 Aug 2025 10:27:49 +0200 Subject: [PATCH 390/695] Add Ornithe, LegacyFabric and Rift mod loader filters Signed-off-by: moehreag --- launcher/modplatform/ModIndex.cpp | 14 +++++++++++-- launcher/modplatform/ModIndex.h | 5 ++++- launcher/modplatform/flame/FlameAPI.h | 3 +++ .../import_ftb/PackInstallTask.cpp | 6 ++++++ launcher/modplatform/modrinth/ModrinthAPI.h | 4 ++-- launcher/ui/widgets/ModFilterWidget.cpp | 13 ++++++++++-- launcher/ui/widgets/ModFilterWidget.ui | 21 +++++++++++++++++++ 7 files changed, 59 insertions(+), 7 deletions(-) diff --git a/launcher/modplatform/ModIndex.cpp b/launcher/modplatform/ModIndex.cpp index edb5e5aa1..be2e5393e 100644 --- a/launcher/modplatform/ModIndex.cpp +++ b/launcher/modplatform/ModIndex.cpp @@ -31,7 +31,7 @@ static const QMap s_indexed_version_ty { "alpha", IndexedVersionType::VersionType::Alpha } }; -static const QList loaderList = { NeoForge, Forge, Cauldron, LiteLoader, Quilt, Fabric, Babric, BTA }; +static const QList loaderList = { NeoForge, Forge, Cauldron, LiteLoader, Quilt, Fabric, Babric, BTA, LegacyFabric, Ornithe }; QList modLoaderTypesToList(ModLoaderTypes flags) { @@ -122,7 +122,7 @@ auto getModLoaderAsString(ModLoaderType type) -> const QString case Cauldron: return "cauldron"; case LiteLoader: - return "liteloader"; + return "liteloader"; case Fabric: return "fabric"; case Quilt: @@ -133,6 +133,12 @@ auto getModLoaderAsString(ModLoaderType type) -> const QString return "babric"; case BTA: return "bta-babric"; + case LegacyFabric: + return "legacy-fabric"; + case Ornithe: + return "ornithe"; + case Rift: + return "rift"; default: break; } @@ -157,6 +163,10 @@ auto getModLoaderFromString(QString type) -> ModLoaderType return Babric; if (type == "bta-babric") return BTA; + if (type == "legacy-fabric") + return LegacyFabric; + if (type == "ornithe") + return Ornithe; return {}; } diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h index 2935eda76..07a256899 100644 --- a/launcher/modplatform/ModIndex.h +++ b/launcher/modplatform/ModIndex.h @@ -38,7 +38,10 @@ enum ModLoaderType { Quilt = 1 << 5, DataPack = 1 << 6, Babric = 1 << 7, - BTA = 1 << 8 + BTA = 1 << 8, + LegacyFabric = 1 << 9, + Ornithe = 1 << 10, + Rift = 1 << 11 }; Q_DECLARE_FLAGS(ModLoaderTypes, ModLoaderType) QList modLoaderTypesToList(ModLoaderTypes flags); diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index f72bdb624..4b2b30645 100644 --- a/launcher/modplatform/flame/FlameAPI.h +++ b/launcher/modplatform/flame/FlameAPI.h @@ -73,6 +73,9 @@ class FlameAPI : public NetworkResourceAPI { case ModPlatform::DataPack: case ModPlatform::Babric: case ModPlatform::BTA: + case ModPlatform::LegacyFabric: + case ModPlatform::Ornithe: + case ModPlatform::Rift: break; // not supported } return 0; diff --git a/launcher/modplatform/import_ftb/PackInstallTask.cpp b/launcher/modplatform/import_ftb/PackInstallTask.cpp index 9ddca008d..3851e198c 100644 --- a/launcher/modplatform/import_ftb/PackInstallTask.cpp +++ b/launcher/modplatform/import_ftb/PackInstallTask.cpp @@ -95,6 +95,12 @@ void PackInstallTask::copySettings() break; case ModPlatform::BTA: break; + case ModPlatform::LegacyFabric: + break; + case ModPlatform::Ornithe: + break; + case ModPlatform::Rift: + break; } components->saveNow(); diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index 2e127dcff..5b426a06a 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -43,7 +43,7 @@ class ModrinthAPI : public NetworkResourceAPI { { QStringList l; for (auto loader : { ModPlatform::NeoForge, ModPlatform::Forge, ModPlatform::Fabric, ModPlatform::Quilt, ModPlatform::LiteLoader, - ModPlatform::DataPack, ModPlatform::Babric, ModPlatform::BTA }) { + ModPlatform::DataPack, ModPlatform::Babric, ModPlatform::BTA, ModPlatform::LegacyFabric, ModPlatform::Ornithe, ModPlatform::Rift }) { if (types & loader) { l << getModLoaderAsString(loader); } @@ -202,7 +202,7 @@ class ModrinthAPI : public NetworkResourceAPI { static inline auto validateModLoaders(ModPlatform::ModLoaderTypes loaders) -> bool { return loaders & (ModPlatform::NeoForge | ModPlatform::Forge | ModPlatform::Fabric | ModPlatform::Quilt | ModPlatform::LiteLoader | - ModPlatform::DataPack | ModPlatform::Babric | ModPlatform::BTA); + ModPlatform::DataPack | ModPlatform::Babric | ModPlatform::BTA | ModPlatform::LegacyFabric | ModPlatform::Ornithe | ModPlatform::Rift); } std::optional getDependencyURL(DependencySearchArgs const& args) const override diff --git a/launcher/ui/widgets/ModFilterWidget.cpp b/launcher/ui/widgets/ModFilterWidget.cpp index 654eb75b1..a19adabff 100644 --- a/launcher/ui/widgets/ModFilterWidget.cpp +++ b/launcher/ui/widgets/ModFilterWidget.cpp @@ -152,9 +152,12 @@ ModFilterWidget::ModFilterWidget(MinecraftInstance* instance, bool extended) connect(ui->liteLoader, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged); connect(ui->babric, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged); connect(ui->btaBabric, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged); - + connect(ui->legacyFabric, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged); + connect(ui->ornithe, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged); + connect(ui->rift, &QCheckBox::stateChanged, this, &ModFilterWidget::onLoadersFilterChanged); + connect(ui->showMoreButton, &QPushButton::clicked, this, &ModFilterWidget::onShowMoreClicked); - + if (!extended) { ui->showMoreButton->setVisible(false); ui->extendedModLoadersWidget->setVisible(false); @@ -289,6 +292,12 @@ void ModFilterWidget::onLoadersFilterChanged() loaders |= ModPlatform::Babric; if (ui->btaBabric->isChecked()) loaders |= ModPlatform::BTA; + if (ui->legacyFabric->isChecked()) + loaders |= ModPlatform::LegacyFabric; + if (ui->ornithe->isChecked()) + loaders |= ModPlatform::Ornithe; + if (ui->rift->isChecked()) + loaders |= ModPlatform::Rift; m_filter_changed = loaders != m_filter->loaders; m_filter->loaders = loaders; if (m_filter_changed) diff --git a/launcher/ui/widgets/ModFilterWidget.ui b/launcher/ui/widgets/ModFilterWidget.ui index 87d9af2e3..d29c9752a 100644 --- a/launcher/ui/widgets/ModFilterWidget.ui +++ b/launcher/ui/widgets/ModFilterWidget.ui @@ -167,6 +167,27 @@ + + + + Legacy Fabric + + + + + + + Ornithe + + + + + + + Rift + + + From 98ae99c513c59dfe6a1d49cb249f681d1f572424 Mon Sep 17 00:00:00 2001 From: moehreag Date: Tue, 5 Aug 2025 12:08:41 +0200 Subject: [PATCH 391/695] add Rift in the two places I missed Signed-off-by: moehreag --- launcher/modplatform/ModIndex.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/launcher/modplatform/ModIndex.cpp b/launcher/modplatform/ModIndex.cpp index be2e5393e..b13087158 100644 --- a/launcher/modplatform/ModIndex.cpp +++ b/launcher/modplatform/ModIndex.cpp @@ -31,7 +31,7 @@ static const QMap s_indexed_version_ty { "alpha", IndexedVersionType::VersionType::Alpha } }; -static const QList loaderList = { NeoForge, Forge, Cauldron, LiteLoader, Quilt, Fabric, Babric, BTA, LegacyFabric, Ornithe }; +static const QList loaderList = { NeoForge, Forge, Cauldron, LiteLoader, Quilt, Fabric, Babric, BTA, LegacyFabric, Ornithe, Rift }; QList modLoaderTypesToList(ModLoaderTypes flags) { @@ -167,6 +167,8 @@ auto getModLoaderFromString(QString type) -> ModLoaderType return LegacyFabric; if (type == "ornithe") return Ornithe; + if (type == "rift") + return Rift; return {}; } From d1f7bcd6c690b7c0c28b92763d5fd5a513660a18 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Tue, 5 Aug 2025 18:55:07 +0100 Subject: [PATCH 392/695] Properly refresh modpack search upon changing filters Signed-off-by: TheKodeToad --- launcher/ui/pages/modplatform/flame/FlamePage.cpp | 4 ++-- launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.cpp b/launcher/ui/pages/modplatform/flame/FlamePage.cpp index 9578eb73e..5bc314cc2 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlamePage.cpp @@ -140,8 +140,8 @@ void FlamePage::triggerSearch() ui->packView->clearSelection(); ui->packDescription->clear(); ui->versionSelectionBox->clear(); - listModel->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex(), m_filterWidget->getFilter(), - m_filterWidget->changed()); + bool filterChanged = m_filterWidget->changed(); + listModel->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex(), m_filterWidget->getFilter(), filterChanged); m_fetch_progress.watch(listModel->activeSearchJob().get()); } diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index 4586bf70d..d9004a1fc 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -361,7 +361,8 @@ void ModrinthPage::triggerSearch() ui->packView->clearSelection(); ui->packDescription->clear(); ui->versionSelectionBox->clear(); - m_model->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex(), m_filterWidget->getFilter(), m_filterWidget->changed()); + bool filterChanged = m_filterWidget->changed(); + m_model->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex(), m_filterWidget->getFilter(), filterChanged); m_fetch_progress.watch(m_model->activeSearchJob().get()); } From 9f5bc882b211dd700fff5f879ac60d1e91e332a0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 6 Aug 2025 02:15:15 +0000 Subject: [PATCH 393/695] chore(deps): update actions/download-artifact action to v5 --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 264dffbc0..606c4f15a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,7 +25,7 @@ jobs: submodules: "true" path: "PrismLauncher-source" - name: Download artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v5 - name: Grab and store version run: | tag_name=$(echo ${{ github.ref }} | grep -oE "[^/]+$") From 1cc6072a94a5cefb373f7d584d6e9d180dc7e893 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 10 Aug 2025 00:32:11 +0000 Subject: [PATCH 394/695] chore(nix): update lockfile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'nixpkgs': 'github:NixOS/nixpkgs/6e987485eb2c77e5dcc5af4e3c70843711ef9251?narHash=sha256-RKwfXA4OZROjBTQAl9WOZQFm7L8Bo93FQwSJpAiSRvo%3D' (2025-07-16) → 'github:NixOS/nixpkgs/c2ae88e026f9525daf89587f3cbee584b92b6134?narHash=sha256-erbiH2agUTD0Z30xcVSFcDHzkRvkRXOQ3lb887bcVrs%3D' (2025-08-06) --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 162ad5baa..80131607b 100644 --- a/flake.lock +++ b/flake.lock @@ -18,11 +18,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1752687322, - "narHash": "sha256-RKwfXA4OZROjBTQAl9WOZQFm7L8Bo93FQwSJpAiSRvo=", + "lastModified": 1754498491, + "narHash": "sha256-erbiH2agUTD0Z30xcVSFcDHzkRvkRXOQ3lb887bcVrs=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "6e987485eb2c77e5dcc5af4e3c70843711ef9251", + "rev": "c2ae88e026f9525daf89587f3cbee584b92b6134", "type": "github" }, "original": { From a1956ec53ad6d5157ad3e4a0590813bc73cfabd3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 11 Aug 2025 06:44:42 +0000 Subject: [PATCH 395/695] chore(deps): update actions/cache action to v4.2.4 --- .github/actions/setup-dependencies/windows/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/setup-dependencies/windows/action.yml b/.github/actions/setup-dependencies/windows/action.yml index 9b045610e..a91540c58 100644 --- a/.github/actions/setup-dependencies/windows/action.yml +++ b/.github/actions/setup-dependencies/windows/action.yml @@ -90,7 +90,7 @@ runs: - name: Retrieve ccache cache (MinGW) if: ${{ inputs.msystem != '' && inputs.build-type == 'Debug' }} - uses: actions/cache@v4.2.3 + uses: actions/cache@v4.2.4 with: path: '${{ github.workspace }}\.ccache' key: ${{ runner.os }}-mingw-w64-ccache-${{ github.run_id }} From 29dc75ec63a07ee55f51b2c0e27e1b6fb8227bc5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 11 Aug 2025 15:25:38 +0000 Subject: [PATCH 396/695] chore(deps): update actions/checkout action to v5 --- .github/workflows/backport.yml | 2 +- .github/workflows/build.yml | 2 +- .github/workflows/codeql.yml | 2 +- .github/workflows/flatpak.yml | 2 +- .github/workflows/nix.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/update-flake.yml | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index d8f9688d7..42369c366 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -21,7 +21,7 @@ jobs: if: github.repository_owner == 'PrismLauncher' && github.event.pull_request.merged == true && (github.event_name != 'labeled' || startsWith('backport', github.event.label.name)) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: ref: ${{ github.event.pull_request.head.sha }} - name: Create backport PRs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b8ba57a45..dc72e2da0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -133,7 +133,7 @@ jobs: ## - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: submodules: true diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 964e322b1..924b81e5f 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -60,7 +60,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: submodules: "true" diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml index 1d5c5e9cc..c16917869 100644 --- a/.github/workflows/flatpak.yml +++ b/.github/workflows/flatpak.yml @@ -84,7 +84,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: submodules: true diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index 6c7b2dac2..ca3803139 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -105,7 +105,7 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: ref: ${{ steps.merge-commit.outputs.merge-commit-sha || github.sha }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 606c4f15a..5fed75215 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,7 @@ jobs: upload_url: ${{ steps.create_release.outputs.upload_url }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: submodules: "true" path: "PrismLauncher-source" diff --git a/.github/workflows/update-flake.yml b/.github/workflows/update-flake.yml index 5822c411b..5648d5477 100644 --- a/.github/workflows/update-flake.yml +++ b/.github/workflows/update-flake.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: cachix/install-nix-action@c134e4c9e34bac6cab09cf239815f9339aaaf84e # v31 - uses: DeterminateSystems/update-flake-lock@v27 From a261718009e1f565464fdc02c75a5dc0e2f4b412 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 14 Aug 2025 17:10:11 +0000 Subject: [PATCH 397/695] chore(deps): update korthout/backport-action action to v3.3.0 --- .github/workflows/backport.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index d8f9688d7..487bbd826 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -25,7 +25,7 @@ jobs: with: ref: ${{ github.event.pull_request.head.sha }} - name: Create backport PRs - uses: korthout/backport-action@v3.2.1 + uses: korthout/backport-action@v3.3.0 with: # Config README: https://github.com/korthout/backport-action#backport-action pull_description: |- From bc76960dd7f2e284b2c821e740a8ae0c80a2efdd Mon Sep 17 00:00:00 2001 From: Seth Flynn Date: Fri, 15 Aug 2025 13:56:28 +0000 Subject: [PATCH 398/695] style(nix): format with modern nixfmt Signed-off-by: Seth Flynn --- nix/unwrapped.nix | 50 +++++++++++++++---------------- nix/wrapper.nix | 75 +++++++++++++++++++++++------------------------ 2 files changed, 61 insertions(+), 64 deletions(-) diff --git a/nix/unwrapped.nix b/nix/unwrapped.nix index d9144410f..93ac21de0 100644 --- a/nix/unwrapped.nix +++ b/nix/unwrapped.nix @@ -76,35 +76,33 @@ stdenv.mkDerivation { stripJavaArchivesHook ]; - buildInputs = - [ - cmark - kdePackages.qtbase - kdePackages.qtnetworkauth - kdePackages.quazip - tomlplusplus - zlib - ] - ++ lib.optionals stdenv.hostPlatform.isDarwin [ apple-sdk_11 ] - ++ lib.optional gamemodeSupport gamemode; + buildInputs = [ + cmark + kdePackages.qtbase + kdePackages.qtnetworkauth + kdePackages.quazip + tomlplusplus + zlib + ] + ++ lib.optionals stdenv.hostPlatform.isDarwin [ apple-sdk_11 ] + ++ lib.optional gamemodeSupport gamemode; hardeningEnable = lib.optionals stdenv.hostPlatform.isLinux [ "pie" ]; - cmakeFlags = - [ - # downstream branding - (lib.cmakeFeature "Launcher_BUILD_PLATFORM" "nixpkgs") - ] - ++ lib.optionals (msaClientID != null) [ - (lib.cmakeFeature "Launcher_MSA_CLIENT_ID" (toString msaClientID)) - ] - ++ lib.optionals stdenv.hostPlatform.isDarwin [ - # we wrap our binary manually - (lib.cmakeFeature "INSTALL_BUNDLE" "nodeps") - # disable built-in updater - (lib.cmakeFeature "MACOSX_SPARKLE_UPDATE_FEED_URL" "''") - (lib.cmakeFeature "CMAKE_INSTALL_PREFIX" "${placeholder "out"}/Applications/") - ]; + cmakeFlags = [ + # downstream branding + (lib.cmakeFeature "Launcher_BUILD_PLATFORM" "nixpkgs") + ] + ++ lib.optionals (msaClientID != null) [ + (lib.cmakeFeature "Launcher_MSA_CLIENT_ID" (toString msaClientID)) + ] + ++ lib.optionals stdenv.hostPlatform.isDarwin [ + # we wrap our binary manually + (lib.cmakeFeature "INSTALL_BUNDLE" "nodeps") + # disable built-in updater + (lib.cmakeFeature "MACOSX_SPARKLE_UPDATE_FEED_URL" "''") + (lib.cmakeFeature "CMAKE_INSTALL_PREFIX" "${placeholder "out"}/Applications/") + ]; doCheck = true; diff --git a/nix/wrapper.nix b/nix/wrapper.nix index 03c1f0421..01edd0d9a 100644 --- a/nix/wrapper.nix +++ b/nix/wrapper.nix @@ -61,14 +61,13 @@ symlinkJoin { nativeBuildInputs = [ kdePackages.wrapQtAppsHook ]; - buildInputs = - [ - kdePackages.qtbase - kdePackages.qtsvg - ] - ++ lib.optional ( - lib.versionAtLeast kdePackages.qtbase.version "6" && stdenv.hostPlatform.isLinux - ) kdePackages.qtwayland; + buildInputs = [ + kdePackages.qtbase + kdePackages.qtsvg + ] + ++ lib.optional ( + lib.versionAtLeast kdePackages.qtbase.version "6" && stdenv.hostPlatform.isLinux + ) kdePackages.qtwayland; postBuild = '' wrapQtAppsHook @@ -76,41 +75,41 @@ symlinkJoin { qtWrapperArgs = let - runtimeLibs = - [ - (lib.getLib stdenv.cc.cc) - ## native versions - glfw3-minecraft - openal - - ## openal - alsa-lib - libjack2 - libpulseaudio - pipewire - - ## glfw - libGL - libX11 - libXcursor - libXext - libXrandr - libXxf86vm - - udev # oshi - - vulkan-loader # VulkanMod's lwjgl - ] - ++ lib.optional textToSpeechSupport flite - ++ lib.optional gamemodeSupport gamemode.lib - ++ lib.optional controllerSupport libusb1 - ++ additionalLibs; + runtimeLibs = [ + (lib.getLib stdenv.cc.cc) + ## native versions + glfw3-minecraft + openal + + ## openal + alsa-lib + libjack2 + libpulseaudio + pipewire + + ## glfw + libGL + libX11 + libXcursor + libXext + libXrandr + libXxf86vm + + udev # oshi + + vulkan-loader # VulkanMod's lwjgl + ] + ++ lib.optional textToSpeechSupport flite + ++ lib.optional gamemodeSupport gamemode.lib + ++ lib.optional controllerSupport libusb1 + ++ additionalLibs; runtimePrograms = [ mesa-demos pciutils # need lspci xrandr # needed for LWJGL [2.9.2, 3) https://github.com/LWJGL/lwjgl/issues/128 - ] ++ additionalPrograms; + ] + ++ additionalPrograms; in [ "--prefix PRISMLAUNCHER_JAVA_PATHS : ${lib.makeSearchPath "bin/java" jdks}" ] From 4d4b8a1fd0900eb0b28448ca96df1b8567210bca Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 17 Aug 2025 00:30:33 +0000 Subject: [PATCH 399/695] chore(nix): update lockfile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'nixpkgs': 'github:NixOS/nixpkgs/c2ae88e026f9525daf89587f3cbee584b92b6134?narHash=sha256-erbiH2agUTD0Z30xcVSFcDHzkRvkRXOQ3lb887bcVrs%3D' (2025-08-06) → 'github:NixOS/nixpkgs/fbcf476f790d8a217c3eab4e12033dc4a0f6d23c?narHash=sha256-wNO3%2BKs2jZJ4nTHMuks%2BcxAiVBGNuEBXsT29Bz6HASo%3D' (2025-08-14) --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 80131607b..15aef086a 100644 --- a/flake.lock +++ b/flake.lock @@ -18,11 +18,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1754498491, - "narHash": "sha256-erbiH2agUTD0Z30xcVSFcDHzkRvkRXOQ3lb887bcVrs=", + "lastModified": 1755186698, + "narHash": "sha256-wNO3+Ks2jZJ4nTHMuks+cxAiVBGNuEBXsT29Bz6HASo=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "c2ae88e026f9525daf89587f3cbee584b92b6134", + "rev": "fbcf476f790d8a217c3eab4e12033dc4a0f6d23c", "type": "github" }, "original": { From c596c6eb48015b8943d5148b094366f652a68cce Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 21 Aug 2025 06:51:14 +0000 Subject: [PATCH 400/695] chore(deps): update actions/setup-java action to v5 --- .github/actions/setup-dependencies/windows/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/setup-dependencies/windows/action.yml b/.github/actions/setup-dependencies/windows/action.yml index a91540c58..73840771c 100644 --- a/.github/actions/setup-dependencies/windows/action.yml +++ b/.github/actions/setup-dependencies/windows/action.yml @@ -26,7 +26,7 @@ runs: vsversion: 2022 - name: Setup Java (MSVC) - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: # NOTE(@getchoo): We should probably stay on Zulu. # Temurin doesn't have Java 17 builds for WoA From 9c3c74d055bbf7998ab2acff49e87ef09d0e41a6 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Sun, 10 Aug 2025 10:52:10 +0300 Subject: [PATCH 401/695] fix: ensure correct skin format Signed-off-by: Trial97 --- launcher/minecraft/skins/SkinModel.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/launcher/minecraft/skins/SkinModel.cpp b/launcher/minecraft/skins/SkinModel.cpp index 209207215..f3d865ea8 100644 --- a/launcher/minecraft/skins/SkinModel.cpp +++ b/launcher/minecraft/skins/SkinModel.cpp @@ -23,10 +23,16 @@ #include "FileSystem.h" #include "Json.h" -static QImage improveSkin(const QImage& skin) +static QImage improveSkin(QImage skin) { + // It seems some older skins may use this format, which can't be drawn onto + // https://github.com/PrismLauncher/PrismLauncher/issues/4032 + // https://doc.qt.io/qt-6/qpainter.html#begin + if (skin.format() == QImage::Format_Indexed8) { + skin = skin.convertToFormat(QImage::Format_RGB32); + } if (skin.size() == QSize(64, 32)) { // old format - QImage newSkin = QImage(QSize(64, 64), skin.format()); + auto newSkin = QImage(QSize(64, 64), skin.format()); newSkin.fill(Qt::transparent); QPainter p(&newSkin); p.drawImage(QPoint(0, 0), skin.copy(QRect(0, 0, 64, 32))); // copy head @@ -110,7 +116,7 @@ SkinModel::SkinModel(QDir skinDir, QJsonObject obj) m_model = Model::SLIM; } m_path = skinDir.absoluteFilePath(name) + ".png"; - m_texture = QImage(getSkin(m_path)); + m_texture = getSkin(m_path); m_preview = generatePreviews(m_texture, m_model == Model::SLIM); } From 31e0c07bf7bfb36207c5ad6d1cb7c56fd192dc59 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Thu, 21 Aug 2025 12:45:48 +0100 Subject: [PATCH 402/695] Fix #4083 - server address text box is enabled even if auto-join is disabled Signed-off-by: TheKodeToad --- launcher/ui/widgets/MinecraftSettingsWidget.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/launcher/ui/widgets/MinecraftSettingsWidget.cpp b/launcher/ui/widgets/MinecraftSettingsWidget.cpp index f46786518..7cd84c8bb 100644 --- a/launcher/ui/widgets/MinecraftSettingsWidget.cpp +++ b/launcher/ui/widgets/MinecraftSettingsWidget.cpp @@ -222,9 +222,9 @@ void MinecraftSettingsWidget::loadSettings() m_ui->useDiscreteGpuCheck->setChecked(settings->get("UseDiscreteGpu").toBool()); m_ui->useZink->setChecked(settings->get("UseZink").toBool()); - m_ui->serverJoinGroupBox->setChecked(settings->get("JoinServerOnLaunch").toBool()); - if (m_instance != nullptr) { + m_ui->serverJoinGroupBox->setChecked(settings->get("JoinServerOnLaunch").toBool()); + if (auto server = settings->get("JoinServerOnLaunchAddress").toString(); !server.isEmpty()) { m_ui->serverJoinAddress->setText(server); m_ui->serverJoinAddressButton->setChecked(true); @@ -240,7 +240,7 @@ void MinecraftSettingsWidget::loadSettings() } else { m_ui->serverJoinAddressButton->setChecked(true); m_ui->worldJoinButton->setChecked(false); - m_ui->serverJoinAddress->setEnabled(true); + m_ui->serverJoinAddress->setEnabled(m_ui->serverJoinGroupBox->isChecked()); m_ui->worldsCb->setEnabled(false); } From 8c9b504382b665116e836b35774b4d73fc6f544a Mon Sep 17 00:00:00 2001 From: Trial97 Date: Thu, 21 Aug 2025 14:48:22 +0300 Subject: [PATCH 403/695] fix: datapack/resourcepack parsing Signed-off-by: Trial97 --- launcher/minecraft/mod/DataPack.h | 2 -- launcher/minecraft/mod/ResourcePack.h | 2 -- .../minecraft/mod/tasks/LocalDataPackParseTask.cpp | 10 ---------- 3 files changed, 14 deletions(-) diff --git a/launcher/minecraft/mod/DataPack.h b/launcher/minecraft/mod/DataPack.h index ac6408bde..2943dd4bc 100644 --- a/launcher/minecraft/mod/DataPack.h +++ b/launcher/minecraft/mod/DataPack.h @@ -64,8 +64,6 @@ class DataPack : public Resource { [[nodiscard]] int compare(Resource const& other, SortType type) const override; [[nodiscard]] bool applyFilter(QRegularExpression filter) const override; - virtual QString directory() { return "/data"; } - protected: mutable QMutex m_data_lock; diff --git a/launcher/minecraft/mod/ResourcePack.h b/launcher/minecraft/mod/ResourcePack.h index 494bdee97..9345e9c27 100644 --- a/launcher/minecraft/mod/ResourcePack.h +++ b/launcher/minecraft/mod/ResourcePack.h @@ -23,6 +23,4 @@ class ResourcePack : public DataPack { /** Gets, respectively, the lower and upper versions supported by the set pack format. */ std::pair compatibleVersions() const override; - - QString directory() override { return "/assets"; } }; diff --git a/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp index c37a25c21..c63e0c65f 100644 --- a/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp @@ -73,11 +73,6 @@ bool processFolder(DataPack* pack, ProcessingLevel level) return mcmeta_invalid(); // mcmeta file isn't a valid file } - QFileInfo data_dir_info(FS::PathCombine(pack->fileinfo().filePath(), pack->directory())); - if (!data_dir_info.exists() || !data_dir_info.isDir()) { - return false; // data dir does not exists or isn't valid - } - if (level == ProcessingLevel::BasicInfoOnly) { return true; // only need basic info already checked } @@ -141,11 +136,6 @@ bool processZIP(DataPack* pack, ProcessingLevel level) return mcmeta_invalid(); // could not set pack.mcmeta as current file. } - QuaZipDir zipDir(&zip); - if (!zipDir.exists(pack->directory())) { - return false; // data dir does not exists at zip root - } - if (level == ProcessingLevel::BasicInfoOnly) { zip.close(); return true; // only need basic info already checked From 22d651f0417a304ac0b22b86ad767d6066deb5bd Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Thu, 21 Aug 2025 13:05:13 +0100 Subject: [PATCH 404/695] Fix buggy movement in screenshots page Signed-off-by: TheKodeToad --- launcher/ui/pages/instance/ScreenshotsPage.ui | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/launcher/ui/pages/instance/ScreenshotsPage.ui b/launcher/ui/pages/instance/ScreenshotsPage.ui index 2e2227a29..db55869cd 100644 --- a/launcher/ui/pages/instance/ScreenshotsPage.ui +++ b/launcher/ui/pages/instance/ScreenshotsPage.ui @@ -26,11 +26,17 @@ + + false + - QAbstractItemView::ExtendedSelection + QAbstractItemView::SelectionMode::ExtendedSelection - QAbstractItemView::SelectRows + QAbstractItemView::SelectionBehavior::SelectRows + + + QListView::Movement::Static @@ -41,7 +47,7 @@ Actions - Qt::ToolButtonTextOnly + Qt::ToolButtonStyle::ToolButtonTextOnly RightToolBarArea From 634a5d18c490e9ab8a7106456daddebd1ee916af Mon Sep 17 00:00:00 2001 From: Trial97 Date: Thu, 21 Aug 2025 15:14:05 +0300 Subject: [PATCH 405/695] fix tests Signed-off-by: Trial97 --- tests/ResourcePackParse_test.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ResourcePackParse_test.cpp b/tests/ResourcePackParse_test.cpp index e9b5244ad..5400d888a 100644 --- a/tests/ResourcePackParse_test.cpp +++ b/tests/ResourcePackParse_test.cpp @@ -69,7 +69,7 @@ class ResourcePackParseTest : public QObject { QVERIFY(pack.packFormat() == 6); QVERIFY(pack.description() == "o quartel pegou fogo, policia deu sinal, acode acode acode a bandeira nacional"); - QVERIFY(valid == false); // no assets dir + QVERIFY(valid == true); // no assets dir but it is still valid based on https://minecraft.wiki/w/Resource_pack } }; From 40f45b19f403ab2683b8e4288fdcb2d518371bcf Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Thu, 21 Aug 2025 13:31:03 +0100 Subject: [PATCH 406/695] Make search bars consistent Signed-off-by: TheKodeToad --- .../pages/instance/ExternalResourcesPage.ui | 44 ++++++++----------- launcher/ui/pages/instance/LogPage.ui | 30 ++++++++----- launcher/ui/pages/instance/OtherLogsPage.ui | 29 +++++++----- launcher/ui/pages/instance/VersionPage.ui | 17 +++---- 4 files changed, 61 insertions(+), 59 deletions(-) diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.ui b/launcher/ui/pages/instance/ExternalResourcesPage.ui index 5df8aafa2..f55218ae5 100644 --- a/launcher/ui/pages/instance/ExternalResourcesPage.ui +++ b/launcher/ui/pages/instance/ExternalResourcesPage.ui @@ -24,31 +24,7 @@ 0 - - - - - - - - - Filter: - - - - - - - - - - 0 - 0 - - - - - + @@ -67,6 +43,23 @@ + + + + + 0 + 0 + + + + + + + + Search + + + @@ -247,7 +240,6 @@
    treeView - filterEdit diff --git a/launcher/ui/pages/instance/LogPage.ui b/launcher/ui/pages/instance/LogPage.ui index fb8690581..66a432edb 100644 --- a/launcher/ui/pages/instance/LogPage.ui +++ b/launcher/ui/pages/instance/LogPage.ui @@ -129,25 +129,27 @@ - - - - Search: - - - + + + 0 + 0 + + Find - - - + + + 0 + 0 + + Scroll all the way to bottom @@ -163,6 +165,13 @@ + + + + Search + + + @@ -185,7 +194,6 @@ btnPaste btnClear text - searchBar findButton diff --git a/launcher/ui/pages/instance/OtherLogsPage.ui b/launcher/ui/pages/instance/OtherLogsPage.ui index 7d60de5c4..be08e0fdb 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.ui +++ b/launcher/ui/pages/instance/OtherLogsPage.ui @@ -33,18 +33,14 @@ Tab 1 - - - - Search: - - - - - - + + + 0 + 0 + + &Find @@ -59,6 +55,12 @@ + + + 0 + 0 + + Scroll all the way to bottom @@ -204,6 +206,13 @@ + + + + Search + + + diff --git a/launcher/ui/pages/instance/VersionPage.ui b/launcher/ui/pages/instance/VersionPage.ui index 9be21d499..d525f56a5 100644 --- a/launcher/ui/pages/instance/VersionPage.ui +++ b/launcher/ui/pages/instance/VersionPage.ui @@ -43,18 +43,11 @@ - - - - - - - - Filter: - - - - + + + Search + + From 81e4f1cf7aabc98499a779475f1eac2afd9f89bd Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Thu, 21 Aug 2025 13:49:49 +0100 Subject: [PATCH 407/695] Prevent View Configs showing in places it shouldn't Signed-off-by: TheKodeToad --- launcher/ui/pages/instance/ExternalResourcesPage.cpp | 2 +- launcher/ui/pages/instance/ExternalResourcesPage.ui | 11 +++++------ launcher/ui/pages/instance/ModFolderPage.cpp | 2 ++ 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.cpp b/launcher/ui/pages/instance/ExternalResourcesPage.cpp index d38d16284..d0fb2347b 100644 --- a/launcher/ui/pages/instance/ExternalResourcesPage.cpp +++ b/launcher/ui/pages/instance/ExternalResourcesPage.cpp @@ -52,7 +52,7 @@ ExternalResourcesPage::ExternalResourcesPage(BaseInstance* instance, std::shared { ui->setupUi(this); - ui->actionsToolbar->insertSpacer(ui->actionViewConfigs); + ui->actionsToolbar->insertSpacer(ui->actionViewFolder); m_filterModel = model->createFilterProxyModel(this); m_filterModel->setDynamicSortFilter(true); diff --git a/launcher/ui/pages/instance/ExternalResourcesPage.ui b/launcher/ui/pages/instance/ExternalResourcesPage.ui index f55218ae5..c6955d0ce 100644 --- a/launcher/ui/pages/instance/ExternalResourcesPage.ui +++ b/launcher/ui/pages/instance/ExternalResourcesPage.ui @@ -36,7 +36,7 @@ true - QAbstractItemView::DropOnly + QAbstractItemView::DragDropMode::DropOnly true @@ -67,7 +67,7 @@ Actions - Qt::ToolButtonIconOnly + Qt::ToolButtonStyle::ToolButtonIconOnly true @@ -85,7 +85,6 @@ - @@ -172,7 +171,7 @@ Reset Update Metadata - QAction::NoRole + QAction::MenuRole::NoRole @@ -180,7 +179,7 @@ Verify Dependencies - QAction::NoRole + QAction::MenuRole::NoRole @@ -205,7 +204,7 @@ Change a resource's version. - QAction::NoRole + QAction::MenuRole::NoRole diff --git a/launcher/ui/pages/instance/ModFolderPage.cpp b/launcher/ui/pages/instance/ModFolderPage.cpp index 7b79766ee..198f336f9 100644 --- a/launcher/ui/pages/instance/ModFolderPage.cpp +++ b/launcher/ui/pages/instance/ModFolderPage.cpp @@ -106,6 +106,8 @@ ModFolderPage::ModFolderPage(BaseInstance* inst, std::shared_ptr ui->actionExportMetadata->setToolTip(tr("Export mod's metadata to text.")); connect(ui->actionExportMetadata, &QAction::triggered, this, &ModFolderPage::exportModMetadata); ui->actionsToolbar->insertActionAfter(ui->actionViewHomepage, ui->actionExportMetadata); + + ui->actionsToolbar->insertActionAfter(ui->actionViewFolder, ui->actionViewConfigs); } bool ModFolderPage::shouldDisplay() const From 83736950d9e63879312b8f432fb823cbbe3476d9 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Thu, 21 Aug 2025 14:06:26 +0100 Subject: [PATCH 408/695] Consistent case style Signed-off-by: TheKodeToad --- launcher/InstancePageProvider.h | 2 +- launcher/ui/MainWindow.ui | 2 +- launcher/ui/pages/global/ProxyPage.ui | 2 +- launcher/ui/pages/instance/DataPackPage.h | 2 +- launcher/ui/pages/instance/ManagedPackPage.cpp | 2 +- launcher/ui/pages/instance/ManagedPackPage.ui | 6 +++--- launcher/ui/pages/instance/ModFolderPage.h | 2 +- launcher/ui/pages/instance/ResourcePackPage.h | 2 +- launcher/ui/pages/instance/ShaderPackPage.h | 2 +- 9 files changed, 11 insertions(+), 11 deletions(-) diff --git a/launcher/InstancePageProvider.h b/launcher/InstancePageProvider.h index 258ed5aa5..d7e985dd2 100644 --- a/launcher/InstancePageProvider.h +++ b/launcher/InstancePageProvider.h @@ -46,7 +46,7 @@ class InstancePageProvider : protected QObject, public BasePageProvider { // values.append(new GameOptionsPage(onesix.get())); values.append(new ScreenshotsPage(FS::PathCombine(onesix->gameRoot(), "screenshots"))); values.append(new InstanceSettingsPage(onesix)); - values.append(new OtherLogsPage("logs", tr("Other logs"), "Other-Logs", inst)); + values.append(new OtherLogsPage("logs", tr("Other Logs"), "Other-Logs", inst)); return values; } diff --git a/launcher/ui/MainWindow.ui b/launcher/ui/MainWindow.ui index 1d29ff628..ff1b4a25a 100644 --- a/launcher/ui/MainWindow.ui +++ b/launcher/ui/MainWindow.ui @@ -239,7 +239,7 @@ - More news... + More News... Open the development blog to read more news about %1. diff --git a/launcher/ui/pages/global/ProxyPage.ui b/launcher/ui/pages/global/ProxyPage.ui index dec8d0a26..436a90ad1 100644 --- a/launcher/ui/pages/global/ProxyPage.ui +++ b/launcher/ui/pages/global/ProxyPage.ui @@ -51,7 +51,7 @@ Uses your system's default proxy settings. - Use S&ystem Settings + Use s&ystem settings proxyGroup diff --git a/launcher/ui/pages/instance/DataPackPage.h b/launcher/ui/pages/instance/DataPackPage.h index 6676c165a..b71ed2965 100644 --- a/launcher/ui/pages/instance/DataPackPage.h +++ b/launcher/ui/pages/instance/DataPackPage.h @@ -28,7 +28,7 @@ class DataPackPage : public ExternalResourcesPage { public: explicit DataPackPage(BaseInstance* instance, std::shared_ptr model, QWidget* parent = nullptr); - QString displayName() const override { return QObject::tr("Data packs"); } + QString displayName() const override { return QObject::tr("Data Packs"); } QIcon icon() const override { return APPLICATION->getThemedIcon("datapacks"); } QString id() const override { return "datapacks"; } QString helpPage() const override { return "Data-packs"; } diff --git a/launcher/ui/pages/instance/ManagedPackPage.cpp b/launcher/ui/pages/instance/ManagedPackPage.cpp index a95762b5d..01bc6c2fd 100644 --- a/launcher/ui/pages/instance/ManagedPackPage.cpp +++ b/launcher/ui/pages/instance/ManagedPackPage.cpp @@ -213,7 +213,7 @@ bool ManagedPackPage::runUpdateTask(InstanceTask* task) void ManagedPackPage::suggestVersion() { - ui->updateButton->setText(tr("Update pack")); + ui->updateButton->setText(tr("Update Pack")); ui->updateButton->setDisabled(false); } diff --git a/launcher/ui/pages/instance/ManagedPackPage.ui b/launcher/ui/pages/instance/ManagedPackPage.ui index 54ff08e94..14b29a249 100644 --- a/launcher/ui/pages/instance/ManagedPackPage.ui +++ b/launcher/ui/pages/instance/ManagedPackPage.ui @@ -34,7 +34,7 @@ - Pack information + Pack Information @@ -42,7 +42,7 @@ - Pack name: + Pack Name: @@ -162,7 +162,7 @@ - Update from file + Update From File diff --git a/launcher/ui/pages/instance/ModFolderPage.h b/launcher/ui/pages/instance/ModFolderPage.h index 8996b1615..9b9665571 100644 --- a/launcher/ui/pages/instance/ModFolderPage.h +++ b/launcher/ui/pages/instance/ModFolderPage.h @@ -82,7 +82,7 @@ class CoreModFolderPage : public ModFolderPage { explicit CoreModFolderPage(BaseInstance* inst, std::shared_ptr mods, QWidget* parent = 0); virtual ~CoreModFolderPage() = default; - virtual QString displayName() const override { return tr("Core mods"); } + virtual QString displayName() const override { return tr("Core Mods"); } virtual QIcon icon() const override { return APPLICATION->getThemedIcon("coremods"); } virtual QString id() const override { return "coremods"; } virtual QString helpPage() const override { return "Core-mods"; } diff --git a/launcher/ui/pages/instance/ResourcePackPage.h b/launcher/ui/pages/instance/ResourcePackPage.h index e39d417c9..40fe10b79 100644 --- a/launcher/ui/pages/instance/ResourcePackPage.h +++ b/launcher/ui/pages/instance/ResourcePackPage.h @@ -50,7 +50,7 @@ class ResourcePackPage : public ExternalResourcesPage { public: explicit ResourcePackPage(MinecraftInstance* instance, std::shared_ptr model, QWidget* parent = 0); - QString displayName() const override { return tr("Resource packs"); } + QString displayName() const override { return tr("Resource Packs"); } QIcon icon() const override { return APPLICATION->getThemedIcon("resourcepacks"); } QString id() const override { return "resourcepacks"; } QString helpPage() const override { return "Resource-packs"; } diff --git a/launcher/ui/pages/instance/ShaderPackPage.h b/launcher/ui/pages/instance/ShaderPackPage.h index f2b141329..128c48ae7 100644 --- a/launcher/ui/pages/instance/ShaderPackPage.h +++ b/launcher/ui/pages/instance/ShaderPackPage.h @@ -47,7 +47,7 @@ class ShaderPackPage : public ExternalResourcesPage { explicit ShaderPackPage(MinecraftInstance* instance, std::shared_ptr model, QWidget* parent = nullptr); ~ShaderPackPage() override = default; - QString displayName() const override { return tr("Shader packs"); } + QString displayName() const override { return tr("Shader Packs"); } QIcon icon() const override { return APPLICATION->getThemedIcon("shaderpacks"); } QString id() const override { return "shaderpacks"; } QString helpPage() const override { return "shader-packs"; } From 2b0d135afd76b31062327620d2aba959ddd343ac Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Thu, 21 Aug 2025 15:01:30 +0100 Subject: [PATCH 409/695] More consistent padding and frames Signed-off-by: TheKodeToad --- launcher/ui/pages/global/JavaPage.ui | 4 +- launcher/ui/pages/instance/LogPage.cpp | 1 - launcher/ui/pages/instance/LogPage.ui | 292 ++++++++-------- launcher/ui/pages/instance/ManagedPackPage.ui | 8 +- launcher/ui/pages/instance/OtherLogsPage.cpp | 1 - launcher/ui/pages/instance/OtherLogsPage.ui | 311 +++++++++--------- .../ui/widgets/MinecraftSettingsWidget.ui | 10 +- 7 files changed, 296 insertions(+), 331 deletions(-) diff --git a/launcher/ui/pages/global/JavaPage.ui b/launcher/ui/pages/global/JavaPage.ui index a40e38868..3ed28cf30 100644 --- a/launcher/ui/pages/global/JavaPage.ui +++ b/launcher/ui/pages/global/JavaPage.ui @@ -24,7 +24,7 @@ 0 - 0 + 6 0 @@ -50,7 +50,7 @@ 0 0 535 - 606 + 612 diff --git a/launcher/ui/pages/instance/LogPage.cpp b/launcher/ui/pages/instance/LogPage.cpp index 9a7ce6039..928368236 100644 --- a/launcher/ui/pages/instance/LogPage.cpp +++ b/launcher/ui/pages/instance/LogPage.cpp @@ -131,7 +131,6 @@ QModelIndex LogFormatProxyModel::find(const QModelIndex& start, const QString& v LogPage::LogPage(InstancePtr instance, QWidget* parent) : QWidget(parent), ui(new Ui::LogPage), m_instance(instance) { ui->setupUi(this); - ui->tabWidget->tabBar()->hide(); m_proxy = new LogFormatProxyModel(this); diff --git a/launcher/ui/pages/instance/LogPage.ui b/launcher/ui/pages/instance/LogPage.ui index 66a432edb..2362e19c0 100644 --- a/launcher/ui/pages/instance/LogPage.ui +++ b/launcher/ui/pages/instance/LogPage.ui @@ -10,170 +10,153 @@ 782 - + 0 0 - - 0 - 0 - - - - 0 + + + + false + + + true + + + + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + false + + + + + + + + + Keep updating + + + true + + + + + + + Wrap lines + + + true + + + + + + + Color lines + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Copy the whole log into the clipboard + + + &Copy + + + + + + + Upload the log to the paste service configured in preferences + + + Upload + + + + + + + Clear the log + + + Clear + + + + + + + + + + 0 + 0 + + + + Find + + + + + + + + 0 + 0 + + + + Scroll all the way to bottom + + + Bottom + + + + + + + Qt::Vertical + + + + + + + Search - - - Tab 1 - - - - - - false - - - true - - - - - - Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse - - - false - - - - - - - - - Keep updating - - - true - - - - - - - Wrap lines - - - true - - - - - - - Color lines - - - true - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Copy the whole log into the clipboard - - - &Copy - - - - - - - Upload the log to the paste service configured in preferences - - - Upload - - - - - - - Clear the log - - - Clear - - - - - - - - - - 0 - 0 - - - - Find - - - - - - - - 0 - 0 - - - - Scroll all the way to bottom - - - Bottom - - - - - - - Qt::Vertical - - - - - - - Search - - - - - @@ -186,7 +169,6 @@
    - tabWidget trackLogCheckbox wrapCheckbox colorCheckbox diff --git a/launcher/ui/pages/instance/ManagedPackPage.ui b/launcher/ui/pages/instance/ManagedPackPage.ui index 14b29a249..62641bc82 100644 --- a/launcher/ui/pages/instance/ManagedPackPage.ui +++ b/launcher/ui/pages/instance/ManagedPackPage.ui @@ -12,16 +12,16 @@ - 9 + 0 - 9 + 0 - 9 + 6 - 9 + 0 diff --git a/launcher/ui/pages/instance/OtherLogsPage.cpp b/launcher/ui/pages/instance/OtherLogsPage.cpp index 6f98db4a8..69e152475 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.cpp +++ b/launcher/ui/pages/instance/OtherLogsPage.cpp @@ -61,7 +61,6 @@ OtherLogsPage::OtherLogsPage(QString id, QString displayName, QString helpPage, , m_logSearchPaths(instance ? instance->getLogFileSearchPaths() : QStringList{ "logs" }) { ui->setupUi(this); - ui->tabWidget->tabBar()->hide(); m_proxy = new LogFormatProxyModel(this); if (m_instance) { diff --git a/launcher/ui/pages/instance/OtherLogsPage.ui b/launcher/ui/pages/instance/OtherLogsPage.ui index be08e0fdb..77076d4ab 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.ui +++ b/launcher/ui/pages/instance/OtherLogsPage.ui @@ -10,7 +10,7 @@ 538 - + 0 @@ -18,203 +18,189 @@ 0 - 0 + 6 0 - - - - 0 + + + + + 0 + 0 + + + + &Find + + + + + + + Qt::Vertical + + + + + + + + 0 + 0 + + + + Scroll all the way to bottom + + + &Bottom + + + + + + + false + + + false + + + true - - - Tab 1 - - - - + + + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + false + + + + + + + + + - + 0 0 + + + + + + Delete the selected log + - &Find + &Delete Selected - - - - Qt::Vertical + + + + Delete all the logs + + + Delete &All - - - - - 0 - 0 - + + + + + + + + Keep updating - - Scroll all the way to bottom + + true + + + + - &Bottom + Wrap lines + + + true - - - - false - - - false + + + + Color lines - + true - - + + + + + + Qt::Horizontal - - Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + 40 + 20 + - - false + + + + + + Copy the whole log into the clipboard + + + &Copy - - - - - - - - - 0 - 0 - - - - - - - - Delete the selected log - - - &Delete Selected - - - - - - - Delete all the logs - - - Delete &All - - - - - - - - - - - Keep updating - - - true - - - - - - - Wrap lines - - - true - - - - - - - Color lines - - - true - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Copy the whole log into the clipboard - - - &Copy - - - - - - - Upload the log to the paste service configured in preferences - - - &Upload - - - - - - - Reload the contents of the log from the disk - - - &Reload - - - - - - + + + + Upload the log to the paste service configured in preferences + + + &Upload + + - - - - Search + + + + Reload the contents of the log from the disk + + + &Reload - + + + + + + + Search + @@ -227,7 +213,6 @@ - tabWidget selectLogBox btnReload btnCopy diff --git a/launcher/ui/widgets/MinecraftSettingsWidget.ui b/launcher/ui/widgets/MinecraftSettingsWidget.ui index 4a35addc0..f8ee2f854 100644 --- a/launcher/ui/widgets/MinecraftSettingsWidget.ui +++ b/launcher/ui/widgets/MinecraftSettingsWidget.ui @@ -18,7 +18,7 @@ 0 - 0 + 6 0 @@ -553,8 +553,8 @@ It is most likely you will need to change the path - please refer to the mod's w 0 0 - 624 - 487 + 100 + 30 @@ -577,8 +577,8 @@ It is most likely you will need to change the path - please refer to the mod's w 0 0 - 624 - 487 + 261 + 434 From 9ecc11787f8004673b51214dbd3bff0e1ad74e71 Mon Sep 17 00:00:00 2001 From: Cinnamon Date: Sun, 31 Aug 2025 10:14:40 -0500 Subject: [PATCH 410/695] Update TranslationsModel.cpp Signed-off-by: Cinnamon --- launcher/translations/TranslationsModel.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/translations/TranslationsModel.cpp b/launcher/translations/TranslationsModel.cpp index 75fc93b3b..860e57e54 100644 --- a/launcher/translations/TranslationsModel.cpp +++ b/launcher/translations/TranslationsModel.cpp @@ -73,7 +73,7 @@ struct Language { if (key == "ja_KANJI") { result = locale.nativeLanguageName() + u8" (漢字)"; } else if (key == "es_UY") { - result = u8"español de Latinoamérica"; + result = u8"Español de Latinoamérica"; } else if (key == "en_NZ") { result = u8"New Zealand English"; // No idea why qt translates this to just english and not to New Zealand English } else if (key == "en@pirate") { From e5653b36ccf3af0e835f2573dccce41d8d2aee6f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 04:47:44 +0000 Subject: [PATCH 411/695] chore(deps): update actions/stale action to v10 --- .github/workflows/stale.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 106a7844f..b8f8137d7 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -17,7 +17,7 @@ jobs: pull-requests: write steps: - - uses: actions/stale@v9 + - uses: actions/stale@v10 with: days-before-stale: 60 days-before-close: -1 # Don't close anything From 7e0b9511b4e854b874824fbb172274e0ccf7e5a9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 6 Sep 2025 12:54:37 +0000 Subject: [PATCH 412/695] chore(deps): update cachix/install-nix-action digest to 56a7bb7 --- .github/workflows/update-flake.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/update-flake.yml b/.github/workflows/update-flake.yml index 5648d5477..9f2139f7b 100644 --- a/.github/workflows/update-flake.yml +++ b/.github/workflows/update-flake.yml @@ -17,7 +17,7 @@ jobs: steps: - uses: actions/checkout@v5 - - uses: cachix/install-nix-action@c134e4c9e34bac6cab09cf239815f9339aaaf84e # v31 + - uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31 - uses: DeterminateSystems/update-flake-lock@v27 with: From d9e5afcaa0166c50a8412e2d125278565d69c4f0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 7 Sep 2025 00:27:45 +0000 Subject: [PATCH 413/695] chore(nix): update lockfile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'nixpkgs': 'github:NixOS/nixpkgs/fbcf476f790d8a217c3eab4e12033dc4a0f6d23c?narHash=sha256-wNO3%2BKs2jZJ4nTHMuks%2BcxAiVBGNuEBXsT29Bz6HASo%3D' (2025-08-14) → 'github:NixOS/nixpkgs/d0fc30899600b9b3466ddb260fd83deb486c32f1?narHash=sha256-rw/PHa1cqiePdBxhF66V7R%2BWAP8WekQ0mCDG4CFqT8Y%3D' (2025-09-02) --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 15aef086a..f6a467306 100644 --- a/flake.lock +++ b/flake.lock @@ -18,11 +18,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1755186698, - "narHash": "sha256-wNO3+Ks2jZJ4nTHMuks+cxAiVBGNuEBXsT29Bz6HASo=", + "lastModified": 1756787288, + "narHash": "sha256-rw/PHa1cqiePdBxhF66V7R+WAP8WekQ0mCDG4CFqT8Y=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "fbcf476f790d8a217c3eab4e12033dc4a0f6d23c", + "rev": "d0fc30899600b9b3466ddb260fd83deb486c32f1", "type": "github" }, "original": { From 170071746f52fff6e8e18183c6fc587ea190e847 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 10 Sep 2025 17:48:01 +0000 Subject: [PATCH 414/695] chore(deps): update hendrikmuhs/ccache-action action to v1.2.19 --- .github/actions/setup-dependencies/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/setup-dependencies/action.yml b/.github/actions/setup-dependencies/action.yml index 6c718f9e3..82395eada 100644 --- a/.github/actions/setup-dependencies/action.yml +++ b/.github/actions/setup-dependencies/action.yml @@ -56,7 +56,7 @@ runs: # TODO(@getchoo): Get this working on MSYS2! - name: Setup ccache if: ${{ (runner.os != 'Windows' || inputs.msystem == '') && inputs.build-type == 'Debug' }} - uses: hendrikmuhs/ccache-action@v1.2.18 + uses: hendrikmuhs/ccache-action@v1.2.19 with: variant: sccache create-symlink: ${{ runner.os != 'Windows' }} From 7f4b157087ca07bc84cd50846ed0e35aa64952ba Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 13 Sep 2025 06:24:19 +0000 Subject: [PATCH 415/695] chore(deps): update cachix/install-nix-action digest to 7be5dee --- .github/workflows/update-flake.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/update-flake.yml b/.github/workflows/update-flake.yml index 9f2139f7b..f1821646a 100644 --- a/.github/workflows/update-flake.yml +++ b/.github/workflows/update-flake.yml @@ -17,7 +17,7 @@ jobs: steps: - uses: actions/checkout@v5 - - uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31 + - uses: cachix/install-nix-action@7be5dee1421f63d07e71ce6e0a9f8a4b07c2a487 # v31 - uses: DeterminateSystems/update-flake-lock@v27 with: From 89d7ffdc685aea54092bb6c10d3f32e2e93c3cac Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 14 Sep 2025 00:27:15 +0000 Subject: [PATCH 416/695] chore(nix): update lockfile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'nixpkgs': 'github:NixOS/nixpkgs/d0fc30899600b9b3466ddb260fd83deb486c32f1?narHash=sha256-rw/PHa1cqiePdBxhF66V7R%2BWAP8WekQ0mCDG4CFqT8Y%3D' (2025-09-02) → 'github:NixOS/nixpkgs/c23193b943c6c689d70ee98ce3128239ed9e32d1?narHash=sha256-hLEO2TPj55KcUFUU1vgtHE9UEIOjRcH/4QbmfHNF820%3D' (2025-09-13) --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index f6a467306..70625e923 100644 --- a/flake.lock +++ b/flake.lock @@ -18,11 +18,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1756787288, - "narHash": "sha256-rw/PHa1cqiePdBxhF66V7R+WAP8WekQ0mCDG4CFqT8Y=", + "lastModified": 1757745802, + "narHash": "sha256-hLEO2TPj55KcUFUU1vgtHE9UEIOjRcH/4QbmfHNF820=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "d0fc30899600b9b3466ddb260fd83deb486c32f1", + "rev": "c23193b943c6c689d70ee98ce3128239ed9e32d1", "type": "github" }, "original": { From ab3bfb0f74bacc8f9e27f574b928624633df4a51 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Mon, 24 Mar 2025 23:06:53 +0200 Subject: [PATCH 417/695] make universal resource type Signed-off-by: Trial97 --- launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index c66fb5655..514b33574 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -38,6 +38,7 @@ #include "BuildConfig.h" #include "Json.h" +#include "modplatform/ModIndex.h" #include "modplatform/modrinth/ModrinthAPI.h" #include "net/NetJob.h" #include "ui/widgets/ProjectItem.h" From 29cff14fd6166e2b885955e72126274f3e48c76b Mon Sep 17 00:00:00 2001 From: Trial97 Date: Sun, 30 Mar 2025 00:32:59 +0200 Subject: [PATCH 418/695] removed some duplicate code Signed-off-by: Trial97 --- launcher/CMakeLists.txt | 9 +- .../mod/tasks/GetModDependenciesTask.cpp | 85 ++--- .../mod/tasks/GetModDependenciesTask.h | 22 +- launcher/modplatform/ModIndex.h | 17 + ...NetworkResourceAPI.cpp => ResourceAPI.cpp} | 204 +++++++++--- launcher/modplatform/ResourceAPI.h | 123 +++----- launcher/modplatform/flame/FlameAPI.h | 25 +- launcher/modplatform/flame/FlameModIndex.cpp | 30 +- launcher/modplatform/flame/FlameModIndex.h | 3 +- launcher/modplatform/flame/FlamePackIndex.cpp | 150 --------- launcher/modplatform/flame/FlamePackIndex.h | 55 ---- .../modplatform/helpers/NetworkResourceAPI.h | 25 -- launcher/modplatform/modrinth/ModrinthAPI.h | 14 +- .../modrinth/ModrinthInstanceCreationTask.cpp | 9 +- .../modrinth/ModrinthInstanceCreationTask.h | 22 +- .../modrinth/ModrinthPackIndex.cpp | 44 --- .../modplatform/modrinth/ModrinthPackIndex.h | 2 - .../modrinth/ModrinthPackManifest.cpp | 196 ------------ .../modrinth/ModrinthPackManifest.h | 129 -------- .../ui/pages/instance/ManagedPackPage.cpp | 99 ++---- launcher/ui/pages/instance/ManagedPackPage.h | 11 +- .../ui/pages/modplatform/DataPackModel.cpp | 4 +- launcher/ui/pages/modplatform/DataPackModel.h | 11 +- launcher/ui/pages/modplatform/ModModel.cpp | 6 +- launcher/ui/pages/modplatform/ModModel.h | 15 +- .../ui/pages/modplatform/ResourceModel.cpp | 197 ++++-------- launcher/ui/pages/modplatform/ResourceModel.h | 32 +- .../pages/modplatform/ResourcePackModel.cpp | 9 +- .../ui/pages/modplatform/ResourcePackModel.h | 11 +- .../ui/pages/modplatform/ShaderPackModel.cpp | 6 +- .../ui/pages/modplatform/ShaderPackModel.h | 11 +- .../ui/pages/modplatform/TexturePackModel.cpp | 5 +- .../ui/pages/modplatform/TexturePackModel.h | 2 +- .../ui/pages/modplatform/flame/FlameModel.cpp | 163 ++++------ .../ui/pages/modplatform/flame/FlameModel.h | 25 +- .../ui/pages/modplatform/flame/FlamePage.cpp | 232 +++++++------- .../ui/pages/modplatform/flame/FlamePage.h | 11 +- .../modplatform/flame/FlameResourceModels.cpp | 157 +--------- .../modplatform/flame/FlameResourceModels.h | 86 ----- .../modplatform/flame/FlameResourcePages.cpp | 12 +- .../modplatform/modrinth/ModrinthModel.cpp | 164 ++++------ .../modplatform/modrinth/ModrinthModel.h | 30 +- .../modplatform/modrinth/ModrinthPage.cpp | 296 ++++++++---------- .../pages/modplatform/modrinth/ModrinthPage.h | 21 +- .../modrinth/ModrinthResourceModels.cpp | 144 --------- .../modrinth/ModrinthResourceModels.h | 121 ------- .../modrinth/ModrinthResourcePages.cpp | 16 +- .../ui/widgets/MinecraftSettingsWidget.cpp | 1 + launcher/ui/widgets/ModFilterWidget.h | 8 + tests/CMakeLists.txt | 3 - tests/DummyResourceAPI.h | 46 --- tests/ResourceModel_test.cpp | 95 ------ 52 files changed, 912 insertions(+), 2302 deletions(-) rename launcher/modplatform/{helpers/NetworkResourceAPI.cpp => ResourceAPI.cpp} (51%) delete mode 100644 launcher/modplatform/flame/FlamePackIndex.cpp delete mode 100644 launcher/modplatform/flame/FlamePackIndex.h delete mode 100644 launcher/modplatform/helpers/NetworkResourceAPI.h delete mode 100644 launcher/modplatform/modrinth/ModrinthPackManifest.cpp delete mode 100644 launcher/modplatform/modrinth/ModrinthPackManifest.h delete mode 100644 launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp delete mode 100644 launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h delete mode 100644 tests/DummyResourceAPI.h delete mode 100644 tests/ResourceModel_test.cpp diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 2d1c62269..c0b4894d7 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -488,6 +488,7 @@ set(API_SOURCES modplatform/ResourceType.cpp modplatform/ResourceAPI.h + modplatform/ResourceAPI.cpp modplatform/EnsureMetadataTask.h modplatform/EnsureMetadataTask.cpp @@ -498,8 +499,6 @@ set(API_SOURCES modplatform/flame/FlameAPI.cpp modplatform/modrinth/ModrinthAPI.h modplatform/modrinth/ModrinthAPI.cpp - modplatform/helpers/NetworkResourceAPI.h - modplatform/helpers/NetworkResourceAPI.cpp modplatform/helpers/HashUtils.h modplatform/helpers/HashUtils.cpp modplatform/helpers/OverrideUtils.h @@ -527,8 +526,6 @@ set(FTB_SOURCES set(FLAME_SOURCES # Flame - modplatform/flame/FlamePackIndex.cpp - modplatform/flame/FlamePackIndex.h modplatform/flame/FlameModIndex.cpp modplatform/flame/FlameModIndex.h modplatform/flame/PackManifest.h @@ -546,8 +543,6 @@ set(FLAME_SOURCES set(MODRINTH_SOURCES modplatform/modrinth/ModrinthPackIndex.cpp modplatform/modrinth/ModrinthPackIndex.h - modplatform/modrinth/ModrinthPackManifest.cpp - modplatform/modrinth/ModrinthPackManifest.h modplatform/modrinth/ModrinthCheckUpdate.cpp modplatform/modrinth/ModrinthCheckUpdate.h modplatform/modrinth/ModrinthInstanceCreationTask.cpp @@ -1035,8 +1030,6 @@ SET(LAUNCHER_SOURCES ui/pages/modplatform/OptionalModDialog.cpp ui/pages/modplatform/OptionalModDialog.h - ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp - ui/pages/modplatform/modrinth/ModrinthResourceModels.h ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp ui/pages/modplatform/modrinth/ModrinthResourcePages.h diff --git a/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp b/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp index 21e7c5a2a..75815cb44 100644 --- a/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp +++ b/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp @@ -27,12 +27,8 @@ #include "minecraft/mod/MetadataHandler.h" #include "modplatform/ModIndex.h" #include "modplatform/ResourceAPI.h" -#include "modplatform/flame/FlameAPI.h" -#include "modplatform/modrinth/ModrinthAPI.h" #include "tasks/SequentialTask.h" #include "ui/pages/modplatform/ModModel.h" -#include "ui/pages/modplatform/flame/FlameResourceModels.h" -#include "ui/pages/modplatform/modrinth/ModrinthResourceModels.h" static Version mcVersion(BaseInstance* inst) { @@ -55,14 +51,7 @@ static bool checkDependencies(std::shared_ptr> selected) - : SequentialTask(tr("Get dependencies")) - , m_selected(selected) - , m_flame_provider{ ModPlatform::ResourceProvider::FLAME, std::make_shared(*instance), - std::make_shared() } - , m_modrinth_provider{ ModPlatform::ResourceProvider::MODRINTH, std::make_shared(*instance), - std::make_shared() } - , m_version(mcVersion(instance)) - , m_loaderType(mcLoaders(instance)) + : SequentialTask(tr("Get dependencies")), m_selected(selected), m_version(mcVersion(instance)), m_loaderType(mcLoaders(instance)) { for (auto mod : folder->allMods()) { m_mods_file_names << mod->fileinfo().fileName(); @@ -144,9 +133,9 @@ QList GetModDependenciesTask::getDependenciesForVersion Task::Ptr GetModDependenciesTask::getProjectInfoTask(std::shared_ptr pDep) { - auto provider = pDep->pack->provider == m_flame_provider.name ? m_flame_provider : m_modrinth_provider; + auto provider = pDep->pack->provider; auto responseInfo = std::make_shared(); - auto info = provider.api->getProject(pDep->pack->addonId.toString(), responseInfo); + auto info = getAPI(provider)->getProject(pDep->pack->addonId.toString(), responseInfo); connect(info.get(), &NetJob::succeeded, [this, responseInfo, provider, pDep] { QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(*responseInfo, &parse_error); @@ -158,9 +147,10 @@ Task::Ptr GetModDependenciesTask::getProjectInfoTask(std::shared_ptrloadIndexedPack(*pDep->pack, obj); + auto obj = provider == ModPlatform::ResourceProvider::FLAME ? Json::requireObject(Json::requireObject(doc), "data") + : Json::requireObject(doc); + + getAPI(provider)->loadIndexedPack(*pDep->pack, obj); } catch (const JSONValidationError& e) { removePack(pDep->pack->addonId); qDebug() << doc; @@ -181,7 +171,8 @@ Task::Ptr GetModDependenciesTask::prepareDependencyTask(const ModPlatform::Depen pDep->pack->provider = providerName; m_pack_dependencies.append(pDep); - auto provider = providerName == m_flame_provider.name ? m_flame_provider : m_modrinth_provider; + + auto provider = providerName; auto tasks = makeShared( QString("DependencyInfo: %1").arg(dep.addonId.toString().isEmpty() ? dep.version : dep.addonId.toString())); @@ -191,46 +182,30 @@ Task::Ptr GetModDependenciesTask::prepareDependencyTask(const ModPlatform::Depen } ResourceAPI::DependencySearchArgs args = { dep, m_version, m_loaderType }; - ResourceAPI::DependencySearchCallbacks callbacks; + ResourceAPI::Callback callbacks; callbacks.on_fail = [](QString reason, int) { qCritical() << tr("A network error occurred. Could not load project dependencies:%1").arg(reason); }; - callbacks.on_succeed = [dep, provider, pDep, level, this](auto& doc, [[maybe_unused]] auto& pack) { - try { - QJsonArray arr; - if (dep.version.length() != 0 && doc.isObject()) { - arr.append(doc.object()); - } else { - arr = doc.isObject() ? Json::ensureArray(doc.object(), "data") : doc.array(); - } - pDep->version = provider.mod->loadDependencyVersions(dep, arr); - if (!pDep->version.addonId.isValid()) { - if (m_loaderType & ModPlatform::Quilt) { // falback for quilt - auto overide = ModPlatform::getOverrideDeps(); - auto over = std::find_if(overide.cbegin(), overide.cend(), [dep, provider](const auto& o) { - return o.provider == provider.name && dep.addonId == o.quilt; - }); - if (over != overide.cend()) { - removePack(dep.addonId); - addTask(prepareDependencyTask({ over->fabric, dep.type }, provider.name, level)); - return; - } + callbacks.on_succeed = [dep, provider, pDep, level, this](auto& pack) { + pDep->version = pack; + if (!pDep->version.addonId.isValid()) { + if (m_loaderType & ModPlatform::Quilt) { // falback for quilt + auto overide = ModPlatform::getOverrideDeps(); + auto over = std::find_if(overide.cbegin(), overide.cend(), + [dep, provider](auto o) { return o.provider == provider && dep.addonId == o.quilt; }); + if (over != overide.cend()) { + removePack(dep.addonId); + addTask(prepareDependencyTask({ over->fabric, dep.type }, provider, level)); + return; } - removePack(dep.addonId); - qWarning() << "Error while reading mod version empty "; - qDebug() << doc; - return; } - pDep->version.is_currently_selected = true; - pDep->pack->versions = { pDep->version }; - pDep->pack->versionsLoaded = true; - - } catch (const JSONValidationError& e) { removePack(dep.addonId); - qDebug() << doc; - qWarning() << "Error while reading mod version: " << e.cause(); return; } + pDep->version.is_currently_selected = true; + pDep->pack->versions = { pDep->version }; + pDep->pack->versionsLoaded = true; + if (level == 0) { removePack(dep.addonId); qWarning() << "Dependency cycle exceeded"; @@ -238,10 +213,10 @@ Task::Ptr GetModDependenciesTask::prepareDependencyTask(const ModPlatform::Depen } if (dep.addonId.toString().isEmpty() && !pDep->version.addonId.toString().isEmpty()) { pDep->pack->addonId = pDep->version.addonId; - auto dep_ = getOverride({ pDep->version.addonId, pDep->dependency.type }, provider.name); + auto dep_ = getOverride({ pDep->version.addonId, pDep->dependency.type }, provider); if (dep_.addonId != pDep->version.addonId) { removePack(pDep->version.addonId); - addTask(prepareDependencyTask(dep_, provider.name, level)); + addTask(prepareDependencyTask(dep_, provider, level)); } else { addTask(getProjectInfoTask(pDep)); } @@ -250,12 +225,12 @@ Task::Ptr GetModDependenciesTask::prepareDependencyTask(const ModPlatform::Depen removePack(pDep->version.addonId); return; } - for (auto dep_ : getDependenciesForVersion(pDep->version, provider.name)) { - addTask(prepareDependencyTask(dep_, provider.name, level - 1)); + for (auto dep_ : getDependenciesForVersion(pDep->version, provider)) { + addTask(prepareDependencyTask(dep_, provider, level - 1)); } }; - auto version = provider.api->getDependencyVersion(std::move(args), std::move(callbacks)); + auto version = getAPI(provider)->getDependencyVersion(std::move(args), std::move(callbacks)); tasks->addTask(version); return tasks; } diff --git a/launcher/minecraft/mod/tasks/GetModDependenciesTask.h b/launcher/minecraft/mod/tasks/GetModDependenciesTask.h index a02ffb4d5..3530d6cc0 100644 --- a/launcher/minecraft/mod/tasks/GetModDependenciesTask.h +++ b/launcher/minecraft/mod/tasks/GetModDependenciesTask.h @@ -28,6 +28,8 @@ #include "minecraft/mod/ModFolderModel.h" #include "modplatform/ModIndex.h" #include "modplatform/ResourceAPI.h" +#include "modplatform/flame/FlameAPI.h" +#include "modplatform/modrinth/ModrinthAPI.h" #include "tasks/SequentialTask.h" #include "tasks/Task.h" #include "ui/pages/modplatform/ModModel.h" @@ -54,17 +56,20 @@ class GetModDependenciesTask : public SequentialTask { QStringList required_by; }; - struct Provider { - ModPlatform::ResourceProvider name; - std::shared_ptr mod; - std::shared_ptr api; - }; - explicit GetModDependenciesTask(BaseInstance* instance, ModFolderModel* folder, QList> selected); auto getDependecies() const -> QList> { return m_pack_dependencies; } QHash getExtraInfo(); + private: + inline ResourceAPI* getAPI(ModPlatform::ResourceProvider provider) + { + if (provider == ModPlatform::ResourceProvider::FLAME) + return &m_flameAPI; + else + return &m_modrinthAPI; + } + protected slots: Task::Ptr prepareDependencyTask(const ModPlatform::Dependency&, ModPlatform::ResourceProvider, int); QList getDependenciesForVersion(const ModPlatform::IndexedVersion&, @@ -82,9 +87,10 @@ class GetModDependenciesTask : public SequentialTask { QList> m_mods; QList> m_selected; QStringList m_mods_file_names; - Provider m_flame_provider; - Provider m_modrinth_provider; Version m_version; ModPlatform::ModLoaderTypes m_loaderType; + + ModrinthAPI m_modrinthAPI; + FlameAPI m_flameAPI; }; diff --git a/launcher/modplatform/ModIndex.h b/launcher/modplatform/ModIndex.h index 07a256899..6cff8c622 100644 --- a/launcher/modplatform/ModIndex.h +++ b/launcher/modplatform/ModIndex.h @@ -128,6 +128,23 @@ struct IndexedVersion { // For internal use, not provided by APIs bool is_currently_selected = false; + + QString getVersionDisplayString() const + { + auto release_type = version_type.isValid() ? QString(" [%1]").arg(version_type.toString()) : ""; + auto versionStr = !version.contains(version_number) ? version_number : ""; + QString gameVersion = ""; + for (auto v : mcVersion) { + if (version.contains(v)) { + gameVersion = ""; + break; + } + if (gameVersion.isEmpty()) { + gameVersion = QObject::tr(" for %1").arg(v); + } + } + return QString("%1%2 — %3%4").arg(version, gameVersion, versionStr, release_type); + } }; struct ExtraPackData { diff --git a/launcher/modplatform/helpers/NetworkResourceAPI.cpp b/launcher/modplatform/ResourceAPI.cpp similarity index 51% rename from launcher/modplatform/helpers/NetworkResourceAPI.cpp rename to launcher/modplatform/ResourceAPI.cpp index d0e1bb912..448efbc24 100644 --- a/launcher/modplatform/helpers/NetworkResourceAPI.cpp +++ b/launcher/modplatform/ResourceAPI.cpp @@ -1,18 +1,14 @@ -// SPDX-FileCopyrightText: 2023 flowln -// -// SPDX-License-Identifier: GPL-3.0-only - -#include "NetworkResourceAPI.h" -#include +#include "modplatform/ResourceAPI.h" #include "Application.h" +#include "Json.h" #include "net/NetJob.h" #include "modplatform/ModIndex.h" #include "net/ApiDownload.h" -Task::Ptr NetworkResourceAPI::searchProjects(SearchArgs&& args, SearchCallbacks&& callbacks) const +Task::Ptr ResourceAPI::searchProjects(SearchArgs&& args, Callback>&& callbacks) const { auto search_url_optional = getSearchURL(args); if (!search_url_optional.has_value()) { @@ -40,7 +36,23 @@ Task::Ptr NetworkResourceAPI::searchProjects(SearchArgs&& args, SearchCallbacks& return; } - callbacks.on_succeed(doc); + QList newList; + auto packs = documentToArray(doc); + + for (auto packRaw : packs) { + auto packObj = packRaw.toObject(); + + ModPlatform::IndexedPack::Ptr pack = std::make_shared(); + try { + loadIndexedPack(*pack, packObj); + newList << pack; + } catch (const JSONValidationError& e) { + qWarning() << "Error while loading resource from " << debugName() << ": " << e.cause(); + continue; + } + } + + callbacks.on_succeed(newList); }); // Capture a weak_ptr instead of a shared_ptr to avoid circular dependency issues. @@ -60,29 +72,7 @@ Task::Ptr NetworkResourceAPI::searchProjects(SearchArgs&& args, SearchCallbacks& return netJob; } -Task::Ptr NetworkResourceAPI::getProjectInfo(ProjectInfoArgs&& args, ProjectInfoCallbacks&& callbacks) const -{ - auto response = std::make_shared(); - auto job = getProject(args.pack.addonId.toString(), response); - - QObject::connect(job.get(), &NetJob::succeeded, [response, callbacks, args] { - QJsonParseError parse_error{}; - QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); - if (parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response for mod info at " << parse_error.offset - << " reason: " << parse_error.errorString(); - qWarning() << *response; - return; - } - - callbacks.on_succeed(doc, args.pack); - }); - QObject::connect(job.get(), &NetJob::failed, [callbacks](QString reason) { callbacks.on_fail(reason); }); - QObject::connect(job.get(), &NetJob::aborted, [callbacks] { callbacks.on_abort(); }); - return job; -} - -Task::Ptr NetworkResourceAPI::getProjectVersions(VersionSearchArgs&& args, VersionSearchCallbacks&& callbacks) const +Task::Ptr ResourceAPI::getProjectVersions(VersionSearchArgs&& args, Callback>&& callbacks) const { auto versions_url_optional = getVersionsURL(args); if (!versions_url_optional.has_value()) @@ -95,7 +85,7 @@ Task::Ptr NetworkResourceAPI::getProjectVersions(VersionSearchArgs&& args, Versi netJob->addNetAction(Net::ApiDownload::makeByteArray(versions_url, response)); - QObject::connect(netJob.get(), &NetJob::succeeded, [response, callbacks, args] { + QObject::connect(netJob.get(), &NetJob::succeeded, [this, response, callbacks, args] { QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { @@ -105,7 +95,32 @@ Task::Ptr NetworkResourceAPI::getProjectVersions(VersionSearchArgs&& args, Versi return; } - callbacks.on_succeed(doc, args.pack); + QVector unsortedVersions; + try { + auto arr = doc.isObject() ? Json::ensureArray(doc.object(), "data") : doc.array(); + + for (auto versionIter : arr) { + auto obj = versionIter.toObject(); + + auto file = loadIndexedPackVersion(obj, args.resourceType); + if (!file.addonId.isValid()) + file.addonId = args.pack.addonId; + + if (file.fileId.isValid() && !file.downloadUrl.isEmpty()) // Heuristic to check if the returned value is valid + unsortedVersions.append(file); + } + + auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool { + // dates are in RFC 3339 format + return a.date > b.date; + }; + std::sort(unsortedVersions.begin(), unsortedVersions.end(), orderSortPredicate); + } catch (const JSONValidationError& e) { + qDebug() << doc; + qWarning() << "Error while reading " << debugName() << " resource version: " << e.cause(); + } + + callbacks.on_succeed(unsortedVersions); }); // Capture a weak_ptr instead of a shared_ptr to avoid circular dependency issues. @@ -120,26 +135,58 @@ Task::Ptr NetworkResourceAPI::getProjectVersions(VersionSearchArgs&& args, Versi } callbacks.on_fail(reason, network_error_code); }); + QObject::connect(netJob.get(), &NetJob::aborted, [callbacks] { callbacks.on_abort(); }); return netJob; } -Task::Ptr NetworkResourceAPI::getProject(QString addonId, std::shared_ptr response) const +Task::Ptr ResourceAPI::getProjectInfo(ProjectInfoArgs&& args, Callback&& callbacks) const { - auto project_url_optional = getInfoURL(addonId); - if (!project_url_optional.has_value()) - return nullptr; - - auto project_url = project_url_optional.value(); - - auto netJob = makeShared(QString("%1::GetProject").arg(addonId), APPLICATION->network()); - - netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(project_url), response)); + auto response = std::make_shared(); + auto job = getProject(args.pack.addonId.toString(), response); - return netJob; + QObject::connect(job.get(), &NetJob::succeeded, [this, response, callbacks, args] { + auto pack = args.pack; + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response for mod info at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *response; + return; + } + try { + auto obj = Json::requireObject(doc); + if (obj.contains("data")) + obj = Json::requireObject(obj, "data"); + loadIndexedPack(pack, obj); + loadExtraPackInfo(pack, obj); + } catch (const JSONValidationError& e) { + qDebug() << doc; + qWarning() << "Error while reading " << debugName() << " resource info: " << e.cause(); + } + callbacks.on_succeed(pack); + }); + // Capture a weak_ptr instead of a shared_ptr to avoid circular dependency issues. + // This prevents the lambda from extending the lifetime of the shared resource, + // as it only temporarily locks the resource when needed. + auto weak = job.toWeakRef(); + QObject::connect(job.get(), &NetJob::failed, [weak, callbacks](const QString& reason) { + int network_error_code = -1; + if (auto job = weak.lock()) { + if (auto netJob = qSharedPointerDynamicCast(job)) { + if (auto* failed_action = netJob->getFailedActions().at(0); failed_action) { + network_error_code = failed_action->replyStatusCode(); + } + } + } + callbacks.on_fail(reason, network_error_code); + }); + QObject::connect(job.get(), &NetJob::aborted, [callbacks] { callbacks.on_abort(); }); + return job; } -Task::Ptr NetworkResourceAPI::getDependencyVersion(DependencySearchArgs&& args, DependencySearchCallbacks&& callbacks) const +Task::Ptr ResourceAPI::getDependencyVersion(DependencySearchArgs&& args, Callback&& callbacks) const { auto versions_url_optional = getDependencyURL(args); if (!versions_url_optional.has_value()) @@ -152,7 +199,7 @@ Task::Ptr NetworkResourceAPI::getDependencyVersion(DependencySearchArgs&& args, netJob->addNetAction(Net::ApiDownload::makeByteArray(versions_url, response)); - QObject::connect(netJob.get(), &NetJob::succeeded, [response, callbacks, args] { + QObject::connect(netJob.get(), &NetJob::succeeded, [this, response, callbacks, args] { QJsonParseError parse_error{}; QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); if (parse_error.error != QJsonParseError::NoError) { @@ -162,7 +209,33 @@ Task::Ptr NetworkResourceAPI::getDependencyVersion(DependencySearchArgs&& args, return; } - callbacks.on_succeed(doc, args.dependency); + QJsonArray arr; + if (args.dependency.version.length() != 0 && doc.isObject()) { + arr.append(doc.object()); + } else { + arr = doc.isObject() ? Json::ensureArray(doc.object(), "data") : doc.array(); + } + + QVector versions; + for (auto versionIter : arr) { + auto obj = versionIter.toObject(); + + auto file = loadIndexedPackVersion(obj, ModPlatform::ResourceType::Mod); + if (!file.addonId.isValid()) + file.addonId = args.dependency.addonId; + + if (file.fileId.isValid() && + (!file.loaders || args.loader & file.loaders)) // Heuristic to check if the returned value is valid + versions.append(file); + } + + auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool { + // dates are in RFC 3339 format + return a.date > b.date; + }; + std::sort(versions.begin(), versions.end(), orderSortPredicate); + auto bestMatch = versions.size() != 0 ? versions.front() : ModPlatform::IndexedVersion(); + callbacks.on_succeed(bestMatch); }); // Capture a weak_ptr instead of a shared_ptr to avoid circular dependency issues. @@ -179,3 +252,40 @@ Task::Ptr NetworkResourceAPI::getDependencyVersion(DependencySearchArgs&& args, }); return netJob; } + +QString ResourceAPI::getGameVersionsString(std::list mcVersions) const +{ + QString s; + for (auto& ver : mcVersions) { + s += QString("\"%1\",").arg(mapMCVersionToModrinth(ver)); + } + s.remove(s.length() - 1, 1); // remove last comma + return s; +} + +QString ResourceAPI::mapMCVersionToModrinth(Version v) const +{ + static const QString preString = " Pre-Release "; + auto verStr = v.toString(); + + if (verStr.contains(preString)) { + verStr.replace(preString, "-pre"); + } + verStr.replace(" ", "-"); + return verStr; +} + +Task::Ptr ResourceAPI::getProject(QString addonId, std::shared_ptr response) const +{ + auto project_url_optional = getInfoURL(addonId); + if (!project_url_optional.has_value()) + return nullptr; + + auto project_url = project_url_optional.value(); + + auto netJob = makeShared(QString("%1::GetProject").arg(addonId), APPLICATION->network()); + + netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(project_url), response)); + + return netJob; +} diff --git a/launcher/modplatform/ResourceAPI.h b/launcher/modplatform/ResourceAPI.h index 4d40432ee..211a6e477 100644 --- a/launcher/modplatform/ResourceAPI.h +++ b/launcher/modplatform/ResourceAPI.h @@ -4,7 +4,7 @@ /* * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu - * Copyright (c) 2023 Trial97 + * Copyright (c) 2023-2025 Trial97 * * 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 @@ -67,6 +67,13 @@ class ResourceAPI { QString readable_name; }; + template + struct Callback { + std::function on_succeed; + std::function on_fail; + std::function on_abort; + }; + struct SearchArgs { ModPlatform::ResourceType type{}; int offset = 0; @@ -79,31 +86,18 @@ class ResourceAPI { std::optional categoryIds; bool openSource; }; - struct SearchCallbacks { - std::function on_succeed; - std::function on_fail; - std::function on_abort; - }; struct VersionSearchArgs { ModPlatform::IndexedPack pack; std::optional> mcVersions; std::optional loaders; - }; - struct VersionSearchCallbacks { - std::function on_succeed; - std::function on_fail; + ModPlatform::ResourceType resourceType; }; struct ProjectInfoArgs { ModPlatform::IndexedPack pack; }; - struct ProjectInfoCallbacks { - std::function on_succeed; - std::function on_fail; - std::function on_abort; - }; struct DependencySearchArgs { ModPlatform::Dependency dependency; @@ -111,73 +105,52 @@ class ResourceAPI { ModPlatform::ModLoaderTypes loader; }; - struct DependencySearchCallbacks { - std::function on_succeed; - std::function on_fail; - }; - public: /** Gets a list of available sorting methods for this API. */ virtual auto getSortingMethods() const -> QList = 0; public slots: - [[nodiscard]] virtual Task::Ptr searchProjects(SearchArgs&&, SearchCallbacks&&) const - { - qWarning() << "TODO: ResourceAPI::searchProjects"; - return nullptr; - } - virtual Task::Ptr getProject([[maybe_unused]] QString addonId, - [[maybe_unused]] std::shared_ptr response) const - { - qWarning() << "TODO: ResourceAPI::getProject"; - return nullptr; - } - virtual Task::Ptr getProjects([[maybe_unused]] QStringList addonIds, - [[maybe_unused]] std::shared_ptr response) const - { - qWarning() << "TODO: ResourceAPI::getProjects"; - return nullptr; - } - - virtual Task::Ptr getProjectInfo(ProjectInfoArgs&&, ProjectInfoCallbacks&&) const - { - qWarning() << "TODO: ResourceAPI::getProjectInfo"; - return nullptr; - } - virtual Task::Ptr getProjectVersions(VersionSearchArgs&&, VersionSearchCallbacks&&) const - { - qWarning() << "TODO: ResourceAPI::getProjectVersions"; - return nullptr; - } - - virtual Task::Ptr getDependencyVersion(DependencySearchArgs&&, DependencySearchCallbacks&&) const - { - qWarning() << "TODO"; - return nullptr; - } + virtual Task::Ptr searchProjects(SearchArgs&&, Callback>&&) const; + + virtual Task::Ptr getProject(QString addonId, std::shared_ptr response) const; + virtual Task::Ptr getProjects(QStringList addonIds, std::shared_ptr response) const = 0; + + virtual Task::Ptr getProjectInfo(ProjectInfoArgs&&, Callback&&) const; + Task::Ptr getProjectVersions(VersionSearchArgs&& args, Callback>&& callbacks) const; + virtual Task::Ptr getDependencyVersion(DependencySearchArgs&&, Callback&&) const; protected: inline QString debugName() const { return "External resource API"; } - inline QString mapMCVersionToModrinth(Version v) const - { - static const QString preString = " Pre-Release "; - auto verStr = v.toString(); - - if (verStr.contains(preString)) { - verStr.replace(preString, "-pre"); - } - verStr.replace(" ", "-"); - return verStr; - } - - inline QString getGameVersionsString(std::list mcVersions) const - { - QString s; - for (auto& ver : mcVersions) { - s += QString("\"%1\",").arg(mapMCVersionToModrinth(ver)); - } - s.remove(s.length() - 1, 1); // remove last comma - return s; - } + QString mapMCVersionToModrinth(Version v) const; + + QString getGameVersionsString(std::list mcVersions) const; + + public: + virtual auto getSearchURL(SearchArgs const& args) const -> std::optional = 0; + virtual auto getInfoURL(QString const& id) const -> std::optional = 0; + virtual auto getVersionsURL(VersionSearchArgs const& args) const -> std::optional = 0; + virtual auto getDependencyURL(DependencySearchArgs const& args) const -> std::optional = 0; + + /** Functions to load data into a pack. + * + * Those are needed for the same reason as documentToArray, and NEED to be re-implemented in the same way. + */ + + virtual void loadIndexedPack(ModPlatform::IndexedPack&, QJsonObject&) const = 0; + virtual ModPlatform::IndexedVersion loadIndexedPackVersion(QJsonObject& obj, ModPlatform::ResourceType) const = 0; + + /** Converts a JSON document to a common array format. + * + * This is needed so that different providers, with different JSON structures, can be parsed + * uniformally. You NEED to re-implement this if you intend on using the default callbacks. + */ + virtual QJsonArray documentToArray(QJsonDocument& obj) const = 0; + + /** Functions to load data into a pack. + * + * Those are needed for the same reason as documentToArray, and NEED to be re-implemented in the same way. + */ + + virtual void loadExtraPackInfo(ModPlatform::IndexedPack&, QJsonObject&) const = 0; }; diff --git a/launcher/modplatform/flame/FlameAPI.h b/launcher/modplatform/flame/FlameAPI.h index 1e39ca994..799e142ce 100644 --- a/launcher/modplatform/flame/FlameAPI.h +++ b/launcher/modplatform/flame/FlameAPI.h @@ -7,11 +7,13 @@ #include #include #include "BuildConfig.h" +#include "Json.h" +#include "Version.h" #include "modplatform/ModIndex.h" #include "modplatform/ResourceAPI.h" -#include "modplatform/helpers/NetworkResourceAPI.h" +#include "modplatform/flame/FlameModIndex.h" -class FlameAPI : public NetworkResourceAPI { +class FlameAPI : public ResourceAPI { public: QString getModFileChangelog(int modId, int fileId); QString getModDescription(int modId); @@ -138,6 +140,25 @@ class FlameAPI : public NetworkResourceAPI { return url; } + QJsonArray documentToArray(QJsonDocument& obj) const override { return Json::ensureArray(obj.object(), "data"); } + void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) const override { FlameMod::loadIndexedPack(m, obj); } + ModPlatform::IndexedVersion loadIndexedPackVersion(QJsonObject& obj, ModPlatform::ResourceType resourceType) const override + { + auto arr = FlameMod::loadIndexedPackVersion(obj); + if (resourceType != ModPlatform::ResourceType::TexturePack) { + return arr; + } + // FIXME: Client-side version filtering. This won't take into account any user-selected filtering. + auto const& mc_versions = arr.mcVersion; + + if (std::any_of(mc_versions.constBegin(), mc_versions.constEnd(), + [](auto const& mc_version) { return Version(mc_version) <= Version("1.6"); })) { + return arr; + } + return {}; + }; + void loadExtraPackInfo(ModPlatform::IndexedPack& m, [[maybe_unused]] QJsonObject&) const override { FlameMod::loadBody(m); } + private: std::optional getInfoURL(QString const& id) const override { diff --git a/launcher/modplatform/flame/FlameModIndex.cpp b/launcher/modplatform/flame/FlameModIndex.cpp index 660dc159c..d92ee729c 100644 --- a/launcher/modplatform/flame/FlameModIndex.cpp +++ b/launcher/modplatform/flame/FlameModIndex.cpp @@ -58,7 +58,7 @@ void FlameMod::loadURLs(ModPlatform::IndexedPack& pack, QJsonObject& obj) pack.extraDataLoaded = true; } -void FlameMod::loadBody(ModPlatform::IndexedPack& pack, [[maybe_unused]] QJsonObject& obj) +void FlameMod::loadBody(ModPlatform::IndexedPack& pack) { pack.extraData.body = api.getModDescription(pack.addonId.toInt()); @@ -204,31 +204,3 @@ auto FlameMod::loadIndexedPackVersion(QJsonObject& obj, bool load_changelog) -> return file; } - -ModPlatform::IndexedVersion FlameMod::loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr, const BaseInstance* inst) -{ - auto profile = (dynamic_cast(inst))->getPackProfile(); - QString mcVersion = profile->getComponentVersion("net.minecraft"); - auto loaders = profile->getSupportedModLoaders(); - QList versions; - for (auto versionIter : arr) { - auto obj = versionIter.toObject(); - - auto file = loadIndexedPackVersion(obj); - if (!file.addonId.isValid()) - file.addonId = m.addonId; - - if (file.fileId.isValid() && - (!loaders.has_value() || !file.loaders || loaders.value() & file.loaders)) // Heuristic to check if the returned value is valid - versions.append(file); - } - - auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool { - // dates are in RFC 3339 format - return a.date > b.date; - }; - std::sort(versions.begin(), versions.end(), orderSortPredicate); - if (versions.size() != 0) - return versions.front(); - return {}; -} diff --git a/launcher/modplatform/flame/FlameModIndex.h b/launcher/modplatform/flame/FlameModIndex.h index f6b4b22be..b583b518f 100644 --- a/launcher/modplatform/flame/FlameModIndex.h +++ b/launcher/modplatform/flame/FlameModIndex.h @@ -12,8 +12,7 @@ namespace FlameMod { void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj); void loadURLs(ModPlatform::IndexedPack& m, QJsonObject& obj); -void loadBody(ModPlatform::IndexedPack& m, QJsonObject& obj); +void loadBody(ModPlatform::IndexedPack& m); void loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr); ModPlatform::IndexedVersion loadIndexedPackVersion(QJsonObject& obj, bool load_changelog = false); -ModPlatform::IndexedVersion loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr, const BaseInstance* inst); } // namespace FlameMod \ No newline at end of file diff --git a/launcher/modplatform/flame/FlamePackIndex.cpp b/launcher/modplatform/flame/FlamePackIndex.cpp deleted file mode 100644 index db2061d99..000000000 --- a/launcher/modplatform/flame/FlamePackIndex.cpp +++ /dev/null @@ -1,150 +0,0 @@ -#include "FlamePackIndex.h" -#include -#include - -#include "Json.h" -#include "modplatform/ModIndex.h" - -void Flame::loadIndexedPack(Flame::IndexedPack& pack, QJsonObject& obj) -{ - pack.addonId = Json::requireInteger(obj, "id"); - pack.name = Json::requireString(obj, "name"); - pack.description = Json::ensureString(obj, "summary", ""); - - auto logo = Json::requireObject(obj, "logo"); - pack.logoUrl = Json::requireString(logo, "thumbnailUrl"); - pack.logoName = Json::requireString(obj, "slug") + "." + QFileInfo(QUrl(pack.logoUrl).fileName()).suffix(); - - auto authors = Json::requireArray(obj, "authors"); - for (auto authorIter : authors) { - auto author = Json::requireObject(authorIter); - Flame::ModpackAuthor packAuthor; - packAuthor.name = Json::requireString(author, "name"); - packAuthor.url = Json::requireString(author, "url"); - pack.authors.append(packAuthor); - } - int defaultFileId = Json::requireInteger(obj, "mainFileId"); - - bool found = false; - // check if there are some files before adding the pack - auto files = Json::requireArray(obj, "latestFiles"); - for (auto fileIter : files) { - auto file = Json::requireObject(fileIter); - int id = Json::requireInteger(file, "id"); - - // NOTE: for now, ignore everything that's not the default... - if (id != defaultFileId) { - continue; - } - - auto versionArray = Json::requireArray(file, "gameVersions"); - if (versionArray.size() < 1) { - continue; - } - - found = true; - break; - } - if (!found) { - throw JSONValidationError(QString("Pack with no good file, skipping: %1").arg(pack.name)); - } - - loadIndexedInfo(pack, obj); -} - -void Flame::loadIndexedInfo(IndexedPack& pack, QJsonObject& obj) -{ - auto links_obj = Json::ensureObject(obj, "links"); - - pack.extra.websiteUrl = Json::ensureString(links_obj, "websiteUrl"); - if (pack.extra.websiteUrl.endsWith('/')) - pack.extra.websiteUrl.chop(1); - - pack.extra.issuesUrl = Json::ensureString(links_obj, "issuesUrl"); - if (pack.extra.issuesUrl.endsWith('/')) - pack.extra.issuesUrl.chop(1); - - pack.extra.sourceUrl = Json::ensureString(links_obj, "sourceUrl"); - if (pack.extra.sourceUrl.endsWith('/')) - pack.extra.sourceUrl.chop(1); - - pack.extra.wikiUrl = Json::ensureString(links_obj, "wikiUrl"); - if (pack.extra.wikiUrl.endsWith('/')) - pack.extra.wikiUrl.chop(1); - - pack.extraInfoLoaded = true; -} - -void Flame::loadIndexedPackVersions(Flame::IndexedPack& pack, QJsonArray& arr) -{ - QList unsortedVersions; - for (auto versionIter : arr) { - auto version = Json::requireObject(versionIter); - Flame::IndexedVersion file; - - file.addonId = pack.addonId; - file.fileId = Json::requireInteger(version, "id"); - auto versionArray = Json::requireArray(version, "gameVersions"); - if (versionArray.size() < 1) { - continue; - } - - for (auto mcVer : versionArray) { - auto str = mcVer.toString(); - - if (str.contains('.')) - file.mcVersion.append(str); - - if (auto loader = str.toLower(); loader == "neoforge") - file.loaders |= ModPlatform::NeoForge; - else if (loader == "forge") - file.loaders |= ModPlatform::Forge; - else if (loader == "cauldron") - file.loaders |= ModPlatform::Cauldron; - else if (loader == "liteloader") - file.loaders |= ModPlatform::LiteLoader; - else if (loader == "fabric") - file.loaders |= ModPlatform::Fabric; - else if (loader == "quilt") - file.loaders |= ModPlatform::Quilt; - } - - // pick the latest version supported - file.version = Json::requireString(version, "displayName"); - - ModPlatform::IndexedVersionType::VersionType ver_type; - switch (Json::requireInteger(version, "releaseType")) { - case 1: - ver_type = ModPlatform::IndexedVersionType::VersionType::Release; - break; - case 2: - ver_type = ModPlatform::IndexedVersionType::VersionType::Beta; - break; - case 3: - ver_type = ModPlatform::IndexedVersionType::VersionType::Alpha; - break; - default: - ver_type = ModPlatform::IndexedVersionType::VersionType::Unknown; - } - file.version_type = ModPlatform::IndexedVersionType(ver_type); - file.downloadUrl = Json::ensureString(version, "downloadUrl"); - - // only add if we have a download URL (third party distribution is enabled) - if (!file.downloadUrl.isEmpty()) { - unsortedVersions.append(file); - } - } - - auto orderSortPredicate = [](const IndexedVersion& a, const IndexedVersion& b) -> bool { return a.fileId > b.fileId; }; - std::sort(unsortedVersions.begin(), unsortedVersions.end(), orderSortPredicate); - pack.versions = unsortedVersions; - pack.versionsLoaded = true; -} - -auto Flame::getVersionDisplayString(const IndexedVersion& version) -> QString -{ - auto release_type = version.version_type.isValid() ? QString(" [%1]").arg(version.version_type.toString()) : ""; - auto mcVersion = - !version.mcVersion.isEmpty() && !version.version.contains(version.mcVersion) ? QObject::tr(" for %1").arg(version.mcVersion) : ""; - return QString("%1%2%3").arg(version.version, mcVersion, release_type); -} diff --git a/launcher/modplatform/flame/FlamePackIndex.h b/launcher/modplatform/flame/FlamePackIndex.h deleted file mode 100644 index 22f7d648a..000000000 --- a/launcher/modplatform/flame/FlamePackIndex.h +++ /dev/null @@ -1,55 +0,0 @@ -#pragma once - -#include -#include -#include -#include "modplatform/ModIndex.h" - -namespace Flame { - -struct ModpackAuthor { - QString name; - QString url; -}; - -struct IndexedVersion { - int addonId; - int fileId; - QString version; - ModPlatform::IndexedVersionType version_type; - ModPlatform::ModLoaderTypes loaders = {}; - QString mcVersion; - QString downloadUrl; -}; - -struct ModpackExtra { - QString websiteUrl; - QString wikiUrl; - QString issuesUrl; - QString sourceUrl; -}; - -struct IndexedPack { - int addonId; - QString name; - QString description; - QList authors; - QString logoName; - QString logoUrl; - - bool versionsLoaded = false; - QList versions; - - bool extraInfoLoaded = false; - ModpackExtra extra; -}; - -void loadIndexedPack(IndexedPack& m, QJsonObject& obj); -void loadIndexedInfo(IndexedPack&, QJsonObject&); -void loadIndexedPackVersions(IndexedPack& m, QJsonArray& arr); - -auto getVersionDisplayString(const IndexedVersion&) -> QString; -} // namespace Flame - -Q_DECLARE_METATYPE(Flame::IndexedPack) -Q_DECLARE_METATYPE(QList) diff --git a/launcher/modplatform/helpers/NetworkResourceAPI.h b/launcher/modplatform/helpers/NetworkResourceAPI.h deleted file mode 100644 index d89014a38..000000000 --- a/launcher/modplatform/helpers/NetworkResourceAPI.h +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-FileCopyrightText: 2023 flowln -// -// SPDX-License-Identifier: GPL-3.0-only - -#pragma once - -#include -#include "modplatform/ResourceAPI.h" - -class NetworkResourceAPI : public ResourceAPI { - public: - Task::Ptr searchProjects(SearchArgs&&, SearchCallbacks&&) const override; - - Task::Ptr getProject(QString addonId, std::shared_ptr response) const override; - - Task::Ptr getProjectInfo(ProjectInfoArgs&&, ProjectInfoCallbacks&&) const override; - Task::Ptr getProjectVersions(VersionSearchArgs&&, VersionSearchCallbacks&&) const override; - Task::Ptr getDependencyVersion(DependencySearchArgs&&, DependencySearchCallbacks&&) const override; - - protected: - virtual auto getSearchURL(SearchArgs const& args) const -> std::optional = 0; - virtual auto getInfoURL(QString const& id) const -> std::optional = 0; - virtual auto getVersionsURL(VersionSearchArgs const& args) const -> std::optional = 0; - virtual auto getDependencyURL(DependencySearchArgs const& args) const -> std::optional = 0; -}; diff --git a/launcher/modplatform/modrinth/ModrinthAPI.h b/launcher/modplatform/modrinth/ModrinthAPI.h index 5b426a06a..c2714b3c8 100644 --- a/launcher/modplatform/modrinth/ModrinthAPI.h +++ b/launcher/modplatform/modrinth/ModrinthAPI.h @@ -5,12 +5,14 @@ #pragma once #include "BuildConfig.h" +#include "Json.h" #include "modplatform/ModIndex.h" -#include "modplatform/helpers/NetworkResourceAPI.h" +#include "modplatform/ResourceAPI.h" +#include "modplatform/modrinth/ModrinthPackIndex.h" #include -class ModrinthAPI : public NetworkResourceAPI { +class ModrinthAPI : public ResourceAPI { public: Task::Ptr currentVersion(QString hash, QString hash_format, std::shared_ptr response); @@ -214,4 +216,12 @@ class ModrinthAPI : public NetworkResourceAPI { .arg(mapMCVersionToModrinth(args.mcVersion)) .arg(getModLoaderStrings(args.loader).join("\",\"")); }; + + QJsonArray documentToArray(QJsonDocument& obj) const override { return obj.object().value("hits").toArray(); } + void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) const override { Modrinth::loadIndexedPack(m, obj); } + ModPlatform::IndexedVersion loadIndexedPackVersion(QJsonObject& obj, ModPlatform::ResourceType) const override + { + return Modrinth::loadIndexedPackVersion(obj); + }; + void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) const override { Modrinth::loadExtraPackData(m, obj); } }; diff --git a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp index 374b7681e..18b435106 100644 --- a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp +++ b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp @@ -13,7 +13,6 @@ #include "modplatform/EnsureMetadataTask.h" #include "modplatform/helpers/OverrideUtils.h" -#include "modplatform/modrinth/ModrinthPackManifest.h" #include "net/ChecksumValidator.h" #include "net/ApiDownload.h" @@ -85,7 +84,7 @@ bool ModrinthCreationTask::updateInstance() QString old_index_path(FS::PathCombine(old_index_folder, "modrinth.index.json")); QFileInfo old_index_file(old_index_path); if (old_index_file.exists()) { - std::vector old_files; + std::vector old_files; parseManifest(old_index_path, old_files, false, false); // Let's remove all duplicated, identical resources! @@ -356,7 +355,7 @@ bool ModrinthCreationTask::createInstance() } bool ModrinthCreationTask::parseManifest(const QString& index_path, - std::vector& files, + std::vector& files, bool set_internal_data, bool show_optional_dialog) { @@ -377,9 +376,9 @@ bool ModrinthCreationTask::parseManifest(const QString& index_path, } auto jsonFiles = Json::requireIsArrayOf(obj, "files", "modrinth.index.json"); - std::vector optionalFiles; + std::vector optionalFiles; for (const auto& modInfo : jsonFiles) { - Modrinth::File file; + File file; file.path = Json::requireString(modInfo, "path").replace("\\", "/"); auto env = Json::ensureObject(modInfo, "env"); diff --git a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h index ddfa7ae95..e02a55877 100644 --- a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h +++ b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h @@ -1,13 +1,27 @@ #pragma once #include + +#include +#include +#include +#include +#include +#include + #include "BaseInstance.h" #include "InstanceCreationTask.h" -#include "modplatform/modrinth/ModrinthPackManifest.h" - class ModrinthCreationTask final : public InstanceCreationTask { Q_OBJECT + struct File { + QString path; + + QCryptographicHash::Algorithm hashAlgorithm; + QByteArray hash; + QQueue downloads; + bool required = true; + }; public: ModrinthCreationTask(QString staging_path, @@ -30,7 +44,7 @@ class ModrinthCreationTask final : public InstanceCreationTask { bool createInstance() override; private: - bool parseManifest(const QString&, std::vector&, bool set_internal_data = true, bool show_optional_dialog = true); + bool parseManifest(const QString&, std::vector&, bool set_internal_data = true, bool show_optional_dialog = true); private: QWidget* m_parent = nullptr; @@ -38,7 +52,7 @@ class ModrinthCreationTask final : public InstanceCreationTask { QString m_minecraft_version, m_fabric_version, m_quilt_version, m_forge_version, m_neoForge_version; QString m_managed_id, m_managed_version_id, m_managed_name; - std::vector m_files; + std::vector m_files; Task::Ptr m_task; std::optional m_instance; diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp index 42fda9df1..8e0552a48 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.cpp +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.cpp @@ -112,25 +112,6 @@ void Modrinth::loadExtraPackData(ModPlatform::IndexedPack& pack, QJsonObject& ob pack.extraDataLoaded = true; } -void Modrinth::loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr) -{ - QList unsortedVersions; - for (auto versionIter : arr) { - auto obj = versionIter.toObject(); - auto file = loadIndexedPackVersion(obj); - - if (file.fileId.isValid()) // Heuristic to check if the returned value is valid - unsortedVersions.append(file); - } - auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool { - // dates are in RFC 3339 format - return a.date > b.date; - }; - std::sort(unsortedVersions.begin(), unsortedVersions.end(), orderSortPredicate); - pack.versions = unsortedVersions; - pack.versionsLoaded = true; -} - ModPlatform::IndexedVersion Modrinth::loadIndexedPackVersion(QJsonObject& obj, QString preferred_hash_type, QString preferred_file_name) { ModPlatform::IndexedVersion file; @@ -244,28 +225,3 @@ ModPlatform::IndexedVersion Modrinth::loadIndexedPackVersion(QJsonObject& obj, Q return {}; } - -ModPlatform::IndexedVersion Modrinth::loadDependencyVersions([[maybe_unused]] const ModPlatform::Dependency& m, - QJsonArray& arr, - const BaseInstance* inst) -{ - auto profile = (dynamic_cast(inst))->getPackProfile(); - QString mcVersion = profile->getComponentVersion("net.minecraft"); - auto loaders = profile->getSupportedModLoaders(); - - QList versions; - for (auto versionIter : arr) { - auto obj = versionIter.toObject(); - auto file = loadIndexedPackVersion(obj); - - if (file.fileId.isValid() && - (!loaders.has_value() || !file.loaders || loaders.value() & file.loaders)) // Heuristic to check if the returned value is valid - versions.append(file); - } - auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a, const ModPlatform::IndexedVersion& b) -> bool { - // dates are in RFC 3339 format - return a.date > b.date; - }; - std::sort(versions.begin(), versions.end(), orderSortPredicate); - return versions.length() != 0 ? versions.front() : ModPlatform::IndexedVersion(); -} diff --git a/launcher/modplatform/modrinth/ModrinthPackIndex.h b/launcher/modplatform/modrinth/ModrinthPackIndex.h index 16f3d262c..5d852cb6f 100644 --- a/launcher/modplatform/modrinth/ModrinthPackIndex.h +++ b/launcher/modplatform/modrinth/ModrinthPackIndex.h @@ -25,8 +25,6 @@ namespace Modrinth { void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj); void loadExtraPackData(ModPlatform::IndexedPack& m, QJsonObject& obj); -void loadIndexedPackVersions(ModPlatform::IndexedPack& pack, QJsonArray& arr); auto loadIndexedPackVersion(QJsonObject& obj, QString hash_type = "sha512", QString filename_prefer = "") -> ModPlatform::IndexedVersion; -auto loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr, const BaseInstance* inst) -> ModPlatform::IndexedVersion; } // namespace Modrinth diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp b/launcher/modplatform/modrinth/ModrinthPackManifest.cpp deleted file mode 100644 index 1e90f713e..000000000 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.cpp +++ /dev/null @@ -1,196 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - 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 - * 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" -#include -#include "Json.h" - -#include "modplatform/modrinth/ModrinthAPI.h" - -#include - -static ModrinthAPI api; - -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"); - auto temp_author_name = Json::ensureString(obj, "author"); - pack.author = std::make_tuple(temp_author_name, api.getAuthorURL(temp_author_name)); - pack.iconUrl = Json::ensureString(obj, "icon_url"); - pack.iconName = QString("modrinth_%1.%2").arg(Json::ensureString(obj, "slug"), QFileInfo(pack.iconUrl.fileName()).suffix()); -} - -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.issuesUrl = Json::ensureString(obj, "issues_url"); - if (pack.extra.issuesUrl.endsWith('/')) - pack.extra.issuesUrl.chop(1); - - pack.extra.sourceUrl = Json::ensureString(obj, "source_url"); - if (pack.extra.sourceUrl.endsWith('/')) - pack.extra.sourceUrl.chop(1); - - pack.extra.wikiUrl = Json::ensureString(obj, "wiki_url"); - if (pack.extra.wikiUrl.endsWith('/')) - pack.extra.wikiUrl.chop(1); - - pack.extra.discordUrl = Json::ensureString(obj, "discord_url"); - if (pack.extra.discordUrl.endsWith('/')) - pack.extra.discordUrl.chop(1); - - auto donate_arr = Json::ensureArray(obj, "donation_urls"); - for (auto d : donate_arr) { - auto d_obj = Json::requireObject(d); - - DonationData donate; - - donate.id = Json::ensureString(d_obj, "id"); - donate.platform = Json::ensureString(d_obj, "platform"); - donate.url = Json::ensureString(d_obj, "url"); - - pack.extra.donate.append(donate); - } - - pack.extra.status = Json::ensureString(obj, "status"); - - pack.extraInfoLoaded = true; -} - -void loadIndexedVersions(Modpack& pack, QJsonDocument& doc) -{ - QList 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"); - auto gameVersions = Json::ensureArray(obj, "game_versions"); - if (!gameVersions.isEmpty()) { - file.gameVersion = Json::ensureString(gameVersions[0]); - file.gameVersion = ModrinthAPI::mapMCVersionFromModrinth(file.gameVersion); - } - auto loaders = Json::requireArray(obj, "loaders"); - for (auto loader : loaders) { - if (loader == "neoforge") - file.loaders |= ModPlatform::NeoForge; - else if (loader == "forge") - file.loaders |= ModPlatform::Forge; - else if (loader == "cauldron") - file.loaders |= ModPlatform::Cauldron; - else if (loader == "liteloader") - file.loaders |= ModPlatform::LiteLoader; - else if (loader == "fabric") - file.loaders |= ModPlatform::Fabric; - else if (loader == "quilt") - file.loaders |= ModPlatform::Quilt; - } - file.version_type = ModPlatform::IndexedVersionType(Json::requireString(obj, "version_type")); - file.changelog = Json::ensureString(obj, "changelog"); - - 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); - auto is_primary = Json::ensureBoolean(parent, (const QString)QStringLiteral("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; - } - - auto url = Json::requireString(parent, "url"); - - file.download_url = url; - if (is_primary) - break; - } - - if (file.download_url.isEmpty()) - return {}; - - return file; -} - -auto getVersionDisplayString(const ModpackVersion& version) -> QString -{ - auto release_type = version.version_type.isValid() ? QString(" [%1]").arg(version.version_type.toString()) : ""; - auto mcVersion = !version.gameVersion.isEmpty() && !version.name.contains(version.gameVersion) - ? QObject::tr(" for %1").arg(version.gameVersion) - : ""; - auto versionStr = !version.name.contains(version.version) ? version.version : ""; - return QString("%1%2 — %3%4").arg(version.name, mcVersion, versionStr, release_type); -} - -} // namespace Modrinth diff --git a/launcher/modplatform/modrinth/ModrinthPackManifest.h b/launcher/modplatform/modrinth/ModrinthPackManifest.h deleted file mode 100644 index a990e9a77..000000000 --- a/launcher/modplatform/modrinth/ModrinthPackManifest.h +++ /dev/null @@ -1,129 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - 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 - * 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 -#include -#include -#include - -#include "modplatform/ModIndex.h" - -class MinecraftInstance; - -namespace Modrinth { - -struct File { - QString path; - - QCryptographicHash::Algorithm hashAlgorithm; - QByteArray hash; - QQueue downloads; - bool required = true; -}; - -struct DonationData { - QString id; - QString platform; - QString url; -}; - -struct ModpackExtra { - QString body; - - QString projectUrl; - - QString issuesUrl; - QString sourceUrl; - QString wikiUrl; - QString discordUrl; - - QList donate; - - QString status; -}; - -struct ModpackVersion { - QString name; - QString version; - QString gameVersion; - ModPlatform::IndexedVersionType version_type; - QString changelog; - ModPlatform::ModLoaderTypes loaders = {}; - - QString id; - QString project_id; - - QString date; - - QString download_url; -}; - -struct Modpack { - QString id; - - QString name; - QString description; - std::tuple author; - QString iconName; - QUrl iconUrl; - - bool versionsLoaded = false; - bool extraInfoLoaded = false; - - ModpackExtra extra; - QList versions; -}; - -void loadIndexedPack(Modpack&, QJsonObject&); -void loadIndexedInfo(Modpack&, QJsonObject&); -void loadIndexedVersions(Modpack&, QJsonDocument&); -auto loadIndexedVersion(QJsonObject&) -> ModpackVersion; - -auto validateDownloadUrl(QUrl) -> bool; - -auto getVersionDisplayString(const ModpackVersion&) -> QString; - -} // namespace Modrinth - -Q_DECLARE_METATYPE(Modrinth::Modpack) -Q_DECLARE_METATYPE(Modrinth::ModpackVersion) -Q_DECLARE_METATYPE(QList) diff --git a/launcher/ui/pages/instance/ManagedPackPage.cpp b/launcher/ui/pages/instance/ManagedPackPage.cpp index 01bc6c2fd..d46f0f8d5 100644 --- a/launcher/ui/pages/instance/ManagedPackPage.cpp +++ b/launcher/ui/pages/instance/ManagedPackPage.cpp @@ -22,8 +22,6 @@ #include "Markdown.h" #include "StringUtils.h" -#include "modplatform/modrinth/ModrinthPackManifest.h" - #include "ui/InstanceWindow.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/ProgressDialog.h" @@ -256,36 +254,13 @@ void ModrinthManagedPackPage::parseManagedPack() if (m_fetch_job && m_fetch_job->isRunning()) m_fetch_job->abort(); - m_fetch_job.reset(new NetJob(QString("Modrinth::PackVersions(%1)").arg(m_inst->getManagedPackName()), APPLICATION->network())); - auto response = std::make_shared(); - - QString id = m_inst->getManagedPackID(); - - m_fetch_job->addNetAction( - Net::ApiDownload::makeByteArray(QString("%1/project/%2/version").arg(BuildConfig.MODRINTH_PROD_URL, id), response)); - - connect(m_fetch_job.get(), &NetJob::succeeded, this, [this, response, id] { - 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; - - setFailState(); + ResourceAPI::Callback> callbacks{}; + m_pack = { m_inst->getManagedPackID() }; - return; - } - - try { - Modrinth::loadIndexedVersions(m_pack, doc); - } catch (const JSONValidationError& e) { - qDebug() << *response; - qWarning() << "Error while reading modrinth modpack version: " << e.cause(); - - setFailState(); - return; - } + // Use default if no callbacks are set + callbacks.on_succeed = [this](auto& doc) { + m_pack.versions = doc; + m_pack.versionsLoaded = true; // We block signals here so that suggestVersion() doesn't get called, causing an assertion fail. ui->versionsComboBox->blockSignals(true); @@ -293,22 +268,23 @@ void ModrinthManagedPackPage::parseManagedPack() ui->versionsComboBox->blockSignals(false); for (const auto& version : m_pack.versions) { - QString name = Modrinth::getVersionDisplayString(version); + QString name = version.getVersionDisplayString(); // NOTE: the id from version isn't the same id in the modpack format spec... // e.g. HexMC's 4.4.0 has versionId 4.0.0 in the modpack index.............. if (version.version == m_inst->getManagedPackVersionName()) name = tr("%1 (Current)").arg(name); - ui->versionsComboBox->addItem(name, QVariant(version.id)); + ui->versionsComboBox->addItem(name, version.fileId); } suggestVersion(); m_loaded = true; - }); - connect(m_fetch_job.get(), &NetJob::failed, this, &ModrinthManagedPackPage::setFailState); - connect(m_fetch_job.get(), &NetJob::aborted, this, &ModrinthManagedPackPage::setFailState); + }; + callbacks.on_fail = [this](QString reason, int) { setFailState(); }; + callbacks.on_abort = [this]() { setFailState(); }; + m_fetch_job = m_api.getProjectVersions({ m_pack, {}, {}, ModPlatform::ResourceType::Modpack }, std::move(callbacks)); ui->changelogTextBrowser->setText(tr("Fetching changelogs...")); @@ -370,10 +346,10 @@ void ModrinthManagedPackPage::update() QMap extra_info; // NOTE: Don't use 'm_pack.id' here, since we didn't completely parse all the metadata for the pack, including this field. extra_info.insert("pack_id", m_inst->getManagedPackID()); - extra_info.insert("pack_version_id", version.id); + extra_info.insert("pack_version_id", version.fileId.toString()); extra_info.insert("original_instance_id", m_inst->id()); - auto extracted = new InstanceImportTask(version.download_url, this, std::move(extra_info)); + auto extracted = new InstanceImportTask(version.downloadUrl, this, std::move(extra_info)); InstanceName inst_name(m_inst->getManagedPackName(), version.version); inst_name.setName(m_inst->name().replace(m_inst->getManagedPackVersionName(), version.version)); @@ -449,37 +425,15 @@ void FlameManagedPackPage::parseManagedPack() if (m_fetch_job && m_fetch_job->isRunning()) m_fetch_job->abort(); - m_fetch_job.reset(new NetJob(QString("Flame::PackVersions(%1)").arg(m_inst->getManagedPackName()), APPLICATION->network())); - auto response = std::make_shared(); - QString id = m_inst->getManagedPackID(); + m_pack = { id }; - m_fetch_job->addNetAction(Net::ApiDownload::makeByteArray(QString("%1/mods/%2/files").arg(BuildConfig.FLAME_BASE_URL, id), response)); - - connect(m_fetch_job.get(), &NetJob::succeeded, this, [this, response, id] { - QJsonParseError parse_error{}; - QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); - if (parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from Flame at " << parse_error.offset - << " reason: " << parse_error.errorString(); - qWarning() << *response; - - setFailState(); - - return; - } - - try { - auto obj = doc.object(); - auto data = Json::ensureArray(obj, "data"); - Flame::loadIndexedPackVersions(m_pack, data); - } catch (const JSONValidationError& e) { - qDebug() << *response; - qWarning() << "Error while reading flame modpack version: " << e.cause(); + ResourceAPI::Callback> callbacks{}; - setFailState(); - return; - } + // Use default if no callbacks are set + callbacks.on_succeed = [this](auto& doc) { + m_pack.versions = doc; + m_pack.versionsLoaded = true; // We block signals here so that suggestVersion() doesn't get called, causing an assertion fail. ui->versionsComboBox->blockSignals(true); @@ -487,7 +441,7 @@ void FlameManagedPackPage::parseManagedPack() ui->versionsComboBox->blockSignals(false); for (const auto& version : m_pack.versions) { - QString name = Flame::getVersionDisplayString(version); + QString name = version.getVersionDisplayString(); if (version.fileId == m_inst->getManagedPackVersionID().toInt()) name = tr("%1 (Current)").arg(name); @@ -498,9 +452,10 @@ void FlameManagedPackPage::parseManagedPack() suggestVersion(); m_loaded = true; - }); - connect(m_fetch_job.get(), &NetJob::failed, this, &FlameManagedPackPage::setFailState); - connect(m_fetch_job.get(), &NetJob::aborted, this, &FlameManagedPackPage::setFailState); + }; + callbacks.on_fail = [this](QString reason, int) { setFailState(); }; + callbacks.on_abort = [this]() { setFailState(); }; + m_fetch_job = m_api.getProjectVersions({ m_pack, {}, {}, ModPlatform::ResourceType::Modpack }, std::move(callbacks)); m_fetch_job->start(); } @@ -521,7 +476,7 @@ void FlameManagedPackPage::suggestVersion() auto version = m_pack.versions.at(index); ui->changelogTextBrowser->setHtml( - StringUtils::htmlListPatch(m_api.getModFileChangelog(m_inst->getManagedPackID().toInt(), version.fileId))); + StringUtils::htmlListPatch(m_api.getModFileChangelog(m_inst->getManagedPackID().toInt(), version.fileId.toInt()))); ManagedPackPage::suggestVersion(); } @@ -537,7 +492,7 @@ void FlameManagedPackPage::update() QMap extra_info; extra_info.insert("pack_id", m_inst->getManagedPackID()); - extra_info.insert("pack_version_id", QString::number(version.fileId)); + extra_info.insert("pack_version_id", version.fileId.toString()); extra_info.insert("original_instance_id", m_inst->id()); auto extracted = new InstanceImportTask(version.downloadUrl, this, std::move(extra_info)); diff --git a/launcher/ui/pages/instance/ManagedPackPage.h b/launcher/ui/pages/instance/ManagedPackPage.h index 966c57768..f319ed069 100644 --- a/launcher/ui/pages/instance/ManagedPackPage.h +++ b/launcher/ui/pages/instance/ManagedPackPage.h @@ -6,11 +6,10 @@ #include "BaseInstance.h" +#include "modplatform/ModIndex.h" #include "modplatform/modrinth/ModrinthAPI.h" -#include "modplatform/modrinth/ModrinthPackManifest.h" #include "modplatform/flame/FlameAPI.h" -#include "modplatform/flame/FlamePackIndex.h" #include "net/NetJob.h" @@ -130,9 +129,9 @@ class ModrinthManagedPackPage final : public ManagedPackPage { void updateFromFile() override; private: - NetJob::Ptr m_fetch_job = nullptr; + Task::Ptr m_fetch_job = nullptr; - Modrinth::Modpack m_pack; + ModPlatform::IndexedPack m_pack; ModrinthAPI m_api; }; @@ -154,8 +153,8 @@ class FlameManagedPackPage final : public ManagedPackPage { void updateFromFile() override; private: - NetJob::Ptr m_fetch_job = nullptr; + Task::Ptr m_fetch_job = nullptr; - Flame::IndexedPack m_pack; + ModPlatform::IndexedPack m_pack; FlameAPI m_api; }; diff --git a/launcher/ui/pages/modplatform/DataPackModel.cpp b/launcher/ui/pages/modplatform/DataPackModel.cpp index 547f0a363..846ef5aa4 100644 --- a/launcher/ui/pages/modplatform/DataPackModel.cpp +++ b/launcher/ui/pages/modplatform/DataPackModel.cpp @@ -9,8 +9,8 @@ namespace ResourceDownload { -DataPackResourceModel::DataPackResourceModel(BaseInstance const& base_inst, ResourceAPI* api) - : ResourceModel(api), m_base_instance(base_inst) +DataPackResourceModel::DataPackResourceModel(BaseInstance const& base_inst, ResourceAPI* api, QString debugName, QString metaEntryBase) + : ResourceModel(api), m_base_instance(base_inst), m_debugName(debugName + " (Model)"), m_metaEntryBase(metaEntryBase) {} /******** Make data requests ********/ diff --git a/launcher/ui/pages/modplatform/DataPackModel.h b/launcher/ui/pages/modplatform/DataPackModel.h index 89e83969c..29b11ffd6 100644 --- a/launcher/ui/pages/modplatform/DataPackModel.h +++ b/launcher/ui/pages/modplatform/DataPackModel.h @@ -21,14 +21,13 @@ class DataPackResourceModel : public ResourceModel { Q_OBJECT public: - DataPackResourceModel(BaseInstance const&, ResourceAPI*); + DataPackResourceModel(BaseInstance const&, ResourceAPI*, QString, QString); /* Ask the API for more information */ void searchWithTerm(const QString& term, unsigned int sort); - void loadIndexedPack(ModPlatform::IndexedPack&, QJsonObject&) override = 0; - void loadExtraPackInfo(ModPlatform::IndexedPack&, QJsonObject&) override = 0; - void loadIndexedPackVersions(ModPlatform::IndexedPack&, QJsonArray&) override = 0; + [[nodiscard]] QString debugName() const override { return m_debugName; } + [[nodiscard]] QString metaEntryBase() const override { return m_metaEntryBase; } public slots: ResourceAPI::SearchArgs createSearchArguments() override; @@ -38,7 +37,9 @@ class DataPackResourceModel : public ResourceModel { protected: const BaseInstance& m_base_instance; - auto documentToArray(QJsonDocument& obj) const -> QJsonArray override = 0; + private: + QString m_debugName; + QString m_metaEntryBase; }; } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/ModModel.cpp b/launcher/ui/pages/modplatform/ModModel.cpp index feaa4cfa7..c5a03e1fd 100644 --- a/launcher/ui/pages/modplatform/ModModel.cpp +++ b/launcher/ui/pages/modplatform/ModModel.cpp @@ -14,7 +14,9 @@ namespace ResourceDownload { -ModModel::ModModel(BaseInstance& base_inst, ResourceAPI* api) : ResourceModel(api), m_base_instance(base_inst) {} +ModModel::ModModel(BaseInstance& base_inst, ResourceAPI* api, QString debugName, QString metaEntryBase) + : ResourceModel(api), m_base_instance(base_inst), m_debugName(debugName + " (Model)"), m_metaEntryBase(metaEntryBase) +{} /******** Make data requests ********/ @@ -60,7 +62,7 @@ ResourceAPI::VersionSearchArgs ModModel::createVersionsArguments(const QModelInd if (m_filter->loaders) loaders = m_filter->loaders; - return { pack, versions, loaders }; + return { pack, versions, loaders, ModPlatform::ResourceType::Mod }; } ResourceAPI::ProjectInfoArgs ModModel::createInfoArguments(const QModelIndex& entry) diff --git a/launcher/ui/pages/modplatform/ModModel.h b/launcher/ui/pages/modplatform/ModModel.h index bb9255cd0..873d4c1f9 100644 --- a/launcher/ui/pages/modplatform/ModModel.h +++ b/launcher/ui/pages/modplatform/ModModel.h @@ -24,26 +24,23 @@ class ModModel : public ResourceModel { Q_OBJECT public: - ModModel(BaseInstance&, ResourceAPI* api); + ModModel(BaseInstance&, ResourceAPI* api, QString debugName, QString metaEntryBase); /* Ask the API for more information */ void searchWithTerm(const QString& term, unsigned int sort, bool filter_changed); - void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override = 0; - void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override = 0; - void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override = 0; - virtual ModPlatform::IndexedVersion loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) = 0; - void setFilter(std::shared_ptr filter) { m_filter = filter; } virtual QVariant getInstalledPackVersion(ModPlatform::IndexedPack::Ptr) const override; + [[nodiscard]] QString debugName() const override { return m_debugName; } + [[nodiscard]] QString metaEntryBase() const override { return m_metaEntryBase; } + public slots: ResourceAPI::SearchArgs createSearchArguments() override; ResourceAPI::VersionSearchArgs createVersionsArguments(const QModelIndex&) override; ResourceAPI::ProjectInfoArgs createInfoArguments(const QModelIndex&) override; protected: - auto documentToArray(QJsonDocument& obj) const -> QJsonArray override = 0; virtual bool isPackInstalled(ModPlatform::IndexedPack::Ptr) const override; virtual bool checkFilters(ModPlatform::IndexedPack::Ptr) override; @@ -53,6 +50,10 @@ class ModModel : public ResourceModel { BaseInstance& m_base_instance; std::shared_ptr m_filter = nullptr; + + private: + QString m_debugName; + QString m_metaEntryBase; }; } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/ResourceModel.cpp b/launcher/ui/pages/modplatform/ResourceModel.cpp index 8e3be5e8f..5ea70789e 100644 --- a/launcher/ui/pages/modplatform/ResourceModel.cpp +++ b/launcher/ui/pages/modplatform/ResourceModel.cpp @@ -15,8 +15,8 @@ #include "Application.h" #include "BuildConfig.h" -#include "Json.h" +#include "modplatform/ResourceAPI.h" #include "net/ApiDownload.h" #include "net/NetJob.h" @@ -141,9 +141,9 @@ void ResourceModel::search() if (m_search_term.startsWith("#")) { auto projectId = m_search_term.mid(1); if (!projectId.isEmpty()) { - ResourceAPI::ProjectInfoCallbacks callbacks; + ResourceAPI::Callback callbacks; - callbacks.on_fail = [this](QString reason) { + callbacks.on_fail = [this](QString reason, int) { if (!s_running_models.constFind(this).value()) return; searchRequestFailed(reason, -1); @@ -154,10 +154,10 @@ void ResourceModel::search() searchRequestAborted(); }; - callbacks.on_succeed = [this](auto& doc, auto& pack) { + callbacks.on_succeed = [this](auto& pack) { if (!s_running_models.constFind(this).value()) return; - searchRequestForOneSucceeded(doc); + searchRequestForOneSucceeded(pack); }; if (auto job = m_api->getProjectInfo({ projectId }, std::move(callbacks)); job) runSearchJob(job); @@ -166,27 +166,23 @@ void ResourceModel::search() } auto args{ createSearchArguments() }; - auto callbacks{ createSearchCallbacks() }; + ResourceAPI::Callback> callbacks{}; - // Use defaults if no callbacks are set - if (!callbacks.on_succeed) - callbacks.on_succeed = [this](auto& doc) { - if (!s_running_models.constFind(this).value()) - return; - searchRequestSucceeded(doc); - }; - if (!callbacks.on_fail) - callbacks.on_fail = [this](QString reason, int network_error_code) { - if (!s_running_models.constFind(this).value()) - return; - searchRequestFailed(reason, network_error_code); - }; - if (!callbacks.on_abort) - callbacks.on_abort = [this] { - if (!s_running_models.constFind(this).value()) - return; - searchRequestAborted(); - }; + callbacks.on_succeed = [this](auto& doc) { + if (!s_running_models.constFind(this).value()) + return; + searchRequestSucceeded(doc); + }; + callbacks.on_fail = [this](QString reason, int network_error_code) { + if (!s_running_models.constFind(this).value()) + return; + searchRequestFailed(reason, network_error_code); + }; + callbacks.on_abort = [this] { + if (!s_running_models.constFind(this).value()) + return; + searchRequestAborted(); + }; if (auto job = m_api->searchProjects(std::move(args), std::move(callbacks)); job) runSearchJob(job); @@ -201,14 +197,15 @@ void ResourceModel::loadEntry(const QModelIndex& entry) if (!pack->versionsLoaded) { auto args{ createVersionsArguments(entry) }; - auto callbacks{ createVersionsCallbacks(entry) }; + ResourceAPI::Callback> callbacks{}; + auto addonId = pack->addonId; // Use default if no callbacks are set if (!callbacks.on_succeed) - callbacks.on_succeed = [this, entry](auto& doc, auto pack) { + callbacks.on_succeed = [this, entry, addonId](auto& doc) { if (!s_running_models.constFind(this).value()) return; - versionRequestSucceeded(doc, pack, entry); + versionRequestSucceeded(doc, addonId, entry); }; if (!callbacks.on_fail) callbacks.on_fail = [](QString reason, int) { @@ -222,28 +219,23 @@ void ResourceModel::loadEntry(const QModelIndex& entry) if (!pack->extraDataLoaded) { auto args{ createInfoArguments(entry) }; - auto callbacks{ createInfoCallbacks(entry) }; + ResourceAPI::Callback callbacks{}; - // Use default if no callbacks are set - if (!callbacks.on_succeed) - callbacks.on_succeed = [this, entry](auto& doc, auto& newpack) { - if (!s_running_models.constFind(this).value()) - return; - auto pack = newpack; - infoRequestSucceeded(doc, pack, entry); - }; - if (!callbacks.on_fail) - callbacks.on_fail = [this](QString reason) { - if (!s_running_models.constFind(this).value()) - return; - QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load project info: %1").arg(reason)); - }; - if (!callbacks.on_abort) - callbacks.on_abort = [this] { - if (!s_running_models.constFind(this).value()) - return; - qCritical() << tr("The request was aborted for an unknown reason"); - }; + callbacks.on_succeed = [this, entry](auto& newpack) { + if (!s_running_models.constFind(this).value()) + return; + infoRequestSucceeded(newpack, entry); + }; + callbacks.on_fail = [this](QString reason, int) { + if (!s_running_models.constFind(this).value()) + return; + QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load project info: %1").arg(reason)); + }; + callbacks.on_abort = [this] { + if (!s_running_models.constFind(this).value()) + return; + qCritical() << tr("The request was aborted for an unknown reason"); + }; if (auto job = m_api->getProjectInfo(std::move(args), std::move(callbacks)); job) runInfoJob(job); @@ -358,68 +350,35 @@ std::optional ResourceModel::getIcon(QModelIndex& index, const QUrl& url) return {}; } -// No 'forgor to implement' shall pass here :blobfox_knife: -#define NEED_FOR_CALLBACK_ASSERT(name) \ - Q_ASSERT_X(0 != 0, #name, "You NEED to re-implement this if you intend on using the default callbacks.") - -QJsonArray ResourceModel::documentToArray([[maybe_unused]] QJsonDocument& doc) const -{ - NEED_FOR_CALLBACK_ASSERT("documentToArray"); - return {}; -} -void ResourceModel::loadIndexedPack(ModPlatform::IndexedPack&, QJsonObject&) -{ - NEED_FOR_CALLBACK_ASSERT("loadIndexedPack"); -} -void ResourceModel::loadExtraPackInfo(ModPlatform::IndexedPack&, QJsonObject&) -{ - NEED_FOR_CALLBACK_ASSERT("loadExtraPackInfo"); -} -void ResourceModel::loadIndexedPackVersions(ModPlatform::IndexedPack&, QJsonArray&) -{ - NEED_FOR_CALLBACK_ASSERT("loadIndexedPackVersions"); -} - /* Default callbacks */ -void ResourceModel::searchRequestSucceeded(QJsonDocument& doc) +void ResourceModel::searchRequestSucceeded(QList& newList) { - QList newList; - auto packs = documentToArray(doc); - - for (auto packRaw : packs) { - auto packObj = packRaw.toObject(); - - ModPlatform::IndexedPack::Ptr pack = std::make_shared(); - try { - loadIndexedPack(*pack, packObj); - if (auto sel = std::find_if(m_selected.begin(), m_selected.end(), - [&pack](const DownloadTaskPtr i) { - const auto ipack = i->getPack(); - return ipack->provider == pack->provider && ipack->addonId == pack->addonId; - }); - sel != m_selected.end()) { - newList.append(sel->get()->getPack()); - } else - newList.append(pack); - } catch (const JSONValidationError& e) { - qWarning() << "Error while loading resource from " << debugName() << ": " << e.cause(); - continue; + QList filteredNewList; + for (auto pack : newList) { + ModPlatform::IndexedPack::Ptr p; + if (auto sel = std::find_if(m_selected.begin(), m_selected.end(), + [&pack](const DownloadTaskPtr i) { + const auto ipack = i->getPack(); + return ipack->provider == pack->provider && ipack->addonId == pack->addonId; + }); + sel != m_selected.end()) { + p = sel->get()->getPack(); + } else { + p = pack; + } + if (checkFilters(p)) { + filteredNewList << p; } } - if (packs.size() < 25) { + if (newList.size() < 25) { m_search_state = SearchState::Finished; } else { m_next_search_offset += 25; m_search_state = SearchState::CanFetchMore; } - QList filteredNewList; - for (auto p : newList) - if (checkFilters(p)) - filteredNewList << p; - // When you have a Qt build with assertions turned on, proceeding here will abort the application if (filteredNewList.size() == 0) return; @@ -429,24 +388,12 @@ void ResourceModel::searchRequestSucceeded(QJsonDocument& doc) endInsertRows(); } -void ResourceModel::searchRequestForOneSucceeded(QJsonDocument& doc) +void ResourceModel::searchRequestForOneSucceeded(ModPlatform::IndexedPack& pack) { - ModPlatform::IndexedPack::Ptr pack = std::make_shared(); - - try { - auto obj = Json::requireObject(doc); - if (obj.contains("data")) - obj = Json::requireObject(obj, "data"); - loadIndexedPack(*pack, obj); - } catch (const JSONValidationError& e) { - qDebug() << doc; - qWarning() << "Error while reading " << debugName() << " resource info: " << e.cause(); - } - m_search_state = SearchState::Finished; beginInsertRows(QModelIndex(), m_packs.size(), m_packs.size() + 1); - m_packs.append(pack); + m_packs.append(std::make_shared(pack)); endInsertRows(); } @@ -479,21 +426,16 @@ void ResourceModel::searchRequestAborted() search(); } -void ResourceModel::versionRequestSucceeded(QJsonDocument& doc, ModPlatform::IndexedPack& pack, const QModelIndex& index) +void ResourceModel::versionRequestSucceeded(QVector& doc, QVariant pack, const QModelIndex& index) { auto current_pack = data(index, Qt::UserRole).value(); // Check if the index is still valid for this resource or not - if (pack.addonId != current_pack->addonId) + if (pack != current_pack->addonId) return; - try { - auto arr = doc.isObject() ? Json::ensureArray(doc.object(), "data") : doc.array(); - loadIndexedPackVersions(*current_pack, arr); - } catch (const JSONValidationError& e) { - qDebug() << doc; - qWarning() << "Error while reading " << debugName() << " resource version: " << e.cause(); - } + current_pack->versions = doc; + current_pack->versionsLoaded = true; // Cache info :^) QVariant new_pack; @@ -506,7 +448,7 @@ void ResourceModel::versionRequestSucceeded(QJsonDocument& doc, ModPlatform::Ind emit versionListUpdated(index); } -void ResourceModel::infoRequestSucceeded(QJsonDocument& doc, ModPlatform::IndexedPack& pack, const QModelIndex& index) +void ResourceModel::infoRequestSucceeded(ModPlatform::IndexedPack& pack, const QModelIndex& index) { auto current_pack = data(index, Qt::UserRole).value(); @@ -514,14 +456,7 @@ void ResourceModel::infoRequestSucceeded(QJsonDocument& doc, ModPlatform::Indexe if (pack.addonId != current_pack->addonId) return; - try { - auto obj = Json::requireObject(doc); - loadExtraPackInfo(*current_pack, obj); - } catch (const JSONValidationError& e) { - qDebug() << doc; - qWarning() << "Error while reading " << debugName() << " resource info: " << e.cause(); - } - + *current_pack = pack; // Cache info :^) QVariant new_pack; new_pack.setValue(current_pack); diff --git a/launcher/ui/pages/modplatform/ResourceModel.h b/launcher/ui/pages/modplatform/ResourceModel.h index cae1d6581..0b56b2b6a 100644 --- a/launcher/ui/pages/modplatform/ResourceModel.h +++ b/launcher/ui/pages/modplatform/ResourceModel.h @@ -43,10 +43,7 @@ class ResourceModel : public QAbstractListModel { virtual auto debugName() const -> QString; virtual auto metaEntryBase() const -> QString = 0; - inline int rowCount(const QModelIndex& parent) const override - { - return parent.isValid() ? 0 : static_cast(m_packs.size()); - } + inline int rowCount(const QModelIndex& parent) const override { return parent.isValid() ? 0 : static_cast(m_packs.size()); } inline int columnCount(const QModelIndex& parent) const override { return parent.isValid() ? 0 : 1; } inline auto flags(const QModelIndex& index) const -> Qt::ItemFlags override { return QAbstractListModel::flags(index); } @@ -77,13 +74,10 @@ class ResourceModel : public QAbstractListModel { void setSearchTerm(QString term) { m_search_term = term; } virtual ResourceAPI::SearchArgs createSearchArguments() = 0; - virtual ResourceAPI::SearchCallbacks createSearchCallbacks() { return {}; } virtual ResourceAPI::VersionSearchArgs createVersionsArguments(const QModelIndex&) = 0; - virtual ResourceAPI::VersionSearchCallbacks createVersionsCallbacks(const QModelIndex&) { return {}; } virtual ResourceAPI::ProjectInfoArgs createInfoArguments(const QModelIndex&) = 0; - virtual ResourceAPI::ProjectInfoCallbacks createInfoCallbacks(const QModelIndex&) { return {}; } /** Requests the API for more entries. */ virtual void search(); @@ -114,22 +108,6 @@ class ResourceModel : public QAbstractListModel { auto getCurrentSortingMethodByIndex() const -> std::optional; - /** Converts a JSON document to a common array format. - * - * This is needed so that different providers, with different JSON structures, can be parsed - * uniformally. You NEED to re-implement this if you intend on using the default callbacks. - */ - virtual auto documentToArray(QJsonDocument&) const -> QJsonArray; - - /** Functions to load data into a pack. - * - * Those are needed for the same reason as documentToArray, and NEED to be re-implemented in the same way. - */ - - virtual void loadIndexedPack(ModPlatform::IndexedPack&, QJsonObject&); - virtual void loadExtraPackInfo(ModPlatform::IndexedPack&, QJsonObject&); - virtual void loadIndexedPackVersions(ModPlatform::IndexedPack&, QJsonArray&); - virtual bool isPackInstalled(ModPlatform::IndexedPack::Ptr) const { return false; } protected: @@ -159,14 +137,14 @@ class ResourceModel : public QAbstractListModel { private: /* Default search request callbacks */ - void searchRequestSucceeded(QJsonDocument&); - void searchRequestForOneSucceeded(QJsonDocument&); + void searchRequestSucceeded(QList&); + void searchRequestForOneSucceeded(ModPlatform::IndexedPack&); void searchRequestFailed(QString reason, int network_error_code); void searchRequestAborted(); - void versionRequestSucceeded(QJsonDocument&, ModPlatform::IndexedPack&, const QModelIndex&); + void versionRequestSucceeded(QVector&, QVariant, const QModelIndex&); - void infoRequestSucceeded(QJsonDocument&, ModPlatform::IndexedPack&, const QModelIndex&); + void infoRequestSucceeded(ModPlatform::IndexedPack&, const QModelIndex&); signals: void versionListUpdated(const QModelIndex& index); diff --git a/launcher/ui/pages/modplatform/ResourcePackModel.cpp b/launcher/ui/pages/modplatform/ResourcePackModel.cpp index 986cb56a6..e774c6f64 100644 --- a/launcher/ui/pages/modplatform/ResourcePackModel.cpp +++ b/launcher/ui/pages/modplatform/ResourcePackModel.cpp @@ -8,8 +8,11 @@ namespace ResourceDownload { -ResourcePackResourceModel::ResourcePackResourceModel(BaseInstance const& base_inst, ResourceAPI* api) - : ResourceModel(api), m_base_instance(base_inst) +ResourcePackResourceModel::ResourcePackResourceModel(BaseInstance const& base_inst, + ResourceAPI* api, + QString debugName, + QString metaEntryBase) + : ResourceModel(api), m_base_instance(base_inst), m_debugName(debugName + " (Model)"), m_metaEntryBase(metaEntryBase) {} /******** Make data requests ********/ @@ -23,7 +26,7 @@ ResourceAPI::SearchArgs ResourcePackResourceModel::createSearchArguments() ResourceAPI::VersionSearchArgs ResourcePackResourceModel::createVersionsArguments(const QModelIndex& entry) { auto& pack = m_packs[entry.row()]; - return { *pack }; + return { *pack, {}, {}, ModPlatform::ResourceType::ResourcePack }; } ResourceAPI::ProjectInfoArgs ResourcePackResourceModel::createInfoArguments(const QModelIndex& entry) diff --git a/launcher/ui/pages/modplatform/ResourcePackModel.h b/launcher/ui/pages/modplatform/ResourcePackModel.h index 4f00808e8..d664ccb05 100644 --- a/launcher/ui/pages/modplatform/ResourcePackModel.h +++ b/launcher/ui/pages/modplatform/ResourcePackModel.h @@ -20,14 +20,13 @@ class ResourcePackResourceModel : public ResourceModel { Q_OBJECT public: - ResourcePackResourceModel(BaseInstance const&, ResourceAPI*); + ResourcePackResourceModel(BaseInstance const&, ResourceAPI*, QString debugName, QString metaEntryBase); /* Ask the API for more information */ void searchWithTerm(const QString& term, unsigned int sort); - void loadIndexedPack(ModPlatform::IndexedPack&, QJsonObject&) override = 0; - void loadExtraPackInfo(ModPlatform::IndexedPack&, QJsonObject&) override = 0; - void loadIndexedPackVersions(ModPlatform::IndexedPack&, QJsonArray&) override = 0; + [[nodiscard]] QString debugName() const override { return m_debugName; } + [[nodiscard]] QString metaEntryBase() const override { return m_metaEntryBase; } public slots: ResourceAPI::SearchArgs createSearchArguments() override; @@ -37,7 +36,9 @@ class ResourcePackResourceModel : public ResourceModel { protected: const BaseInstance& m_base_instance; - auto documentToArray(QJsonDocument& obj) const -> QJsonArray override = 0; + private: + QString m_debugName; + QString m_metaEntryBase; }; } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/ShaderPackModel.cpp b/launcher/ui/pages/modplatform/ShaderPackModel.cpp index b59bf182b..f54a868db 100644 --- a/launcher/ui/pages/modplatform/ShaderPackModel.cpp +++ b/launcher/ui/pages/modplatform/ShaderPackModel.cpp @@ -8,8 +8,8 @@ namespace ResourceDownload { -ShaderPackResourceModel::ShaderPackResourceModel(BaseInstance const& base_inst, ResourceAPI* api) - : ResourceModel(api), m_base_instance(base_inst) +ShaderPackResourceModel::ShaderPackResourceModel(BaseInstance const& base_inst, ResourceAPI* api, QString debugName, QString metaEntryBase) + : ResourceModel(api), m_base_instance(base_inst), m_debugName(debugName + " (Model)"), m_metaEntryBase(metaEntryBase) {} /******** Make data requests ********/ @@ -23,7 +23,7 @@ ResourceAPI::SearchArgs ShaderPackResourceModel::createSearchArguments() ResourceAPI::VersionSearchArgs ShaderPackResourceModel::createVersionsArguments(const QModelIndex& entry) { auto& pack = m_packs[entry.row()]; - return { *pack }; + return { *pack, {}, {}, ModPlatform::ResourceType::ShaderPack }; } ResourceAPI::ProjectInfoArgs ShaderPackResourceModel::createInfoArguments(const QModelIndex& entry) diff --git a/launcher/ui/pages/modplatform/ShaderPackModel.h b/launcher/ui/pages/modplatform/ShaderPackModel.h index 5bb9e58b1..9856be93e 100644 --- a/launcher/ui/pages/modplatform/ShaderPackModel.h +++ b/launcher/ui/pages/modplatform/ShaderPackModel.h @@ -20,14 +20,13 @@ class ShaderPackResourceModel : public ResourceModel { Q_OBJECT public: - ShaderPackResourceModel(BaseInstance const&, ResourceAPI*); + ShaderPackResourceModel(BaseInstance const&, ResourceAPI*, QString debugName, QString metaEntryBase); /* Ask the API for more information */ void searchWithTerm(const QString& term, unsigned int sort); - void loadIndexedPack(ModPlatform::IndexedPack&, QJsonObject&) override = 0; - void loadExtraPackInfo(ModPlatform::IndexedPack&, QJsonObject&) override = 0; - void loadIndexedPackVersions(ModPlatform::IndexedPack&, QJsonArray&) override = 0; + [[nodiscard]] QString debugName() const override { return m_debugName; } + [[nodiscard]] QString metaEntryBase() const override { return m_metaEntryBase; } public slots: ResourceAPI::SearchArgs createSearchArguments() override; @@ -37,7 +36,9 @@ class ShaderPackResourceModel : public ResourceModel { protected: const BaseInstance& m_base_instance; - auto documentToArray(QJsonDocument& obj) const -> QJsonArray override = 0; + private: + QString m_debugName; + QString m_metaEntryBase; }; } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/TexturePackModel.cpp b/launcher/ui/pages/modplatform/TexturePackModel.cpp index d56f9334b..7c1490671 100644 --- a/launcher/ui/pages/modplatform/TexturePackModel.cpp +++ b/launcher/ui/pages/modplatform/TexturePackModel.cpp @@ -12,8 +12,8 @@ static std::list s_availableVersions = {}; namespace ResourceDownload { -TexturePackResourceModel::TexturePackResourceModel(BaseInstance const& inst, ResourceAPI* api) - : ResourcePackResourceModel(inst, api), m_version_list(APPLICATION->metadataIndex()->get("net.minecraft")) +TexturePackResourceModel::TexturePackResourceModel(BaseInstance const& inst, ResourceAPI* api, QString debugName, QString metaEntryBase) + : ResourcePackResourceModel(inst, api, debugName, metaEntryBase), m_version_list(APPLICATION->metadataIndex()->get("net.minecraft")) { if (!m_version_list->isLoaded()) { qDebug() << "Loading version list..."; @@ -73,6 +73,7 @@ ResourceAPI::SearchArgs TexturePackResourceModel::createSearchArguments() ResourceAPI::VersionSearchArgs TexturePackResourceModel::createVersionsArguments(const QModelIndex& entry) { auto args = ResourcePackResourceModel::createVersionsArguments(entry); + args.resourceType = ModPlatform::ResourceType::TexturePack; if (!m_version_list->isLoaded()) { qCritical() << "The version list could not be loaded. Falling back to showing all entries."; return args; diff --git a/launcher/ui/pages/modplatform/TexturePackModel.h b/launcher/ui/pages/modplatform/TexturePackModel.h index 885bbced8..bb7348b33 100644 --- a/launcher/ui/pages/modplatform/TexturePackModel.h +++ b/launcher/ui/pages/modplatform/TexturePackModel.h @@ -13,7 +13,7 @@ class TexturePackResourceModel : public ResourcePackResourceModel { Q_OBJECT public: - TexturePackResourceModel(BaseInstance const& inst, ResourceAPI* api); + TexturePackResourceModel(BaseInstance const& inst, ResourceAPI* api, QString debugName, QString metaEntryBase); inline ::Version maximumTexturePackVersion() const { return { "1.6" }; } diff --git a/launcher/ui/pages/modplatform/flame/FlameModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModel.cpp index f8fca6570..ea051bd39 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModel.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModel.cpp @@ -20,7 +20,7 @@ ListModel::~ListModel() {} int ListModel::rowCount(const QModelIndex& parent) const { - return parent.isValid() ? 0 : modpacks.size(); + return parent.isValid() ? 0 : m_modpacks.size(); } int ListModel::columnCount(const QModelIndex& parent) const @@ -31,27 +31,27 @@ int ListModel::columnCount(const QModelIndex& parent) const QVariant ListModel::data(const QModelIndex& index, int role) const { int pos = index.row(); - if (pos >= modpacks.size() || pos < 0 || !index.isValid()) { + if (pos >= m_modpacks.size() || pos < 0 || !index.isValid()) { return QString("INVALID INDEX %1").arg(pos); } - IndexedPack pack = modpacks.at(pos); + auto pack = m_modpacks.at(pos); switch (role) { case Qt::ToolTipRole: { - if (pack.description.length() > 100) { + if (pack->description.length() > 100) { // some magic to prevent to long tooltips and replace html linebreaks - QString edit = pack.description.left(97); + QString edit = pack->description.left(97); edit = edit.left(edit.lastIndexOf("
    ")).left(edit.lastIndexOf(" ")).append("..."); return edit; } - return pack.description; + return pack->description; } case Qt::DecorationRole: { - if (m_logoMap.contains(pack.logoName)) { - return (m_logoMap.value(pack.logoName)); + if (m_logoMap.contains(pack->logoName)) { + return (m_logoMap.value(pack->logoName)); } QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); - ((ListModel*)this)->requestLogo(pack.logoName, pack.logoUrl); + ((ListModel*)this)->requestLogo(pack->logoName, pack->logoUrl); return icon; } case Qt::UserRole: { @@ -62,9 +62,9 @@ QVariant ListModel::data(const QModelIndex& index, int role) const case Qt::SizeHintRole: return QSize(0, 58); case UserDataTypes::TITLE: - return pack.name; + return pack->name; case UserDataTypes::DESCRIPTION: - return pack.description; + return pack->description; case UserDataTypes::INSTALLED: return false; default: @@ -76,11 +76,10 @@ QVariant ListModel::data(const QModelIndex& index, int role) const bool ListModel::setData(const QModelIndex& index, const QVariant& value, [[maybe_unused]] int role) { int pos = index.row(); - if (pos >= modpacks.size() || pos < 0 || !index.isValid()) + if (pos >= m_modpacks.size() || pos < 0 || !index.isValid()) return false; - Q_ASSERT(value.canConvert()); - modpacks[pos] = value.value(); + m_modpacks[pos] = value.value(); return true; } @@ -89,8 +88,8 @@ void ListModel::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].logoName == logo) { + for (int i = 0; i < m_modpacks.size(); i++) { + if (m_modpacks[i]->logoName == logo) { emit dataChanged(createIndex(i, 0), createIndex(i, 0), { Qt::DecorationRole }); } } @@ -117,8 +116,8 @@ void ListModel::requestLogo(QString logo, QString url) connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] { job->deleteLater(); emit logoLoaded(logo, QIcon(fullPath)); - if (waitingCallbacks.contains(logo)) { - waitingCallbacks.value(logo)(fullPath); + if (m_waitingCallbacks.contains(logo)) { + m_waitingCallbacks.value(logo)(fullPath); } }); @@ -148,14 +147,14 @@ Qt::ItemFlags ListModel::flags(const QModelIndex& index) const bool ListModel::canFetchMore([[maybe_unused]] const QModelIndex& parent) const { - return searchState == CanPossiblyFetchMore; + return m_searchState == CanPossiblyFetchMore; } void ListModel::fetchMore(const QModelIndex& parent) { if (parent.isValid()) return; - if (nextSearchOffset == 0) { + if (m_nextSearchOffset == 0) { qWarning() << "fetchMore with 0 offset is wrong..."; return; } @@ -164,138 +163,106 @@ void ListModel::fetchMore(const QModelIndex& parent) void ListModel::performPaginatedSearch() { - if (currentSearchTerm.startsWith("#")) { - auto projectId = currentSearchTerm.mid(1); + static const FlameAPI api; + if (m_currentSearchTerm.startsWith("#")) { + auto projectId = m_currentSearchTerm.mid(1); if (!projectId.isEmpty()) { - ResourceAPI::ProjectInfoCallbacks callbacks; + ResourceAPI::Callback callbacks; - callbacks.on_fail = [this](QString reason) { searchRequestFailed(reason); }; - callbacks.on_succeed = [this](auto& doc, auto& pack) { searchRequestForOneSucceeded(doc); }; + callbacks.on_fail = [this](QString reason, int) { searchRequestFailed(reason); }; + callbacks.on_succeed = [this](auto& pack) { searchRequestForOneSucceeded(pack); }; callbacks.on_abort = [this] { qCritical() << "Search task aborted by an unknown reason!"; searchRequestFailed("Aborted"); }; - static const FlameAPI api; - if (auto job = api.getProjectInfo({ projectId }, std::move(callbacks)); job) { - jobPtr = job; - jobPtr->start(); + if (auto job = api.getProjectInfo({ { projectId } }, std::move(callbacks)); job) { + m_jobPtr = job; + m_jobPtr->start(); } return; } } ResourceAPI::SortingMethod sort{}; - sort.index = currentSort + 1; - - auto netJob = makeShared("Flame::Search", APPLICATION->network()); - auto searchUrl = - FlameAPI().getSearchURL({ ModPlatform::ResourceType::Modpack, nextSearchOffset, currentSearchTerm, sort, m_filter->loaders, - m_filter->versions, ModPlatform::Side::NoSide, m_filter->categoryIds, m_filter->openSource }); - - netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(searchUrl.value()), response)); - jobPtr = netJob; - jobPtr->start(); - connect(netJob.get(), &NetJob::succeeded, this, &ListModel::searchRequestFinished); - connect(netJob.get(), &NetJob::failed, this, &ListModel::searchRequestFailed); + sort.index = m_currentSort + 1; + + ResourceAPI::Callback> callbacks{}; + + callbacks.on_succeed = [this](auto& doc) { searchRequestFinished(doc); }; + callbacks.on_fail = [this](QString reason, int) { searchRequestFailed(reason); }; + + auto netJob = api.searchProjects({ ModPlatform::ResourceType::Modpack, m_nextSearchOffset, m_currentSearchTerm, sort, m_filter->loaders, + m_filter->versions, ModPlatform::Side::NoSide, m_filter->categoryIds, m_filter->openSource }, + std::move(callbacks)); + + m_jobPtr = netJob; + m_jobPtr->start(); } void ListModel::searchWithTerm(const QString& term, int sort, std::shared_ptr filter, bool filterChanged) { - if (currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort && !filterChanged) { + if (m_currentSearchTerm == term && m_currentSearchTerm.isNull() == term.isNull() && m_currentSort == sort && !filterChanged) { return; } - currentSearchTerm = term; - currentSort = sort; + m_currentSearchTerm = term; + m_currentSort = sort; m_filter = filter; if (hasActiveSearchJob()) { - jobPtr->abort(); - searchState = ResetRequested; + m_jobPtr->abort(); + m_searchState = ResetRequested; return; } beginResetModel(); - modpacks.clear(); + m_modpacks.clear(); endResetModel(); - searchState = None; + m_searchState = None; - nextSearchOffset = 0; + m_nextSearchOffset = 0; performPaginatedSearch(); } -void Flame::ListModel::searchRequestFinished() +void Flame::ListModel::searchRequestFinished(QList& newList) { if (hasActiveSearchJob()) return; - QJsonParseError parse_error; - QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); - if (parse_error.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from CurseForge at " << parse_error.offset - << " reason: " << parse_error.errorString(); - qWarning() << *response; - return; - } - - QList newList; - auto packs = Json::ensureArray(doc.object(), "data"); - for (auto packRaw : packs) { - auto packObj = packRaw.toObject(); - - Flame::IndexedPack pack; - try { - Flame::loadIndexedPack(pack, packObj); - newList.append(pack); - } catch (const JSONValidationError& e) { - qWarning() << "Error while loading pack from CurseForge: " << e.cause(); - continue; - } - } - if (packs.size() < 25) { - searchState = Finished; + if (newList.size() < 25) { + m_searchState = Finished; } else { - nextSearchOffset += 25; - searchState = CanPossiblyFetchMore; + m_nextSearchOffset += 25; + m_searchState = CanPossiblyFetchMore; } // When you have a Qt build with assertions turned on, proceeding here will abort the application if (newList.size() == 0) return; - beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1); - modpacks.append(newList); + beginInsertRows(QModelIndex(), m_modpacks.size(), m_modpacks.size() + newList.size() - 1); + m_modpacks.append(newList); endInsertRows(); } -void Flame::ListModel::searchRequestForOneSucceeded(QJsonDocument& doc) +void Flame::ListModel::searchRequestForOneSucceeded(ModPlatform::IndexedPack& pack) { - jobPtr.reset(); - - auto packObj = Json::ensureObject(doc.object(), "data"); - - Flame::IndexedPack pack; - try { - Flame::loadIndexedPack(pack, packObj); - } catch (const JSONValidationError& e) { - qWarning() << "Error while loading pack from CurseForge: " << e.cause(); - return; - } + m_jobPtr.reset(); - beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + 1); - modpacks.append({ pack }); + beginInsertRows(QModelIndex(), m_modpacks.size(), m_modpacks.size() + 1); + m_modpacks.append(std::make_shared(pack)); endInsertRows(); } void Flame::ListModel::searchRequestFailed(QString reason) { - jobPtr.reset(); + m_jobPtr.reset(); - if (searchState == ResetRequested) { + if (m_searchState == ResetRequested) { beginResetModel(); - modpacks.clear(); + m_modpacks.clear(); endResetModel(); - nextSearchOffset = 0; + m_nextSearchOffset = 0; performPaginatedSearch(); } else { - searchState = Finished; + m_searchState = Finished; } } diff --git a/launcher/ui/pages/modplatform/flame/FlameModel.h b/launcher/ui/pages/modplatform/flame/FlameModel.h index bfdd81810..f98e2be96 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModel.h +++ b/launcher/ui/pages/modplatform/flame/FlameModel.h @@ -16,8 +16,6 @@ #include #include "ui/widgets/ModFilterWidget.h" -#include - namespace Flame { using LogoMap = QMap; @@ -41,8 +39,8 @@ class ListModel : public QAbstractListModel { void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback); void searchWithTerm(const QString& term, int sort, std::shared_ptr filter, bool filterChanged); - bool hasActiveSearchJob() const { return jobPtr && jobPtr->isRunning(); } - Task::Ptr activeSearchJob() { return hasActiveSearchJob() ? jobPtr : nullptr; } + bool hasActiveSearchJob() const { return m_jobPtr && m_jobPtr->isRunning(); } + Task::Ptr activeSearchJob() { return hasActiveSearchJob() ? m_jobPtr : nullptr; } private slots: void performPaginatedSearch(); @@ -50,27 +48,26 @@ class ListModel : public QAbstractListModel { void logoFailed(QString logo); void logoLoaded(QString logo, QIcon out); - void searchRequestFinished(); + void searchRequestFinished(QList&); void searchRequestFailed(QString reason); - void searchRequestForOneSucceeded(QJsonDocument&); + void searchRequestForOneSucceeded(ModPlatform::IndexedPack&); private: void requestLogo(QString file, QString url); private: - QList modpacks; + QList m_modpacks; QStringList m_failedLogos; QStringList m_loadingLogos; LogoMap m_logoMap; - QMap waitingCallbacks; + QMap m_waitingCallbacks; - QString currentSearchTerm; - int currentSort = 0; + QString m_currentSearchTerm; + int m_currentSort = 0; std::shared_ptr m_filter; - int nextSearchOffset = 0; - enum SearchState { None, CanPossiblyFetchMore, ResetRequested, Finished } searchState = None; - Task::Ptr jobPtr; - std::shared_ptr response = std::make_shared(); + int m_nextSearchOffset = 0; + enum SearchState { None, CanPossiblyFetchMore, ResetRequested, Finished } m_searchState = None; + Task::Ptr m_jobPtr; }; } // namespace Flame diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.cpp b/launcher/ui/pages/modplatform/flame/FlamePage.cpp index 5bc314cc2..059d65438 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.cpp +++ b/launcher/ui/pages/modplatform/flame/FlamePage.cpp @@ -35,7 +35,8 @@ #include "FlamePage.h" #include "Version.h" -#include "modplatform/flame/FlamePackIndex.h" +#include "modplatform/ModIndex.h" +#include "modplatform/ResourceAPI.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui/widgets/ModFilterWidget.h" #include "ui_FlamePage.h" @@ -43,29 +44,25 @@ #include #include -#include "Application.h" #include "FlameModel.h" #include "InstanceImportTask.h" -#include "Json.h" #include "StringUtils.h" #include "modplatform/flame/FlameAPI.h" #include "ui/dialogs/NewInstanceDialog.h" #include "ui/widgets/ProjectItem.h" -#include "net/ApiDownload.h" - static FlameAPI api; FlamePage::FlamePage(NewInstanceDialog* dialog, QWidget* parent) - : QWidget(parent), ui(new Ui::FlamePage), dialog(dialog), m_fetch_progress(this, false) + : QWidget(parent), m_ui(new Ui::FlamePage), m_dialog(dialog), m_fetch_progress(this, false) { - ui->setupUi(this); - ui->searchEdit->installEventFilter(this); - listModel = new Flame::ListModel(this); - ui->packView->setModel(listModel); + m_ui->setupUi(this); + m_ui->searchEdit->installEventFilter(this); + m_listModel = new Flame::ListModel(this); + m_ui->packView->setModel(m_listModel); - ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); - ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); + m_ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + m_ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); m_search_timer.setTimerType(Qt::TimerType::CoarseTimer); m_search_timer.setSingleShot(true); @@ -76,33 +73,33 @@ FlamePage::FlamePage(NewInstanceDialog* dialog, QWidget* parent) m_fetch_progress.setFixedHeight(24); m_fetch_progress.progressFormat(""); - ui->verticalLayout->insertWidget(2, &m_fetch_progress); + m_ui->verticalLayout->insertWidget(2, &m_fetch_progress); // index is used to set the sorting with the curseforge api - 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, &QComboBox::currentIndexChanged, this, &FlamePage::triggerSearch); - connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlamePage::onSelectionChanged); - connect(ui->versionSelectionBox, &QComboBox::currentIndexChanged, this, &FlamePage::onVersionSelectionChanged); - - ui->packView->setItemDelegate(new ProjectItemDelegate(this)); - ui->packDescription->setMetaEntry("FlamePacks"); + m_ui->sortByBox->addItem(tr("Sort by Featured")); + m_ui->sortByBox->addItem(tr("Sort by Popularity")); + m_ui->sortByBox->addItem(tr("Sort by Last Updated")); + m_ui->sortByBox->addItem(tr("Sort by Name")); + m_ui->sortByBox->addItem(tr("Sort by Author")); + m_ui->sortByBox->addItem(tr("Sort by Total Downloads")); + + connect(m_ui->sortByBox, &QComboBox::currentIndexChanged, this, &FlamePage::triggerSearch); + connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &FlamePage::onSelectionChanged); + connect(m_ui->versionSelectionBox, &QComboBox::currentIndexChanged, this, &FlamePage::onVersionSelectionChanged); + + m_ui->packView->setItemDelegate(new ProjectItemDelegate(this)); + m_ui->packDescription->setMetaEntry("FlamePacks"); createFilterWidget(); } FlamePage::~FlamePage() { - delete ui; + delete m_ui; } bool FlamePage::eventFilter(QObject* watched, QEvent* event) { - if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) { + if (watched == m_ui->searchEdit && event->type() == QEvent::KeyPress) { QKeyEvent* keyEvent = static_cast(event); if (keyEvent->key() == Qt::Key_Return) { triggerSearch(); @@ -125,7 +122,7 @@ bool FlamePage::shouldDisplay() const void FlamePage::retranslate() { - ui->retranslateUi(this); + m_ui->retranslateUi(this); } void FlamePage::openedImpl() @@ -136,109 +133,91 @@ void FlamePage::openedImpl() void FlamePage::triggerSearch() { - ui->packView->selectionModel()->setCurrentIndex({}, QItemSelectionModel::SelectionFlag::ClearAndSelect); - ui->packView->clearSelection(); - ui->packDescription->clear(); - ui->versionSelectionBox->clear(); + m_ui->packView->selectionModel()->setCurrentIndex({}, QItemSelectionModel::SelectionFlag::ClearAndSelect); + m_ui->packView->clearSelection(); + m_ui->packDescription->clear(); + m_ui->versionSelectionBox->clear(); bool filterChanged = m_filterWidget->changed(); - listModel->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex(), m_filterWidget->getFilter(), filterChanged); - m_fetch_progress.watch(listModel->activeSearchJob().get()); -} - -bool checkVersionFilters(const Flame::IndexedVersion& v, std::shared_ptr filter) -{ - if (!filter) - return true; - return ((!filter->loaders || !v.loaders || filter->loaders & v.loaders) && // loaders - (filter->releases.empty() || // releases - std::find(filter->releases.cbegin(), filter->releases.cend(), v.version_type) != filter->releases.cend()) && - filter->checkMcVersions({ v.mcVersion })); // mcVersions} + m_listModel->searchWithTerm(m_ui->searchEdit->text(), m_ui->sortByBox->currentIndex(), m_filterWidget->getFilter(), filterChanged); + m_fetch_progress.watch(m_listModel->activeSearchJob().get()); } void FlamePage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelIndex prev) { - ui->versionSelectionBox->clear(); + m_ui->versionSelectionBox->clear(); if (!curr.isValid()) { if (isOpened) { - dialog->setSuggestedPack(); + m_dialog->setSuggestedPack(); } return; } - QVariant raw = listModel->data(curr, Qt::UserRole); - Q_ASSERT(raw.canConvert()); - current = raw.value(); + m_current = m_listModel->data(curr, Qt::UserRole).value(); - if (!current.versionsLoaded || m_filterWidget->changed()) { + if (!m_current->versionsLoaded || m_filterWidget->changed()) { qDebug() << "Loading flame modpack versions"; - auto netJob = new NetJob(QString("Flame::PackVersions(%1)").arg(current.name), APPLICATION->network()); - auto response = std::make_shared(); - int addonId = current.addonId; - netJob->addNetAction( - Net::ApiDownload::makeByteArray(QString(BuildConfig.FLAME_BASE_URL + "/mods/%1/files").arg(addonId), response)); - - connect(netJob, &NetJob::succeeded, this, [this, response, addonId, curr] { - if (addonId != current.addonId) { + + ResourceAPI::Callback > callbacks{}; + + auto addonId = m_current->addonId; + // Use default if no callbacks are set + callbacks.on_succeed = [this, curr, addonId](auto& doc) { + if (addonId != m_current->addonId) { 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 CurseForge at " << parse_error.offset - << " reason: " << parse_error.errorString(); - qWarning() << *response; - return; - } - auto arr = Json::ensureArray(doc.object(), "data"); - try { - Flame::loadIndexedPackVersions(current, arr); - } catch (const JSONValidationError& e) { - qDebug() << *response; - qWarning() << "Error while reading flame modpack version: " << e.cause(); - } - auto pred = [this](const Flame::IndexedVersion& v) { return !checkVersionFilters(v, m_filterWidget->getFilter()); }; + m_current->versions = doc; + m_current->versionsLoaded = true; + auto pred = [this](const ModPlatform::IndexedVersion& v) { + if (auto filter = m_filterWidget->getFilter()) + return !filter->checkModpackFilters(v); + return false; + }; #if QT_VERSION >= QT_VERSION_CHECK(6, 1, 0) - current.versions.removeIf(pred); + m_current->versions.removeIf(pred); #else - for (auto it = current.versions.begin(); it != current.versions.end();) - if (pred(*it)) - it = current.versions.erase(it); - else - ++it; + for (auto it = m_current->versions.begin(); it != m_current->versions.end();) + if (pred(*it)) + it = m_current->versions.erase(it); + else + ++it; #endif - for (const auto& version : current.versions) { - ui->versionSelectionBox->addItem(Flame::getVersionDisplayString(version), QVariant(version.downloadUrl)); + for (auto version : m_current->versions) { + m_ui->versionSelectionBox->addItem(version.getVersionDisplayString(), QVariant(version.downloadUrl)); } QVariant current_updated; - current_updated.setValue(current); + current_updated.setValue(m_current); - if (!listModel->setData(curr, current_updated, Qt::UserRole)) + if (!m_listModel->setData(curr, current_updated, Qt::UserRole)) qWarning() << "Failed to cache versions for the current pack!"; // TODO: Check whether it's a connection issue or the project disabled 3rd-party distribution. - if (current.versionsLoaded && ui->versionSelectionBox->count() < 1) { - ui->versionSelectionBox->addItem(tr("No version is available!"), -1); + if (m_current->versionsLoaded && m_ui->versionSelectionBox->count() < 1) { + m_ui->versionSelectionBox->addItem(tr("No version is available!"), -1); } suggestCurrent(); - }); - connect(netJob, &NetJob::finished, this, [response, netJob] { netJob->deleteLater(); }); - connect(netJob, &NetJob::failed, - [this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); + }; + callbacks.on_fail = [this](QString reason, int) { + CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); + }; + + auto netJob = api.getProjectVersions({ *m_current, {}, {}, ModPlatform::ResourceType::Modpack }, std::move(callbacks)); + + m_job = netJob; netJob->start(); } else { - for (auto version : current.versions) { - ui->versionSelectionBox->addItem(version.version, QVariant(version.downloadUrl)); + for (auto version : m_current->versions) { + m_ui->versionSelectionBox->addItem(version.version, QVariant(version.downloadUrl)); } suggestCurrent(); } // TODO: Check whether it's a connection issue or the project disabled 3rd-party distribution. - if (current.versionsLoaded && ui->versionSelectionBox->count() < 1) { - ui->versionSelectionBox->addItem(tr("No version is available!"), -1); + if (m_current->versionsLoaded && m_ui->versionSelectionBox->count() < 1) { + m_ui->versionSelectionBox->addItem(tr("No version is available!"), -1); } updateUi(); @@ -251,26 +230,26 @@ void FlamePage::suggestCurrent() } if (m_selected_version_index == -1) { - dialog->setSuggestedPack(); + m_dialog->setSuggestedPack(); return; } - auto version = current.versions.at(m_selected_version_index); + auto version = m_current->versions.at(m_selected_version_index); QMap extra_info; - extra_info.insert("pack_id", QString::number(current.addonId)); - extra_info.insert("pack_version_id", QString::number(version.fileId)); + extra_info.insert("pack_id", m_current->addonId.toString()); + extra_info.insert("pack_version_id", version.fileId.toString()); - dialog->setSuggestedPack(current.name, new InstanceImportTask(version.downloadUrl, this, std::move(extra_info))); - QString editedLogoName = "curseforge_" + current.logoName; - listModel->getLogo(current.logoName, current.logoUrl, - [this, editedLogoName](QString logo) { dialog->setSuggestedIconFromFile(logo, editedLogoName); }); + m_dialog->setSuggestedPack(m_current->name, new InstanceImportTask(version.downloadUrl, this, std::move(extra_info))); + QString editedLogoName = "curseforge_" + m_current->logoName; + m_listModel->getLogo(m_current->logoName, m_current->logoUrl, + [this, editedLogoName](QString logo) { m_dialog->setSuggestedIconFromFile(logo, editedLogoName); }); } void FlamePage::onVersionSelectionChanged(int index) { bool is_blocked = false; - ui->versionSelectionBox->itemData(index).toInt(&is_blocked); + m_ui->versionSelectionBox->itemData(index).toInt(&is_blocked); if (index == -1 || is_blocked) { m_selected_version_index = -1; @@ -279,7 +258,7 @@ void FlamePage::onVersionSelectionChanged(int index) m_selected_version_index = index; - Q_ASSERT(current.versions.at(m_selected_version_index).downloadUrl == ui->versionSelectionBox->currentData().toString()); + Q_ASSERT(m_current->versions.at(m_selected_version_index).downloadUrl == m_ui->versionSelectionBox->currentData().toString()); suggestCurrent(); } @@ -287,66 +266,67 @@ void FlamePage::onVersionSelectionChanged(int index) void FlamePage::updateUi() { QString text = ""; - QString name = current.name; + QString name = m_current->name; - if (current.extra.websiteUrl.isEmpty()) + if (m_current->websiteUrl.isEmpty()) text = name; else - text = "" + name + ""; - if (!current.authors.empty()) { - auto authorToStr = [](Flame::ModpackAuthor& author) { + text = "websiteUrl + "\">" + name + ""; + if (!m_current->authors.empty()) { + auto authorToStr = [](ModPlatform::ModpackAuthor& author) { if (author.url.isEmpty()) { return author.name; } return QString("%2").arg(author.url, author.name); }; QStringList authorStrs; - for (auto& author : current.authors) { + for (auto& author : m_current->authors) { authorStrs.push_back(authorToStr(author)); } text += "
    " + tr(" by ") + authorStrs.join(", "); } - if (current.extraInfoLoaded) { - if (!current.extra.issuesUrl.isEmpty() || !current.extra.sourceUrl.isEmpty() || !current.extra.wikiUrl.isEmpty()) { + if (m_current->extraDataLoaded) { + if (!m_current->extraData.issuesUrl.isEmpty() || !m_current->extraData.sourceUrl.isEmpty() || + !m_current->extraData.wikiUrl.isEmpty()) { text += "

    " + tr("External links:") + "
    "; } - if (!current.extra.issuesUrl.isEmpty()) - text += "- " + tr("Issues: %1").arg(current.extra.issuesUrl) + "
    "; - if (!current.extra.wikiUrl.isEmpty()) - text += "- " + tr("Wiki: %1").arg(current.extra.wikiUrl) + "
    "; - if (!current.extra.sourceUrl.isEmpty()) - text += "- " + tr("Source code: %1").arg(current.extra.sourceUrl) + "
    "; + if (!m_current->extraData.issuesUrl.isEmpty()) + text += "- " + tr("Issues: %1").arg(m_current->extraData.issuesUrl) + "
    "; + if (!m_current->extraData.wikiUrl.isEmpty()) + text += "- " + tr("Wiki: %1").arg(m_current->extraData.wikiUrl) + "
    "; + if (!m_current->extraData.sourceUrl.isEmpty()) + text += "- " + tr("Source code: %1").arg(m_current->extraData.sourceUrl) + "
    "; } text += "
    "; - text += api.getModDescription(current.addonId).toUtf8(); + text += api.getModDescription(m_current->addonId.toInt()).toUtf8(); - ui->packDescription->setHtml(StringUtils::htmlListPatch(text + current.description)); - ui->packDescription->flush(); + m_ui->packDescription->setHtml(StringUtils::htmlListPatch(text + m_current->description)); + m_ui->packDescription->flush(); } QString FlamePage::getSerachTerm() const { - return ui->searchEdit->text(); + return m_ui->searchEdit->text(); } void FlamePage::setSearchTerm(QString term) { - ui->searchEdit->setText(term); + m_ui->searchEdit->setText(term); } void FlamePage::createFilterWidget() { auto widget = ModFilterWidget::create(nullptr, false); m_filterWidget.swap(widget); - auto old = ui->splitter->replaceWidget(0, m_filterWidget.get()); + auto old = m_ui->splitter->replaceWidget(0, m_filterWidget.get()); // because we replaced the widget we also need to delete it if (old) { delete old; } - connect(ui->filterButton, &QPushButton::clicked, this, [this] { m_filterWidget->setHidden(!m_filterWidget->isHidden()); }); + connect(m_ui->filterButton, &QPushButton::clicked, this, [this] { m_filterWidget->setHidden(!m_filterWidget->isHidden()); }); connect(m_filterWidget.get(), &ModFilterWidget::filterChanged, this, &FlamePage::triggerSearch); auto response = std::make_shared(); diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.h b/launcher/ui/pages/modplatform/flame/FlamePage.h index 19c8d4dbc..2252efa07 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.h +++ b/launcher/ui/pages/modplatform/flame/FlamePage.h @@ -38,8 +38,8 @@ #include #include -#include #include +#include "modplatform/ModIndex.h" #include "ui/pages/modplatform/ModpackProviderBasePage.h" #include "ui/widgets/ModFilterWidget.h" #include "ui/widgets/ProgressWidget.h" @@ -88,10 +88,10 @@ class FlamePage : public QWidget, public ModpackProviderBasePage { void createFilterWidget(); private: - Ui::FlamePage* ui = nullptr; - NewInstanceDialog* dialog = nullptr; - Flame::ListModel* listModel = nullptr; - Flame::IndexedPack current; + Ui::FlamePage* m_ui = nullptr; + NewInstanceDialog* m_dialog = nullptr; + Flame::ListModel* m_listModel = nullptr; + ModPlatform::IndexedPack::Ptr m_current; int m_selected_version_index = -1; @@ -102,4 +102,5 @@ class FlamePage : public QWidget, public ModpackProviderBasePage { std::unique_ptr m_filterWidget; Task::Ptr m_categoriesTask; + Task::Ptr m_job; }; diff --git a/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp b/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp index 9917c29e6..a40e6d5a3 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameResourceModels.cpp @@ -8,7 +8,7 @@ #include "minecraft/PackProfile.h" #include "modplatform/flame/FlameAPI.h" -#include "modplatform/flame/FlameModIndex.h" +#include "ui/pages/modplatform/flame/FlameResourcePages.h" namespace ResourceDownload { @@ -17,97 +17,9 @@ static bool isOptedOut(const ModPlatform::IndexedVersion& ver) return ver.downloadUrl.isEmpty(); } -FlameModModel::FlameModModel(BaseInstance& base) : ModModel(base, new FlameAPI) {} - -void FlameModModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) -{ - FlameMod::loadIndexedPack(m, obj); -} - -// We already deal with the URLs when initializing the pack, due to the API response's structure -void FlameModModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) -{ - FlameMod::loadBody(m, obj); -} - -void FlameModModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) -{ - FlameMod::loadIndexedPackVersions(m, arr); -} - -auto FlameModModel::loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) -> ModPlatform::IndexedVersion -{ - return FlameMod::loadDependencyVersions(m, arr, &m_base_instance); -} - -bool FlameModModel::optedOut(const ModPlatform::IndexedVersion& ver) const -{ - return isOptedOut(ver); -} - -auto FlameModModel::documentToArray(QJsonDocument& obj) const -> QJsonArray -{ - return Json::ensureArray(obj.object(), "data"); -} - -FlameResourcePackModel::FlameResourcePackModel(const BaseInstance& base) : ResourcePackResourceModel(base, new FlameAPI) {} - -void FlameResourcePackModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) -{ - FlameMod::loadIndexedPack(m, obj); -} - -// We already deal with the URLs when initializing the pack, due to the API response's structure -void FlameResourcePackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) -{ - FlameMod::loadBody(m, obj); -} - -void FlameResourcePackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) -{ - FlameMod::loadIndexedPackVersions(m, arr); -} - -bool FlameResourcePackModel::optedOut(const ModPlatform::IndexedVersion& ver) const -{ - return isOptedOut(ver); -} - -auto FlameResourcePackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray -{ - return Json::ensureArray(obj.object(), "data"); -} - -FlameTexturePackModel::FlameTexturePackModel(const BaseInstance& base) : TexturePackResourceModel(base, new FlameAPI) {} - -void FlameTexturePackModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) -{ - FlameMod::loadIndexedPack(m, obj); -} - -// We already deal with the URLs when initializing the pack, due to the API response's structure -void FlameTexturePackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) -{ - FlameMod::loadBody(m, obj); -} - -void FlameTexturePackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) -{ - FlameMod::loadIndexedPackVersions(m, arr); - - QList filtered_versions(m.versions.size()); - - // FIXME: Client-side version filtering. This won't take into account any user-selected filtering. - for (auto const& version : m.versions) { - auto const& mc_versions = version.mcVersion; - - if (std::any_of(mc_versions.constBegin(), mc_versions.constEnd(), - [this](auto const& mc_version) { return Version(mc_version) <= maximumTexturePackVersion(); })) - filtered_versions.push_back(version); - } - - m.versions = filtered_versions; -} +FlameTexturePackModel::FlameTexturePackModel(const BaseInstance& base) + : TexturePackResourceModel(base, new FlameAPI, Flame::debugName(), Flame::metaEntryBase()) +{} ResourceAPI::SearchArgs FlameTexturePackModel::createSearchArguments() { @@ -137,65 +49,4 @@ bool FlameTexturePackModel::optedOut(const ModPlatform::IndexedVersion& ver) con return isOptedOut(ver); } -auto FlameTexturePackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray -{ - return Json::ensureArray(obj.object(), "data"); -} - -FlameShaderPackModel::FlameShaderPackModel(const BaseInstance& base) : ShaderPackResourceModel(base, new FlameAPI) {} - -void FlameShaderPackModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) -{ - FlameMod::loadIndexedPack(m, obj); -} - -// We already deal with the URLs when initializing the pack, due to the API response's structure -void FlameShaderPackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) -{ - FlameMod::loadBody(m, obj); -} - -void FlameShaderPackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) -{ - FlameMod::loadIndexedPackVersions(m, arr); -} - -bool FlameShaderPackModel::optedOut(const ModPlatform::IndexedVersion& ver) const -{ - return isOptedOut(ver); -} - -auto FlameShaderPackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray -{ - return Json::ensureArray(obj.object(), "data"); -} - -FlameDataPackModel::FlameDataPackModel(const BaseInstance& base) : DataPackResourceModel(base, new FlameAPI) {} - -void FlameDataPackModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) -{ - FlameMod::loadIndexedPack(m, obj); -} - -// We already deal with the URLs when initializing the pack, due to the API response's structure -void FlameDataPackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) -{ - FlameMod::loadBody(m, obj); -} - -void FlameDataPackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) -{ - FlameMod::loadIndexedPackVersions(m, arr); -} - -bool FlameDataPackModel::optedOut(const ModPlatform::IndexedVersion& ver) const -{ - return isOptedOut(ver); -} - -auto FlameDataPackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray -{ - return Json::ensureArray(obj.object(), "data"); -} - } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/flame/FlameResourceModels.h b/launcher/ui/pages/modplatform/flame/FlameResourceModels.h index 2cdc2910d..76062f8e6 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourceModels.h +++ b/launcher/ui/pages/modplatform/flame/FlameResourceModels.h @@ -5,52 +5,10 @@ #pragma once #include "ui/pages/modplatform/ModModel.h" -#include "ui/pages/modplatform/ResourcePackModel.h" #include "ui/pages/modplatform/flame/FlameResourcePages.h" namespace ResourceDownload { -class FlameModModel : public ModModel { - Q_OBJECT - - public: - FlameModModel(BaseInstance&); - ~FlameModModel() override = default; - - bool optedOut(const ModPlatform::IndexedVersion& ver) const override; - - private: - QString debugName() const override { return Flame::debugName() + " (Model)"; } - QString metaEntryBase() const override { return Flame::metaEntryBase(); } - - void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; - void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override; - void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override; - auto loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) -> ModPlatform::IndexedVersion override; - - auto documentToArray(QJsonDocument& obj) const -> QJsonArray override; -}; - -class FlameResourcePackModel : public ResourcePackResourceModel { - Q_OBJECT - - public: - FlameResourcePackModel(const BaseInstance&); - ~FlameResourcePackModel() override = default; - - bool optedOut(const ModPlatform::IndexedVersion& ver) const override; - - private: - QString debugName() const override { return Flame::debugName() + " (Model)"; } - QString metaEntryBase() const override { return Flame::metaEntryBase(); } - - void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; - void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override; - void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override; - - auto documentToArray(QJsonDocument& obj) const -> QJsonArray override; -}; - class FlameTexturePackModel : public TexturePackResourceModel { Q_OBJECT @@ -64,52 +22,8 @@ class FlameTexturePackModel : public TexturePackResourceModel { QString debugName() const override { return Flame::debugName() + " (Model)"; } QString metaEntryBase() const override { return Flame::metaEntryBase(); } - void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; - void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override; - void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override; - ResourceAPI::SearchArgs createSearchArguments() override; ResourceAPI::VersionSearchArgs createVersionsArguments(const QModelIndex&) override; - - auto documentToArray(QJsonDocument& obj) const -> QJsonArray override; -}; - -class FlameShaderPackModel : public ShaderPackResourceModel { - Q_OBJECT - - public: - FlameShaderPackModel(const BaseInstance&); - ~FlameShaderPackModel() override = default; - - bool optedOut(const ModPlatform::IndexedVersion& ver) const override; - - private: - QString debugName() const override { return Flame::debugName() + " (Model)"; } - QString metaEntryBase() const override { return Flame::metaEntryBase(); } - - void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; - void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override; - void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override; - auto documentToArray(QJsonDocument& obj) const -> QJsonArray override; -}; - -class FlameDataPackModel : public DataPackResourceModel { - Q_OBJECT - - public: - FlameDataPackModel(const BaseInstance&); - ~FlameDataPackModel() override = default; - - bool optedOut(const ModPlatform::IndexedVersion& ver) const override; - - private: - QString debugName() const override { return Flame::debugName() + " (Model)"; } - QString metaEntryBase() const override { return Flame::metaEntryBase(); } - - void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; - void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override; - void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override; - auto documentToArray(QJsonDocument& obj) const -> QJsonArray override; }; } // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp index bf421c036..6ff435854 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameResourcePages.cpp @@ -40,7 +40,6 @@ #include "FlameResourcePages.h" #include #include -#include "modplatform/ModIndex.h" #include "modplatform/flame/FlameAPI.h" #include "ui_ResourcePage.h" @@ -51,7 +50,7 @@ namespace ResourceDownload { FlameModPage::FlameModPage(ModDownloadDialog* dialog, BaseInstance& instance) : ModPage(dialog, instance) { - m_model = new FlameModModel(instance); + m_model = new ModModel(instance, new FlameAPI(), Flame::debugName(), Flame::metaEntryBase()); m_ui->packView->setModel(m_model); addSortings(); @@ -85,7 +84,7 @@ void FlameModPage::openUrl(const QUrl& url) FlameResourcePackPage::FlameResourcePackPage(ResourcePackDownloadDialog* dialog, BaseInstance& instance) : ResourcePackResourcePage(dialog, instance) { - m_model = new FlameResourcePackModel(instance); + m_model = new ResourcePackResourceModel(instance, new FlameAPI(), Flame::debugName(), Flame::metaEntryBase()); m_ui->packView->setModel(m_model); addSortings(); @@ -169,7 +168,7 @@ void FlameDataPackPage::openUrl(const QUrl& url) FlameShaderPackPage::FlameShaderPackPage(ShaderPackDownloadDialog* dialog, BaseInstance& instance) : ShaderPackResourcePage(dialog, instance) { - m_model = new FlameShaderPackModel(instance); + m_model = new ShaderPackResourceModel(instance, new FlameAPI(), Flame::debugName(), Flame::metaEntryBase()); m_ui->packView->setModel(m_model); addSortings(); @@ -184,10 +183,9 @@ FlameShaderPackPage::FlameShaderPackPage(ShaderPackDownloadDialog* dialog, BaseI m_ui->packDescription->setMetaEntry(metaEntryBase()); } -FlameDataPackPage::FlameDataPackPage(DataPackDownloadDialog* dialog, BaseInstance& instance) - : DataPackResourcePage(dialog, instance) +FlameDataPackPage::FlameDataPackPage(DataPackDownloadDialog* dialog, BaseInstance& instance) : DataPackResourcePage(dialog, instance) { - m_model = new FlameDataPackModel(instance); + m_model = new DataPackResourceModel(instance, new FlameAPI(), Flame::debugName(), Flame::metaEntryBase()); m_ui->packView->setModel(m_model); addSortings(); diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index 514b33574..bf6215356 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -62,7 +62,7 @@ void ModpackListModel::fetchMore(const QModelIndex& parent) { if (parent.isValid()) return; - if (nextSearchOffset == 0) { + if (m_nextSearchOffset == 0) { qWarning() << "fetchMore with 0 offset is wrong..."; return; } @@ -72,27 +72,27 @@ void ModpackListModel::fetchMore(const QModelIndex& parent) auto ModpackListModel::data(const QModelIndex& index, int role) const -> QVariant { int pos = index.row(); - if (pos >= modpacks.size() || pos < 0 || !index.isValid()) { + if (pos >= m_modpacks.size() || pos < 0 || !index.isValid()) { return QString("INVALID INDEX %1").arg(pos); } - Modrinth::Modpack pack = modpacks.at(pos); + auto pack = m_modpacks.at(pos); switch (role) { case Qt::ToolTipRole: { - if (pack.description.length() > 100) { + if (pack->description.length() > 100) { // some magic to prevent to long tooltips and replace html linebreaks - QString edit = pack.description.left(97); + QString edit = pack->description.left(97); edit = edit.left(edit.lastIndexOf("
    ")).left(edit.lastIndexOf(" ")).append("..."); return edit; } - return pack.description; + return pack->description; } case Qt::DecorationRole: { - if (m_logoMap.contains(pack.iconName)) - return m_logoMap.value(pack.iconName); + if (m_logoMap.contains(pack->logoName)) + return m_logoMap.value(pack->logoName); QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); - ((ModpackListModel*)this)->requestLogo(pack.iconName, pack.iconUrl.toString()); + ((ModpackListModel*)this)->requestLogo(pack->logoName, pack->logoUrl); return icon; } case Qt::UserRole: { @@ -104,9 +104,9 @@ auto ModpackListModel::data(const QModelIndex& index, int role) const -> QVarian return QSize(0, 58); // Custom data case UserDataTypes::TITLE: - return pack.name; + return pack->name; case UserDataTypes::DESCRIPTION: - return pack.description; + return pack->description; case UserDataTypes::INSTALLED: return false; default: @@ -119,11 +119,10 @@ auto ModpackListModel::data(const QModelIndex& index, int role) const -> QVarian bool ModpackListModel::setData(const QModelIndex& index, const QVariant& value, [[maybe_unused]] int role) { int pos = index.row(); - if (pos >= modpacks.size() || pos < 0 || !index.isValid()) + if (pos >= m_modpacks.size() || pos < 0 || !index.isValid()) return false; - Q_ASSERT(value.canConvert()); - modpacks[pos] = value.value(); + m_modpacks[pos] = value.value(); return true; } @@ -132,68 +131,56 @@ void ModpackListModel::performPaginatedSearch() { if (hasActiveSearchJob()) return; + static const ModrinthAPI api; - if (currentSearchTerm.startsWith("#")) { - auto projectId = currentSearchTerm.mid(1); + if (m_currentSearchTerm.startsWith("#")) { + auto projectId = m_currentSearchTerm.mid(1); if (!projectId.isEmpty()) { - ResourceAPI::ProjectInfoCallbacks callbacks; + ResourceAPI::Callback callbacks; - callbacks.on_fail = [this](QString reason) { searchRequestFailed(reason); }; - callbacks.on_succeed = [this](auto& doc, auto&) { searchRequestForOneSucceeded(doc); }; + callbacks.on_fail = [this](QString reason, int) { searchRequestFailed(reason); }; + callbacks.on_succeed = [this](auto& pack) { searchRequestForOneSucceeded(pack); }; callbacks.on_abort = [this] { qCritical() << "Search task aborted by an unknown reason!"; searchRequestFailed("Aborted"); }; - static const ModrinthAPI api; if (auto job = api.getProjectInfo({ projectId }, std::move(callbacks)); job) { - jobPtr = job; - jobPtr->start(); + m_jobPtr = job; + m_jobPtr->start(); } return; } } // TODO: Move to standalone API ResourceAPI::SortingMethod sort{}; - sort.name = currentSort; - auto searchUrl = - ModrinthAPI().getSearchURL({ ModPlatform::ResourceType::Modpack, nextSearchOffset, currentSearchTerm, sort, m_filter->loaders, - m_filter->versions, ModPlatform::Side::NoSide, m_filter->categoryIds, m_filter->openSource }); - - auto netJob = makeShared("Modrinth::SearchModpack", APPLICATION->network()); - netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(searchUrl.value()), m_allResponse)); - - connect(netJob.get(), &NetJob::succeeded, this, [this] { - QJsonParseError parseError{}; - - QJsonDocument doc = QJsonDocument::fromJson(*m_allResponse, &parseError); - if (parseError.error != QJsonParseError::NoError) { - qWarning() << "Error while parsing JSON response from " << debugName() << " at " << parseError.offset - << " reason: " << parseError.errorString(); - qWarning() << *m_allResponse; - return; - } + sort.name = m_currentSort; - searchRequestFinished(doc); - }); - connect(netJob.get(), &NetJob::failed, this, &ModpackListModel::searchRequestFailed); + ResourceAPI::Callback> callbacks{}; + + callbacks.on_succeed = [this](auto& doc) { searchRequestFinished(doc); }; + callbacks.on_fail = [this](QString reason, int) { searchRequestFailed(reason); }; - jobPtr = netJob; - jobPtr->start(); + auto netJob = api.searchProjects({ ModPlatform::ResourceType::Modpack, m_nextSearchOffset, m_currentSearchTerm, sort, m_filter->loaders, + m_filter->versions, ModPlatform::Side::NoSide, m_filter->categoryIds, m_filter->openSource }, + std::move(callbacks)); + + m_jobPtr = netJob; + m_jobPtr->start(); } void ModpackListModel::refresh() { if (hasActiveSearchJob()) { - jobPtr->abort(); - searchState = ResetRequested; + m_jobPtr->abort(); + m_searchState = ResetRequested; return; } beginResetModel(); - modpacks.clear(); + m_modpacks.clear(); endResetModel(); - searchState = None; + m_searchState = None; - nextSearchOffset = 0; + m_nextSearchOffset = 0; performPaginatedSearch(); } @@ -224,12 +211,12 @@ void ModpackListModel::searchWithTerm(const QString& term, auto sort_str = sortFromIndex(sort); - if (currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort_str && !filterChanged) { + if (m_currentSearchTerm == term && m_currentSearchTerm.isNull() == term.isNull() && m_currentSort == sort_str && !filterChanged) { return; } - currentSearchTerm = term; - currentSort = sort_str; + m_currentSearchTerm = term; + m_currentSort = sort_str; m_filter = filter; refresh(); @@ -259,8 +246,8 @@ void ModpackListModel::requestLogo(QString logo, QString url) connect(job, &NetJob::succeeded, this, [this, logo, fullPath, job] { job->deleteLater(); emit logoLoaded(logo, QIcon(fullPath)); - if (waitingCallbacks.contains(logo)) { - waitingCallbacks.value(logo)(fullPath); + if (m_waitingCallbacks.contains(logo)) { + m_waitingCallbacks.value(logo)(fullPath); } }); @@ -279,8 +266,8 @@ 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) { + for (int i = 0; i < m_modpacks.size(); i++) { + if (m_modpacks[i]->logoName == logo) { emit dataChanged(createIndex(i, 0), createIndex(i, 0), { Qt::DecorationRole }); } } @@ -292,65 +279,38 @@ void ModpackListModel::logoFailed(QString logo) m_loadingLogos.removeAll(logo); } -void ModpackListModel::searchRequestFinished(QJsonDocument& doc_all) +void ModpackListModel::searchRequestFinished(QList& newList) { - jobPtr.reset(); - - QList newList; - - auto packs_all = doc_all.object().value("hits").toArray(); - for (auto packRaw : packs_all) { - auto packObj = packRaw.toObject(); + m_jobPtr.reset(); - 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() < m_modpacks_per_page) { - searchState = Finished; + if (newList.size() < m_modpacks_per_page) { + m_searchState = Finished; } else { - nextSearchOffset += m_modpacks_per_page; - searchState = CanPossiblyFetchMore; + m_nextSearchOffset += m_modpacks_per_page; + m_searchState = CanPossiblyFetchMore; } // When you have a Qt build with assertions turned on, proceeding here will abort the application if (newList.size() == 0) return; - beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1); - modpacks.append(newList); + beginInsertRows(QModelIndex(), m_modpacks.size(), m_modpacks.size() + newList.size() - 1); + m_modpacks.append(newList); endInsertRows(); } -void ModpackListModel::searchRequestForOneSucceeded(QJsonDocument& doc) +void ModpackListModel::searchRequestForOneSucceeded(ModPlatform::IndexedPack& pack) { - jobPtr.reset(); - - auto packObj = doc.object(); - - Modrinth::Modpack pack; - try { - Modrinth::loadIndexedPack(pack, packObj); - pack.id = Json::ensureString(packObj, "id", pack.id); - } catch (const JSONValidationError& e) { - qWarning() << "Error while loading mod from " << m_parent->debugName() << ": " << e.cause(); - return; - } + m_jobPtr.reset(); - beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + 1); - modpacks.append({ pack }); + beginInsertRows(QModelIndex(), m_modpacks.size(), m_modpacks.size() + 1); + m_modpacks.append(std::make_shared(pack)); endInsertRows(); } void ModpackListModel::searchRequestFailed(QString) { - auto failed_action = dynamic_cast(jobPtr.get())->getFailedActions().at(0); + auto failed_action = dynamic_cast(m_jobPtr.get())->getFailedActions().at(0); if (failed_action->replyStatusCode() == -1) { // Network error QMessageBox::critical(nullptr, tr("Error"), tr("A network error occurred. Could not load modpacks.")); @@ -362,17 +322,17 @@ void ModpackListModel::searchRequestFailed(QString) .arg(m_parent->displayName()) .arg(tr("API version too old!\nPlease update %1!").arg(BuildConfig.LAUNCHER_DISPLAYNAME))); } - jobPtr.reset(); + m_jobPtr.reset(); - if (searchState == ResetRequested) { + if (m_searchState == ResetRequested) { beginResetModel(); - modpacks.clear(); + m_modpacks.clear(); endResetModel(); - nextSearchOffset = 0; + m_nextSearchOffset = 0; performPaginatedSearch(); } else { - searchState = Finished; + m_searchState = Finished; } } diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h index cdd5c4e79..7037f4745 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h @@ -37,7 +37,7 @@ #include -#include "modplatform/modrinth/ModrinthPackManifest.h" +#include "modplatform/ModIndex.h" #include "net/NetJob.h" #include "ui/pages/modplatform/modrinth/ModrinthPage.h" @@ -56,7 +56,7 @@ class ModpackListModel : public QAbstractListModel { ModpackListModel(ModrinthPage* parent); ~ModpackListModel() override = default; - inline auto rowCount(const QModelIndex& parent) const -> int override { return parent.isValid() ? 0 : modpacks.size(); }; + inline auto rowCount(const QModelIndex& parent) const -> int override { return parent.isValid() ? 0 : m_modpacks.size(); }; inline auto columnCount(const QModelIndex& parent) const -> int override { return parent.isValid() ? 0 : 1; }; inline auto flags(const QModelIndex& index) const -> Qt::ItemFlags override { return QAbstractListModel::flags(index); }; @@ -66,27 +66,27 @@ class ModpackListModel : public QAbstractListModel { auto data(const QModelIndex& index, int role) const -> QVariant override; bool setData(const QModelIndex& index, const QVariant& value, int role) override; - inline void setActiveJob(NetJob::Ptr ptr) { jobPtr = ptr; } + inline void setActiveJob(NetJob::Ptr ptr) { m_jobPtr = ptr; } /* Ask the API for more information */ void fetchMore(const QModelIndex& parent) override; void refresh(); void searchWithTerm(const QString& term, int sort, std::shared_ptr filter, bool filterChanged); - bool hasActiveSearchJob() const { return jobPtr && jobPtr->isRunning(); } - Task::Ptr activeSearchJob() { return hasActiveSearchJob() ? jobPtr : nullptr; } + bool hasActiveSearchJob() const { return m_jobPtr && m_jobPtr->isRunning(); } + Task::Ptr activeSearchJob() { return hasActiveSearchJob() ? m_jobPtr : nullptr; } void getLogo(const QString& logo, const QString& logoUrl, LogoCallback callback); inline auto canFetchMore(const QModelIndex& parent) const -> bool override { - return parent.isValid() ? false : searchState == CanPossiblyFetchMore; + return parent.isValid() ? false : m_searchState == CanPossiblyFetchMore; }; public slots: - void searchRequestFinished(QJsonDocument& doc_all); + void searchRequestFinished(QList& doc_all); void searchRequestFailed(QString reason); - void searchRequestForOneSucceeded(QJsonDocument&); + void searchRequestForOneSucceeded(ModPlatform::IndexedPack&); protected slots: @@ -103,20 +103,20 @@ class ModpackListModel : public QAbstractListModel { protected: ModrinthPage* m_parent; - QList modpacks; + QList m_modpacks; LogoMap m_logoMap; - QMap waitingCallbacks; + QMap m_waitingCallbacks; QStringList m_failedLogos; QStringList m_loadingLogos; - QString currentSearchTerm; - QString currentSort; + QString m_currentSearchTerm; + QString m_currentSort; std::shared_ptr m_filter; - int nextSearchOffset = 0; - enum SearchState { None, CanPossiblyFetchMore, ResetRequested, Finished } searchState = None; + int m_nextSearchOffset = 0; + enum SearchState { None, CanPossiblyFetchMore, ResetRequested, Finished } m_searchState = None; - Task::Ptr jobPtr; + Task::Ptr m_jobPtr; std::shared_ptr m_allResponse = std::make_shared(); QByteArray m_specific_response; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp index d9004a1fc..768f2f492 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -36,6 +36,7 @@ #include "ModrinthPage.h" #include "Version.h" +#include "modplatform/ModIndex.h" #include "modplatform/modrinth/ModrinthAPI.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui_ModrinthPage.h" @@ -57,17 +58,17 @@ #include ModrinthPage::ModrinthPage(NewInstanceDialog* dialog, QWidget* parent) - : QWidget(parent), ui(new Ui::ModrinthPage), dialog(dialog), m_fetch_progress(this, false) + : QWidget(parent), m_ui(new Ui::ModrinthPage), m_dialog(dialog), m_fetch_progress(this, false) { - ui->setupUi(this); + m_ui->setupUi(this); createFilterWidget(); - ui->searchEdit->installEventFilter(this); + m_ui->searchEdit->installEventFilter(this); m_model = new Modrinth::ModpackListModel(this); - ui->packView->setModel(m_model); + m_ui->packView->setModel(m_model); - ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); - ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); + m_ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + m_ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); m_search_timer.setTimerType(Qt::TimerType::CoarseTimer); m_search_timer.setSingleShot(true); @@ -78,30 +79,30 @@ ModrinthPage::ModrinthPage(NewInstanceDialog* dialog, QWidget* parent) m_fetch_progress.setFixedHeight(24); m_fetch_progress.progressFormat(""); - ui->verticalLayout->insertWidget(1, &m_fetch_progress); + m_ui->verticalLayout->insertWidget(1, &m_fetch_progress); - 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")); + m_ui->sortByBox->addItem(tr("Sort by Relevance")); + m_ui->sortByBox->addItem(tr("Sort by Total Downloads")); + m_ui->sortByBox->addItem(tr("Sort by Follows")); + m_ui->sortByBox->addItem(tr("Sort by Newest")); + m_ui->sortByBox->addItem(tr("Sort by Last Updated")); - connect(ui->sortByBox, &QComboBox::currentIndexChanged, this, &ModrinthPage::triggerSearch); - connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthPage::onSelectionChanged); - connect(ui->versionSelectionBox, &QComboBox::currentIndexChanged, this, &ModrinthPage::onVersionSelectionChanged); + connect(m_ui->sortByBox, &QComboBox::currentIndexChanged, this, &ModrinthPage::triggerSearch); + connect(m_ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthPage::onSelectionChanged); + connect(m_ui->versionSelectionBox, &QComboBox::currentIndexChanged, this, &ModrinthPage::onVersionSelectionChanged); - ui->packView->setItemDelegate(new ProjectItemDelegate(this)); - ui->packDescription->setMetaEntry(metaEntryBase()); + m_ui->packView->setItemDelegate(new ProjectItemDelegate(this)); + m_ui->packDescription->setMetaEntry(metaEntryBase()); } ModrinthPage::~ModrinthPage() { - delete ui; + delete m_ui; } void ModrinthPage::retranslate() { - ui->retranslateUi(this); + m_ui->retranslateUi(this); } void ModrinthPage::openedImpl() @@ -113,7 +114,7 @@ void ModrinthPage::openedImpl() bool ModrinthPage::eventFilter(QObject* watched, QEvent* event) { - if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) { + if (watched == m_ui->searchEdit && event->type() == QEvent::KeyPress) { auto* keyEvent = reinterpret_cast(event); if (keyEvent->key() == Qt::Key_Return) { this->triggerSearch(); @@ -129,146 +130,108 @@ bool ModrinthPage::eventFilter(QObject* watched, QEvent* event) return QObject::eventFilter(watched, event); } -bool checkVersionFilters(const Modrinth::ModpackVersion& v, std::shared_ptr filter) -{ - if (!filter) - return true; - return ((!filter->loaders || !v.loaders || filter->loaders & v.loaders) && // loaders - (filter->releases.empty() || // releases - std::find(filter->releases.cbegin(), filter->releases.cend(), v.version_type) != filter->releases.cend()) && - filter->checkMcVersions({ v.gameVersion })); // gameVersion} -} - void ModrinthPage::onSelectionChanged(QModelIndex curr, [[maybe_unused]] QModelIndex prev) { - ui->versionSelectionBox->clear(); + m_ui->versionSelectionBox->clear(); if (!curr.isValid()) { if (isOpened) { - dialog->setSuggestedPack(); + m_dialog->setSuggestedPack(); } return; } - QVariant raw = m_model->data(curr, Qt::UserRole); - Q_ASSERT(raw.canConvert()); - current = raw.value(); - auto name = current.name; + m_current = m_model->data(curr, Qt::UserRole).value(); + auto name = m_current->name; - if (!current.extraInfoLoaded) { + if (!m_current->extraDataLoaded) { qDebug() << "Loading modrinth modpack information"; - - auto netJob = new NetJob(QString("Modrinth::PackInformation(%1)").arg(current.name), APPLICATION->network()); - auto response = std::make_shared(); - - QString id = current.id; - - netJob->addNetAction(Net::ApiDownload::makeByteArray(QString("%1/project/%2").arg(BuildConfig.MODRINTH_PROD_URL, id), response)); - - connect(netJob, &NetJob::succeeded, this, [this, response, id, curr] { - if (id != current.id) { + ResourceAPI::Callback callbacks; + + auto id = m_current->addonId; + callbacks.on_fail = [this](QString reason, int) { + CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); + }; + callbacks.on_succeed = [this, id, curr](auto& pack) { + if (id != m_current->addonId) { 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(); + *m_current = pack; QVariant current_updated; - current_updated.setValue(current); + current_updated.setValue(m_current); if (!m_model->setData(curr, current_updated, Qt::UserRole)) qWarning() << "Failed to cache extra info for the current pack!"; suggestCurrent(); - }); - connect(netJob, &NetJob::finished, this, [response, netJob] { netJob->deleteLater(); }); - connect(netJob, &NetJob::failed, - [this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); - netJob->start(); + updateUI(); + }; + if (auto netJob = m_api.getProjectInfo({ { m_current->addonId } }, std::move(callbacks)); netJob) { + m_job = netJob; + m_job->start(); + } + } else updateUI(); - if (!current.versionsLoaded || m_filterWidget->changed()) { + if (!m_current->versionsLoaded || m_filterWidget->changed()) { qDebug() << "Loading modrinth modpack versions"; - auto netJob = new NetJob(QString("Modrinth::PackVersions(%1)").arg(current.name), APPLICATION->network()); - auto response = std::make_shared(); - - QString id = current.id; - - netJob->addNetAction( - Net::ApiDownload::makeByteArray(QString("%1/project/%2/version").arg(BuildConfig.MODRINTH_PROD_URL, id), response)); - - connect(netJob, &NetJob::succeeded, this, [this, response, id, curr] { - if (id != current.id) { - return; // wrong request? - } + ResourceAPI::Callback> callbacks{}; - 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 addonId = m_current->addonId; + // Use default if no callbacks are set + callbacks.on_succeed = [this, curr, addonId](auto& doc) { + if (addonId != m_current->addonId) { + return; // wrong request } - try { - Modrinth::loadIndexedVersions(current, doc); - } catch (const JSONValidationError& e) { - qDebug() << *response; - qWarning() << "Error while reading modrinth modpack version: " << e.cause(); - } - auto pred = [this](const Modrinth::ModpackVersion& v) { return !checkVersionFilters(v, m_filterWidget->getFilter()); }; + m_current->versions = doc; + m_current->versionsLoaded = true; + auto pred = [this](const ModPlatform::IndexedVersion& v) { + if (auto filter = m_filterWidget->getFilter()) + return !filter->checkModpackFilters(v); + return false; + }; #if QT_VERSION >= QT_VERSION_CHECK(6, 1, 0) - current.versions.removeIf(pred); + m_current->versions.removeIf(pred); #else - for (auto it = current.versions.begin(); it != current.versions.end();) - if (pred(*it)) - it = current.versions.erase(it); - else - ++it; + for (auto it = m_current->versions.begin(); it != m_current->versions.end();) + if (pred(*it)) + it = m_current->versions.erase(it); + else + ++it; #endif - for (const auto& version : current.versions) { - ui->versionSelectionBox->addItem(Modrinth::getVersionDisplayString(version), QVariant(version.id)); + for (const auto& version : m_current->versions) { + m_ui->versionSelectionBox->addItem(version.getVersionDisplayString(), QVariant(version.addonId)); } QVariant current_updated; - current_updated.setValue(current); + current_updated.setValue(m_current); if (!m_model->setData(curr, current_updated, Qt::UserRole)) qWarning() << "Failed to cache versions for the current pack!"; suggestCurrent(); - }); - connect(netJob, &NetJob::finished, this, [response, netJob] { netJob->deleteLater(); }); - connect(netJob, &NetJob::failed, - [this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); }); - netJob->start(); + }; + callbacks.on_fail = [this](QString reason, int) { + CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->exec(); + }; + + auto netJob = m_api.getProjectVersions({ *m_current, {}, {}, ModPlatform::ResourceType::Modpack }, std::move(callbacks)); + + m_job2 = netJob; + m_job2->start(); } else { - for (auto version : current.versions) { - if (!version.name.contains(version.version)) - ui->versionSelectionBox->addItem(QString("%1 - %2").arg(version.name, version.version), QVariant(version.id)); + for (auto version : m_current->versions) { + if (!version.version.contains(version.version)) + m_ui->versionSelectionBox->addItem(QString("%1 - %2").arg(version.version, version.version_number), + QVariant(version.addonId)); else - ui->versionSelectionBox->addItem(version.name, QVariant(version.id)); + m_ui->versionSelectionBox->addItem(version.version, QVariant(version.addonId)); } suggestCurrent(); @@ -279,53 +242,64 @@ void ModrinthPage::updateUI() { QString text = ""; - if (current.extra.projectUrl.isEmpty()) - text = current.name; + if (m_current->websiteUrl.isEmpty()) + text = m_current->name; else - text = "" + current.name + ""; + text = "websiteUrl + "\">" + m_current->name + ""; - // TODO: Implement multiple authors with links - text += "
    " + tr(" by ") + QString("%2").arg(std::get<1>(current.author).toString(), std::get<0>(current.author)); + if (!m_current->authors.empty()) { + auto authorToStr = [](ModPlatform::ModpackAuthor& author) { + if (author.url.isEmpty()) { + return author.name; + } + return QString("%2").arg(author.url, author.name); + }; + QStringList authorStrs; + for (auto& author : m_current->authors) { + authorStrs.push_back(authorToStr(author)); + } + text += "
    " + tr(" by ") + authorStrs.join(", "); + } - if (current.extraInfoLoaded) { - if (current.extra.status == "archived") { + if (m_current->extraDataLoaded) { + if (m_current->extraData.status == "archived") { text += "

    " + tr("This project has been archived. It will not receive any further updates unless the author decides " "to unarchive the project."); } - if (!current.extra.donate.isEmpty()) { + if (!m_current->extraData.donate.isEmpty()) { text += "

    " + tr("Donate information: "); - auto donateToStr = [](Modrinth::DonationData& donate) -> QString { + auto donateToStr = [](ModPlatform::DonationData& donate) -> QString { return QString("%2").arg(donate.url, donate.platform); }; QStringList donates; - for (auto& donate : current.extra.donate) { + for (auto& donate : m_current->extraData.donate) { donates.append(donateToStr(donate)); } text += donates.join(", "); } - if (!current.extra.issuesUrl.isEmpty() || !current.extra.sourceUrl.isEmpty() || !current.extra.wikiUrl.isEmpty() || - !current.extra.discordUrl.isEmpty()) { + if (!m_current->extraData.issuesUrl.isEmpty() || !m_current->extraData.sourceUrl.isEmpty() || + !m_current->extraData.wikiUrl.isEmpty() || !m_current->extraData.discordUrl.isEmpty()) { text += "

    " + tr("External links:") + "
    "; } - if (!current.extra.issuesUrl.isEmpty()) - text += "- " + tr("Issues: %1").arg(current.extra.issuesUrl) + "
    "; - if (!current.extra.wikiUrl.isEmpty()) - text += "- " + tr("Wiki: %1").arg(current.extra.wikiUrl) + "
    "; - if (!current.extra.sourceUrl.isEmpty()) - text += "- " + tr("Source code: %1").arg(current.extra.sourceUrl) + "
    "; - if (!current.extra.discordUrl.isEmpty()) - text += "- " + tr("Discord: %1").arg(current.extra.discordUrl) + "
    "; + if (!m_current->extraData.issuesUrl.isEmpty()) + text += "- " + tr("Issues: %1").arg(m_current->extraData.issuesUrl) + "
    "; + if (!m_current->extraData.wikiUrl.isEmpty()) + text += "- " + tr("Wiki: %1").arg(m_current->extraData.wikiUrl) + "
    "; + if (!m_current->extraData.sourceUrl.isEmpty()) + text += "- " + tr("Source code: %1").arg(m_current->extraData.sourceUrl) + "
    "; + if (!m_current->extraData.discordUrl.isEmpty()) + text += "- " + tr("Discord: %1").arg(m_current->extraData.discordUrl) + "
    "; } text += "
    "; - text += markdownToHTML(current.extra.body.toUtf8()); + text += markdownToHTML(m_current->extraData.body.toUtf8()); - ui->packDescription->setHtml(StringUtils::htmlListPatch(text + current.description)); - ui->packDescription->flush(); + m_ui->packDescription->setHtml(StringUtils::htmlListPatch(text + m_current->description)); + m_ui->packDescription->flush(); } void ModrinthPage::suggestCurrent() @@ -334,21 +308,21 @@ void ModrinthPage::suggestCurrent() return; } - if (selectedVersion.isEmpty()) { - dialog->setSuggestedPack(); + if (m_selectedVersion.isEmpty()) { + m_dialog->setSuggestedPack(); return; } - for (auto& ver : current.versions) { - if (ver.id == selectedVersion) { + for (auto& ver : m_current->versions) { + if (ver.addonId == m_selectedVersion) { QMap extra_info; - extra_info.insert("pack_id", current.id); - extra_info.insert("pack_version_id", ver.id); + extra_info.insert("pack_id", m_current->addonId.toString()); + extra_info.insert("pack_version_id", ver.fileId.toString()); - dialog->setSuggestedPack(current.name, ver.version, new InstanceImportTask(ver.download_url, this, std::move(extra_info))); - auto iconName = current.iconName; - m_model->getLogo(iconName, current.iconUrl.toString(), - [this, iconName](QString logo) { dialog->setSuggestedIconFromFile(logo, iconName); }); + m_dialog->setSuggestedPack(m_current->name, ver.version, new InstanceImportTask(ver.downloadUrl, this, std::move(extra_info))); + auto iconName = m_current->logoName; + m_model->getLogo(iconName, m_current->logoUrl, + [this, iconName](QString logo) { m_dialog->setSuggestedIconFromFile(logo, iconName); }); break; } @@ -357,46 +331,46 @@ void ModrinthPage::suggestCurrent() void ModrinthPage::triggerSearch() { - ui->packView->selectionModel()->setCurrentIndex({}, QItemSelectionModel::SelectionFlag::ClearAndSelect); - ui->packView->clearSelection(); - ui->packDescription->clear(); - ui->versionSelectionBox->clear(); + m_ui->packView->selectionModel()->setCurrentIndex({}, QItemSelectionModel::SelectionFlag::ClearAndSelect); + m_ui->packView->clearSelection(); + m_ui->packDescription->clear(); + m_ui->versionSelectionBox->clear(); bool filterChanged = m_filterWidget->changed(); - m_model->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->currentIndex(), m_filterWidget->getFilter(), filterChanged); + m_model->searchWithTerm(m_ui->searchEdit->text(), m_ui->sortByBox->currentIndex(), m_filterWidget->getFilter(), filterChanged); m_fetch_progress.watch(m_model->activeSearchJob().get()); } void ModrinthPage::onVersionSelectionChanged(int index) { if (index == -1) { - selectedVersion = ""; + m_selectedVersion = ""; return; } - selectedVersion = ui->versionSelectionBox->itemData(index).toString(); + m_selectedVersion = m_ui->versionSelectionBox->itemData(index).toString(); suggestCurrent(); } void ModrinthPage::setSearchTerm(QString term) { - ui->searchEdit->setText(term); + m_ui->searchEdit->setText(term); } QString ModrinthPage::getSerachTerm() const { - return ui->searchEdit->text(); + return m_ui->searchEdit->text(); } void ModrinthPage::createFilterWidget() { auto widget = ModFilterWidget::create(nullptr, true); m_filterWidget.swap(widget); - auto old = ui->splitter->replaceWidget(0, m_filterWidget.get()); + auto old = m_ui->splitter->replaceWidget(0, m_filterWidget.get()); // because we replaced the widget we also need to delete it if (old) { delete old; } - connect(ui->filterButton, &QPushButton::clicked, this, [this] { m_filterWidget->setHidden(!m_filterWidget->isHidden()); }); + connect(m_ui->filterButton, &QPushButton::clicked, this, [this] { m_filterWidget->setHidden(!m_filterWidget->isHidden()); }); connect(m_filterWidget.get(), &ModFilterWidget::filterChanged, this, &ModrinthPage::triggerSearch); auto response = std::make_shared(); diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h index 77cc173dd..600500c6d 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h @@ -37,9 +37,10 @@ #pragma once #include "Application.h" +#include "modplatform/ModIndex.h" +#include "modplatform/modrinth/ModrinthAPI.h" #include "ui/dialogs/NewInstanceDialog.h" -#include "modplatform/modrinth/ModrinthPackManifest.h" #include "ui/pages/modplatform/ModpackProviderBasePage.h" #include "ui/widgets/ModFilterWidget.h" #include "ui/widgets/ProgressWidget.h" @@ -67,10 +68,10 @@ class ModrinthPage : public QWidget, public ModpackProviderBasePage { QString id() const override { return "modrinth"; } QString helpPage() const override { return "Modrinth-platform"; } - inline auto debugName() const -> QString { return "Modrinth"; } - inline auto metaEntryBase() const -> QString { return "ModrinthModpacks"; }; + inline QString debugName() const { return "Modrinth"; } + inline QString metaEntryBase() const { return "ModrinthModpacks"; }; - auto getCurrent() -> Modrinth::Modpack& { return current; } + ModPlatform::IndexedPack::Ptr getCurrent() { return m_current; } void suggestCurrent(); void updateUI(); @@ -91,12 +92,12 @@ class ModrinthPage : public QWidget, public ModpackProviderBasePage { void createFilterWidget(); private: - Ui::ModrinthPage* ui; - NewInstanceDialog* dialog; + Ui::ModrinthPage* m_ui; + NewInstanceDialog* m_dialog; Modrinth::ModpackListModel* m_model; - Modrinth::Modpack current; - QString selectedVersion; + ModPlatform::IndexedPack::Ptr m_current; + QString m_selectedVersion; ProgressWidget m_fetch_progress; @@ -105,4 +106,8 @@ class ModrinthPage : public QWidget, public ModpackProviderBasePage { std::unique_ptr m_filterWidget; Task::Ptr m_categoriesTask; + + ModrinthAPI m_api; + Task::Ptr m_job; + Task::Ptr m_job2; }; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp deleted file mode 100644 index 91e9ad791..000000000 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.cpp +++ /dev/null @@ -1,144 +0,0 @@ -// SPDX-FileCopyrightText: 2023 flowln -// -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - 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 . - */ - -#include "ModrinthResourceModels.h" - -#include "modplatform/modrinth/ModrinthAPI.h" -#include "modplatform/modrinth/ModrinthPackIndex.h" - -namespace ResourceDownload { - -ModrinthModModel::ModrinthModModel(BaseInstance& base) : ModModel(base, new ModrinthAPI) {} - -void ModrinthModModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) -{ - ::Modrinth::loadIndexedPack(m, obj); -} - -void ModrinthModModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) -{ - ::Modrinth::loadExtraPackData(m, obj); -} - -void ModrinthModModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) -{ - ::Modrinth::loadIndexedPackVersions(m, arr); -} - -auto ModrinthModModel::loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) -> ModPlatform::IndexedVersion -{ - return ::Modrinth::loadDependencyVersions(m, arr, &m_base_instance); -} - -auto ModrinthModModel::documentToArray(QJsonDocument& obj) const -> QJsonArray -{ - return obj.object().value("hits").toArray(); -} - -ModrinthResourcePackModel::ModrinthResourcePackModel(const BaseInstance& base) : ResourcePackResourceModel(base, new ModrinthAPI) {} - -void ModrinthResourcePackModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) -{ - ::Modrinth::loadIndexedPack(m, obj); -} - -void ModrinthResourcePackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) -{ - ::Modrinth::loadExtraPackData(m, obj); -} - -void ModrinthResourcePackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) -{ - ::Modrinth::loadIndexedPackVersions(m, arr); -} - -auto ModrinthResourcePackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray -{ - return obj.object().value("hits").toArray(); -} - -ModrinthTexturePackModel::ModrinthTexturePackModel(const BaseInstance& base) : TexturePackResourceModel(base, new ModrinthAPI) {} - -void ModrinthTexturePackModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) -{ - ::Modrinth::loadIndexedPack(m, obj); -} - -void ModrinthTexturePackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) -{ - ::Modrinth::loadExtraPackData(m, obj); -} - -void ModrinthTexturePackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) -{ - ::Modrinth::loadIndexedPackVersions(m, arr); -} - -auto ModrinthTexturePackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray -{ - return obj.object().value("hits").toArray(); -} - -ModrinthShaderPackModel::ModrinthShaderPackModel(const BaseInstance& base) : ShaderPackResourceModel(base, new ModrinthAPI) {} - -void ModrinthShaderPackModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) -{ - ::Modrinth::loadIndexedPack(m, obj); -} - -void ModrinthShaderPackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) -{ - ::Modrinth::loadExtraPackData(m, obj); -} - -void ModrinthShaderPackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) -{ - ::Modrinth::loadIndexedPackVersions(m, arr); -} - -auto ModrinthShaderPackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray -{ - return obj.object().value("hits").toArray(); -} - -ModrinthDataPackModel::ModrinthDataPackModel(const BaseInstance& base) : DataPackResourceModel(base, new ModrinthAPI) {} - -void ModrinthDataPackModel::loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) -{ - ::Modrinth::loadIndexedPack(m, obj); -} - -void ModrinthDataPackModel::loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) -{ - ::Modrinth::loadExtraPackData(m, obj); -} - -void ModrinthDataPackModel::loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) -{ - ::Modrinth::loadIndexedPackVersions(m, arr); -} - -auto ModrinthDataPackModel::documentToArray(QJsonDocument& obj) const -> QJsonArray -{ - return obj.object().value("hits").toArray(); -} - - -} // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h b/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h deleted file mode 100644 index 7f68ed47d..000000000 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourceModels.h +++ /dev/null @@ -1,121 +0,0 @@ -// SPDX-FileCopyrightText: 2023 flowln -// -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - 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 "ui/pages/modplatform/DataPackModel.h" -#include "ui/pages/modplatform/ModModel.h" -#include "ui/pages/modplatform/ResourcePackModel.h" -#include "ui/pages/modplatform/modrinth/ModrinthResourcePages.h" - -namespace ResourceDownload { - -class ModrinthModModel : public ModModel { - Q_OBJECT - - public: - ModrinthModModel(BaseInstance&); - ~ModrinthModModel() override = default; - - private: - QString debugName() const override { return Modrinth::debugName() + " (Model)"; } - QString metaEntryBase() const override { return Modrinth::metaEntryBase(); } - - void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; - void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override; - void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override; - auto loadDependencyVersions(const ModPlatform::Dependency& m, QJsonArray& arr) -> ModPlatform::IndexedVersion override; - - auto documentToArray(QJsonDocument& obj) const -> QJsonArray override; -}; - -class ModrinthResourcePackModel : public ResourcePackResourceModel { - Q_OBJECT - - public: - ModrinthResourcePackModel(const BaseInstance&); - ~ModrinthResourcePackModel() override = default; - - private: - QString debugName() const override { return Modrinth::debugName() + " (Model)"; } - QString metaEntryBase() const override { return Modrinth::metaEntryBase(); } - - void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; - void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override; - void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override; - - auto documentToArray(QJsonDocument& obj) const -> QJsonArray override; -}; - -class ModrinthTexturePackModel : public TexturePackResourceModel { - Q_OBJECT - - public: - ModrinthTexturePackModel(const BaseInstance&); - ~ModrinthTexturePackModel() override = default; - - private: - QString debugName() const override { return Modrinth::debugName() + " (Model)"; } - QString metaEntryBase() const override { return Modrinth::metaEntryBase(); } - - void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; - void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override; - void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override; - - auto documentToArray(QJsonDocument& obj) const -> QJsonArray override; -}; - -class ModrinthShaderPackModel : public ShaderPackResourceModel { - Q_OBJECT - - public: - ModrinthShaderPackModel(const BaseInstance&); - ~ModrinthShaderPackModel() override = default; - - private: - QString debugName() const override { return Modrinth::debugName() + " (Model)"; } - QString metaEntryBase() const override { return Modrinth::metaEntryBase(); } - - void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; - void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override; - void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override; - - auto documentToArray(QJsonDocument& obj) const -> QJsonArray override; -}; - -class ModrinthDataPackModel : public DataPackResourceModel { - Q_OBJECT - - public: - ModrinthDataPackModel(const BaseInstance&); - ~ModrinthDataPackModel() override = default; - - private: - QString debugName() const override { return Modrinth::debugName() + " (Model)"; } - QString metaEntryBase() const override { return Modrinth::metaEntryBase(); } - - void loadIndexedPack(ModPlatform::IndexedPack& m, QJsonObject& obj) override; - void loadExtraPackInfo(ModPlatform::IndexedPack& m, QJsonObject& obj) override; - void loadIndexedPackVersions(ModPlatform::IndexedPack& m, QJsonArray& arr) override; - - auto documentToArray(QJsonDocument& obj) const -> QJsonArray override; -}; - -} // namespace ResourceDownload diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp index aca71cde5..32296316f 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.cpp @@ -37,19 +37,18 @@ */ #include "ModrinthResourcePages.h" +#include "ui/pages/modplatform/DataPackModel.h" #include "ui_ResourcePage.h" #include "modplatform/modrinth/ModrinthAPI.h" #include "ui/dialogs/ResourceDownloadDialog.h" -#include "ui/pages/modplatform/modrinth/ModrinthResourceModels.h" - namespace ResourceDownload { ModrinthModPage::ModrinthModPage(ModDownloadDialog* dialog, BaseInstance& instance) : ModPage(dialog, instance) { - m_model = new ModrinthModModel(instance); + m_model = new ModModel(instance, new ModrinthAPI(), Modrinth::debugName(), Modrinth::metaEntryBase()); m_ui->packView->setModel(m_model); addSortings(); @@ -67,7 +66,7 @@ ModrinthModPage::ModrinthModPage(ModDownloadDialog* dialog, BaseInstance& instan ModrinthResourcePackPage::ModrinthResourcePackPage(ResourcePackDownloadDialog* dialog, BaseInstance& instance) : ResourcePackResourcePage(dialog, instance) { - m_model = new ModrinthResourcePackModel(instance); + m_model = new ResourcePackResourceModel(instance, new ModrinthAPI(), Modrinth::debugName(), Modrinth::metaEntryBase()); m_ui->packView->setModel(m_model); addSortings(); @@ -85,7 +84,7 @@ ModrinthResourcePackPage::ModrinthResourcePackPage(ResourcePackDownloadDialog* d ModrinthTexturePackPage::ModrinthTexturePackPage(TexturePackDownloadDialog* dialog, BaseInstance& instance) : TexturePackResourcePage(dialog, instance) { - m_model = new ModrinthTexturePackModel(instance); + m_model = new TexturePackResourceModel(instance, new ModrinthAPI(), Modrinth::debugName(), Modrinth::metaEntryBase()); m_ui->packView->setModel(m_model); addSortings(); @@ -103,7 +102,7 @@ ModrinthTexturePackPage::ModrinthTexturePackPage(TexturePackDownloadDialog* dial ModrinthShaderPackPage::ModrinthShaderPackPage(ShaderPackDownloadDialog* dialog, BaseInstance& instance) : ShaderPackResourcePage(dialog, instance) { - m_model = new ModrinthShaderPackModel(instance); + m_model = new ShaderPackResourceModel(instance, new ModrinthAPI(), Modrinth::debugName(), Modrinth::metaEntryBase()); m_ui->packView->setModel(m_model); addSortings(); @@ -118,10 +117,9 @@ ModrinthShaderPackPage::ModrinthShaderPackPage(ShaderPackDownloadDialog* dialog, m_ui->packDescription->setMetaEntry(metaEntryBase()); } -ModrinthDataPackPage::ModrinthDataPackPage(DataPackDownloadDialog* dialog, BaseInstance& instance) - : DataPackResourcePage(dialog, instance) +ModrinthDataPackPage::ModrinthDataPackPage(DataPackDownloadDialog* dialog, BaseInstance& instance) : DataPackResourcePage(dialog, instance) { - m_model = new ModrinthDataPackModel(instance); + m_model = new DataPackResourceModel(instance, new ModrinthAPI(), Modrinth::debugName(), Modrinth::metaEntryBase()); m_ui->packView->setModel(m_model); addSortings(); diff --git a/launcher/ui/widgets/MinecraftSettingsWidget.cpp b/launcher/ui/widgets/MinecraftSettingsWidget.cpp index 7cd84c8bb..9c8e2b405 100644 --- a/launcher/ui/widgets/MinecraftSettingsWidget.cpp +++ b/launcher/ui/widgets/MinecraftSettingsWidget.cpp @@ -36,6 +36,7 @@ */ #include "MinecraftSettingsWidget.h" +#include "modplatform/ModIndex.h" #include "ui_MinecraftSettingsWidget.h" #include diff --git a/launcher/ui/widgets/ModFilterWidget.h b/launcher/ui/widgets/ModFilterWidget.h index 8a858fd30..f00b98eb0 100644 --- a/launcher/ui/widgets/ModFilterWidget.h +++ b/launcher/ui/widgets/ModFilterWidget.h @@ -81,6 +81,14 @@ class ModFilterWidget : public QTabWidget { return versions.empty(); } + + bool checkModpackFilters(const ModPlatform::IndexedVersion& v) + { + return ((!loaders || !v.loaders || loaders & v.loaders) && // loaders + (releases.empty() || // releases + std::find(releases.cbegin(), releases.cend(), v.version_type) != releases.cend()) && + checkMcVersions({ v.mcVersion })); // gameVersion} + } }; static std::unique_ptr create(MinecraftInstance* instance, bool extended); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 31b887ff1..2165cd03d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -21,9 +21,6 @@ ecm_add_test(ResourceFolderModel_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_V ecm_add_test(ResourcePackParse_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME ResourcePackParse) -ecm_add_test(ResourceModel_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test - TEST_NAME ResourceModel) - ecm_add_test(TexturePackParse_test.cpp LINK_LIBRARIES Launcher_logic Qt${QT_VERSION_MAJOR}::Test TEST_NAME TexturePackParse) diff --git a/tests/DummyResourceAPI.h b/tests/DummyResourceAPI.h deleted file mode 100644 index d5ae1392d..000000000 --- a/tests/DummyResourceAPI.h +++ /dev/null @@ -1,46 +0,0 @@ -#pragma once - -#include - -#include - -class SearchTask : public Task { - Q_OBJECT - - public: - void executeTask() override { emitSucceeded(); } -}; - -class DummyResourceAPI : public ResourceAPI { - public: - static auto searchRequestResult() - { - static QByteArray json_response = - "{\"hits\":[" - "{" - "\"author\":\"flowln\"," - "\"description\":\"the bestest mod\"," - "\"project_id\":\"something\"," - "\"project_type\":\"mod\"," - "\"slug\":\"bip_bop\"," - "\"title\":\"AAAAAAAA\"," - "\"versions\":[\"2.71\"]" - "}" - "]}"; - - return QJsonDocument::fromJson(json_response); - } - - DummyResourceAPI() : ResourceAPI() {} - auto getSortingMethods() const -> QList override { return {}; } - - Task::Ptr searchProjects(SearchArgs&&, SearchCallbacks&& callbacks) const override - { - auto task = makeShared(); - QObject::connect(task.get(), &Task::succeeded, [callbacks] { - auto json = searchRequestResult(); - callbacks.on_succeed(json); - }); - return task; - } -}; diff --git a/tests/ResourceModel_test.cpp b/tests/ResourceModel_test.cpp deleted file mode 100644 index c4ea1a20f..000000000 --- a/tests/ResourceModel_test.cpp +++ /dev/null @@ -1,95 +0,0 @@ -#include -#include -#include - -#include - -#include - -#include "DummyResourceAPI.h" - -using ResourceDownload::ResourceModel; - -#define EXEC_TASK(EXEC) \ - QEventLoop loop; \ - \ - connect(model, &ResourceModel::dataChanged, &loop, &QEventLoop::quit); \ - \ - QTimer expire_timer; \ - expire_timer.callOnTimeout(&loop, &QEventLoop::quit); \ - expire_timer.setSingleShot(true); \ - expire_timer.start(4000); \ - \ - EXEC; \ - if (model->hasActiveSearchJob()) \ - loop.exec(); \ - \ - QVERIFY2(expire_timer.isActive(), "Timer has expired. The search never finished."); \ - expire_timer.stop(); \ - \ - disconnect(model, nullptr, &loop, nullptr) - -class ResourceModelTest; - -class DummyResourceModel : public ResourceModel { - Q_OBJECT - - friend class ResourceModelTest; - - public: - DummyResourceModel() : ResourceModel(new DummyResourceAPI) {} - ~DummyResourceModel() {} - - auto metaEntryBase() const -> QString override { return ""; } - - ResourceAPI::SearchArgs createSearchArguments() override { return {}; } - ResourceAPI::VersionSearchArgs createVersionsArguments(const QModelIndex&) override { return {}; } - ResourceAPI::ProjectInfoArgs createInfoArguments(const QModelIndex&) override { return {}; } - - QJsonArray documentToArray(QJsonDocument& doc) const override { return doc.object().value("hits").toArray(); } - - void loadIndexedPack(ModPlatform::IndexedPack& pack, QJsonObject& obj) override - { - pack.authors.append({ Json::requireString(obj, "author"), "" }); - pack.description = Json::requireString(obj, "description"); - pack.addonId = Json::requireString(obj, "project_id"); - } -}; - -class ResourceModelTest : public QObject { - Q_OBJECT - private slots: - void test_abstract_item_model() - { - auto dummy = DummyResourceModel(); - auto tester = QAbstractItemModelTester(&dummy); - } - - void test_search() - { - auto model = new DummyResourceModel; - - QVERIFY(model->m_packs.isEmpty()); - - EXEC_TASK(model->search()); - - QVERIFY(model->m_packs.size() == 1); - QVERIFY(model->m_search_state == DummyResourceModel::SearchState::Finished); - - auto processed_pack = model->m_packs.at(0); - auto search_json = DummyResourceAPI::searchRequestResult(); - auto processed_response = model->documentToArray(search_json).first().toObject(); - - QVERIFY(processed_pack->addonId.toString() == Json::requireString(processed_response, "project_id")); - QVERIFY(processed_pack->description == Json::requireString(processed_response, "description")); - QVERIFY(processed_pack->authors.first().name == Json::requireString(processed_response, "author")); - - delete model; - } -}; - -QTEST_GUILESS_MAIN(ResourceModelTest) - -#include "ResourceModel_test.moc" - -#include "moc_DummyResourceAPI.cpp" From 5ef61aa445b6d5c3986bb1bec60ddfa80007928a Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Wed, 17 Sep 2025 10:30:00 +0100 Subject: [PATCH 419/695] Make BaseVersion const-correct in order to remove const-cast from Meta::Version Signed-off-by: TheKodeToad --- launcher/BaseVersion.h | 8 ++++---- launcher/java/JavaInstall.cpp | 10 +++++----- launcher/java/JavaInstall.h | 14 +++++++------- launcher/java/JavaMetadata.cpp | 10 +++++----- launcher/java/JavaMetadata.h | 14 +++++++------- launcher/meta/Version.cpp | 6 +++--- launcher/meta/Version.h | 4 ++-- 7 files changed, 33 insertions(+), 33 deletions(-) diff --git a/launcher/BaseVersion.h b/launcher/BaseVersion.h index 2837ff3a9..02a7212e5 100644 --- a/launcher/BaseVersion.h +++ b/launcher/BaseVersion.h @@ -30,21 +30,21 @@ class BaseVersion { * A string used to identify this version in config files. * This should be unique within the version list or shenanigans will occur. */ - virtual QString descriptor() = 0; + virtual QString descriptor() const = 0; /*! * The name of this version as it is displayed to the user. * For example: "1.5.1" */ - virtual QString name() = 0; + virtual QString name() const = 0; /*! * This should return a string that describes * the kind of version this is (Stable, Beta, Snapshot, whatever) */ virtual QString typeString() const = 0; - virtual bool operator<(BaseVersion& a) { return name() < a.name(); } - virtual bool operator>(BaseVersion& a) { return name() > a.name(); } + virtual bool operator<(BaseVersion& a) const { return name() < a.name(); } + virtual bool operator>(BaseVersion& a) const { return name() > a.name(); } }; Q_DECLARE_METATYPE(BaseVersion::Ptr) diff --git a/launcher/java/JavaInstall.cpp b/launcher/java/JavaInstall.cpp index 8e97e0e14..30cb77e08 100644 --- a/launcher/java/JavaInstall.cpp +++ b/launcher/java/JavaInstall.cpp @@ -21,7 +21,7 @@ #include "BaseVersion.h" #include "StringUtils.h" -bool JavaInstall::operator<(const JavaInstall& rhs) +bool JavaInstall::operator<(const JavaInstall& rhs) const { auto archCompare = StringUtils::naturalCompare(arch, rhs.arch, Qt::CaseInsensitive); if (archCompare != 0) @@ -35,17 +35,17 @@ bool JavaInstall::operator<(const JavaInstall& rhs) return StringUtils::naturalCompare(path, rhs.path, Qt::CaseInsensitive) < 0; } -bool JavaInstall::operator==(const JavaInstall& rhs) +bool JavaInstall::operator==(const JavaInstall& rhs) const { return arch == rhs.arch && id == rhs.id && path == rhs.path; } -bool JavaInstall::operator>(const JavaInstall& rhs) +bool JavaInstall::operator>(const JavaInstall& rhs) const { return (!operator<(rhs)) && (!operator==(rhs)); } -bool JavaInstall::operator<(BaseVersion& a) +bool JavaInstall::operator<(BaseVersion& a) const { try { return operator<(dynamic_cast(a)); @@ -54,7 +54,7 @@ bool JavaInstall::operator<(BaseVersion& a) } } -bool JavaInstall::operator>(BaseVersion& a) +bool JavaInstall::operator>(BaseVersion& a) const { try { return operator>(dynamic_cast(a)); diff --git a/launcher/java/JavaInstall.h b/launcher/java/JavaInstall.h index 7d8d392fa..d8fd477fd 100644 --- a/launcher/java/JavaInstall.h +++ b/launcher/java/JavaInstall.h @@ -24,17 +24,17 @@ struct JavaInstall : public BaseVersion { JavaInstall() {} JavaInstall(QString id, QString arch, QString path) : id(id), arch(arch), path(path) {} - virtual QString descriptor() override { return id.toString(); } + virtual QString descriptor() const override { return id.toString(); } - virtual QString name() override { return id.toString(); } + virtual QString name() const override { return id.toString(); } virtual QString typeString() const override { return arch; } - virtual bool operator<(BaseVersion& a) override; - virtual bool operator>(BaseVersion& a) override; - bool operator<(const JavaInstall& rhs); - bool operator==(const JavaInstall& rhs); - bool operator>(const JavaInstall& rhs); + virtual bool operator<(BaseVersion& a) const override; + virtual bool operator>(BaseVersion& a) const override; + bool operator<(const JavaInstall& rhs) const; + bool operator==(const JavaInstall& rhs) const; + bool operator>(const JavaInstall& rhs) const; JavaVersion id; QString arch; diff --git a/launcher/java/JavaMetadata.cpp b/launcher/java/JavaMetadata.cpp index 2d68f55c8..d4da95457 100644 --- a/launcher/java/JavaMetadata.cpp +++ b/launcher/java/JavaMetadata.cpp @@ -78,7 +78,7 @@ MetadataPtr parseJavaMeta(const QJsonObject& in) return meta; } -bool Metadata::operator<(const Metadata& rhs) +bool Metadata::operator<(const Metadata& rhs) const { auto id = version; if (id < rhs.version) { @@ -97,17 +97,17 @@ bool Metadata::operator<(const Metadata& rhs) return StringUtils::naturalCompare(m_name, rhs.m_name, Qt::CaseInsensitive) < 0; } -bool Metadata::operator==(const Metadata& rhs) +bool Metadata::operator==(const Metadata& rhs) const { return version == rhs.version && m_name == rhs.m_name; } -bool Metadata::operator>(const Metadata& rhs) +bool Metadata::operator>(const Metadata& rhs) const { return (!operator<(rhs)) && (!operator==(rhs)); } -bool Metadata::operator<(BaseVersion& a) +bool Metadata::operator<(BaseVersion& a) const { try { return operator<(dynamic_cast(a)); @@ -116,7 +116,7 @@ bool Metadata::operator<(BaseVersion& a) } } -bool Metadata::operator>(BaseVersion& a) +bool Metadata::operator>(BaseVersion& a) const { try { return operator>(dynamic_cast(a)); diff --git a/launcher/java/JavaMetadata.h b/launcher/java/JavaMetadata.h index 77a42fd78..2e569ee39 100644 --- a/launcher/java/JavaMetadata.h +++ b/launcher/java/JavaMetadata.h @@ -32,17 +32,17 @@ enum class DownloadType { Manifest, Archive, Unknown }; class Metadata : public BaseVersion { public: - virtual QString descriptor() override { return version.toString(); } + virtual QString descriptor() const override { return version.toString(); } - virtual QString name() override { return m_name; } + virtual QString name() const override { return m_name; } virtual QString typeString() const override { return vendor; } - virtual bool operator<(BaseVersion& a) override; - virtual bool operator>(BaseVersion& a) override; - bool operator<(const Metadata& rhs); - bool operator==(const Metadata& rhs); - bool operator>(const Metadata& rhs); + virtual bool operator<(BaseVersion& a) const override; + virtual bool operator>(BaseVersion& a) const override; + bool operator<(const Metadata& rhs) const; + bool operator==(const Metadata& rhs) const; + bool operator>(const Metadata& rhs) const; QString m_name; QString vendor; diff --git a/launcher/meta/Version.cpp b/launcher/meta/Version.cpp index 74e71e91c..ce9a9cc8a 100644 --- a/launcher/meta/Version.cpp +++ b/launcher/meta/Version.cpp @@ -21,11 +21,11 @@ Meta::Version::Version(const QString& uid, const QString& version) : BaseVersion(), m_uid(uid), m_version(version) {} -QString Meta::Version::descriptor() +QString Meta::Version::descriptor() const { return m_version; } -QString Meta::Version::name() +QString Meta::Version::name() const { if (m_data) return m_data->name; @@ -88,7 +88,7 @@ QString Meta::Version::localFilename() const ::Version Meta::Version::toComparableVersion() const { - return { const_cast(this)->descriptor() }; + return { descriptor() }; } void Meta::Version::setType(const QString& type) diff --git a/launcher/meta/Version.h b/launcher/meta/Version.h index 6bddbf473..a2bbc6176 100644 --- a/launcher/meta/Version.h +++ b/launcher/meta/Version.h @@ -40,8 +40,8 @@ class Version : public QObject, public BaseVersion, public BaseEntity { explicit Version(const QString& uid, const QString& version); virtual ~Version() = default; - QString descriptor() override; - QString name() override; + QString descriptor() const override; + QString name() const override; QString typeString() const override; QString uid() const { return m_uid; } From 24726ea62167f349557638ed9fb8513b72d1c94c Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Wed, 17 Sep 2025 10:50:54 +0100 Subject: [PATCH 420/695] Avoid const-cast in WorldList - subclass is unnecessary and result->formats() is correct Signed-off-by: TheKodeToad --- launcher/minecraft/WorldList.cpp | 52 ++++++++++---------------------- 1 file changed, 16 insertions(+), 36 deletions(-) diff --git a/launcher/minecraft/WorldList.cpp b/launcher/minecraft/WorldList.cpp index 6a821ba60..059feabde 100644 --- a/launcher/minecraft/WorldList.cpp +++ b/launcher/minecraft/WorldList.cpp @@ -36,6 +36,7 @@ #include "WorldList.h" #include +#include #include #include #include @@ -301,50 +302,31 @@ QStringList WorldList::mimeTypes() const return types; } -class WorldMimeData : public QMimeData { - Q_OBJECT - - public: - WorldMimeData(QList worlds) { m_worlds = worlds; } - QStringList formats() const { return QMimeData::formats() << "text/uri-list"; } - - protected: - QVariant retrieveData(const QString& mimetype, QMetaType type) const - { - QList urls; - for (auto& world : m_worlds) { - if (!world.isValid() || !world.isOnFS()) - continue; - QString worldPath = world.container().absoluteFilePath(); - qDebug() << worldPath; - urls.append(QUrl::fromLocalFile(worldPath)); - } - const_cast(this)->setUrls(urls); - return QMimeData::retrieveData(mimetype, type); - } - - private: - QList m_worlds; -}; - QMimeData* WorldList::mimeData(const QModelIndexList& indexes) const { - if (indexes.size() == 0) - return new QMimeData(); + QList urls; - QList worlds_; for (auto idx : indexes) { if (idx.column() != 0) continue; + int row = idx.row(); if (row < 0 || row >= this->m_worlds.size()) continue; - worlds_.append(this->m_worlds[row]); - } - if (!worlds_.size()) { - return new QMimeData(); + + const World& world = m_worlds[row]; + + if (!world.isValid() || !world.isOnFS()) + continue; + + QString worldPath = world.container().absoluteFilePath(); + qDebug() << worldPath; + urls.append(QUrl::fromLocalFile(worldPath)); } - return new WorldMimeData(worlds_); + + auto result = new QMimeData(); + result->setUrls(urls); + return result; } Qt::ItemFlags WorldList::flags(const QModelIndex& index) const @@ -453,5 +435,3 @@ void WorldList::loadWorldsAsync() }); } } - -#include "WorldList.moc" From 7c9c9432dd2a5aa9a8027b35e2e5dafa5f3562f6 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Wed, 17 Sep 2025 11:24:10 +0100 Subject: [PATCH 421/695] Remove pointless const_cast in ProgressWidget Signed-off-by: TheKodeToad --- launcher/ui/widgets/ProgressWidget.cpp | 6 +++--- launcher/ui/widgets/ProgressWidget.h | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/launcher/ui/widgets/ProgressWidget.cpp b/launcher/ui/widgets/ProgressWidget.cpp index 9181de7f8..69c7e6f17 100644 --- a/launcher/ui/widgets/ProgressWidget.cpp +++ b/launcher/ui/widgets/ProgressWidget.cpp @@ -39,7 +39,7 @@ void ProgressWidget::progressFormat(QString format) m_bar->setFormat(format); } -void ProgressWidget::watch(const Task* task) +void ProgressWidget::watch(Task* task) { if (!task) return; @@ -61,11 +61,11 @@ void ProgressWidget::watch(const Task* task) connect(m_task, &Task::started, this, &ProgressWidget::show); } -void ProgressWidget::start(const Task* task) +void ProgressWidget::start(Task* task) { watch(task); if (!m_task->isRunning()) - QMetaObject::invokeMethod(const_cast(m_task), "start", Qt::QueuedConnection); + QMetaObject::invokeMethod(m_task, "start", Qt::QueuedConnection); } bool ProgressWidget::exec(std::shared_ptr task) diff --git a/launcher/ui/widgets/ProgressWidget.h b/launcher/ui/widgets/ProgressWidget.h index b0458f335..4d9097b8a 100644 --- a/launcher/ui/widgets/ProgressWidget.h +++ b/launcher/ui/widgets/ProgressWidget.h @@ -27,10 +27,10 @@ class ProgressWidget : public QWidget { public slots: /** Watch the progress of a task. */ - void watch(const Task* task); + void watch(Task* task); /** Watch the progress of a task, and start it if needed */ - void start(const Task* task); + void start(Task* task); /** Blocking way of waiting for a task to finish. */ bool exec(std::shared_ptr task); @@ -50,7 +50,7 @@ class ProgressWidget : public QWidget { private: QLabel* m_label = nullptr; QProgressBar* m_bar = nullptr; - const Task* m_task = nullptr; + Task* m_task = nullptr; bool m_hide_if_inactive = false; }; From cd4f119e4b73256464c8db42ad7d7abfc8e27a36 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Fri, 19 Sep 2025 00:03:22 +0100 Subject: [PATCH 422/695] Replace getThemedIcon with APPLICATION->logo() Signed-off-by: TheKodeToad --- launcher/Application.cpp | 7 ++----- launcher/Application.h | 2 +- launcher/VersionProxyModel.cpp | 6 +++--- launcher/minecraft/ShortcutUtils.cpp | 2 +- launcher/minecraft/mod/DataPackFolderModel.cpp | 3 +-- launcher/minecraft/mod/ModFolderModel.cpp | 4 +--- launcher/minecraft/mod/ResourceFolderModel.cpp | 5 ++--- launcher/minecraft/mod/ResourcePackFolderModel.cpp | 3 +-- launcher/minecraft/mod/TexturePackFolderModel.cpp | 6 +----- launcher/ui/MainWindow.cpp | 12 ++++++------ launcher/ui/ViewLogWindow.cpp | 2 +- launcher/ui/dialogs/AboutDialog.cpp | 2 +- launcher/ui/dialogs/CreateShortcutDialog.cpp | 3 +-- launcher/ui/dialogs/InstallLoaderDialog.cpp | 2 +- launcher/ui/dialogs/NewInstanceDialog.cpp | 2 +- launcher/ui/dialogs/ProfileSetupDialog.cpp | 6 +++--- launcher/ui/dialogs/ResourceDownloadDialog.cpp | 2 +- launcher/ui/dialogs/ReviewMessageBox.cpp | 4 +--- launcher/ui/dialogs/UpdateAvailableDialog.cpp | 3 +-- launcher/ui/java/InstallJavaDialog.cpp | 2 +- launcher/ui/pages/global/APIPage.h | 3 +-- launcher/ui/pages/global/AccountListPage.h | 5 ++--- launcher/ui/pages/global/AppearancePage.h | 3 +-- launcher/ui/pages/global/ExternalToolsPage.h | 5 ++--- launcher/ui/pages/global/JavaPage.h | 3 +-- launcher/ui/pages/global/LanguagePage.cpp | 1 + launcher/ui/pages/global/LanguagePage.h | 3 +-- launcher/ui/pages/global/LauncherPage.h | 3 +-- launcher/ui/pages/global/MinecraftPage.h | 3 +-- launcher/ui/pages/global/ProxyPage.h | 3 +-- launcher/ui/pages/instance/DataPackPage.h | 2 +- launcher/ui/pages/instance/GameOptionsPage.h | 3 +-- launcher/ui/pages/instance/InstanceSettingsPage.h | 3 +-- launcher/ui/pages/instance/LogPage.h | 3 +-- launcher/ui/pages/instance/ManagedPackPage.cpp | 2 +- launcher/ui/pages/instance/ModFolderPage.h | 6 +++--- launcher/ui/pages/instance/NotesPage.h | 5 ++--- launcher/ui/pages/instance/OtherLogsPage.h | 2 +- launcher/ui/pages/instance/ResourcePackPage.h | 2 +- launcher/ui/pages/instance/ScreenshotsPage.cpp | 2 +- launcher/ui/pages/instance/ScreenshotsPage.h | 3 +-- launcher/ui/pages/instance/ServersPage.cpp | 3 ++- launcher/ui/pages/instance/ServersPage.h | 4 ++-- launcher/ui/pages/instance/ShaderPackPage.h | 2 +- launcher/ui/pages/instance/TexturePackPage.h | 2 +- launcher/ui/pages/instance/VersionPage.cpp | 6 +++--- launcher/ui/pages/instance/WorldListPage.cpp | 4 ++-- launcher/ui/pages/instance/WorldListPage.h | 3 +-- launcher/ui/pages/modplatform/CustomPage.h | 4 ++-- launcher/ui/pages/modplatform/ImportPage.h | 3 +-- launcher/ui/pages/modplatform/ResourceModel.cpp | 2 +- .../ui/pages/modplatform/atlauncher/AtlListModel.cpp | 2 +- launcher/ui/pages/modplatform/atlauncher/AtlPage.h | 3 +-- launcher/ui/pages/modplatform/flame/FlameModel.cpp | 2 +- launcher/ui/pages/modplatform/flame/FlamePage.h | 3 +-- .../ui/pages/modplatform/flame/FlameResourcePages.h | 5 +---- .../ui/pages/modplatform/import_ftb/ImportFTBPage.h | 3 +-- .../ui/pages/modplatform/legacy_ftb/ListModel.cpp | 2 +- launcher/ui/pages/modplatform/legacy_ftb/Page.h | 3 +-- .../ui/pages/modplatform/modrinth/ModrinthModel.cpp | 3 ++- .../ui/pages/modplatform/modrinth/ModrinthPage.h | 3 +-- .../modplatform/modrinth/ModrinthResourcePages.h | 4 +--- .../ui/pages/modplatform/technic/TechnicModel.cpp | 2 +- launcher/ui/pages/modplatform/technic/TechnicPage.h | 3 +-- launcher/ui/widgets/JavaWizardWidget.cpp | 8 ++++---- launcher/ui/widgets/MinecraftSettingsWidget.cpp | 2 +- launcher/ui/widgets/PageContainer.cpp | 2 +- 67 files changed, 93 insertions(+), 133 deletions(-) diff --git a/launcher/Application.cpp b/launcher/Application.cpp index a8bfd52e3..dbf55f425 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -1489,12 +1489,9 @@ std::shared_ptr Application::javalist() return m_javalist; } -QIcon Application::getThemedIcon(const QString& name) +QIcon Application::logo() { - if (name == "logo") { - return QIcon(":/" + BuildConfig.LAUNCHER_SVGFILENAME); - } - return QIcon::fromTheme(name); + return QIcon(":/" + BuildConfig.LAUNCHER_SVGFILENAME); } bool Application::openJsonEditor(const QString& filename) diff --git a/launcher/Application.h b/launcher/Application.h index 52a84b461..0fd733b50 100644 --- a/launcher/Application.h +++ b/launcher/Application.h @@ -116,7 +116,7 @@ class Application : public QApplication { qint64 timeSinceStart() const { return m_startTime.msecsTo(QDateTime::currentDateTime()); } - QIcon getThemedIcon(const QString& name); + QIcon logo(); ThemeManager* themeManager() { return m_themeManager.get(); } diff --git a/launcher/VersionProxyModel.cpp b/launcher/VersionProxyModel.cpp index 950b2276a..32048db8e 100644 --- a/launcher/VersionProxyModel.cpp +++ b/launcher/VersionProxyModel.cpp @@ -37,9 +37,9 @@ #include "VersionProxyModel.h" #include #include +#include #include #include -#include "Application.h" class VersionFilterModel : public QSortFilterProxyModel { Q_OBJECT @@ -206,11 +206,11 @@ QVariant VersionProxyModel::data(const QModelIndex& index, int role) const if (column == Name && hasRecommended) { auto recommenced = sourceModel()->data(parentIndex, BaseVersionList::RecommendedRole); if (recommenced.toBool()) { - return APPLICATION->getThemedIcon("star"); + return QIcon::fromTheme("star"); } else if (hasLatest) { auto latest = sourceModel()->data(parentIndex, BaseVersionList::LatestRole); if (latest.toBool()) { - return APPLICATION->getThemedIcon("bug"); + return QIcon::fromTheme("bug"); } } QPixmap pixmap; diff --git a/launcher/minecraft/ShortcutUtils.cpp b/launcher/minecraft/ShortcutUtils.cpp index 0336a9512..ec4ebb31a 100644 --- a/launcher/minecraft/ShortcutUtils.cpp +++ b/launcher/minecraft/ShortcutUtils.cpp @@ -123,7 +123,7 @@ bool createInstanceShortcut(const Shortcut& shortcut, const QString& filePath) // part of fix for weird bug involving the window icon being replaced // dunno why it happens, but parent 2-line fix seems to be enough, so w/e - auto appIcon = APPLICATION->getThemedIcon("logo"); + auto appIcon = APPLICATION->logo(); QFile iconFile(iconPath); if (!iconFile.open(QFile::WriteOnly)) { diff --git a/launcher/minecraft/mod/DataPackFolderModel.cpp b/launcher/minecraft/mod/DataPackFolderModel.cpp index 45cf1271f..1a2badd77 100644 --- a/launcher/minecraft/mod/DataPackFolderModel.cpp +++ b/launcher/minecraft/mod/DataPackFolderModel.cpp @@ -42,7 +42,6 @@ #include #include -#include "Application.h" #include "Version.h" #include "minecraft/mod/tasks/LocalDataPackParseTask.h" @@ -92,7 +91,7 @@ QVariant DataPackFolderModel::data(const QModelIndex& index, int role) const } case Qt::DecorationRole: { if (column == NameColumn && (at(row).isSymLinkUnder(instDirPath()) || at(row).isMoreThanOneHardLink())) - return APPLICATION->getThemedIcon("status-yellow"); + return QIcon::fromTheme("status-yellow"); if (column == ImageColumn) { return at(row).image({ 32, 32 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding); } diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index b613e0af1..45ec76f19 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -49,8 +49,6 @@ #include #include -#include "Application.h" - #include "minecraft/mod/tasks/LocalModParseTask.h" ModFolderModel::ModFolderModel(const QDir& dir, BaseInstance* instance, bool is_indexed, bool create_dir, QObject* parent) @@ -132,7 +130,7 @@ QVariant ModFolderModel::data(const QModelIndex& index, int role) const return m_resources[row]->internal_id(); case Qt::DecorationRole: { if (column == NameColumn && (at(row).isSymLinkUnder(instDirPath()) || at(row).isMoreThanOneHardLink())) - return APPLICATION->getThemedIcon("status-yellow"); + return QIcon::fromTheme("status-yellow"); if (column == ImageColumn) { return at(row).icon({ 32, 32 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding); } diff --git a/launcher/minecraft/mod/ResourceFolderModel.cpp b/launcher/minecraft/mod/ResourceFolderModel.cpp index f93002f06..2b4f9eb14 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.cpp +++ b/launcher/minecraft/mod/ResourceFolderModel.cpp @@ -503,7 +503,7 @@ QVariant ResourceFolderModel::data(const QModelIndex& index, int role) const return m_resources[row]->internal_id(); case Qt::DecorationRole: { if (column == NameColumn && (at(row).isSymLinkUnder(instDirPath()) || at(row).isMoreThanOneHardLink())) - return APPLICATION->getThemedIcon("status-yellow"); + return QIcon::fromTheme("status-yellow"); return {}; } @@ -709,8 +709,7 @@ SortType ResourceFolderModel::columnToSortKey(size_t column) const } /* Standard Proxy Model for createFilterProxyModel */ -bool ResourceFolderModel::ProxyModel::filterAcceptsRow(int source_row, - [[maybe_unused]] const QModelIndex& source_parent) const +bool ResourceFolderModel::ProxyModel::filterAcceptsRow(int source_row, [[maybe_unused]] const QModelIndex& source_parent) const { auto* model = qobject_cast(sourceModel()); if (!model) diff --git a/launcher/minecraft/mod/ResourcePackFolderModel.cpp b/launcher/minecraft/mod/ResourcePackFolderModel.cpp index d9f27a043..df572484b 100644 --- a/launcher/minecraft/mod/ResourcePackFolderModel.cpp +++ b/launcher/minecraft/mod/ResourcePackFolderModel.cpp @@ -41,7 +41,6 @@ #include #include -#include "Application.h" #include "Version.h" #include "minecraft/mod/tasks/LocalDataPackParseTask.h" @@ -96,7 +95,7 @@ QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const } case Qt::DecorationRole: { if (column == NameColumn && (at(row).isSymLinkUnder(instDirPath()) || at(row).isMoreThanOneHardLink())) - return APPLICATION->getThemedIcon("status-yellow"); + return QIcon::fromTheme("status-yellow"); if (column == ImageColumn) { return at(row).image({ 32, 32 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding); } diff --git a/launcher/minecraft/mod/TexturePackFolderModel.cpp b/launcher/minecraft/mod/TexturePackFolderModel.cpp index 8b89b45cd..57c5f8ee9 100644 --- a/launcher/minecraft/mod/TexturePackFolderModel.cpp +++ b/launcher/minecraft/mod/TexturePackFolderModel.cpp @@ -33,10 +33,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#include - -#include "Application.h" - #include "TexturePackFolderModel.h" #include "minecraft/mod/tasks/LocalTexturePackParseTask.h" @@ -98,7 +94,7 @@ QVariant TexturePackFolderModel::data(const QModelIndex& index, int role) const return m_resources[row]->internal_id(); case Qt::DecorationRole: { if (column == NameColumn && (at(row).isSymLinkUnder(instDirPath()) || at(row).isMoreThanOneHardLink())) - return APPLICATION->getThemedIcon("status-yellow"); + return QIcon::fromTheme("status-yellow"); if (column == ImageColumn) { return at(row).image({ 32, 32 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding); } diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index d89224504..3cec0ae53 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -148,7 +148,7 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi { ui->setupUi(this); - setWindowIcon(APPLICATION->getThemedIcon("logo")); + setWindowIcon(APPLICATION->logo()); setWindowTitle(APPLICATION->applicationDisplayName()); #ifndef QT_NO_ACCESSIBILITY setAccessibleName(BuildConfig.LAUNCHER_DISPLAYNAME); @@ -165,7 +165,7 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi // qt designer will delete it when you save the file >:( changeIconButton = new LabeledToolButton(this); changeIconButton->setObjectName(QStringLiteral("changeIconButton")); - changeIconButton->setIcon(APPLICATION->getThemedIcon("news")); + changeIconButton->setIcon(QIcon::fromTheme("news")); changeIconButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); connect(changeIconButton, &QToolButton::clicked, this, &MainWindow::on_actionChangeInstIcon_triggered); ui->instanceToolBar->insertWidgetBefore(ui->actionLaunchInstance, changeIconButton); @@ -277,7 +277,7 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi { m_newsChecker.reset(new NewsChecker(APPLICATION->network(), BuildConfig.NEWS_RSS_URL)); newsLabel = new QToolButton(); - newsLabel->setIcon(APPLICATION->getThemedIcon("news")); + newsLabel->setIcon(QIcon::fromTheme("news")); newsLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); newsLabel->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); newsLabel->setFocusPolicy(Qt::NoFocus); @@ -684,7 +684,7 @@ void MainWindow::repopulateAccountsMenu() if (!face.isNull()) { action->setIcon(face); } else { - action->setIcon(APPLICATION->getThemedIcon("noaccount")); + action->setIcon(QIcon::fromTheme("noaccount")); } const int highestNumberKey = 9; @@ -755,7 +755,7 @@ void MainWindow::defaultAccountChanged() ui->actionAccountsButton->setText(profileLabel); auto face = account->getFace(); if (face.isNull()) { - ui->actionAccountsButton->setIcon(APPLICATION->getThemedIcon("noaccount")); + ui->actionAccountsButton->setIcon(QIcon::fromTheme("noaccount")); } else { ui->actionAccountsButton->setIcon(face); } @@ -763,7 +763,7 @@ void MainWindow::defaultAccountChanged() } // Set the icon to the "no account" icon. - ui->actionAccountsButton->setIcon(APPLICATION->getThemedIcon("noaccount")); + ui->actionAccountsButton->setIcon(QIcon::fromTheme("noaccount")); ui->actionAccountsButton->setText(tr("Accounts")); } diff --git a/launcher/ui/ViewLogWindow.cpp b/launcher/ui/ViewLogWindow.cpp index c0c56f3ee..f4390e2f1 100644 --- a/launcher/ui/ViewLogWindow.cpp +++ b/launcher/ui/ViewLogWindow.cpp @@ -8,7 +8,7 @@ ViewLogWindow::ViewLogWindow(QWidget* parent) : QMainWindow(parent), m_page(new OtherLogsPage("launcher-logs", tr("Launcher Logs"), "Launcher-Logs", nullptr, parent)) { setAttribute(Qt::WA_DeleteOnClose); - setWindowIcon(APPLICATION->getThemedIcon("log")); + setWindowIcon(QIcon::fromTheme("log")); setWindowTitle(tr("View Launcher Logs")); setCentralWidget(m_page); setMinimumSize(m_page->size()); diff --git a/launcher/ui/dialogs/AboutDialog.cpp b/launcher/ui/dialogs/AboutDialog.cpp index 5b7d44ff7..0f3067719 100644 --- a/launcher/ui/dialogs/AboutDialog.cpp +++ b/launcher/ui/dialogs/AboutDialog.cpp @@ -147,7 +147,7 @@ AboutDialog::AboutDialog(QWidget* parent) : QDialog(parent), ui(new Ui::AboutDia ui->urlLabel->setOpenExternalLinks(true); - ui->icon->setPixmap(APPLICATION->getThemedIcon("logo").pixmap(64)); + ui->icon->setPixmap(APPLICATION->logo().pixmap(64)); ui->title->setText(launcherName); ui->versionLabel->setText(BuildConfig.printableVersionString()); diff --git a/launcher/ui/dialogs/CreateShortcutDialog.cpp b/launcher/ui/dialogs/CreateShortcutDialog.cpp index 5cfe33c7f..574881ad0 100644 --- a/launcher/ui/dialogs/CreateShortcutDialog.cpp +++ b/launcher/ui/dialogs/CreateShortcutDialog.cpp @@ -38,7 +38,6 @@ #include #include -#include "Application.h" #include "BuildConfig.h" #include "CreateShortcutDialog.h" #include "ui_CreateShortcutDialog.h" @@ -112,7 +111,7 @@ CreateShortcutDialog::CreateShortcutDialog(InstancePtr instance, QWidget* parent if (account->isInUse()) profileLabel = tr("%1 (in use)").arg(profileLabel); auto face = account->getFace(); - QIcon icon = face.isNull() ? APPLICATION->getThemedIcon("noaccount") : face; + QIcon icon = face.isNull() ? QIcon::fromTheme("noaccount") : face; ui->accountSelectionBox->addItem(profileLabel, account->profileName()); ui->accountSelectionBox->setItemIcon(i, icon); if (defaultAccount == account) diff --git a/launcher/ui/dialogs/InstallLoaderDialog.cpp b/launcher/ui/dialogs/InstallLoaderDialog.cpp index 7082125f2..552b83776 100644 --- a/launcher/ui/dialogs/InstallLoaderDialog.cpp +++ b/launcher/ui/dialogs/InstallLoaderDialog.cpp @@ -53,7 +53,7 @@ class InstallLoaderPage : public VersionSelectWidget, public BasePage { QString id() const override { return uid; } QString displayName() const override { return name; } - QIcon icon() const override { return APPLICATION->getThemedIcon(iconName); } + QIcon icon() const override { return QIcon::fromTheme(iconName); } void openedImpl() override { diff --git a/launcher/ui/dialogs/NewInstanceDialog.cpp b/launcher/ui/dialogs/NewInstanceDialog.cpp index 9e74cd7ac..5542f6986 100644 --- a/launcher/ui/dialogs/NewInstanceDialog.cpp +++ b/launcher/ui/dialogs/NewInstanceDialog.cpp @@ -74,7 +74,7 @@ NewInstanceDialog::NewInstanceDialog(const QString& initialGroup, { ui->setupUi(this); - setWindowIcon(APPLICATION->getThemedIcon("new")); + setWindowIcon(QIcon::fromTheme("new")); InstIconKey = "default"; ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey)); diff --git a/launcher/ui/dialogs/ProfileSetupDialog.cpp b/launcher/ui/dialogs/ProfileSetupDialog.cpp index 0b5e1a784..af8b26c66 100644 --- a/launcher/ui/dialogs/ProfileSetupDialog.cpp +++ b/launcher/ui/dialogs/ProfileSetupDialog.cpp @@ -55,9 +55,9 @@ ProfileSetupDialog::ProfileSetupDialog(MinecraftAccountPtr accountToSetup, QWidg ui->setupUi(this); ui->errorLabel->setVisible(false); - goodIcon = APPLICATION->getThemedIcon("status-good"); - yellowIcon = APPLICATION->getThemedIcon("status-yellow"); - badIcon = APPLICATION->getThemedIcon("status-bad"); + goodIcon = QIcon::fromTheme("status-good"); + yellowIcon = QIcon::fromTheme("status-yellow"); + badIcon = QIcon::fromTheme("status-bad"); static const QRegularExpression s_permittedNames("[a-zA-Z0-9_]{3,16}"); auto nameEdit = ui->nameEdit; diff --git a/launcher/ui/dialogs/ResourceDownloadDialog.cpp b/launcher/ui/dialogs/ResourceDownloadDialog.cpp index 191feeb88..3015ae6e7 100644 --- a/launcher/ui/dialogs/ResourceDownloadDialog.cpp +++ b/launcher/ui/dialogs/ResourceDownloadDialog.cpp @@ -59,7 +59,7 @@ ResourceDownloadDialog::ResourceDownloadDialog(QWidget* parent, const std::share resize(static_cast(std::max(0.5 * parent->width(), 400.0)), static_cast(std::max(0.75 * parent->height(), 400.0))); - setWindowIcon(APPLICATION->getThemedIcon("new")); + setWindowIcon(QIcon::fromTheme("new")); // Bonk Qt over its stupid head and make sure it understands which button is the default one... // See: https://stackoverflow.com/questions/24556831/qbuttonbox-set-default-button diff --git a/launcher/ui/dialogs/ReviewMessageBox.cpp b/launcher/ui/dialogs/ReviewMessageBox.cpp index 96cc8149f..c5f7b5fbe 100644 --- a/launcher/ui/dialogs/ReviewMessageBox.cpp +++ b/launcher/ui/dialogs/ReviewMessageBox.cpp @@ -1,8 +1,6 @@ #include "ReviewMessageBox.h" #include "ui_ReviewMessageBox.h" -#include "Application.h" - #include ReviewMessageBox::ReviewMessageBox(QWidget* parent, [[maybe_unused]] QString const& title, [[maybe_unused]] QString const& icon) @@ -56,7 +54,7 @@ void ReviewMessageBox::appendResource(ResourceInformation&& info) itemTop->insertChildren(1, { customPathItem }); - itemTop->setIcon(1, QIcon(APPLICATION->getThemedIcon("status-yellow"))); + itemTop->setIcon(1, QIcon(QIcon::fromTheme("status-yellow"))); itemTop->setToolTip( childIndx++, tr("This file will be downloaded to a folder location different from the default, possibly due to its loader requiring it.")); diff --git a/launcher/ui/dialogs/UpdateAvailableDialog.cpp b/launcher/ui/dialogs/UpdateAvailableDialog.cpp index 810a1f089..f288fe760 100644 --- a/launcher/ui/dialogs/UpdateAvailableDialog.cpp +++ b/launcher/ui/dialogs/UpdateAvailableDialog.cpp @@ -22,7 +22,6 @@ #include "UpdateAvailableDialog.h" #include -#include "Application.h" #include "BuildConfig.h" #include "Markdown.h" #include "StringUtils.h" @@ -41,7 +40,7 @@ UpdateAvailableDialog::UpdateAvailableDialog(const QString& currentVersion, ui->headerLabel->setText(tr("A new version of %1 is available!").arg(launcherName)); ui->versionAvailableLabel->setText( tr("Version %1 is now available - you have %2 . Would you like to download it now?").arg(availableVersion).arg(currentVersion)); - ui->icon->setPixmap(APPLICATION->getThemedIcon("checkupdate").pixmap(64)); + ui->icon->setPixmap(QIcon::fromTheme("checkupdate").pixmap(64)); auto releaseNotesHtml = markdownToHTML(releaseNotes); ui->releaseNotes->setHtml(StringUtils::htmlListPatch(releaseNotesHtml)); diff --git a/launcher/ui/java/InstallJavaDialog.cpp b/launcher/ui/java/InstallJavaDialog.cpp index 4a628b003..1db4971f4 100644 --- a/launcher/ui/java/InstallJavaDialog.cpp +++ b/launcher/ui/java/InstallJavaDialog.cpp @@ -97,7 +97,7 @@ class InstallJavaPage : public QWidget, public BasePage { QString id() const override { return uid; } QString displayName() const override { return name; } - QIcon icon() const override { return APPLICATION->getThemedIcon(iconName); } + QIcon icon() const override { return QIcon::fromTheme(iconName); } void openedImpl() override { diff --git a/launcher/ui/pages/global/APIPage.h b/launcher/ui/pages/global/APIPage.h index 9252a9ab3..7a22aa069 100644 --- a/launcher/ui/pages/global/APIPage.h +++ b/launcher/ui/pages/global/APIPage.h @@ -39,7 +39,6 @@ #include -#include #include "ui/pages/BasePage.h" namespace Ui { @@ -54,7 +53,7 @@ class APIPage : public QWidget, public BasePage { ~APIPage(); QString displayName() const override { return tr("Services"); } - QIcon icon() const override { return APPLICATION->getThemedIcon("worlds"); } + QIcon icon() const override { return QIcon::fromTheme("worlds"); } QString id() const override { return "apis"; } QString helpPage() const override { return "APIs"; } virtual bool apply() override; diff --git a/launcher/ui/pages/global/AccountListPage.h b/launcher/ui/pages/global/AccountListPage.h index 7bd5101c0..2841b9456 100644 --- a/launcher/ui/pages/global/AccountListPage.h +++ b/launcher/ui/pages/global/AccountListPage.h @@ -41,7 +41,6 @@ #include "ui/pages/BasePage.h" -#include "Application.h" #include "minecraft/auth/AccountList.h" namespace Ui { @@ -59,9 +58,9 @@ class AccountListPage : public QMainWindow, public BasePage { QString displayName() const override { return tr("Accounts"); } QIcon icon() const override { - auto icon = APPLICATION->getThemedIcon("accounts"); + auto icon = QIcon::fromTheme("accounts"); if (icon.isNull()) { - icon = APPLICATION->getThemedIcon("noaccount"); + icon = QIcon::fromTheme("noaccount"); } return icon; } diff --git a/launcher/ui/pages/global/AppearancePage.h b/launcher/ui/pages/global/AppearancePage.h index 29b2d34bf..2220db2cd 100644 --- a/launcher/ui/pages/global/AppearancePage.h +++ b/launcher/ui/pages/global/AppearancePage.h @@ -37,7 +37,6 @@ #include #include -#include "Application.h" #include "java/JavaChecker.h" #include "translations/TranslationsModel.h" #include "ui/pages/BasePage.h" @@ -53,7 +52,7 @@ class AppearancePage : public AppearanceWidget, public BasePage { explicit AppearancePage(QWidget* parent = nullptr) : AppearanceWidget(false, parent) { layout()->setContentsMargins(0, 0, 6, 0); } QString displayName() const override { return tr("Appearance"); } - QIcon icon() const override { return APPLICATION->getThemedIcon("appearance"); } + QIcon icon() const override { return QIcon::fromTheme("appearance"); } QString id() const override { return "appearance-settings"; } QString helpPage() const override { return "Launcher-settings"; } diff --git a/launcher/ui/pages/global/ExternalToolsPage.h b/launcher/ui/pages/global/ExternalToolsPage.h index 377488ccf..702ace557 100644 --- a/launcher/ui/pages/global/ExternalToolsPage.h +++ b/launcher/ui/pages/global/ExternalToolsPage.h @@ -37,7 +37,6 @@ #include -#include #include "ui/pages/BasePage.h" namespace Ui { @@ -54,9 +53,9 @@ class ExternalToolsPage : public QWidget, public BasePage { QString displayName() const override { return tr("Tools"); } QIcon icon() const override { - auto icon = APPLICATION->getThemedIcon("externaltools"); + auto icon = QIcon::fromTheme("externaltools"); if (icon.isNull()) { - icon = APPLICATION->getThemedIcon("loadermods"); + icon = QIcon::fromTheme("loadermods"); } return icon; } diff --git a/launcher/ui/pages/global/JavaPage.h b/launcher/ui/pages/global/JavaPage.h index b30fa22e3..79a3d1b96 100644 --- a/launcher/ui/pages/global/JavaPage.h +++ b/launcher/ui/pages/global/JavaPage.h @@ -35,7 +35,6 @@ #pragma once -#include #include #include #include @@ -57,7 +56,7 @@ class JavaPage : public QWidget, public BasePage { ~JavaPage(); QString displayName() const override { return tr("Java"); } - QIcon icon() const override { return APPLICATION->getThemedIcon("java"); } + QIcon icon() const override { return QIcon::fromTheme("java"); } QString id() const override { return "java-settings"; } QString helpPage() const override { return "Java-settings"; } void retranslate() override; diff --git a/launcher/ui/pages/global/LanguagePage.cpp b/launcher/ui/pages/global/LanguagePage.cpp index af6fc1727..f4be75782 100644 --- a/launcher/ui/pages/global/LanguagePage.cpp +++ b/launcher/ui/pages/global/LanguagePage.cpp @@ -37,6 +37,7 @@ #include "LanguagePage.h" #include +#include "Application.h" #include "ui/widgets/LanguageSelectionWidget.h" LanguagePage::LanguagePage(QWidget* parent) : QWidget(parent) diff --git a/launcher/ui/pages/global/LanguagePage.h b/launcher/ui/pages/global/LanguagePage.h index ff7ce7ddc..b376e1cf2 100644 --- a/launcher/ui/pages/global/LanguagePage.h +++ b/launcher/ui/pages/global/LanguagePage.h @@ -36,7 +36,6 @@ #pragma once -#include #include #include #include "ui/pages/BasePage.h" @@ -51,7 +50,7 @@ class LanguagePage : public QWidget, public BasePage { virtual ~LanguagePage(); QString displayName() const override { return tr("Language"); } - QIcon icon() const override { return APPLICATION->getThemedIcon("language"); } + QIcon icon() const override { return QIcon::fromTheme("language"); } QString id() const override { return "language-settings"; } QString helpPage() const override { return "Language-settings"; } bool apply() override; diff --git a/launcher/ui/pages/global/LauncherPage.h b/launcher/ui/pages/global/LauncherPage.h index d76c84b63..263bf08bb 100644 --- a/launcher/ui/pages/global/LauncherPage.h +++ b/launcher/ui/pages/global/LauncherPage.h @@ -38,7 +38,6 @@ #include #include -#include #include #include "java/JavaChecker.h" #include "ui/pages/BasePage.h" @@ -58,7 +57,7 @@ class LauncherPage : public QWidget, public BasePage { ~LauncherPage(); QString displayName() const override { return tr("General"); } - QIcon icon() const override { return APPLICATION->getThemedIcon("settings"); } + QIcon icon() const override { return QIcon::fromTheme("settings"); } QString id() const override { return "launcher-settings"; } QString helpPage() const override { return "Launcher-settings"; } bool apply() override; diff --git a/launcher/ui/pages/global/MinecraftPage.h b/launcher/ui/pages/global/MinecraftPage.h index b21862536..c21d59a6b 100644 --- a/launcher/ui/pages/global/MinecraftPage.h +++ b/launcher/ui/pages/global/MinecraftPage.h @@ -38,7 +38,6 @@ #include #include -#include "Application.h" #include "java/JavaChecker.h" #include "ui/pages/BasePage.h" #include "ui/widgets/MinecraftSettingsWidget.h" @@ -53,7 +52,7 @@ class MinecraftPage : public MinecraftSettingsWidget, public BasePage { ~MinecraftPage() override {} QString displayName() const override { return tr("Minecraft"); } - QIcon icon() const override { return APPLICATION->getThemedIcon("minecraft"); } + QIcon icon() const override { return QIcon::fromTheme("minecraft"); } QString id() const override { return "minecraft-settings"; } QString helpPage() const override { return "Minecraft-settings"; } bool apply() override diff --git a/launcher/ui/pages/global/ProxyPage.h b/launcher/ui/pages/global/ProxyPage.h index 26118f181..8689a5c80 100644 --- a/launcher/ui/pages/global/ProxyPage.h +++ b/launcher/ui/pages/global/ProxyPage.h @@ -40,7 +40,6 @@ #include #include -#include #include "ui/pages/BasePage.h" namespace Ui { @@ -55,7 +54,7 @@ class ProxyPage : public QWidget, public BasePage { ~ProxyPage(); QString displayName() const override { return tr("Proxy"); } - QIcon icon() const override { return APPLICATION->getThemedIcon("proxy"); } + QIcon icon() const override { return QIcon::fromTheme("proxy"); } QString id() const override { return "proxy-settings"; } QString helpPage() const override { return "Proxy-settings"; } bool apply() override; diff --git a/launcher/ui/pages/instance/DataPackPage.h b/launcher/ui/pages/instance/DataPackPage.h index b71ed2965..bed84540a 100644 --- a/launcher/ui/pages/instance/DataPackPage.h +++ b/launcher/ui/pages/instance/DataPackPage.h @@ -29,7 +29,7 @@ class DataPackPage : public ExternalResourcesPage { explicit DataPackPage(BaseInstance* instance, std::shared_ptr model, QWidget* parent = nullptr); QString displayName() const override { return QObject::tr("Data Packs"); } - QIcon icon() const override { return APPLICATION->getThemedIcon("datapacks"); } + QIcon icon() const override { return QIcon::fromTheme("datapacks"); } QString id() const override { return "datapacks"; } QString helpPage() const override { return "Data-packs"; } bool shouldDisplay() const override { return true; } diff --git a/launcher/ui/pages/instance/GameOptionsPage.h b/launcher/ui/pages/instance/GameOptionsPage.h index a132843e7..43f91976c 100644 --- a/launcher/ui/pages/instance/GameOptionsPage.h +++ b/launcher/ui/pages/instance/GameOptionsPage.h @@ -38,7 +38,6 @@ #include #include -#include #include "ui/pages/BasePage.h" namespace Ui { @@ -59,7 +58,7 @@ class GameOptionsPage : public QWidget, public BasePage { void closedImpl() override; virtual QString displayName() const override { return tr("Game Options"); } - virtual QIcon icon() const override { return APPLICATION->getThemedIcon("settings"); } + virtual QIcon icon() const override { return QIcon::fromTheme("settings"); } virtual QString id() const override { return "gameoptions"; } virtual QString helpPage() const override { return "Game-Options-management"; } void retranslate() override; diff --git a/launcher/ui/pages/instance/InstanceSettingsPage.h b/launcher/ui/pages/instance/InstanceSettingsPage.h index 2c507e84b..aca47e2c7 100644 --- a/launcher/ui/pages/instance/InstanceSettingsPage.h +++ b/launcher/ui/pages/instance/InstanceSettingsPage.h @@ -36,7 +36,6 @@ #pragma once #include -#include "Application.h" #include "BaseInstance.h" #include "ui/pages/BasePage.h" #include "ui/widgets/MinecraftSettingsWidget.h" @@ -53,7 +52,7 @@ class InstanceSettingsPage : public MinecraftSettingsWidget, public BasePage { } ~InstanceSettingsPage() override {} QString displayName() const override { return tr("Settings"); } - QIcon icon() const override { return APPLICATION->getThemedIcon("instance-settings"); } + QIcon icon() const override { return QIcon::fromTheme("instance-settings"); } QString id() const override { return "settings"; } bool apply() override { diff --git a/launcher/ui/pages/instance/LogPage.h b/launcher/ui/pages/instance/LogPage.h index b4d74fb9c..636a8b70d 100644 --- a/launcher/ui/pages/instance/LogPage.h +++ b/launcher/ui/pages/instance/LogPage.h @@ -38,7 +38,6 @@ #include #include -#include #include "BaseInstance.h" #include "launch/LaunchTask.h" #include "ui/pages/BasePage.h" @@ -67,7 +66,7 @@ class LogPage : public QWidget, public BasePage { explicit LogPage(InstancePtr instance, QWidget* parent = 0); virtual ~LogPage(); virtual QString displayName() const override { return tr("Minecraft Log"); } - virtual QIcon icon() const override { return APPLICATION->getThemedIcon("log"); } + virtual QIcon icon() const override { return QIcon::fromTheme("log"); } virtual QString id() const override { return "console"; } virtual bool apply() override; virtual QString helpPage() const override { return "Minecraft-Logs"; } diff --git a/launcher/ui/pages/instance/ManagedPackPage.cpp b/launcher/ui/pages/instance/ManagedPackPage.cpp index d46f0f8d5..e97935650 100644 --- a/launcher/ui/pages/instance/ManagedPackPage.cpp +++ b/launcher/ui/pages/instance/ManagedPackPage.cpp @@ -166,7 +166,7 @@ QString ManagedPackPage::displayName() const QIcon ManagedPackPage::icon() const { - return APPLICATION->getThemedIcon(m_inst->getManagedPackType()); + return QIcon::fromTheme(m_inst->getManagedPackType()); } QString ManagedPackPage::helpPage() const diff --git a/launcher/ui/pages/instance/ModFolderPage.h b/launcher/ui/pages/instance/ModFolderPage.h index 9b9665571..b33992470 100644 --- a/launcher/ui/pages/instance/ModFolderPage.h +++ b/launcher/ui/pages/instance/ModFolderPage.h @@ -52,7 +52,7 @@ class ModFolderPage : public ExternalResourcesPage { void setFilter(const QString& filter) { m_fileSelectionFilter = filter; } virtual QString displayName() const override { return tr("Mods"); } - virtual QIcon icon() const override { return APPLICATION->getThemedIcon("loadermods"); } + virtual QIcon icon() const override { return QIcon::fromTheme("loadermods"); } virtual QString id() const override { return "mods"; } virtual QString helpPage() const override { return "Loader-mods"; } @@ -83,7 +83,7 @@ class CoreModFolderPage : public ModFolderPage { virtual ~CoreModFolderPage() = default; virtual QString displayName() const override { return tr("Core Mods"); } - virtual QIcon icon() const override { return APPLICATION->getThemedIcon("coremods"); } + virtual QIcon icon() const override { return QIcon::fromTheme("coremods"); } virtual QString id() const override { return "coremods"; } virtual QString helpPage() const override { return "Core-mods"; } @@ -97,7 +97,7 @@ class NilModFolderPage : public ModFolderPage { virtual ~NilModFolderPage() = default; virtual QString displayName() const override { return tr("Nilmods"); } - virtual QIcon icon() const override { return APPLICATION->getThemedIcon("coremods"); } + virtual QIcon icon() const override { return QIcon::fromTheme("coremods"); } virtual QString id() const override { return "nilmods"; } virtual QString helpPage() const override { return "Nilmods"; } diff --git a/launcher/ui/pages/instance/NotesPage.h b/launcher/ui/pages/instance/NotesPage.h index 3351d25fc..f11e2ad7c 100644 --- a/launcher/ui/pages/instance/NotesPage.h +++ b/launcher/ui/pages/instance/NotesPage.h @@ -37,7 +37,6 @@ #include -#include #include "BaseInstance.h" #include "ui/pages/BasePage.h" @@ -54,9 +53,9 @@ class NotesPage : public QWidget, public BasePage { virtual QString displayName() const override { return tr("Notes"); } virtual QIcon icon() const override { - auto icon = APPLICATION->getThemedIcon("notes"); + auto icon = QIcon::fromTheme("notes"); if (icon.isNull()) - icon = APPLICATION->getThemedIcon("news"); + icon = QIcon::fromTheme("news"); return icon; } virtual QString id() const override { return "notes"; } diff --git a/launcher/ui/pages/instance/OtherLogsPage.h b/launcher/ui/pages/instance/OtherLogsPage.h index fbf9991e1..9fc0ba3b9 100644 --- a/launcher/ui/pages/instance/OtherLogsPage.h +++ b/launcher/ui/pages/instance/OtherLogsPage.h @@ -57,7 +57,7 @@ class OtherLogsPage : public QWidget, public BasePage { QString id() const override { return m_id; } QString displayName() const override { return m_displayName; } - QIcon icon() const override { return APPLICATION->getThemedIcon("log"); } + QIcon icon() const override { return QIcon::fromTheme("log"); } QString helpPage() const override { return m_helpPage; } void retranslate() override; diff --git a/launcher/ui/pages/instance/ResourcePackPage.h b/launcher/ui/pages/instance/ResourcePackPage.h index 40fe10b79..0ad24fc45 100644 --- a/launcher/ui/pages/instance/ResourcePackPage.h +++ b/launcher/ui/pages/instance/ResourcePackPage.h @@ -51,7 +51,7 @@ class ResourcePackPage : public ExternalResourcesPage { explicit ResourcePackPage(MinecraftInstance* instance, std::shared_ptr model, QWidget* parent = 0); QString displayName() const override { return tr("Resource Packs"); } - QIcon icon() const override { return APPLICATION->getThemedIcon("resourcepacks"); } + QIcon icon() const override { return QIcon::fromTheme("resourcepacks"); } QString id() const override { return "resourcepacks"; } QString helpPage() const override { return "Resource-packs"; } diff --git a/launcher/ui/pages/instance/ScreenshotsPage.cpp b/launcher/ui/pages/instance/ScreenshotsPage.cpp index 082b44308..9aa7ea50a 100644 --- a/launcher/ui/pages/instance/ScreenshotsPage.cpp +++ b/launcher/ui/pages/instance/ScreenshotsPage.cpp @@ -134,7 +134,7 @@ class FilterModel : public QIdentityProxyModel { { m_thumbnailingPool.setMaxThreadCount(4); m_thumbnailCache = std::make_shared(); - m_thumbnailCache->add("placeholder", APPLICATION->getThemedIcon("screenshot-placeholder")); + m_thumbnailCache->add("placeholder", QIcon::fromTheme("screenshot-placeholder")); connect(&watcher, &QFileSystemWatcher::fileChanged, this, &FilterModel::fileChanged); } virtual ~FilterModel() diff --git a/launcher/ui/pages/instance/ScreenshotsPage.h b/launcher/ui/pages/instance/ScreenshotsPage.h index bb127b429..b9c750a1f 100644 --- a/launcher/ui/pages/instance/ScreenshotsPage.h +++ b/launcher/ui/pages/instance/ScreenshotsPage.h @@ -37,7 +37,6 @@ #include -#include #include "ui/pages/BasePage.h" #include "settings/Setting.h" @@ -67,7 +66,7 @@ class ScreenshotsPage : public QMainWindow, public BasePage { virtual bool eventFilter(QObject*, QEvent*) override; virtual QString displayName() const override { return tr("Screenshots"); } - virtual QIcon icon() const override { return APPLICATION->getThemedIcon("screenshots"); } + virtual QIcon icon() const override { return QIcon::fromTheme("screenshots"); } virtual QString id() const override { return "screenshots"; } virtual QString helpPage() const override { return "Screenshots-management"; } virtual bool apply() override { return !m_uploadActive; } diff --git a/launcher/ui/pages/instance/ServersPage.cpp b/launcher/ui/pages/instance/ServersPage.cpp index d1f39bb88..f616a5b22 100644 --- a/launcher/ui/pages/instance/ServersPage.cpp +++ b/launcher/ui/pages/instance/ServersPage.cpp @@ -36,6 +36,7 @@ */ #include "ServersPage.h" +#include "Application.h" #include "ServerPingTask.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui_ServersPage.h" @@ -319,7 +320,7 @@ class ServersModel : public QAbstractListModel { if (px.loadFromData(bytes)) return QIcon(px); } - return APPLICATION->getThemedIcon("unknown_server"); + return QIcon::fromTheme("unknown_server"); } case 1: return m_servers[row].m_address; diff --git a/launcher/ui/pages/instance/ServersPage.h b/launcher/ui/pages/instance/ServersPage.h index 77710d6cc..49a746245 100644 --- a/launcher/ui/pages/instance/ServersPage.h +++ b/launcher/ui/pages/instance/ServersPage.h @@ -39,7 +39,7 @@ #include #include -#include +#include "BaseInstance.h" #include "ui/pages/BasePage.h" #include "settings/Setting.h" @@ -63,7 +63,7 @@ class ServersPage : public QMainWindow, public BasePage { void closedImpl() override; virtual QString displayName() const override { return tr("Servers"); } - virtual QIcon icon() const override { return APPLICATION->getThemedIcon("server"); } + virtual QIcon icon() const override { return QIcon::fromTheme("server"); } virtual QString id() const override { return "servers"; } virtual QString helpPage() const override { return "Servers-management"; } void retranslate() override; diff --git a/launcher/ui/pages/instance/ShaderPackPage.h b/launcher/ui/pages/instance/ShaderPackPage.h index 128c48ae7..c6ae3bc24 100644 --- a/launcher/ui/pages/instance/ShaderPackPage.h +++ b/launcher/ui/pages/instance/ShaderPackPage.h @@ -48,7 +48,7 @@ class ShaderPackPage : public ExternalResourcesPage { ~ShaderPackPage() override = default; QString displayName() const override { return tr("Shader Packs"); } - QIcon icon() const override { return APPLICATION->getThemedIcon("shaderpacks"); } + QIcon icon() const override { return QIcon::fromTheme("shaderpacks"); } QString id() const override { return "shaderpacks"; } QString helpPage() const override { return "shader-packs"; } diff --git a/launcher/ui/pages/instance/TexturePackPage.h b/launcher/ui/pages/instance/TexturePackPage.h index 3ebca3e87..2c92212b9 100644 --- a/launcher/ui/pages/instance/TexturePackPage.h +++ b/launcher/ui/pages/instance/TexturePackPage.h @@ -51,7 +51,7 @@ class TexturePackPage : public ExternalResourcesPage { explicit TexturePackPage(MinecraftInstance* instance, std::shared_ptr model, QWidget* parent = nullptr); QString displayName() const override { return tr("Texture packs"); } - QIcon icon() const override { return APPLICATION->getThemedIcon("resourcepacks"); } + QIcon icon() const override { return QIcon::fromTheme("resourcepacks"); } QString id() const override { return "texturepacks"; } QString helpPage() const override { return "Texture-packs"; } diff --git a/launcher/ui/pages/instance/VersionPage.cpp b/launcher/ui/pages/instance/VersionPage.cpp index d355f38fb..ef5427a00 100644 --- a/launcher/ui/pages/instance/VersionPage.cpp +++ b/launcher/ui/pages/instance/VersionPage.cpp @@ -91,12 +91,12 @@ class IconProxy : public QIdentityProxyModel { if (!var.isNull()) { auto string = var.toString(); if (string == "warning") { - return APPLICATION->getThemedIcon("status-yellow"); + return QIcon::fromTheme("status-yellow"); } else if (string == "error") { - return APPLICATION->getThemedIcon("status-bad"); + return QIcon::fromTheme("status-bad"); } } - return APPLICATION->getThemedIcon("status-good"); + return QIcon::fromTheme("status-good"); } return var; } diff --git a/launcher/ui/pages/instance/WorldListPage.cpp b/launcher/ui/pages/instance/WorldListPage.cpp index 31f3bfd3e..c7eaf94a0 100644 --- a/launcher/ui/pages/instance/WorldListPage.cpp +++ b/launcher/ui/pages/instance/WorldListPage.cpp @@ -48,10 +48,10 @@ #include #include #include +#include #include #include #include -#include #include "FileSystem.h" #include "tools/MCEditTool.h" @@ -77,7 +77,7 @@ class WorldListProxyModel : public QSortFilterProxyModel { auto iconFile = worlds->data(sourceIndex, WorldList::IconFileRole).toString(); if (iconFile.isNull()) { // NOTE: Minecraft uses the same placeholder for servers AND worlds - return APPLICATION->getThemedIcon("unknown_server"); + return QIcon::fromTheme("unknown_server"); } return QIcon(iconFile); } diff --git a/launcher/ui/pages/instance/WorldListPage.h b/launcher/ui/pages/instance/WorldListPage.h index 08cf7dc5f..9b931066d 100644 --- a/launcher/ui/pages/instance/WorldListPage.h +++ b/launcher/ui/pages/instance/WorldListPage.h @@ -37,7 +37,6 @@ #include -#include #include #include "minecraft/MinecraftInstance.h" #include "ui/pages/BasePage.h" @@ -57,7 +56,7 @@ class WorldListPage : public QMainWindow, public BasePage { virtual ~WorldListPage(); virtual QString displayName() const override { return tr("Worlds"); } - virtual QIcon icon() const override { return APPLICATION->getThemedIcon("worlds"); } + virtual QIcon icon() const override { return QIcon::fromTheme("worlds"); } virtual QString id() const override { return "worlds"; } virtual QString helpPage() const override { return "Worlds"; } virtual bool shouldDisplay() const override; diff --git a/launcher/ui/pages/modplatform/CustomPage.h b/launcher/ui/pages/modplatform/CustomPage.h index c5d6d5af5..2bfb1de29 100644 --- a/launcher/ui/pages/modplatform/CustomPage.h +++ b/launcher/ui/pages/modplatform/CustomPage.h @@ -37,7 +37,7 @@ #include -#include +#include "BaseVersion.h" #include "tasks/Task.h" #include "ui/pages/BasePage.h" @@ -54,7 +54,7 @@ class CustomPage : public QWidget, public BasePage { explicit CustomPage(NewInstanceDialog* dialog, QWidget* parent = 0); virtual ~CustomPage(); virtual QString displayName() const override { return tr("Custom"); } - virtual QIcon icon() const override { return APPLICATION->getThemedIcon("minecraft"); } + virtual QIcon icon() const override { return QIcon::fromTheme("minecraft"); } virtual QString id() const override { return "vanilla"; } virtual QString helpPage() const override { return "Vanilla-platform"; } virtual bool shouldDisplay() const override; diff --git a/launcher/ui/pages/modplatform/ImportPage.h b/launcher/ui/pages/modplatform/ImportPage.h index 70d7736eb..1119e709a 100644 --- a/launcher/ui/pages/modplatform/ImportPage.h +++ b/launcher/ui/pages/modplatform/ImportPage.h @@ -37,7 +37,6 @@ #include -#include #include "tasks/Task.h" #include "ui/pages/BasePage.h" @@ -54,7 +53,7 @@ class ImportPage : public QWidget, public BasePage { explicit ImportPage(NewInstanceDialog* dialog, QWidget* parent = 0); virtual ~ImportPage(); virtual QString displayName() const override { return tr("Import"); } - virtual QIcon icon() const override { return APPLICATION->getThemedIcon("viewfolder"); } + virtual QIcon icon() const override { return QIcon::fromTheme("viewfolder"); } virtual QString id() const override { return "import"; } virtual QString helpPage() const override { return "Zip-import"; } virtual bool shouldDisplay() const override; diff --git a/launcher/ui/pages/modplatform/ResourceModel.cpp b/launcher/ui/pages/modplatform/ResourceModel.cpp index 5ea70789e..eea7af25e 100644 --- a/launcher/ui/pages/modplatform/ResourceModel.cpp +++ b/launcher/ui/pages/modplatform/ResourceModel.cpp @@ -65,7 +65,7 @@ auto ResourceModel::data(const QModelIndex& index, int role) const -> QVariant icon_or_none.has_value()) return icon_or_none.value(); - return APPLICATION->getThemedIcon("screenshot-placeholder"); + return QIcon::fromTheme("screenshot-placeholder"); } else { return {}; } diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp index 9e2e7a2ca..c5fec48d7 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp +++ b/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp @@ -61,7 +61,7 @@ QVariant ListModel::data(const QModelIndex& index, int role) const if (m_logoMap.contains(pack.safeName)) { return (m_logoMap.value(pack.safeName)); } - auto icon = APPLICATION->getThemedIcon("atlauncher-placeholder"); + auto icon = QIcon::fromTheme("atlauncher-placeholder"); auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/images/%1").arg(pack.safeName); ((ListModel*)this)->requestLogo(pack.safeName, url); diff --git a/launcher/ui/pages/modplatform/atlauncher/AtlPage.h b/launcher/ui/pages/modplatform/atlauncher/AtlPage.h index 556e90b1d..8c8bf53b3 100644 --- a/launcher/ui/pages/modplatform/atlauncher/AtlPage.h +++ b/launcher/ui/pages/modplatform/atlauncher/AtlPage.h @@ -41,7 +41,6 @@ #include #include -#include "Application.h" #include "ui/pages/modplatform/ModpackProviderBasePage.h" namespace Ui { @@ -57,7 +56,7 @@ class AtlPage : public QWidget, public ModpackProviderBasePage { explicit AtlPage(NewInstanceDialog* dialog, QWidget* parent = 0); virtual ~AtlPage(); virtual QString displayName() const override { return "ATLauncher"; } - virtual QIcon icon() const override { return APPLICATION->getThemedIcon("atlauncher"); } + virtual QIcon icon() const override { return QIcon::fromTheme("atlauncher"); } virtual QString id() const override { return "atl"; } virtual QString helpPage() const override { return "ATL-platform"; } virtual bool shouldDisplay() const override; diff --git a/launcher/ui/pages/modplatform/flame/FlameModel.cpp b/launcher/ui/pages/modplatform/flame/FlameModel.cpp index ea051bd39..e5d880dca 100644 --- a/launcher/ui/pages/modplatform/flame/FlameModel.cpp +++ b/launcher/ui/pages/modplatform/flame/FlameModel.cpp @@ -50,7 +50,7 @@ QVariant ListModel::data(const QModelIndex& index, int role) const if (m_logoMap.contains(pack->logoName)) { return (m_logoMap.value(pack->logoName)); } - QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); + QIcon icon = QIcon::fromTheme("screenshot-placeholder"); ((ListModel*)this)->requestLogo(pack->logoName, pack->logoUrl); return icon; } diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.h b/launcher/ui/pages/modplatform/flame/FlamePage.h index 2252efa07..eb763229f 100644 --- a/launcher/ui/pages/modplatform/flame/FlamePage.h +++ b/launcher/ui/pages/modplatform/flame/FlamePage.h @@ -37,7 +37,6 @@ #include -#include #include #include "modplatform/ModIndex.h" #include "ui/pages/modplatform/ModpackProviderBasePage.h" @@ -61,7 +60,7 @@ class FlamePage : public QWidget, public ModpackProviderBasePage { explicit FlamePage(NewInstanceDialog* dialog, QWidget* parent = 0); virtual ~FlamePage(); virtual QString displayName() const override { return "CurseForge"; } - virtual QIcon icon() const override { return APPLICATION->getThemedIcon("flame"); } + virtual QIcon icon() const override { return QIcon::fromTheme("flame"); } virtual QString id() const override { return "flame"; } virtual QString helpPage() const override { return "Flame-platform"; } virtual bool shouldDisplay() const override; diff --git a/launcher/ui/pages/modplatform/flame/FlameResourcePages.h b/launcher/ui/pages/modplatform/flame/FlameResourcePages.h index 19f3731c7..d4b697ae0 100644 --- a/launcher/ui/pages/modplatform/flame/FlameResourcePages.h +++ b/launcher/ui/pages/modplatform/flame/FlameResourcePages.h @@ -40,7 +40,6 @@ #pragma once #include -#include "Application.h" #include "modplatform/ResourceAPI.h" @@ -58,7 +57,7 @@ static inline QString displayName() } static inline QIcon icon() { - return APPLICATION->getThemedIcon("flame"); + return QIcon::fromTheme("flame"); } static inline QString id() { @@ -181,8 +180,6 @@ class FlameShaderPackPage : public ShaderPackResourcePage { void openUrl(const QUrl& url) override; }; - - class FlameDataPackPage : public DataPackResourcePage { Q_OBJECT diff --git a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.h b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.h index c00a93dfa..25b900f97 100644 --- a/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.h +++ b/launcher/ui/pages/modplatform/import_ftb/ImportFTBPage.h @@ -23,7 +23,6 @@ #include #include -#include #include "modplatform/import_ftb/PackHelpers.h" #include "ui/pages/modplatform/ModpackProviderBasePage.h" #include "ui/pages/modplatform/import_ftb/ListModel.h" @@ -42,7 +41,7 @@ class ImportFTBPage : public QWidget, public ModpackProviderBasePage { explicit ImportFTBPage(NewInstanceDialog* dialog, QWidget* parent = 0); virtual ~ImportFTBPage(); QString displayName() const override { return tr("FTB App Import"); } - QIcon icon() const override { return APPLICATION->getThemedIcon("ftb_logo"); } + QIcon icon() const override { return QIcon::fromTheme("ftb_logo"); } QString id() const override { return "import_ftb"; } QString helpPage() const override { return "FTB-import"; } bool shouldDisplay() const override { return true; } diff --git a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp index efa9bd3bc..eb95b291c 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp +++ b/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp @@ -173,7 +173,7 @@ QVariant ListModel::data(const QModelIndex& index, int role) const if (m_logoMap.contains(pack.logo)) { return (m_logoMap.value(pack.logo)); } - QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); + QIcon icon = QIcon::fromTheme("screenshot-placeholder"); ((ListModel*)this)->requestLogo(pack.logo); return icon; } diff --git a/launcher/ui/pages/modplatform/legacy_ftb/Page.h b/launcher/ui/pages/modplatform/legacy_ftb/Page.h index fc789971f..db70ae79e 100644 --- a/launcher/ui/pages/modplatform/legacy_ftb/Page.h +++ b/launcher/ui/pages/modplatform/legacy_ftb/Page.h @@ -39,7 +39,6 @@ #include #include -#include #include "QObjectPtr.h" #include "modplatform/legacy_ftb/PackFetchTask.h" #include "modplatform/legacy_ftb/PackHelpers.h" @@ -64,7 +63,7 @@ class Page : public QWidget, public ModpackProviderBasePage { explicit Page(NewInstanceDialog* dialog, QWidget* parent = 0); virtual ~Page(); QString displayName() const override { return "FTB Legacy"; } - QIcon icon() const override { return APPLICATION->getThemedIcon("ftb_logo"); } + QIcon icon() const override { return QIcon::fromTheme("ftb_logo"); } QString id() const override { return "legacy_ftb"; } QString helpPage() const override { return "FTB-legacy"; } bool shouldDisplay() const override; diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp index bf6215356..acd321d4b 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -36,6 +36,7 @@ #include "ModrinthModel.h" +#include "Application.h" #include "BuildConfig.h" #include "Json.h" #include "modplatform/ModIndex.h" @@ -91,7 +92,7 @@ auto ModpackListModel::data(const QModelIndex& index, int role) const -> QVarian if (m_logoMap.contains(pack->logoName)) return m_logoMap.value(pack->logoName); - QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); + QIcon icon = QIcon::fromTheme("screenshot-placeholder"); ((ModpackListModel*)this)->requestLogo(pack->logoName, pack->logoUrl); return icon; } diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h index 600500c6d..4ca41a3e0 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h @@ -36,7 +36,6 @@ #pragma once -#include "Application.h" #include "modplatform/ModIndex.h" #include "modplatform/modrinth/ModrinthAPI.h" #include "ui/dialogs/NewInstanceDialog.h" @@ -64,7 +63,7 @@ class ModrinthPage : public QWidget, public ModpackProviderBasePage { ~ModrinthPage() override; QString displayName() const override { return tr("Modrinth"); } - QIcon icon() const override { return APPLICATION->getThemedIcon("modrinth"); } + QIcon icon() const override { return QIcon::fromTheme("modrinth"); } QString id() const override { return "modrinth"; } QString helpPage() const override { return "Modrinth-platform"; } diff --git a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h index cb0f4d85c..3f41a3d5e 100644 --- a/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h +++ b/launcher/ui/pages/modplatform/modrinth/ModrinthResourcePages.h @@ -38,8 +38,6 @@ #pragma once -#include "Application.h" - #include "modplatform/ResourceAPI.h" #include "ui/pages/modplatform/DataPackPage.h" @@ -57,7 +55,7 @@ static inline QString displayName() } static inline QIcon icon() { - return APPLICATION->getThemedIcon("modrinth"); + return QIcon::fromTheme("modrinth"); } static inline QString id() { diff --git a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp index c425044a2..62bad833b 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp @@ -71,7 +71,7 @@ QVariant Technic::ListModel::data(const QModelIndex& index, int role) const if (m_logoMap.contains(pack.logoName)) { return (m_logoMap.value(pack.logoName)); } - QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); + QIcon icon = QIcon::fromTheme("screenshot-placeholder"); ((ListModel*)this)->requestLogo(pack.logoName, pack.logoUrl); return icon; } diff --git a/launcher/ui/pages/modplatform/technic/TechnicPage.h b/launcher/ui/pages/modplatform/technic/TechnicPage.h index 71b6390ef..a131a6db1 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicPage.h +++ b/launcher/ui/pages/modplatform/technic/TechnicPage.h @@ -38,7 +38,6 @@ #include #include -#include #include "TechnicData.h" #include "net/NetJob.h" #include "ui/pages/modplatform/ModpackProviderBasePage.h" @@ -61,7 +60,7 @@ class TechnicPage : public QWidget, public ModpackProviderBasePage { explicit TechnicPage(NewInstanceDialog* dialog, QWidget* parent = 0); virtual ~TechnicPage(); virtual QString displayName() const override { return "Technic"; } - virtual QIcon icon() const override { return APPLICATION->getThemedIcon("technic"); } + virtual QIcon icon() const override { return QIcon::fromTheme("technic"); } virtual QString id() const override { return "technic"; } virtual QString helpPage() const override { return "Technic-platform"; } virtual bool shouldDisplay() const override; diff --git a/launcher/ui/widgets/JavaWizardWidget.cpp b/launcher/ui/widgets/JavaWizardWidget.cpp index 6158dc7a3..955bc5334 100644 --- a/launcher/ui/widgets/JavaWizardWidget.cpp +++ b/launcher/ui/widgets/JavaWizardWidget.cpp @@ -33,9 +33,9 @@ JavaWizardWidget::JavaWizardWidget(QWidget* parent) : QWidget(parent) { m_availableMemory = Sys::getSystemRam() / Sys::mebibyte; - goodIcon = APPLICATION->getThemedIcon("status-good"); - yellowIcon = APPLICATION->getThemedIcon("status-yellow"); - badIcon = APPLICATION->getThemedIcon("status-bad"); + goodIcon = QIcon::fromTheme("status-good"); + yellowIcon = QIcon::fromTheme("status-yellow"); + badIcon = QIcon::fromTheme("status-bad"); m_memoryTimer = new QTimer(this); setupUi(); @@ -532,7 +532,7 @@ void JavaWizardWidget::updateThresholds() { auto height = m_labelMaxMemIcon->fontInfo().pixelSize(); - QIcon icon = APPLICATION->getThemedIcon(iconName); + QIcon icon = QIcon::fromTheme(iconName); QPixmap pix = icon.pixmap(height, height); m_labelMaxMemIcon->setPixmap(pix); } diff --git a/launcher/ui/widgets/MinecraftSettingsWidget.cpp b/launcher/ui/widgets/MinecraftSettingsWidget.cpp index 9c8e2b405..b738cab74 100644 --- a/launcher/ui/widgets/MinecraftSettingsWidget.cpp +++ b/launcher/ui/widgets/MinecraftSettingsWidget.cpp @@ -500,7 +500,7 @@ void MinecraftSettingsWidget::updateAccountsMenu(const SettingsObject& settings) QIcon face = account->getFace(); if (face.isNull()) - face = APPLICATION->getThemedIcon("noaccount"); + face = QIcon::fromTheme("noaccount"); m_ui->instanceAccountSelector->addItem(face, account->profileName(), i); if (i == accountIndex) diff --git a/launcher/ui/widgets/PageContainer.cpp b/launcher/ui/widgets/PageContainer.cpp index e3054c17a..cffda086c 100644 --- a/launcher/ui/widgets/PageContainer.cpp +++ b/launcher/ui/widgets/PageContainer.cpp @@ -228,7 +228,7 @@ void PageContainer::showPage(int row) } else { m_pageStack->setCurrentIndex(0); m_header->setText(QString()); - m_iconHeader->setIcon(APPLICATION->getThemedIcon("bug")); + m_iconHeader->setIcon(QIcon::fromTheme("bug")); } } From 5d46f79f195053817b88356b5e1883e801bab067 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Fri, 19 Sep 2025 00:25:26 +0100 Subject: [PATCH 423/695] Fix crash in Rule::apply Signed-off-by: TheKodeToad --- launcher/minecraft/Rule.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/minecraft/Rule.cpp b/launcher/minecraft/Rule.cpp index 1a7c7c768..606776e8a 100644 --- a/launcher/minecraft/Rule.cpp +++ b/launcher/minecraft/Rule.cpp @@ -85,7 +85,7 @@ QJsonObject Rule::toJson() Rule::Action Rule::apply(const RuntimeContext& runtimeContext) { - if (!runtimeContext.classifierMatches(m_os->name)) + if (m_os.has_value() && !runtimeContext.classifierMatches(m_os->name)) return Defer; return m_action; From 7789ba86088f3fb5c6f9f4680e829ac9834e4400 Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Fri, 19 Sep 2025 09:03:34 +0100 Subject: [PATCH 424/695] Remove unused code GameOptions.{h,cpp}, GameOptionsPage.{h,cpp}, ThemeWizardPage.cpp Signed-off-by: TheKodeToad --- launcher/CMakeLists.txt | 6 - .../minecraft/gameoptions/GameOptions.cpp | 124 ------------------ launcher/minecraft/gameoptions/GameOptions.h | 32 ----- .../ui/pages/instance/GameOptionsPage.cpp | 74 ----------- launcher/ui/pages/instance/GameOptionsPage.h | 70 ---------- launcher/ui/pages/instance/GameOptionsPage.ui | 88 ------------- launcher/ui/setupwizard/ThemeWizardPage.cpp | 39 ------ 7 files changed, 433 deletions(-) delete mode 100644 launcher/minecraft/gameoptions/GameOptions.cpp delete mode 100644 launcher/minecraft/gameoptions/GameOptions.h delete mode 100644 launcher/ui/pages/instance/GameOptionsPage.cpp delete mode 100644 launcher/ui/pages/instance/GameOptionsPage.h delete mode 100644 launcher/ui/pages/instance/GameOptionsPage.ui delete mode 100644 launcher/ui/setupwizard/ThemeWizardPage.cpp diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 23b2fbfad..9bbf2f7ca 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -252,9 +252,6 @@ set(MINECRAFT_SOURCES minecraft/auth/steps/XboxUserStep.cpp minecraft/auth/steps/XboxUserStep.h - minecraft/gameoptions/GameOptions.h - minecraft/gameoptions/GameOptions.cpp - minecraft/update/AssetUpdateTask.h minecraft/update/AssetUpdateTask.cpp minecraft/update/FMLLibrariesTask.cpp @@ -915,8 +912,6 @@ SET(LAUNCHER_SOURCES # GUI - instance pages ui/pages/instance/ExternalResourcesPage.cpp ui/pages/instance/ExternalResourcesPage.h - ui/pages/instance/GameOptionsPage.cpp - ui/pages/instance/GameOptionsPage.h ui/pages/instance/VersionPage.cpp ui/pages/instance/VersionPage.h ui/pages/instance/ManagedPackPage.cpp @@ -1210,7 +1205,6 @@ qt_wrap_ui(LAUNCHER_UI ui/pages/instance/NotesPage.ui ui/pages/instance/LogPage.ui ui/pages/instance/ServersPage.ui - ui/pages/instance/GameOptionsPage.ui ui/pages/instance/OtherLogsPage.ui ui/pages/instance/VersionPage.ui ui/pages/instance/ManagedPackPage.ui diff --git a/launcher/minecraft/gameoptions/GameOptions.cpp b/launcher/minecraft/gameoptions/GameOptions.cpp deleted file mode 100644 index 25f7074ec..000000000 --- a/launcher/minecraft/gameoptions/GameOptions.cpp +++ /dev/null @@ -1,124 +0,0 @@ -#include "GameOptions.h" -#include -#include -#include "FileSystem.h" - -namespace { -bool load(const QString& path, std::vector& contents, int& version) -{ - contents.clear(); - QFile file(path); - if (!file.open(QFile::ReadOnly)) { - qWarning() << "Failed to read options file."; - return false; - } - version = 0; - while (!file.atEnd()) { - auto line = file.readLine(); - if (line.endsWith('\n')) { - line.chop(1); - } - auto separatorIndex = line.indexOf(':'); - if (separatorIndex == -1) { - continue; - } - auto key = QString::fromUtf8(line.data(), separatorIndex); - auto value = QString::fromUtf8(line.data() + separatorIndex + 1, line.size() - 1 - separatorIndex); - qDebug() << "!!" << key << "!!"; - if (key == "version") { - version = value.toInt(); - continue; - } - contents.emplace_back(GameOptionItem{ key, value }); - } - qDebug() << "Loaded" << path << "with version:" << version; - return true; -} -bool save(const QString& path, std::vector& mapping, int version) -{ - QSaveFile out(path); - if (!out.open(QIODevice::WriteOnly)) { - return false; - } - if (version != 0) { - QString versionLine = QString("version:%1\n").arg(version); - out.write(versionLine.toUtf8()); - } - auto iter = mapping.begin(); - while (iter != mapping.end()) { - out.write(iter->key.toUtf8()); - out.write(":"); - out.write(iter->value.toUtf8()); - out.write("\n"); - iter++; - } - return out.commit(); -} -} // namespace - -GameOptions::GameOptions(const QString& path) : path(path) -{ - reload(); -} - -QVariant GameOptions::headerData(int section, Qt::Orientation orientation, int role) const -{ - if (role != Qt::DisplayRole) { - return QAbstractListModel::headerData(section, orientation, role); - } - switch (section) { - case 0: - return tr("Key"); - case 1: - return tr("Value"); - default: - return QVariant(); - } -} - -QVariant GameOptions::data(const QModelIndex& index, int role) const -{ - if (!index.isValid()) - return QVariant(); - - int row = index.row(); - int column = index.column(); - - if (row < 0 || row >= int(contents.size())) - return QVariant(); - - if (role == Qt::DisplayRole) { - if (column == 0) - return contents[row].key; - return contents[row].value; - } - return QVariant(); -} - -int GameOptions::rowCount(const QModelIndex&) const -{ - return static_cast(contents.size()); -} - -int GameOptions::columnCount(const QModelIndex&) const -{ - return 2; -} - -bool GameOptions::isLoaded() const -{ - return loaded; -} - -bool GameOptions::reload() -{ - beginResetModel(); - loaded = load(path, contents, version); - endResetModel(); - return loaded; -} - -bool GameOptions::save() -{ - return ::save(path, contents, version); -} diff --git a/launcher/minecraft/gameoptions/GameOptions.h b/launcher/minecraft/gameoptions/GameOptions.h deleted file mode 100644 index ae031efb2..000000000 --- a/launcher/minecraft/gameoptions/GameOptions.h +++ /dev/null @@ -1,32 +0,0 @@ -#pragma once - -#include -#include -#include - -struct GameOptionItem { - QString key; - QString value; -}; - -class GameOptions : public QAbstractListModel { - Q_OBJECT - public: - explicit GameOptions(const QString& path); - virtual ~GameOptions() = default; - - int rowCount(const QModelIndex& parent = QModelIndex()) const override; - int columnCount(const QModelIndex& parent) const override; - QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; - QVariant headerData(int section, Qt::Orientation orientation, int role) const override; - - bool isLoaded() const; - bool reload(); - bool save(); - - private: - std::vector contents; - bool loaded = false; - QString path; - int version = 0; -}; diff --git a/launcher/ui/pages/instance/GameOptionsPage.cpp b/launcher/ui/pages/instance/GameOptionsPage.cpp deleted file mode 100644 index 8db392b1d..000000000 --- a/launcher/ui/pages/instance/GameOptionsPage.cpp +++ /dev/null @@ -1,74 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - 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 . - * - * 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 "GameOptionsPage.h" -#include "minecraft/MinecraftInstance.h" -#include "minecraft/gameoptions/GameOptions.h" -#include "ui_GameOptionsPage.h" - -GameOptionsPage::GameOptionsPage(MinecraftInstance* inst, QWidget* parent) : QWidget(parent), ui(new Ui::GameOptionsPage) -{ - ui->setupUi(this); - ui->tabWidget->tabBar()->hide(); - m_model = inst->gameOptionsModel(); - ui->optionsView->setModel(m_model.get()); - auto head = ui->optionsView->header(); - if (head->count()) { - head->setSectionResizeMode(0, QHeaderView::ResizeToContents); - for (int i = 1; i < head->count(); i++) { - head->setSectionResizeMode(i, QHeaderView::Stretch); - } - } -} - -GameOptionsPage::~GameOptionsPage() -{ - // m_model->save(); -} - -void GameOptionsPage::openedImpl() -{ - // m_model->observe(); -} - -void GameOptionsPage::closedImpl() -{ - // m_model->unobserve(); -} - -void GameOptionsPage::retranslate() -{ - ui->retranslateUi(this); -} diff --git a/launcher/ui/pages/instance/GameOptionsPage.h b/launcher/ui/pages/instance/GameOptionsPage.h deleted file mode 100644 index a132843e7..000000000 --- a/launcher/ui/pages/instance/GameOptionsPage.h +++ /dev/null @@ -1,70 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - 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 . - * - * 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 "ui/pages/BasePage.h" - -namespace Ui { -class GameOptionsPage; -} - -class GameOptions; -class MinecraftInstance; - -class GameOptionsPage : public QWidget, public BasePage { - Q_OBJECT - - public: - explicit GameOptionsPage(MinecraftInstance* inst, QWidget* parent = 0); - virtual ~GameOptionsPage(); - - void openedImpl() override; - void closedImpl() override; - - virtual QString displayName() const override { return tr("Game Options"); } - virtual QIcon icon() const override { return APPLICATION->getThemedIcon("settings"); } - virtual QString id() const override { return "gameoptions"; } - virtual QString helpPage() const override { return "Game-Options-management"; } - void retranslate() override; - - private: // data - Ui::GameOptionsPage* ui = nullptr; - std::shared_ptr m_model; -}; diff --git a/launcher/ui/pages/instance/GameOptionsPage.ui b/launcher/ui/pages/instance/GameOptionsPage.ui deleted file mode 100644 index f0a5ce0ee..000000000 --- a/launcher/ui/pages/instance/GameOptionsPage.ui +++ /dev/null @@ -1,88 +0,0 @@ - - - GameOptionsPage - - - - 0 - 0 - 706 - 575 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - 0 - - - - - 0 - 0 - - - - Tab 1 - - - - - - - 0 - 0 - - - - true - - - true - - - QAbstractItemView::SingleSelection - - - QAbstractItemView::SelectRows - - - - 64 - 64 - - - - false - - - false - - - - - - - - - - - tabWidget - optionsView - - - - diff --git a/launcher/ui/setupwizard/ThemeWizardPage.cpp b/launcher/ui/setupwizard/ThemeWizardPage.cpp deleted file mode 100644 index c97037f9f..000000000 --- a/launcher/ui/setupwizard/ThemeWizardPage.cpp +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only -/* - * Prism Launcher - Minecraft Launcher - * Copyright (C) 2022 Tayou - * - * 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 "ThemeWizardPage.h" -#include "ui_ThemeWizardPage.h" - -#include "Application.h" -#include "ui/themes/ITheme.h" -#include "ui/themes/ThemeManager.h" -#include "ui/widgets/ThemeCustomizationWidget.h" -#include "ui_ThemeCustomizationWidget.h" - -ThemeWizardPage::ThemeWizardPage(QWidget* parent) : BaseWizardPage(parent), ui(new Ui::ThemeWizardPage) -{ - ui->setupUi(this); - - connect(ui->themeCustomizationWidget, &ThemeCustomizationWidget::currentIconThemeChanged, this, &ThemeWizardPage::updateIcons); - connect(ui->themeCustomizationWidget, &ThemeCustomizationWidget::currentCatChanged, this, &ThemeWizardPage::updateCat); - - updateIcons(); - updateCat(); -} -{ - ui->retranslateUi(this); -} From 49b238f384bdfdccf23f7b3fa3b072ef517a572e Mon Sep 17 00:00:00 2001 From: TheKodeToad Date: Fri, 19 Sep 2025 09:42:13 +0100 Subject: [PATCH 425/695] Properly remove gameoptions Signed-off-by: TheKodeToad --- launcher/InstancePageProvider.h | 1 - launcher/minecraft/MinecraftInstance.cpp | 9 --------- launcher/minecraft/MinecraftInstance.h | 3 --- 3 files changed, 13 deletions(-) diff --git a/launcher/InstancePageProvider.h b/launcher/InstancePageProvider.h index 258ed5aa5..ebbab0f3a 100644 --- a/launcher/InstancePageProvider.h +++ b/launcher/InstancePageProvider.h @@ -43,7 +43,6 @@ class InstancePageProvider : protected QObject, public BasePageProvider { values.append(new NotesPage(onesix.get())); values.append(new WorldListPage(onesix, onesix->worldList())); values.append(new ServersPage(onesix)); - // values.append(new GameOptionsPage(onesix.get())); values.append(new ScreenshotsPage(FS::PathCombine(onesix->gameRoot(), "screenshots"))); values.append(new InstanceSettingsPage(onesix)); values.append(new OtherLogsPage("logs", tr("Other logs"), "Other-Logs", inst)); diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index 7749d0f6b..403aeb4bb 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -85,7 +85,6 @@ #include "AssetsUtils.h" #include "MinecraftLoadAndCheck.h" #include "PackProfile.h" -#include "minecraft/gameoptions/GameOptions.h" #include "minecraft/update/FoldersTask.h" #include "tools/BaseProfiler.h" @@ -1287,14 +1286,6 @@ std::shared_ptr MinecraftInstance::worldList() return m_world_list; } -std::shared_ptr MinecraftInstance::gameOptionsModel() -{ - if (!m_game_options) { - m_game_options.reset(new GameOptions(FS::PathCombine(gameRoot(), "options.txt"))); - } - return m_game_options; -} - QList MinecraftInstance::getJarMods() const { auto profile = m_components->getProfile(); diff --git a/launcher/minecraft/MinecraftInstance.h b/launcher/minecraft/MinecraftInstance.h index a37164169..d4e0a8626 100644 --- a/launcher/minecraft/MinecraftInstance.h +++ b/launcher/minecraft/MinecraftInstance.h @@ -49,7 +49,6 @@ class ResourcePackFolderModel; class ShaderPackFolderModel; class TexturePackFolderModel; class WorldList; -class GameOptions; class LaunchStep; class PackProfile; @@ -121,7 +120,6 @@ class MinecraftInstance : public BaseInstance { std::shared_ptr dataPackList(); QList> resourceLists(); std::shared_ptr worldList(); - std::shared_ptr gameOptionsModel(); ////// Launch stuff ////// QList createUpdateTask() override; @@ -171,7 +169,6 @@ class MinecraftInstance : public BaseInstance { mutable std::shared_ptr m_texture_pack_list; mutable std::shared_ptr m_data_pack_list; mutable std::shared_ptr m_world_list; - mutable std::shared_ptr m_game_options; }; using MinecraftInstancePtr = std::shared_ptr; From d0737eecc5d78116ac8af8a8e05fd71a11d612a8 Mon Sep 17 00:00:00 2001 From: Richard Voigtmann Date: Sat, 20 Sep 2025 19:42:04 +0200 Subject: [PATCH 426/695] Added macOS 26 Liquid Glass Icon Support. See: #4149 Signed-off-by: Richard Voigtmann --- CMakeLists.txt | 1 + cmake/MacOSXBundleInfo.plist.in | 4 +++- program_info/Assets.car | Bin 0 -> 1271768 bytes program_info/CMakeLists.txt | 1 + 4 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 program_info/Assets.car diff --git a/CMakeLists.txt b/CMakeLists.txt index 1360b82ba..06c42438f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -415,6 +415,7 @@ if(UNIX AND APPLE) # Add the icon install(FILES ${Launcher_Branding_ICNS} DESTINATION ${RESOURCES_DEST_DIR} RENAME ${Launcher_Name}.icns) + install(FILES ${Launcher_Branding_macos_ASSETSCAR} DESTINATION ${RESOURCES_DEST_DIR} ) elseif(UNIX) include(KDEInstallDirs) diff --git a/cmake/MacOSXBundleInfo.plist.in b/cmake/MacOSXBundleInfo.plist.in index 3a8c8fbfe..cfd671d68 100644 --- a/cmake/MacOSXBundleInfo.plist.in +++ b/cmake/MacOSXBundleInfo.plist.in @@ -21,7 +21,9 @@ CFBundleGetInfoString ${MACOSX_BUNDLE_INFO_STRING} CFBundleIconFile - ${MACOSX_BUNDLE_ICON_FILE} + ${Launcher_Name} + CFBundleIconName + ${Launcher_Name} CFBundleIdentifier ${MACOSX_BUNDLE_GUI_IDENTIFIER} CFBundleInfoDictionaryVersion diff --git a/program_info/Assets.car b/program_info/Assets.car new file mode 100644 index 0000000000000000000000000000000000000000..2259fcbd0fcae5d3d2fe43cf0480f869ddd24809 GIT binary patch literal 1271768 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ℜv`c;ehwD5>;CMazVuXh?%4J7#NwsOlmpYd zm^CkmZ>Os&{FY| zG%2PC2p111!qoBZxLH;h(`(=qF7f1^+2HH0j{8P$^0uG-+R=ye*{Ci=YArmai18Sv z2(y7hfB8BQ*qBexH&CN@NIN)xM>*azKe7CsYyRq^fK_siHT5HJZVD=hMD|e<--=bN z5~5vW{z!wxeObQk{whGwe-`Vj45S>sJ0rgqy6N-gh{{QymgdfETrClSp(yy&#=8Et zM{9W+8Mfu#5}wV7m5ChNpsMYPY%z|7Xs%7UFgC&J0T`c$yt|#zd@=^90&98+`yVv1 zwvnnCShk<^&a>hq+$9&CsC`UVE8 zQNf+JhZn(ladd2-2gut?Q(4-By|1tzZS-E#zSxyB`x7;pH`x-|A~MwNxMn(5uU(7> zg4fX#1mO;n=`lcys42LC5Z+SY=4CwcRM(+ZdR~w=K~pVUe=iO zAYZ~(FYbXCm(j`S9Qb)f=RLn>%Uq;*SjJmAZoJf}$a?@RZK_x(;J5DVe%K%Rax=W$ ze;Pw_D%~*^OxS~A46T|k9)VNx&dS53UMfSYw96yFCVaW@XQ7ezSklh&Yx+t5sUTtx zTTOp!s9QBv;*@8z{?b^XISbe6u>fK9=gTli9|DFBG_+`-^>u_a@Wr3^zOG=j;lnQ@!Y z%565l^^w>EzHPO5|~-S5UYK!|82YerR1 z4De)LMz+}vrGAy8@0h-@l#IbDc=qILw8p-vGV!%ek3aokpTbu5eJw``ioKG30XboZ->_aps0L3W^8`4Y`#%~f|BgV>a92A~ju76twHg%S&rwdE-J<$kG)O2VPy`;4G z)#No;$K=V`J{lK84tRhN{nqX&Q02K`E#rtdHjX@I!cKwp3H6p#mS!yuC^Q~tG0Mbk zp7LwvGm)FAtAs+0F}wuw?sS~#%|cj{`fTeCkL=}>?uvTNV_;l^#a+DEBO&SeN;tVl zz9uJI_YmK*H@bZ{Wb_R7lK9S_n=S4is`S#Gs+GiPDj@DWq}V+!|FZSnY_sZKz>*?=Ij2*jydDi)n_G5D3x1NOn%^bvUz3KV@P(ZUPDhx;GxKnt#F*VP9en!Rz>5)0Cv<|{0`cX2 zJ81Lj^bslGr=okrbztBN@bcXwRACw$$4C6vw4V(5GDWIyhi^GD(D2e>4{Qj!-_}{J z(S=}DY0=Y8hY7YXWa?8beR3UxhrolhBi(Ps=LdagaZ_oj++)X1sKRg5OGo%T8|Q&$ z2H668CKti`fFDmIN14Dvgf*MI0NP6Cu7vC!!l-6f6S|XHryhg#{67%OqnPwJVI!2QwEhD+XJAXg`lFo4 zfuVPuT|Nsc7sEvRPqVw#x(Lt1opIH|Ly+0Cb&|G_u}cOccu^^I4hkfD;OrYXE=8N@ zMy4JeolCjm$qp*v=Ru$B3E0BU!$9uQR>)KHg?F6iTb>JQX?wqvAbZHpRpG9_jTFJq z0O=(Pt&%1=rrPpbTvJMXtikYbKiT=gh1k>i%U5q5qnyvH@@aRLF{Eu5gJkY}NXke= zY{X_$jUS-zoQn`8gyj8?g5Pzf1);4U1x|pjIQt6^p-eeZ+4twh(qX34yN=2!%zxhH5rtgV&u}?7* zXl@@`9q~ARs3cMm+qKxQ(dsR=cR7l~Ob6vG`uiC(^Nu1|9JCYB7OI)S-g?4 zUR~PsO9sx@cE>q?Mi|r-#pBsTN%ToEXc~&IQ5H|{>9iQ>;HlMY?osPQukt>NB9j(y zDnx$BY*3)UFF=2-c69@Q8#BqxBU?cK=wc@~YFDLFWuD~xRse>x;`;iKd!gpz^joDU z?y{hDX_P~6@KFU`R=Dw(Xz$u08a^4dSxizUb|G33iza2dmYN3oQFKc!CG=awY{mZd z+t;buwn+hjE$yP;PG+r6k2UdUXkl%pgV%~@PZQ)<3yg8HGS_tb z9rhPT>u}H~5_Ns9#UU=IpUY?TRQXN7gp)M{nI94_=vMxI(!$x}Y}ZZe<;J zV;)(OgXOBhz3s6=&IO4PEVHM53l^sdy9GgRfN^{6d`|Gfg{uqxSE`kde3qK2nI~nr zXY%@U*`3$%RCJYeO4Of<8PKlJdGS0j*yYbL|2->i5weh=q{@EQdt3XxvaYNT!`;(Q z>hCW+&Q^EMhEJa0JsI##MB8;W+BAO(o^!!u#gP!|D}}MxduG|bY>B4r-8WqBa;mc{ z?IBN03c|Z~ti~XEm;*wgRgo`@U7oWMM@PC^QB;$THy@!tqfTgYM}=Mw!OAb}JJ`KF zB|?vhlLm7Bzuj#sI5h$JL=u7#Q_hFnW2@L6Yd9+-CYp8#cWf$`bqe+8VP?t}~iW(+<=t8QUY zc_M-xG#cBvNE=L5|8Y=t_TNj`sfVV6_D6g34Bp#wr!+~$764y)zuikKyQ;J(7rgyc za=+>B$QMHMryiLkKie-kCR(14%h1}hA!_+UauHeqrUX8IaP|N6f2*hCnPRgci(iK6 zOYnbj=>5FoLX(__?xu2+~>&KdhY7RXYkGqw+DUQG?| z|8P<@#;)I`$*RjD>)>v+3g}Dp4@lCHi|DWQ%KYfXkd%pW93{mYP$EdpER%-L?2Tqc(SR)%9{4&i?lH zi;#qN<5Pzyx}D>>5H4S3$-6Iylj#tr&W$8#3pm|i4&;x8)zlTw3S7rt-2B@F%V|WH`1906oL&=

    k@!F?yR0m6Vn<2+&D^Qx>4WtoIB~CkXn` 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

    &Y+k)!!t(0hCVT4! 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̞DxGK9T{ZV?!YSD+OgUaJ@`n+u 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

    !U~F_)is0i2 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 z z^0&f!AZ|{4erQRev<`Ww`Mzi-s*Vx3E&8~_=941ZXzDU&>YV7wu^*VGQ5XTsWOM8= z$odcqYf(v(nq-8{_1?(9WbgtPLC|?+xX{n5ST)mTiipTtGQE)SDH1pn#;bdS-&tX; z5xJ`z1odYzvZ>}xF$;_|4};NGMt0%|NmvXGYdGpwaP$Xy1bny9I}51Ym{I|ezq@i& z69Nf;Rx2Qtp88FEN<6&Aof?5_We>u(W{wdu33DD<=s&CK{?c6r>Bb6YzZ^Xe0wTvii!AJ^eeG)gYXUY0ata+G(_U&uFHpG2$;Q|8=n2r zkrq1T-)u8yLG>Pl*aNR-gu@II$lipjrkWBT5s-0B?S|}cOWtOVY$jRxvBLAXvU9$f zetA9X>-gBVVuj64rvq85A5s2kvfkj@gMmh$Y)nN)h?m&pB86RMYkuqb^TH0jfm>tt zp_W=BRL^^3n6?bSjTvSg``2G1k>t%Ny$h#pG5OO-NOVLepb0ZZ-oIesGc8`YU>3Do zFZ?XqvA@vPHapFhGZou5(=YDesRDFmEGD(RTh+(2f?qZiJ{Xa&l6__q!B~iipx8!(w_Xj?Z<0NU#mF_JAN&B zR;x8w?>hAQ(5tmKBX8umgZCGHF9Atm$`AS;Y%7D;S+?d!@KID&(VW1MA$RZ&+ASnFwQ4U)XHm1y(0v>UT4PPJ5jcly*-o8zn-W*KI&R+KO8ca=QyT8M!ah*qI|4B`KM^VUFE zD9ObIbuQLhgY?lECy|eVFPCkY5;C3pc;=OCV|SD|Z%Q){cAOljMR(c4oLWVvdHU_} zAA|chV|`8wjkKbwZC~6~UKg&z{EyF}G1p+k8*@gP>P2eC@lS+u%>Q=0#oCm}xr5?LT& z=Kr?qUb1Mv_OZZdbaoECL{Rh2u=RrtI9t9&Z~XH_(0$H%$O82Q{dgB@X5TeS#>0Vy zYs0^)ty>L- zyPW91fLihmM+LHckD-}kumC@gt0i-M(XIrzo`%{oA_Unyq;h^Q3=fr9Ha>3+DVd7u zMx`&5kDzZlU8PuGP4e-0r&J5a7$WG9d{e3MWub#DDQ2(D$Z==oLEp7-ZQD@?aewR5O>eVmGWD8|NA%~41^V;fgm4x^jvzS^ zqH?%rCijFvUuI2kxz+HSooS(PfvE(oa?o{7^g1Nr3!)lp(kH8)F@+!WH*GLa=Zz!A zT>`R+AU7XpZZiQfuY}2K3A8f$|8{1^zrOeW5tWgheo9) z}NhmpX=X=Q(i*-3&WM#mdRMCaXK8hda;vsHzWI^m}BOVm|fxukEh z#$UTOX(}cU!Y(+%g4Y4JN&^if{hM8Oe_^@JV#ch?EGA`{Nhd+oVCwaCR zxvgo_J&VTFyFAq>1Qy1^Oh@v`W2_)J63Pd7&>%twSX2@-m+hK9n#th1;k$zf%B#Ul zCPp=Ua_0r6c^Oy9PaUNkObc{^TB-z&smtWrl3*PdALbtI#i(IWRt>Kh-ZPZL{>}rH zA=q-DTS9HrgoI8WW#3; zju+06*)A&AItU1fC8!NRV0cXlb7E-B?wfQ3M$cmVTeBg{402G>GG(B4&uHD6JpP>R zwinuT>>m#$V|F1v+XP#bJ4-t* z3eOzOox{I1m1m;0Lx#6~H(0t6=pi%sP_*WUcl1+~&v8KlFz1)AB-nn5C|+pgOo% zFBZw0R^_2+|}3z+)J$Z)EtDm zEZrOmpU7ZG7H(K|RhGW^1nfuBx2xfj&g zlW93iVZBw&*=RWST*}&@n6PSjrAua(No86YvLfa!OX-GwH+yAxA<3D#wjl5^8rTwn zqlbRO(6m=L`2nX^3n889Jv_ZWj9K=>t1>F8y;E{nmAKnn!~3r=p^>zSR3DNH({EG7 zXL3`zPB`YPfyFZ}&LB<73A!jJA6wG(6w3+i_Ph=@z<9e93(2_K(7hdIgJ8Qp3)%^I zVt{V6Cir?MF{_!mVzrw2e*g*0a5bu7TI

    ^eM+BckcyVJY~{x_&bm(|9AVZ8&Fs@ z_3Q|zc^+uxUp%U)6byXPI(DgJY79?6m1V9KTo;#wtZSdoqlU-adTIKZ{GFuIuN6Sj zDUe$t#t4vp0>%}osorw~VU&e+&5(Rv1=$M`L+jT+slQbffiltrfot6x*r}CbrudQn^H)RoOJNxO} z<(i*SY0t3c6kl}pYN8=M=1kNc&S_rX9|BnFd@FA4yge|>jH>r6G0((E4^(~$r$Zd5 z-fhswTzb@<;8HU3S^l(mH~cMZCYwRDg-c zt-q5 zwC*5ga(|?|P)>Z~`=+iZ8xEwH*s{`=TrI`vFjQm`hHCPlii9W$xml#lMMP9VjbRc2 zBTVV}B4&R0kb*HKR3;t3)Rho-6XZ6?B7dCrJ2;oilTC_0$D*2o>!m|4V!hw=vOF_B zwGbv60iEr$9*glV&C*#FtMv9f`M%=JH>Qo^lHku8@yNS+dckWlCcp_jBb*FVlR^GO zAa~Z~6a(euLM5h-SH@_=J{o<>*?2i+S(%-#O!Xq~#ZOlseyH1c!ua@^jYILbYI11C zj_c8quNtnv(c>OW6XEs~|P zg*Dcd5k$PZp&b%f>2})I-Xmd+pt?MaL(gHvcvlZ&FCMmf9Z0p6jPBZ&JV_eNgwo>V zITVq*B6luF@nTGiKqaInFpg*-H>0|?z-&|EXR2=7Z}~?iBo7AXdZY@*aYn0?&Z)g_ z-Sl+e9>g5%Wf}$7S}sp03>GM*)Ryc>&Z<-2SVJ$GVK|YHu<{Fl+&zVw=>QcAsnrwl z%ECZxj-@VIAm@(Iv@IYFkVm%cR1-n!O=>IhJc{bzt15@n&%V<40UQ&a+>5E$>Bq%j z-mNgY^)0?MHd>r49TLHA6{dYGpptN{4Vv5J-qv6s)Ef&(Lqb4OQH%~$%l#bbQp$A3 z8jU_2{~m2+b=S7gnK3R%yFZGQhVe9Zp!5c>NmN%OXDH{;H1!`TT)k)AH`TX&OfPqw z>E+&@n0K{e&1Q_&`lA`34V;p05RBRiXKQ_CB-wAlHrHmM=NIG*+uHo7a0pE={j+k* z_OQfGN4-vDVyp?+`=s)mi{1fu_|{y}nl4JV^P1GU@}!(9;>>m3iCcdo&meJFuY zO`tVpwF>qBjbPpc+i>`>efSNhLB?Umj=fUI9$YUCMCuL-G*2C=FKd)3<^HC29$KHT!+Dco69 z8wpLq2<^j_!IMnSBu~^CbvO)qI*8F50VlB``)A8N!*Xx!dGEWf0J@4UDV_x{i`X4n zwoB0t{xn}u-I%_)teNE^vi4xe84av#5fsZJ;u|C&&*4y(&$PcZ* zoH9WA^1&fwII9&(C(Xu4V;ubTe}uz?EjeKr?eLCGE=1`hzLM3j#GR18XwoOB-Y{ir&=76V@j1ppW3!7HX+pm3f>Q;S zh`V*N-`)rZ8nN%@bLKOMg;~+CmAU`**3^=U7T$Azx*XGv4bsIb%KVuD*6$}5o57Z? z1b1`cSmdf0@vwfJrOv#&;hEjERgYuWO`jKv(up&4J*}%4Y)A~MNUS#sUfB(MBa00} zpIP5n$U|qq+kHrw_M~TI%Dj8^cJVA=k8m5mAirrPWgcI8p%1>%}uPryB z=;oz=ATHZ)^LtFPel4%}b{b`mWUN7HOH97GiTUjlpHhCW1gd!0iLKb`hs#~jE^DMY zs5WdI{}=NEs9NafVK9==Nds*#iM!c_J%Wphv?{(YGCEIX>5j`xeqcM6tpkOhVtr8IY&*hJMmwT++uI*S^#n2Df3U$Leq}T=!wH z%>sELK^mhd^FjS31krk<$v(M4?V0IG_AH3}{#vT%yATrwFYJBFzijj3ORZm5Lr30l z=~Y&VWUbuw+Jt?lKA`EYG26JJCq*&%W_x*ORr~_p&RF(FE5WVC0@v66!tOEa^aE9-B+x zgZQ>&$%V_WeF#e&VNfpLl1J1{=Hc*?!grx9}Ij1{lwlGH}rdK zJ}98Z1{OKcn}^pDq?Jy*1ORIdN1O3A%%qp^>K`wH~z@Q!Jb#=9tKU$7*}dS!sa&z;88&+j&$==2@rXYtYmP*i{UsjET=Y7S`*w9n#96c{Bp*QrO3WuJF@)^OJ)V zQ@7wA%^8hNVi(xqA&a;!c!G{pQg@grv8YPt4hGs;o-MsLw$BpIvOTL~L?Au&V& zEK=vw%HcQ6jxeL0aitF`A>6=Mw+e+(*K;1?1O-nJIl=gdnPKtTvSMJo>tr!5q^Tix zZ2}qRf$(Qx7tWZ%x=6^L&=&0f}psefyMjz{`=~S#^!_0326_tCxnRL{0N|fVjz4gc)1}e zr<6>41%CFFEdSzNqL|L*8jdN(o2VipoQS$1rNhE4ms7}0^0}SqEAK5iPyVnvWIb@= zHC)jS+jD3!n$bv&Mg{4ETYl=!xf1~g&SP}hV8hJ8mkyXLX)w&6FF=eGUK~jj%qyfu z8VDl(aaV}qNcIX4c`3%^GOFy+v0+GX_;2gOWjh#ZqpWBOu6=5URPBf7yO`I@6Dgc= z=U`#vz`^lp`dyYuMJrU!!K^tm2-+gUfCbL^AZrC87!x_Loit1TbM-`DBD1P>+}eNO z;;?CST#>r*@p<2|&TDOn6g##V1+>~8Q_ zFin8zD9++s3T>UoQt%XJz9c^--3NC(Cs(|ssxF`oXlwq8eFp5l#8UM4IWT`KVnmZZ z!y=)cT#kWFW<@jpbrl!Mrr?w_q6vqwR0)cy7O!OfCZl~(!e*E_1lSb#>t5)_;lCqb zkH$(0RSK9lp7GEE*({#=1QTvt3iC(J{8?cB&jIjRR{l>N=ddZ>{d-L=M2n3|ktEfJ zw*twQi2>`J8K?BuM8Ro>#QP2B`g;Zbs7|>{R*!JsdkQ_+QS= z@Oe%ek0houuo*d=!o75{p@b%2gXTs{5 zesO?jOj#mEWKO_F>C9g%T&v-b88zF9Sq3E=>BUj`2xi`Brip`Khk4545r^o)`=8Td z!J+k5afZBzo&4Sh!$;RoVLRN)&yN2D8YN=V?QBE_igGJDn;EyCJ3abc!?@38qUOK&2Z`dbP|Ndq@W9-L65{}VsA@uw&En4rjM zn=rE~^WN2!)b`84xY0gqU&CI9aGS=xhf?bI-r&!}AP6P0F-c1n<3+FuXXftfg3-#^ zpWyxOu9G_@chzJvF{g4O%VElO_R~~e(|W-M`MedvXamt%n1Zt``<>c+7|U*|5v z(wJsxE&lzU@RzQ8sx$Q?Lugu%pWo?k{ax)ZT9agSA{r4EOYTuY@%1G#vGxcs! zL&z=7H@<=KJ-F3J>{vSuB9sf4$(>XB}qQ%KG8$SA11k=XW73JVHhqZ#lf zu|TpKPcs5R3CvkK7HLmebH=_&uuPcr1|01Dd(-MKM$>o)`-e|$ilb%8YPt;;#lXdB z_~4HDXz!3g&dWiu0`^O2a~zBSSBFVCvdVh`(g)Y@_>jLJ_&>wwn-r|7LN)@GK~NT& zFp@%%dbo^p6yR6dn|pe$~Au!3A(}*ZN-wh(iVeO`vbHQx{lDC)cyDB z)r)7AJlEla3#iibp$`}hU(V{D3ntF!^h*`~!qJ{JStX0m&^dCm4-cQ*cWiAL6)%ri zKks}%=aq7fs-2>Uo5G|80;E{$U0{eS;JdSsCPPx2y&(b^3~RewUjd8X9y^!O z--Zm-G&Z=zg~w! zNuJ-aV+rf(pL=1x0A_jo;Xtx%(Tt&3bL(C_tv>!7H@7snK7x4+|Kb~m+ISQ6QyKi| zpwxlT2isr<*XYL7rMZlq4kGWFUAIM<}Np<5ar1Bl0SpKzuvD4 zY@Oy)dzbafc&BOLscangnf?- zpS!aB+oI-(#5ObP9KNsX1(E<4lB7$SV3UJ$OjNsri4Ohwzu3A371krVhJ3~=B*@RIoyA{QRTEBuB{=Eg)r>O?GU zF?nkf(0n8U3E{}UV&n!~(lXzA0uD!j9>Z5ff^7k^DGDR%7Th+@KJSl=E-rZM_V4N2 zf8~FtuY1jmQsl*n(QkzH(|L5MK(i(WweTbLYypO@EoWebSzym8GXnS?>gM{rgQ}4om*!_Q6HydqPVRDuAqH-W1$IOi3z#%6&8-;#TYIiJmiT|7EaiUw@!5#%@J z7z?Q1MZaG9w-$4MIrV>p$eRf0tHm|4Yj%1AX93;09m0>f7cDP5w6Jrb%TnCl%m}V= zAdn&9y=`El?N?St8&RGAVr}SAJA^9w{(LG%9oreA-Mgf2FnPeVI5Ds{FBJI`-TuR> z)k!ry*=7~z(6W^O2#P?*9%X!I30r+2wDlmDzx&f^CqG&-YV6fd!DC!;R#D4&kz;T? zT@gRi^z2$8ph#*4DiPnj|7EMsekO{@(;Zzs$aG?9M{xSt&rVPV)UkX&^!dQ}DnbUB z%OwFmVXGu<oszL7R;?3(HuphC7RNrFEk@c^{iZd@^G6FO{V7lhJ{m>5oBo* zHE?S=^1tmFx@*A$rZ|YHnPGs`)Zy~=(O~L&ZrmH~}NZk)yvXoove#fN6^ufL{dkEXH+AKz@i!aoOsEkJ!_gFe+Ez z(8bxEcmeKK3SJc&>s^M#T<@{6_-%_i2S{UVtAi~jdAw7b^S1z=%0?IPNOobnCgBG*`18?Q zxO9Sjdu?#SVbQvNg~hF&;&S~+fo54YHYW`|5p|m&pLv^dlp*a zi%rRYMJvv04l|A{3P^H9dl_hS@>3V4bR#HZ#`zwltMXT7tLjjRfvsr?=N)cl8?K&H zkta^VF0_xjHaC2Z$nI4=iN-oN+ht&>EZ^@Dyl+BoNX=@u&1LgjP2ibG!-eNk15if* zcj!D9whI^+Dr#$EgBx2DgM@3^ewRc2EePPq%*`nic+`>8CnPgaI3{s;qUNP}JxUV) zi^A;uc+C8aacF(`U{zv&7#penC8vv1MG`*uHmR4JGX ze%|UL^FBSrUqtQyu;96Wt5fTz=oKmJ7PYf@w@HqF)cvrRXyHwg_+g|At$O$Tsn4Fc z59z4D&oL_>PD}!W`pEUoLMjPeAqArKJp~y8s=|~qE{^@1TGnKX`lw~sEq)2~trg}^ zAb5ziqrlR^H6IH|m6L26J|4hfFC7%6v5?mRWH6T`tvKe4@=+CGgW)G|^a_oVn>qFC z*zjJYGC7K?1p3)WE?~4ZEQyAjF4R|%YDEjRba)I0D;1FS0;|nvj8~G794(~RNu)n0 z20UCLt3EpA4g(&RLU9h79@pxRauyMCY*jk@4GB^$#hkNaeO9WQlnoork$?aGzE3kWQa7BGqK zQE!m>@@{o*9PS}-3(Bz5uO=mtv^nKG}^ds2h9BN%DD3ksV6Lc4& za*x#CP0(hK<$y(sY`T_SIi|OW=(b#Filz5%x+BQ!4=E68R?QM?GI1%Wj#tBC6VOg6 z43D*!vtT5qVUE5&jNAezE@QoyUpWfhB0QK(`>Cje5udyo5$?lv z9L{ni)rzA_is2QnKL#&xa&jy#Zp!m%?bP()z|n6_fr^bYS5I9v4zs}o592>1w;X2g z(68DBrT`b~YfAiZSPO&qa=IPJZINIuB3*_n*ouvu`aHfYMnL=oayn=C7hL;G9<@uT zG0iv~8veijXW_rMZ^1y|!LEP%pIhc=coL5qzz=|A>+_cbV_m*BM83_Wdv|<1cDDi1 zDoT^BlXt-oPi5{#U!-ES6;b)wC$=825*Bmw=2K(X%^hAzl8~=8oE-f8Eb32TaUMCQ z4O1&Ra&bDMm|iDqf1CEuc%Sv=V0qdCjpy4*mzM`{RyP;BD2vA0-x^9gonn~)@exoM z58Wi(JsI!UY>3rGz{EhC4H|>~KqbkiM1{muQ(VlZVq^;Jx zGW}dm>FhxvlilrpA8TG%BfZcPHr%M5Q3k&Wp`bIzoeIliu4O-VOqGCc6M)Ym81F;9 z!E^2kNGJWM!;fpM~E{mzsALx=~L{qMeb z(6cw3dRiASfLD6FE279%wG8v&IOTqux?4Yg7A{^AzWqlGBc7a(+Ri@A79){{Apae6 zM9+@oQNfE0hr9XZEsGQ;+%J(G(Qi7jD>kj~rk;%ZE2d||z~51qA_eQ4tQ9WWZq^{S zfj`SwB{x#^C}qcj^MyCcyOsvfK1vpTU9sVbM0K|_?pMAMa&?V$Sx;~g1R#hYhl$2O z)8cW`=JIG%{$T=)Hzi*ND2;0abKwSVW6m*~GhGm_OT@#QfAX`nikYy&nK50y78b}q z&1>a+;?oW_X9R>u?`FbQO0e%tTEH?B${5$!*e!w}0va%-Ge^sVR!qe=sGsBx&nf5@ zBhA~aF^vMLx(*q}!?P87b`+X~*?{Hz&(V~7f#WyY3cnDo*uO8F(3Ty0?cOP_oK)jL z8xrK8{|$jA`X%x(qBOepfBfD52qiz6Jql((ex5+tF^UXxthd&bjA;>N-?J8pJ&BOk zFdNZyEV3*DSV#r$Ec#>ZgwgJ&PWdQ*F~#)w$fiwN+}gk(vz@xO<|oUgS_`kA4@5Zx zEh#Q8v*{VTXw93EcB=?ww`M%c)D)@0KoXB>72uxz*i`L&&tC3>Q8HAX2dx}bvIU?0 ztg$ky(5WxxbYD<97HA~qz`|ATKb-a= z{h!2-nC#j?ljRse>D!%P0!nSiV>J5z49(w%z)c>ACrQxyKmoNyS5pKNBd?yulxL1S zN2F5wb|Sm%aOFSM$*ZdoX}IeG%1gZ@v%&y;Ig1DgVqTJpU3v3l@ohp?9Mooo<6n($=;o4u5 zjG|3<3!E`ZP@d@O5!$Sg7$=*C3t$mW-M;!t-NV+u8M6TwY+D~&6x0~4cew|j2_|D% zZ^QVmp+wgXe7Hk6x5U?1TwVqND|FCdR z85Zt4ZSbH7B4y}>o*TKa_q3KEM){(Z!}&`62d?p z=D(PU0lFvExc5R{7={2*1dAe3*n4}KE3&*&_`Pc>{`oTFzB`UzWrc0ptDh%plXP1q z&p4TLEpD^BMdoO)8&il}#VX0sQ`VK2Zfg=pp2hY`Dl zD`ZMQ^Ci1dkZ9dW6Wz6&P4N~uy*x2No|o0N?50Q_jkxPS7J={QoK?&f3(ZrY_>&%G z%OvOxgi4#k1$C#`4Aq&_g`ncqGOjWpp9e8G5;tsO4`1_%Et}uE(oe%Yw!qy4$WhJxYEDcTJddl&2O4-gZuDb%MGM{ z)@|Ii**^QES*DLZ;nYJj(is!XbHM1D3%LBE6V*Ej*2h6mIcyc|`&PPb@f0Pp;`n4G z#W7YBxiy)|H`1isv99?J-g6_tYR-TOT87E@qRE zLGGq$hiFEL?!Y=*3#VO&dk!0*GAUCYhGmL0j;F++G}GGihzw)uJV)t}l|2h6VZYvnz4vDqB&TNGzxT>d~B zzS!2_V?P$AvaboOk2O9jExI>S87iN2W7LgHEAFP4nAy9+(NDIgC0BlOfp^QxCRUlk z8;r&f_duLB#wlZ@acllQuDRq&$~v>#H)21?Pwc}dQ&_*$=0Y8 zS44C_I(hhFQvQ$U&F@BhCN(wI?<~qTzwpp;qjNGB!>>TsGNMXwS8U6f-k4KsMrDJt zdU%e`Q%E1~SQP;mjFA)7`VG;rzJNLggnhFnVVhnUh1L5i;44=NA>2{@hW~D-{>!@; zJUhjmE(;|R@z&UrU$xtbuWbz%$$2O?-yAYR-^9G;X~!Xg-+*o%PPIekNF{ar%WcrpCpVNH$?8pFiL z!Cy4|W~o2JM$;4aKx!=%Tg#nY1{=I_{8d=H47`VG+!1pM%1sctp?)Dt2%i^k0LI5T z^Z2NLc9h+%&$CLGwb(PNBL+b=fDOekrc<%z{!pUN>-puYwh5`1rV7U;i7wwqK0Tw( z`hUe0uTQ98%s|DjXiF8<-v#7$cl_8QH=DD zu@jAsi$~7R|G)^U4XWKwI1)CD^! zxWNv~&2R`a$n(LM@krBbOj9$zuW0+I;a`1_Vpj^o!Hm%#a?xi%-wuJ~;h(pGz#Hu) zR6Y8+m}aWn7-qRamo=;hRVH5^ipvG4Dj0(v-N&)vR8v^ds2Ny>_$C6c1oS&xyLR6> z;s4?Nle|B)vpn5F909tqxZo32G|ClG?H+!>^1?yY zQ>>3<|9j{cH?b&JGm2sUN(orY^WE3A)}!;2D|oZ&_{RKtbg+^=u>R0C{@$7dShe@vd5)Nk=y%D2dGb8o_-zNY22=~Dd zb+L0QnJie{$bLnyHg@LtS}GFdG?Fc#{2U0bP713X^mhk(3Xmvy$T1O!RnR&D?P8(s zg-F_BKslmC=JJ+P1QaloEMIovGB7UMzXQ5Y%<6JO>!KI~YgAz9LT*8bMk$F-{3YaH zPWsm0b4r&n_lCo!7bCH%T1P6eR}^$yz@k)q?XN{taV8C)obIod;^=Kp9=FobdiR6ZLX5Iv+YOsPjvb#Y$VZ*7DR+9=CAxYY zJnZzy9{ba$i-3d%Z_f8XgJV121dtl24hS2puPM=Ts{X~_$@*-2YB zYHN(X|LA0~lG-N)YIE(;k(~BD!zY{c4{pL`7_aZ4fcfl>u=xzD%vNT1*(0(^aJ5C> z?M(|n-~lH(=9{7F8hzHl)9td6lVGHKGbiV*8~rkPS`_ZlPaD7fG>@S*e(!wXBtPEA zCMgMZ@y(74Eq+L(*i`0WjHcdqvz~f*QZ^3L*y3-op4H=6F&wIHP+YY6lmb$k%C_pq zCv82y`pG~Yd%?}=mgidjlvvsNXH#d21$TCo2&|{y7IFee?=y*x$DJBh#u2Ov&Lu-&FqfD<~CeztF(I);ax%abI-WQR$}E zr6c-RG=_)x2RDZJIF_6 zQ+VV$6i>4?!KnUy&Xa91@~uS_+!d;}U=%MzptNx+=7#lm2-9VlrITrPYL}eyEiJyv zQjr~H4lrEjoPPjZYzMwY56v>{kZVVsNTe)~w=U>5h zhG`$mF08rWICpPuA{r{@t` zZ0rQB7n)llseV6JGJ&-i*s%o))p>UlPkDbJhsFQ`k8^OEXnn=3z_MF(2wTm zJ&*C@&{xYlST@!$`oOB(>OU9RbGoK)L5?1%z~os`9DB6O;)%`;hpVwLl@TB<+->-{dZ@eZN&rPrbPIfaBQxJWRE# z{HBS`6WeT)CD%w%vV%haR}zqxbJ&Ykw;-CI!Jes#n9IzCdh^&|R{%!XA?=RimV_Z^ zG3q%pRPthCZx`BB7$wZNi&0#p4kA3G18Ix^DN`P|#J;%zH7sJWso?dwdh1?`J)c&E zA-L1{q4SCc;>Wl{P@>3&kP<@78qJbHqSRSqD8 zA$X!sl28)0d2A*4XSq85S+2DvCkSRT)^ahklN|92G4Fiue4gEnOm8%?&R)1g(p6odKWm?L}@(i-6?0$Qs zQ{!RQy3@Cp#ZxY`e~!#iCv%g`TjR@1=8^Ya`Ce%2h?6*C-xOwVI)8BI*?gqFnxjk( zkZivx^>r_7dm5AUs`=m{0o-?hPwanu;YhhskB zJytD&NEInwR?M*e-;1wR`&Vm690sx3w%+qH2=1Nz&=I9+y{i;?;z*4y(4XpTVCFf7 z|12uiZ&+uFsdEoi=4{kmbELMAs(5`p#e^=%6I`uFA_iB0K8s!0RQ_syo>22*s_Xx$ zmaV47jsHp;yw5t?J`UAtZHtDzVhu9fDdfcZSiD#sR^s~ikGIoi9pg@4PIMha^gHiW ztgdh99^ZHG`1xD06F~5kWt5)_84hT`DwJA`(Ip5xI91rV=}&9c)l(|z_po92*7+M- z#7^B6^~QHb#Obgbr&%YpBkLAMBs@2Bh*f81A5b1MCU)nIXe~IK%A#$J!c}mbh+XB> z1~009P|am?<7QN`Ns|X-@2$`Oy4@N0VkGM18TZ#hsyr9YN5fbPsI+RF3w@Z!DuUR#y+ZlXaVDl!l0O@^{Hp}OQm?yFcknb0 z3PR*VrQYPN?dWgSW0LHD!WU5r$wYG`lzw;Tvc1klJ-guBYN2@Lt1RQ6Hnu(?Lp~uC z{8)SxN2YwP#ojp?NdF(k-ZQF+HhTBIXEKBd1PIm86FQ0#iu4kCk!A%IF%$t26~O|6 zFroLNsHl`sRP0f)U?UU-6>0h?_D~e;G&LY3XMCRjdCytv{qVkgbs^zq)}FoheOGBwl~ryih|WT{k?zMbV(0xkoN#L*&{k(pquren*ehY zf>VS{ri5%D`c`-q2{3V0+jMcj3Z^{6g+DLIm5kLf{Qq2h0R6JFAkhP24S8}S_ zUkkAN4iyDWiQHM3ffmPD4b=zxIxy30a1AO2AO*t?A%KY&2|1C{eA;Rda1Zdzp*|{% zuWGxp!EQieT~E}$&HPuH`M$vLLDha%mcY7!pO*i^A9VIkus>=toB_-O%d>2x8NZ$+ zik~Pa+zik>!=h!@5czk<%az3+C%kL#S?>nEKHo?HTn}w1HA+ZV1I&-oawMiViS~R{}!EFG9D;U$z9p!5ZxsfZAiz$Nx18dHyhTc$D8ox`2^E|mpMGK8ZMQE zsJMr4$vqA3Y)NC=PWF|a`y=yk3-)crv8Gz#n;@8tr6D+x#(35SRq z=X!t(la5k|ftDn!O~ZUoh<-QQIDtoS(^v3!b4<50K2@=A9N!${7ylx|5JyODXRt6K zC>={kY)B+FJ^+#Wiz%}eIkvo+6wcu}PdwPpIEAC~a^wO1SLGPj_UyZ}hyJzD*f{PJ zhPr@&JvuyFc(9Gm+eP@;+DmDl=5+hEeJp2BW{7rxNF+@^M?6H{B1>(-$#^a!$n})E zi{oRIb7Q$y9F6()AALgG>B~&|zsnyVIo=p^?jZi!U|X0Ce|i zfHH}O2WI9kLhmT>>?IGhww%XA^}dkjBGA{qnVq(j%#Z)InHWC*qfnIPfq$hWi6pLjp6HU9E+H4N0{^Rk45W=C? zIWW_uL8{tUu`Rq4<5GDCI{NUdvQt`sSt*MN@i}9Ip|2Y3Il!(rMwy*-@D=4`5Hxa& z3Fw{Bthz)mwP;R8d#i-QDhk%hV|Zj*NwYwhD*=yq&Obl}9B;TZ%#*z$3h)`c0|;}# zfLqi;ML;N426gX&OU<66%q0%u7;)nLL8z&_-7}virUrPj+{ccxLd^q6fU*I!PN^_L z^Y2XYZ?gibl33#7^(Az4A~OdC-j`d`+!3V9jPSr1ZdxqMIVW@Hgz)6a+rFdzH<>bq z&zN1p0{!oY$5Dzu@WvO_otBWAm{az=kFuwRze~y=`CEg5IugwDhOC^8GHi`p zt}{)RmNVi?;ZK~^0ap07>J!TF!6@)eU!-sDrN6VS7|{t4AD(REVNb)B(gWgnxz4$c zc0y}}ly)DYN^G2nnrxt9_Onotj9?exy=}_C&P4==mCj(3z#5N2Y2Pdf#ZloVcOo|m zUtZB5U&aqrSTRhOfQ7@jDe~&AOdysv&R2*%VVty z@V&wHy=fBiNM+|r@MJy|JNGWjW>80+pEc`w5(pe+69YoyNUSww2yccA&dZYeN+GOt z3(TT4j7A--ok~<2RuzfVj832(Ek(Tqf3oM9sI9lE$Nh$CkLPyBUWe*d=;NFNt$uBZ zTVy0uTU+1hlnawbLzjVP^zT+|jug@;b}y3lzSZYvT(OOgh{l{hOXJz^LU`9mz#irK zfgxE8w>8YtaEMvoM6fBSEYaz!C|z&JD5A;Dv1Dr?CasRHJ8U)X1ys5&N8K~Z{h?0~ zEN(>ILnhq`uxxaDtO24%$ouC~ zh~T=Mp9hqCzCR_>px$xIfKg`Z4J|e5CIG}-gT}4 zoN2&+D0dtUD5abojmn=a>9`d;S92OK)kcbMk$1qFwQT9{+4D=xn>boR%RzUbx?0_P zyl78QWQ2sP)Vc@e8q_Vi?V5`gPfQQhmTj2g&p#Nh-p#*A-^$$I=aK9Uf>jZwPzQ)! zECt@XP`tFpkx&LN^I*U|%%#uD6|BFE3zU2yaf@y2I#(3-p7_)`(k3l#UuJ?CD{z@Q z(y*?MD;c7bIrMgi(9eH*K-A#FYT8f@N$3H#QvbSTb0;n#rXw|Cggbx`jfXaC3VQYM z6CzabHXlvfg2okro*~D*^}rtiKv-5IVLyA~wPVN$J&fQ0C%*J5N6@I#ufTsQDVbB$ zT6rA1;h0nY$KMGX^FSv0%Bt;1U6h2S(A}2ZJBv;($OewnvxrP1hh^LpqSU zg_|ZTa?BoHxky@~>b!UvgLg9XS*HUx@>O1U-uqiLzw@@Pb9mlvV6@jb;VL+k*^2cS zQ#U0&mYA-5jN@U~Tzzv!)*siGdVm>Sa7<=&h6(iWjP*uE{H8_^Sdf_N5KjX#Pd8YM zzbs3mY*B*Fl3eX*w87-&o2>g;N_2yiU^BW&KTnEe$1b&+$E^jTL6kvQK(uP;Glv0C z5kAfVIt^Sy(WN+eo&o~ou(eX)6o5zw9|#v2=qSGT6@47y>Z?XyRQ9_=YQO3YS3rmbgh2BQyh6ByHu4_P6xMoa0OhC z@hMf(wfJaO<#+gIx!DD`3o`|%ft#L7dBjheoJ1+5-6JWLHF6*zteDKUOKjA;;|e*~ zOsnzbFIPE%9xc?3g8*T;%yQW0LpPF>UQ+g;)@W|oH$BjzDpP3$FWeM0%H`e1Qd`DnbBL*u8dJZvNCM}bP*Jwu5wxCU4PhoPVw2qX$N@z$;uD^ z&MX1d;;Vi7cZu(ft}z2b5}ilLgKnrp{e~VnQx``z&uJ;{BG@;W0){3fDE!JsN=&g# z^SCS+S6#>J-7xgh)h?%XYaU;|UopdPxh-#Hxn36vmyRDG2M%$M3_)vC}jxkgzY8rl-D=Vrd~R}XAUtF7UTp)6FX2Nu&ok18xk z@-5fZkj2a)+>_2Y*VnfU9%jNN>&^`?N98m-$dv}4zyr0#o-}D`qN#MelJiNnEJ0yH zTMjzkx6c4}9+W|N7o3RZxJKU@*VRf#)BwS1rY+-s4GI-1KvQJqs4Pbo9-x77z?H`J zy*eWb5T^vbS&*jM97RGWsxmOSDkViI{cYpkzil>7A;~a&Fy2J4ZR%Cl#dpkb@l08` zwD>df;_Qq&U*2rK)g+eWtg)&L-Oa+vmik$bt9go-tl)0DU zy(J)7MzU6om_;qCAXg|Ep{4JNYbFt1Haednq60> z1++{Sd$R`i!-QDf+`q+-lY$q&`lr%gDUSLlML#`7Ioc?*2!)(I2yD;yQtBK|+RJbE zAf9G~c)E{W19YU4KKlc5v$;cu1zO=CoCS!Wpx<*F&1# z@ZrNTVQAb#_iKdzE;=-POgv7j57JZprbR9&q`8a2d~BspX|_$xTy!F>`RHMi++@!Mx-jm#K`B-#aU)n zQUc|;7hSI{es6G`ykbe6(b+BNd7Lhm*Pi#_&Kavw8KgaJqDESQSVT&uYp=_m4WGNJ zFp}_lmj6%X_GX0t5TPh`Z&8E<2s0$-M4k({qK)5^0=?rP0#l0B66?{WtY4nx;8FAF zsg*%3rgq-SiL$CJG&eZ-vCB4wL|@e`Ax#`9eWr;umpWo7rV57Hi|`v?8I;2+Ycc@q zCAhoE*dGEhRGLfYx0U^#0R{fFHdn zeZToQxd8BYBYie*AO=UL!MnS9Uva|hvt808JE)#()Cu1|olX|!+@1Kj=w6xha1S^p z_nfq#Q+oQ~1^Y0gedj0JdsQzYW2j=T{1=Ett10w7c7Jy>uxo4I>EuOO#~BRh%XZr& z0aYFX=GEpN)hMr$#7f%kqUS&dtK`8R7cfm?x+AO`B&J*A9cFFylHL3*F+KT`mz!wr zs03x#1g9Haik@aK5kEu&kht^(@4MLw(R$#@7&uuE)FGhG7pe1GN|0D5pp^kC#9>Yr zG$n9c-#`P^^Y3|LaD}GGUlob=0?J;Xa6Ur<#dKwPKf_SmV}y+FlK>~Rd51$H{rv_ucN+)mGjHm1-au-Ve1|JHtso|E0o)}kRTLp7HocLngHj|pH8at7nBYCF>YiZ zfm#R@hz!@Li<*DoP{=zTAPXOcOr*7Wg(${^X(U`vop_SQV+hjflvEYdRJ{*zHwhw$ z%!Zn?eeImFC&@V8?Y^bfz3?(%lxNVIp_(3$4ovL6Ox|2{rRfi;W@xnJ2c^UbPS}sF z1AXx5N_j$5cdZS_4>Th*Q;74T;^;6p{7fd=j64ZsI ziw`RgS<>XXQE@P$+e(-tBfsaffv7f$Ohi4P*g+?D+Op3u=uHtusUrp|8BBcy^#jYQ zpb7vp+=M?I2otzJO}ukL5R%AV^f!*vxt+PDr5O*lh6 zfWQaGrN3|6Xab$D!@&9PpHWT}%92K+b4~0h*dLfMpk1peMr<*&Vo6IN30V!)I)Bf+ zDjn}0pmVVSHvYKl3Tq3~Q#u#svJH61E`Ayt>NiPC5#pov+GSz}>O^ViGRHXyCKYj%s@nhR)qRI7xQt$YMysr_|MNg za$f;1ezr1gwIv zDoP(euxLFTKBK|~Vmy`R{i$Mn`pKb8Qv<#h7|6{0(^w)CiILWyp8$!uKV5rr61^Zf z+yK>g5L^gc)G;iJeMOm3UO<%sbfcGN{*nMl{Z<>b!AG%8EN9HgSvU_bXYVG#d%RGiRvt{ zB15=HjW(#RbXLf8RfE(>?RvuxHc6h5wUWKUnMja6ZD#jrR}%q`ONJUX9@TC`xQ`|2 z0ROEyn^E4rrf3V-0TZN-tJT;yG$O6AJ@NsQiCRGG*vGf#MRPNlI7$RsO|tR>Z6hUO zce4zb8MZHr(imc#ty3duD`>!x%u@jq47@Bc-ztN7Gp0YVQfAx-wG`gkp|p_Mu?jf0 z+8t@q!n}rB+Hg<}PgWdmTBg&e`9C7JFr1)~Mmc<(!fPsmIO@)`{DkEm<2c()8d#NSfk#A8 zuI(;Gf$qUrgDN518V{(7*G<~YOVi8>g%?T0KG}c&O$B#zgl&!c(xoJWu3j0PF!e?w zWno zBvOt2NElBZOB01xjdwtuI(_{FOaYvS*8Sc&onQaR7qaz|H%t_w>uZA)cnGVi_fuBz&(LqcWV^%q=`e@$e>E%*U2IH|a z1*xW`vAJ1yXAJ$$+p~{&tCuUsdCM;3jNNT;zwRtYcmCiZaiNe&PxQ&WtGKNVoDZNo za5_vHb(2UU|2&amMxn$oj?hrh4UbdWFY`VoBJ(Mi<@rV5 z7To)2hcMw=uf^?vK<$UemFPEc|Ok6>&RZ$Ib81}w~F?i`C-dUw}%N!`JxGp=GTn% zAwNX{HKY@Z|4!0H$G~V-yb+nE1(Jr(A${qrqC~exE<6mXr_>Kj|CQ<00)?6^L(~NdnNE= zxzE-R`6eKC`VtDiaZP>t9v^Ke;IcCIi3!YPc{^6~bYR??>lKg-;{SrWm=g0f3$%HgcdN^Sfa)mmtOd) z0ZM~~=#6pnl3;2V*JWSz#ykW>!f4Ki2o9J>SkZS3E#v%sFY5s{Nr>+U_S&{?HVZX4 zxiUJM;`4wRH=MI*y|bq)1dwzGDb+X@rjWh@#qR=XCwWV5{vbkeq`$eKOIlD6C&wBC zs6Hja@m}+E!e(#_0H8hr(D%IBm1|{jU0Tlp#;dc*#UYBRV9_@7bH4R1D+3VY{gi~U z1=YTp#zn`@spokq;NSIszG%t1F@Ix0|7kw5ePeyh!H$o7ot5ENe#M3#(#gwnFLKBK zttjSMW6YYh;e{q9USV=(HqR>)dhI(mg#RcgIIxQ=E&c1C51&3w{+wMjLA8@CX}=Md zwrAhI{YipFe}4>SLms-oW8ZkElCc`ur!Kzsq?z5F<&LRs$=_$Mc1dnvGXKeTFw&oy zxcWk;@?BMThuisIUnQn~>^m^+pih1PAK{nWPM_h9I6jwD901D~JG16jF{=1-OoZ-1_oxv zIlKbEH2er%lr2VdkO|YU<6oKz{;BzK{oCJP>b{*3ChRNTr}SKh`7AioP3oYH#bRVm{qZ+4@^jd$^>h|@>9D13|^XSFW)46N=vBTaiJ4ptD za1Uw6#~Vxs|Ih!zTuqTb?-Uf2%ZuEsck|7?3C%0bFE+K#zSZ*@kc|?Zx;kSJ^`^tJ z+G{T0-PYC5hDS!u9F+5N?t6RtMxRIh@i1ilx(;(A?b0Dx+5Ye(nWSeL&%@(yT%{kq z`ZblZ^>uKRPhotP#G0WKTh}*yx~X7%QLtO4YA4T9(G9)Mb!EU)t$1i#pw$s2xqY?w z;Geaw-Zg_$Cqm!7yiRyjov==nR_=o{2r$9-C}m;&kL28`Vu3MA~|wL$K;`py!%MIfAeRJXX4(5HV>sW?9;|-TCNGh zTuS6|UcmD`a%4?S{=Guo>gyZw)=0E$kH{N4p%PPKN^pst*bw4FyJ+b{3ET3d!!>24 z^PlRSj>Larg0C2|CFAdF-Qw3Cj7wdsVDn;O>i)A^`&_~=(ANc7sJna^wp4Yilg zH08K@FS8m>FAq<0)Zl4yf7Gt6YweiW5LI^7LQi|nms>e9E<-1oh64rpmtN-Y_Uk`a zS8G%tcKrC6Eek_R@8Zu+r+0MH*cO_;qEeAnsrxeR6X(^r-A;}J zCj>7Ff18$^Hk8jaKPu*ReT9dc<16YD-3R3Bfua3!>bAM@tyKBld|K9q&kxU+tZII{ zry--oV#)E3k3wd>U{Zei$&*{gKmKYm8A(-YH2wGIde)67zZ)95tg^*O+dNXC6=Fiv zGZ{x$*$2G%y}hPk-0F{w^*lXCn6tkTdwlNXX&*ISP>|B;LWOs0L%LYWib~6AXYA}9 zBt}qE{qWUxAyYF)EVoG{EWc48Nd0vtQY)?F=kBW?^*?Ot?Cd;KE-$ZR6y$4AAiMXK z)asjjMXa{@m%u=FNf)Dk+uioNHO9Fw*m(+v+r_vy_p`(xn0B=9UY8nuY{k2f0~vvq zCff})g%2asj(r{=qO~2*OEdkKDiM0F{#dHf)MZfX^d4_Q$VL;F8Wx@Q=e*@R))|^ucRqL);H!S+AK5S4>~u%Iix>@_KRR%vD1-aZ<|ItNPrbO{Q*XsQK2b-_Urc zCmXh`_uaTcaf$Za)#b)Y7ibhIwOxvW+1K6Np%up>v=H+>gSR|(gHc`XxgBSOksJK# z-A?{;E=+EI5t9{?`{&nzf|8P22}N(}NF6!o8AcYXkxafHt@>QEaq$i7-(OkM4;{bm zIehYzdvfT5$(}?LgX5edW%pZkbaWN9XmCf)2DkfN6m1R8mIn{yt|?0_k!jrWXR@Vm z;K=FKwzk`PYOTgeW>MCU1M7W*HvR0=OrG-U_0tmfQkOaE;A)Z5_Gz?271ASFXX8^HIj#>obg`rI#p3tXu7?_(;5Z zqp@c^dtgicul}l)PuMM9G^K{y|5&&b6DHRZ7XNx5s7n59U*fK0=|`hYe_p|`ig!AH zIVQByU25P*KLyrgK0d$w$;Fd5beneP{8P8ybw};Ord=cRFU8%znfN%ZG;na(Wv;@H z{iI`Z*Cv>55yHKkWEnE)xs+{MDU<(V4~1ByeQ`@v!S{{*n#@}#yKs5fKfdcGXpFm@Gh z+VyjVOI!8b@QO_Prs<5skm+kZ2a9zC!rDg$^^-eGo9{n6v^{p@P=1Kz^y&cU+4mz? zw%5vOy?nWaz3PoHit*Cu;DX>+@z#?+!}KIwTLV~S?M>X6mas!dj(m`K%rcUAx%Wlh zpUIRdm((?HE_F+-)?0PrRB?%>%-rMqLF9~DSuuyY_K3Ipx9b&LVv3bE+1$>ku6hw7 z^V;lO(eHU#m&1Sje$Gr8KMr!MTV1?y^Vtaox2v_-`n-*3)xij{8-+u^w#!;X^(Y@* z)NCc_`7zpAtYPhVRr#XSxv%p6G2A}maW|t<-o9^dnL00zTp`A1RDCQ|ITxYr@$P8e z@LK0S>9?b)$wuFP^qMOuET`oar^zOUidxJXf(^R|Vty%4zIZ%ey20_s7yagBYsRtK zYj0mGAOp$C?qt|APU-#O#8YRLsF?1xcAeec&@wN$z$0I~r5h zy8P^T>!r)T-xhR3YrxjA@0pya_b;yP_gtp>=G0Ty37bFE^D!Pn>9q$NSy_XNcZ4kb zSY7io#@y_7kC*e;dk)Pl5uz;OwF^~wzYH2;z`!mJ(xfcp^|$?fWHhzR>2>H?pj#Il z*Rf4n;i&n_0wuy(9sY-6$ZOdGvGT%m%GsC4M{JY5|EyU0LOCNP>(0@4OgrDwyAuEN zHg}CCvgHh$`CVcS$A?Bq@9rdhNvqE588iF&I-9rN-}=xui{B|84SQMHcY#q^fN} zOW&W^DB7I2hGX<{(SuMarP0AYftt+4(`SbaXAfY~N0RQ|NqISFlD&gko^CT29rx&K zU7v{KspWaIntZm|MZJ0A>)G*~dC|(xshysu*7-jh`?*fXsmI0G=2z4PMJ4Of28k=$ zESfHW0{4a-llt2Ba3LN_+PH_9u9>oS`;B8N3i#zWnmzTY%F;r($nogLWy|H_lkRGe zJh9{v%hv`y*VCZtHH+U{JU@T3k6?sr!OkM1N#L0`>^cUjT`KgqkXQk9Hicij*G5O7-q*ryzcjf(vIohCYlPuKR zYTHLT0G)SzR%^V+ysbLkbc2SyO=DgD{x-3T1S#XT;ho)AoE_1)8+a=s`I#%imZ+v-nC0fI4rK}>%B<;Ou zpOA6mq?Fqq;r0f&j0v#5v_Li#N*vRW%{`IR*SkJ_TlTSjvsh%F+ zur|kG{`jAQonaj2$~;MrVAhBhn=D&qd#JPX&EhVnt7PcruHW=ey-y#NMO`Hq#O^<7 z|KZ{}V}s1;_H9gAPc&!a_0>DrBje-;3)-z!FD9Er>%ZIE=IE%uEBo-}tVQ#?tStSF zw~cP=sT4W2ldrBaIkbp581g@p$7KDl#}yCRS>K)3q#b*7T&(UN_vbO&eKXX2tL)>2 zCg)prC9T@#EYY;sY|mjg3%@1Z8*+;{!@+NUCkh@mKaCI=t2}w$V`k(PzpJw9{Uwjo zk$Z|E#*u+;E@omGUfXLC7ih;WeQ|QO(*1ZV`g`j7dURs7!g8V4_u+fpah@`Y20NaG zku=ZtbG#KCCRSl}L}ppo=e#}HrrC3T4A5aZkbkwE6-e7B+)6c9srC8u{+{{oF4rFC zl|>1UaPvDu%FbJOw#;}* znw*!F>a~ukTE|~A1d%W7udqB}?TcMh=AJ{)|DJBi0K;5W#X>&SB&P$W6`7Wadxm+5 z0EIwx8K_Ve^2at8f}%Q5zyjTe)7~OL6Y6Qq!Jr%$Ceha8iyVtlIn9?k^}f*C`q!Aj z2*0Kt9~q=i7A{yTu=5;`PQ`Wgt~Gc6g@Z_z#NHjwnSccq`xv z+d~v^Cs1W7TFNO$r1EFIt3w!{dAniKc|cwINEbU?MSOK}sJf%bYN^_at+x-llAqaS z{_rf36F*R?7*2Dd#z^MAZA)G?B^e1L@UzQrM&i3r-GNzuV0sV1-96x0G|bVXHKj%>=deP zQJYUFkN?_RTYlLwM~g8zwn{^Kw%I#fB=6I=**jz|c1}gpioN%;D~K_M$ljx+dnVin zhpsjycwlr6S-U)pqmA^6vk$kOJ|^DUwWRe(S*KHl7_>M4;>I1VHWukwN>_XymDLQb zDYNOIwdWPdcsr9CbKOsk(>_lvE6uLg8Vu~SDJcWmk#}Xh1GNtVko`788KM*eHJX|f z0y$;O}5I3&&$iQ5J9%kd~aS1@5NzDBl{Dn(k7Pj*N@lHuzOZ1z?TNojdF4{AzU znTKsPfToKiY7D-f)?7x=nmM!(P!V`dW>7^%5M)6qtwCsRDCbXK+WLKc`(g4>6hf(#Tf_U0wz-Ytjw{24DXj@{KuR_WhBp zPcZHtRW_(Qs8^>Ty}}DDqeEL>Rwd(0@Hx8bC>Eu7bD>pyq`b zpde5n-vy{VOdA+bKqbT^%xqf$hIOFtkij21js#$7AER*Q@D-dB@I^6sB1(JX287`> z5I>?8o$+*M_Nw9q>ql0Cv4BT|H7agh(@4;<6za0zlgbTlI8N2`<1powdv zM;5u!=YZ=^hT88?%|4rVs;=R$eC`-Wut>a60+<#C_1J>GxCsITzi9Ka+E=6xo>v3P z39tdMWC1a(J32T;bGz;{S?)`9?H)+0i&>+#igkofBxY!rQZb6V^UB$h!q~+Vd4R`Y zAT#`tUVVU$O0FWuMSZ@IBa}sG^1PooBx*v_3z{GZ591oC0V+@o1sAlj>0!}Tg*YRa zgNR-d2pwivXBPOZK;ehz5kMKh>w{>92+ns3 zM(QTZ{vaE9Bl^fZQcdFQYB(;jHFh*j2im#|eXZZVNKQXRuc54hYvpC)IXA5_<#`11 zlpNV+VPeCjM;M)gA!Zdac3T!Wl>K#%je8fV`2&s`gveooT3{RW*Y#290>-QOXYXXz zx)b>n9a>Ccy^OxSa=LCI#AcYWqe~^J*Q0UhRz8Nmf0(||BO1uR+v3PY(}s#kA+(-1(Wde# zlL><40Z+5NRv+Q)cmqnWX@YL8U^yVK4b085DKPKN*4mw!-rpnXHQb+`GbcDGKSv$m zP${2$P*-<_Qns#+*5bBA12zc~h$G|O%&y|JmCFSP0oDL~DX2Reaz(pXp1_j=j^Y7;mf75osxL43?sB#sJ2>j_)Cfdm;PBPGx(48)L2N%LOW;JVn+Sur?0 z<)Pk$nw2tjLHC#-ZTDqxhgS$J>1y)-e|iKB;%qZTQNGT+3!6YWayvZDJ>P811ta1F z@Z{0H704h$ZDdHNeoP0&L9YF*zW!?PWU*ITRhAo;QJrFQl<>Sx-SOQ%|N#9a>1rT?sR z4U3eLP!GP2!&?!A6P0QG1ET4Jc|A-fFdmsjIhPUIn_VuxT9_5EdR)+(9pk)m@u}{h zfC$&c4Uf_WfaM9smhrRnsvpJm)MhGS&?eC_`_gLePKpg@!uj{Zu)C10MQc$KWaC049pYB z>&^vs6g2pTj1UG7{=?a+6g#qmvJqahJQSJl%RE*I3Sas(memBX+uMxBGuVfZqI_(@ zXa}z}rchb$&gG+@_a)XAuehnyX=R^bEg+|?m$Ub{{Os}r(#dRnUsbyk{wi5zfycL4 zo*#iK`y{TWqsrc0p&YTDuM&b#mN8@w^Ye$hNfEjr{+xsTn~_eUKGn+d*Q^?4sNFbtm{ za(|ToMjd0=6RbM|6h0S9O}MBc)NV5}u^#OUGeHt9eYp4XndkW&qt_L4eUwPW~oZvK;NdY(MRYFf^{r97BxZcr@P>n6$DN{LyvJkA#fTo|5l0@`C^!`(n@=5l=W1731GYZ_0uBLWNn@Ie_!Exee& zK|*M$+=L|B%lw)<$!o8LX#N0MjWWYef7NqKM;U_`hQSt z(~%7(o8v?Dcr2?iXa_m%>0$S;{;Rs09X^{OG^SUFAE3>y+)?0<{#quqI(ZLRsMit z5;Eoxwq73?QP}M=D{5c3=wnnZ7I8kI6>&I{{)2R!eqlv5N1C*ewPO1#iPZh2L5VKs zd-a^|=a~^loRi3FC(v72I5@^=KySV31vFMR=QHHa+}bn1&Ec?at)*%PDuG6&8|~A5bosm z8!(M|S2HFWSWFX&3~=K!1)DoqwHO<&a0L#9fN#hwIV6C1E9N(A!W0rXjg%_GjF|Zh zF@P3gyAvhU>HK`q(|uExmzNH{-a#;R^6u7Wb?|m$(Ui zj5hC$jfa{B6w87s_i*8J7MQQ%nLw;BfLfn4_;xK5y31Umu!;LZjggQK|CAUaZ1#+T421INTb657>au3 z7gmrlae2&!2$NNjb0_7!u=L4*^)yb`(G-ct2M)3OHy;#(pre`KA6>MOzd{{iJ~b(M zLl&M|QZ!i8fd3JS|JIU!`!gc(Z+}L23?#XTgYvcg5M#@=!Em zI*Z_#^~!FH53r$+4}E|e!aN7CnG$d(_}cS!H!KpxBK*PUOt1l8n=MijYM-eE?9B4a zS%N+|G)waC|7tR?%G|0zo^ZW0T2H08)K9!1$P?1*j%2b;qLF~h@AU=WWC*eSnvaxm zUU*U@2HfN4We7-LKgzsL<$WasO2?-gbWq^nFWO|v%dmu)E9W1ZVx3ydB{T5|dfbC~ zQIFaCX`M7`Fr{157XCgHxL8;)bz$sOxW*mjdEY>l%Z^_EHX)UGUD@)zxAt!nQl8_I zdNH23U~RR=bcm=qym%Es^ma}ge5=^ffaHp4gN$y#)F$z96-R$yu3Ec122PML+(K!d zfbjFoaLJL_R|zb{lrS^NQYR&s7Uco&P3P9OtByVpgP11(W(Wmn;u#4O zwoo-UF|Z+PF}&S$#LUS_OkhJGuPP5vmX`xyl$6Ky&v8)-Mo{u)GypbuDm|C~7?|y( zV(vH?O>)Ax3mk`eGL}S&y)dn6NOLEJ)+@432RIvhThy%MEE#u?!6GXdWb#F^ct|d} zOB`sxzziwtj(An}g2^Ku=6GX@F*>$t zDh)$6Ze5M85U9R^@%sflRu_X=_moBuCK0$;opHiz^J+A+wUJ(hY<1?HsO&ND0B7~1 z5#BvBDThcn>xfS1P6l)5jAb2_g{4MvI-`FT?{zZ2YncN$R89AaP7tNyTL$`>t*dH;iQtCdEyrqCLCdt8S-#_M~$*$775+XBze??>Z8?6|RYAU()v^fm3650F!SHTGPWC6LXmmyoTojJA2J7 zlE>7A7++s$K|Gy?%hOnSB1aX`T_$j_E)X?-s?o+C$YK!6dEQSK5Kz!Q(b7EWxSvit z2uC==jZ$g4WS;DFnyvOn$80&;b^cWcCbP`=r9$-|J?)GZ9eGiS9v(dp6RqHzFDdY= zLLeSG{uJ&~c#;pq7(IK;2x(9McZ>JG==$!kCepR!Rz*P(*Z9u&u1?YEV@4xEio*)Ih#r_nhxN z@AX~Zzx+8od7k_J)r4VmE$_zgh{w-9IUu1i^p-Az()E`g_xe-Q#$6#lV-+8T(d4Ib z2-aa^)904Tk*hi-f)d+1kt$d0(zRD5YcTbi99QuApoDB|1=YnZ8|mz$Q!8Q$7Su}kIIyuZIC4XO`g!Qos%|I3*pX)L(7TE7HGM{mi~ z6s!K+X;d8q=Ut%D75YV>Iqyan5|QPxV9Vr{WCgJi=4W>9|31~P9n;V@+Ct~pn8M$3 z+o&)yO!V{T@k5Ln%I9>#9>mon{Ev|xC8MSk{eI_{6J{e=jB8ebaYI)Jt=7!7w1|vm zcHDbs(;r3sige37!1sxd2Y&u8#{goz<< zTcNY1jZlJmV3MF1%_;mv_0>^Wcbrhw8;Jh`$wa{(B*~Li#p29Z=QkcYFM;X;0%>;vBxmv<}?u*1w=IY;o($Gz*q zCZqM?U6|@s6<4PYOb8Ucm z%>hC0%a)$hh?4~eb}}#c|j%S?gpVI$VnGCx1{~W7=#>;RPSC=nOj(4qS$b@YgS^uo zuPP+uMH{9G#J;fFTIKF?V_X-{*@2Vw4V~kdjplI3VMu|?{g@2`Og0HGLBN-z9f7zp zg5(GY0n%-eN(?*@i51+$eRCHxg{OAnB~Bva@47(NraXE!H>&*CKJaQqsmoSLP(BH17ybEUG!SQZn30#c zu+J7##AC8I$vBmFgwXo>^wNxmB%^MndQtODb0ncP_HJ1$ld`^Y?ALU}Gq>M@kj9;x zp!S(?tMz9x%e>Ggcywu!hy0}3AUC;zlS`Rb`^`rsmqK=$A@1eR+@0bQ%izfT9Y~)A z&ARQ61(QgS1JDcbqnK^-Xz|e~wP;bxBLyOm;!?9#66~ve$5<*uxRzlY7pH|namF3T zqX&fRBB(V?b(?C_&QK`V|I0@11YR7C9qQ|VrqUgZ28&Vz@5lNgARvg0(>V&ef?y6; zV+_8QNG5aWb1s6@w8#7G?_J^bQTI8ApPyZXvh!Dutf7a41pE5HNE?R1{xJLN+R=+> zdJT{H_i#|5F<4%%bI(Z4CGR$z-oY2v&2BFUbVeISguyPFF-&GjP;}Jhw^Iu+LECgx z@}r2FjA%9&!n73EX5$4i`O>TGBM_p$S<-BwM(iJ5MOTejb~MAKwQCW+XGTW!J+0@+ zRzb*|#)4+$!q!C>-!f;bSrS2w!r-SSUImLU$yd1;axB)#lf-a;FVI00HBT_%NFjJH zFqT9(!FF%5H5}FM~miJnoXrJ@m8aoJ`PK3fhRt>eN#aA_Zp)-Ql(>+o%f7|Y*a)d| z4%kD62_wxM#rQ~Lu86$IR-3WO9z!KX7blZ%dXL|!f$7~r%0y zk%ndLhoR=R7Z$to%FQX;#0f!|V5t)yj`W{+*lgvi4dWz8OC%j0Ib)v_P=_!TJXoEV zzq!){nRsS52kx3n2sD&jR%8re+kgg8)LItF2RO$Cu6&>VhUrX`po!XPW#B#V?dXQp zUmL#W92ObiQ@ago@rUP$nb%9A=7b!(+7`h$`0Duv2QT#+++oo8anX}jJMh=%oOBzk z=BFp2vJS315(f+$i3-<^8&mM@OxsB!1n1CoLhx<^dM~16LoZSwy=PNA(^er(dGljR zqSM&99@70gorMykw!q^jQaQY9Ve0C_17cN4heVk|N}L~Y4b-AA~Hh?WIx6kkAbiT#*aP~pVs(SI!A7|F~`Ux4RxQ@FPJb8%6G3xWxN_^ z^s0hvqSV%yCUf$KJ+S2Ell?!qy?eI(aK@rS1{C6pR=dsKe8i92k+&~q;uJ}uhhuQ8 z`U%7-<8OR53;4xOAYO@X%IFE|H%b}DJ??}g(hWGH(fT#_F|8fq;Y7LOlr!SXLuA`) zqUr}tj?8B<^qEHHGwS@Wa0-Q!jVxw0kPYs|Jq$Ecth5N!f1cCZRFZq98rz6=X&uQv zQ!|Fg0pq!-w#^Vu7YvwTXt$lQB!WzaH zX}qxd6+i{*{>31T&t(7Pj3iG~kVOTrgdlf*S#A^4#I5q~D~oTZ0>ekO4%Pe(1EKtE zk3|~FaIIl1y4H(MVb4MOtfJcVK|3*bKft+$ANO&o?VNj~&pq1gfj4YVr_dh`-rabryk3d?q!^YX5<$k74o<^@4n||BW{WSOT%~%~==3-pPUvf1z|CbaFf&rVgDIkSWize_{~rhRG=UFv_`09FI_E0jKx%UG2IC^rrP`pm%T4*pqIAvTX{4EZ zZU~q+3h#&xtq{)-t|`}BZLvA#_b_~__)dYJc288fr?Aro>dgm)N<4Uk4@B74By;B7 z3TZL5b$NTWCC79Z=y`p~PS7DH4?RfOd)sws4b1mMW&T_;y9nOy1b;iI@xCzGbf!_2 zS%CAXXBLm}wXB2D6Qo+<298hC;jo*4JmXI|d%DPEgNM4mEF^TI!jjDX{swEv-G=;= zqvk-G4THX#l&!DWRP3j-9}F2LgwJO~V8{+cGY0Lp9%@FwYfT~S(btMG-}a#tmOIcp z^m#wD!^-rO*)U!zYk&lzo@g-LmhZdVdhfh3!H8`Qzz0rfJkN>2H74q2sQYIhxBDN zhGioX%E42PpxtIer;KyxzBRN&jC}Js@SV=1WABhTvyq2H$)P||0570oB!TZy0+e<@JO~4OK74=DrTP#uz|4onNDwM+ z4bp$zgmoESUI`SzH@L2sb*@6$DV!*=tQU`U$JXcIh4)tnFN+pM+V+#qb@|XN;uh)& zQI~=vRC|>#Zb|Mj1R$R*@w^cLhbVKT5kVR)ksG5iIze0|ge5FMf@3}h=Gk}g8TJJv zjmJFGJ$OmUoKfv0tfS7)ygh<+$RVrmp{_Vo-`q&rv_6qsQGBVi^m^MF-_aO&H2KJo zpqOf_jb?=#_*aE@ZtBTNQuT$y4=@x1?|S7a)!KJ94L)FwcbOk&jj1CiO}Hsr$@#zP z5HBw@5%W49*37y+jo#o6F#JMJm}f9^78BVI5?tNP&z(-%drC5HQy7xioL@hJjRx&T zj+k?WB2FHoenyC+vxeh&BeHHiu`wjAaZblDYNsg z77&o^spcBJ#;RO~7;~^|O;%2t6+_u{D*Mx^JJ}3`7|RtJ>kW-~8i(wB zZuot>A4-&GIb#iJU#^%rAofQSk4h#XGmA;>9){13N{6@eefV_Y(G+D>6G6+wA4Y=EOx*=E1>lk1MB`0uI6@# zbkPRw6DeW>amA~F{_RLv@)9nH!Ya0OnJ_ysZ?s) z`br@p4(`<=$3a^LE=Xz=lsxrkR_}tDELgKtL=kK{97nGJ>ya5;ZxOP+^so2j&FXI$i>`L7BLkv3c2J9 z1kT)#SVUY7-V28W4A}F73;7N}MnD~qYI98e!zx>Gyx`ZAcKIQZ%2`+iU^o8M(=u?)!}`DOgXO3R9Zhmn zVKIh|bnS@@j)MFWXfs0Uo2*bkmg00Fk%l%MJ8;SKCLe`p_#9^Xwy)Lb|tN1H@tkz>7qfA z5z!!~+rwEJck_mw0?HB8qmwWpRM{hQ&r#XJLu5aTu+mw~)Q4|#N3dklPyZn4{h2kn zcbA|i@{@*PZ?jqU{|$RDeqHv@W~-oiwfZab6&LBUf_WmWnvcULmJFo}+4n)18E9{x z?(Q};O0(c>)4V-?i^95Z zDxQe^rHui{QwH;EGNW&eCp&|9H@gp56&=dQp4Z%a%GfGO$0Od4Ha(D!^e3n>2fr#W z_jCzHxTBK%3j6oL@GE^<*BBWa=lTb1vPW>5pQf9penOFmQP8zG_3^oM7pdQyQP+&5 z$MBy53u`lM^?aX!*~?7fLIpE_To72!Z;R2Wi6YDcT+I*i2nQ4A9#SVDJv&xe8#U4m z5F$XBbq6|UwF%@o*_+rJ4rVzGw%8+$FE92tT{m2NHhwePz1�%5AVfiYS>w^A=5- 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 ~*gDQHvwlUB83v}yhw{V3MJW}6d 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 z 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

    =qYbVqe&eX8I7vV07xy zI^@BHB=Hb#HFR#t*;9burvC2N5uUh=$AL(s32;w>CVK*7GyZwfb{_vJ;Kp5+`oK>Z z6j=TgYKkOnjt^nyL0edSTuRQwWSe1~)Xl7LOqjGC0nv`&U≫a7{K9^4X)v3(O^8 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�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 z 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