diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 2163db45b..528b128b1 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -2,3 +2,6 @@ # tabs -> spaces bbb3b3e6f6e3c0f95873f22e6d0a4aaf350f49d9 + +# (nix) alejandra -> nixfmt +4c81d8c53d09196426568c4a31a4e752ed05397a diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ccba62541..f26884c2c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -39,6 +39,9 @@ on: APPLE_NOTARIZE_PASSWORD: description: Password used for notarizing macOS builds required: false + CACHIX_AUTH_TOKEN: + description: Private token for authenticating against Cachix cache + required: false GPG_PRIVATE_KEY: description: Private key for AppImage signing required: false @@ -631,3 +634,53 @@ jobs: with: bundle: "Prism Launcher.flatpak" manifest-path: flatpak/org.prismlauncher.PrismLauncher.yml + + nix: + name: Nix (${{ matrix.system }}) + + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-22.04 + system: x86_64-linux + + - os: macos-13 + system: x86_64-darwin + + - os: macos-14 + system: aarch64-darwin + + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Nix + uses: cachix/install-nix-action@v27 + + # For PRs + - name: Setup Nix Magic Cache + uses: DeterminateSystems/magic-nix-cache-action@v8 + + # For in-tree builds + - name: Setup Cachix + uses: cachix/cachix-action@v15 + with: + name: prismlauncher + authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} + + - name: Run flake checks + run: | + nix flake check --print-build-logs --show-trace + + - name: Build debug package + if: ${{ inputs.build_type == 'Debug' }} + run: | + nix build --print-build-logs .#prismlauncher-debug + + - name: Build release package + if: ${{ inputs.build_type != 'Debug' }} + run: | + nix build --print-build-logs .#prismlauncher diff --git a/.github/workflows/trigger_builds.yml b/.github/workflows/trigger_builds.yml index 9efafc8cc..0b8386d69 100644 --- a/.github/workflows/trigger_builds.yml +++ b/.github/workflows/trigger_builds.yml @@ -38,5 +38,6 @@ jobs: 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 }} + CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }} GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} GPG_PRIVATE_KEY_ID: ${{ secrets.GPG_PRIVATE_KEY_ID }} diff --git a/.github/workflows/trigger_release.yml b/.github/workflows/trigger_release.yml index 134281b2c..e800653e3 100644 --- a/.github/workflows/trigger_release.yml +++ b/.github/workflows/trigger_release.yml @@ -22,6 +22,7 @@ jobs: 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 }} + CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }} GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} GPG_PRIVATE_KEY_ID: ${{ secrets.GPG_PRIVATE_KEY_ID }} diff --git a/CMakeLists.txt b/CMakeLists.txt index 8727c2ef3..3d70fe79b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -176,6 +176,8 @@ endif() set(Launcher_NEWS_RSS_URL "https://prismlauncher.org/feed/feed.xml" CACHE STRING "URL to fetch Prism Launcher's news RSS feed from.") set(Launcher_NEWS_OPEN_URL "https://prismlauncher.org/news" CACHE STRING "URL that gets opened when the user clicks 'More News'") set(Launcher_HELP_URL "https://prismlauncher.org/wiki/help-pages/%1" CACHE STRING "URL (with arg %1 to be substituted with page-id) that gets opened when the user requests help") +set(Launcher_LOGIN_CALLBACK_URL "https://prismlauncher.org/successful-login" CACHE STRING "URL that gets opened when the user successfully logins.") +set(Launcher_FMLLIBS_BASE_URL "https://files.prismlauncher.org/fmllibs/" CACHE STRING "URL for FML Libraries.") ######## Set version numbers ######## set(Launcher_VERSION_MAJOR 9) @@ -205,6 +207,7 @@ set(Launcher_BUG_TRACKER_URL "https://github.com/PrismLauncher/PrismLauncher/iss # Translations Platform URL set(Launcher_TRANSLATIONS_URL "https://hosted.weblate.org/projects/prismlauncher/launcher/" CACHE STRING "URL for the translations platform.") +set(Launcher_TRANSLATION_FILES_URL "https://i18n.prismlauncher.org/" CACHE STRING "URL for the translations files.") # Matrix Space set(Launcher_MATRIX_URL "https://prismlauncher.org/matrix" CACHE STRING "URL to the Matrix Space") diff --git a/buildconfig/BuildConfig.cpp.in b/buildconfig/BuildConfig.cpp.in index f85b84dfb..b48232b43 100644 --- a/buildconfig/BuildConfig.cpp.in +++ b/buildconfig/BuildConfig.cpp.in @@ -116,16 +116,19 @@ Config::Config() NEWS_RSS_URL = "@Launcher_NEWS_RSS_URL@"; NEWS_OPEN_URL = "@Launcher_NEWS_OPEN_URL@"; HELP_URL = "@Launcher_HELP_URL@"; + LOGIN_CALLBACK_URL = "@Launcher_LOGIN_CALLBACK_URL@"; IMGUR_CLIENT_ID = "@Launcher_IMGUR_CLIENT_ID@"; MSA_CLIENT_ID = "@Launcher_MSA_CLIENT_ID@"; FLAME_API_KEY = "@Launcher_CURSEFORGE_API_KEY@"; META_URL = "@Launcher_META_URL@"; + FMLLIBS_BASE_URL = "@Launcher_FMLLIBS_BASE_URL@"; GLFW_LIBRARY_NAME = "@Launcher_GLFW_LIBRARY_NAME@"; OPENAL_LIBRARY_NAME = "@Launcher_OPENAL_LIBRARY_NAME@"; BUG_TRACKER_URL = "@Launcher_BUG_TRACKER_URL@"; TRANSLATIONS_URL = "@Launcher_TRANSLATIONS_URL@"; + TRANSLATION_FILES_URL = "@Launcher_TRANSLATION_FILES_URL@"; MATRIX_URL = "@Launcher_MATRIX_URL@"; DISCORD_URL = "@Launcher_DISCORD_URL@"; SUBREDDIT_URL = "@Launcher_SUBREDDIT_URL@"; diff --git a/buildconfig/BuildConfig.h b/buildconfig/BuildConfig.h index bb633f297..ae705d098 100644 --- a/buildconfig/BuildConfig.h +++ b/buildconfig/BuildConfig.h @@ -133,6 +133,11 @@ class Config { */ QString HELP_URL; + /** + * URL that gets opened when the user succesfully logins. + */ + QString LOGIN_CALLBACK_URL; + /** * Client ID you can get from Imgur when you register an application */ @@ -165,8 +170,8 @@ class Config { QString 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 = "https://files.prismlauncher.org/fmllibs/"; // FIXME: move into CMakeLists - QString TRANSLATIONS_BASE_URL = "https://i18n.prismlauncher.org/"; // FIXME: move into CMakeLists + QString FMLLIBS_BASE_URL; + QString TRANSLATION_FILES_URL; QString MODPACKSCH_API_BASE_URL = "https://api.modpacks.ch/"; diff --git a/flake.nix b/flake.nix index 987fc0eda..f4ca782ec 100644 --- a/flake.nix +++ b/flake.nix @@ -2,8 +2,10 @@ description = "A custom launcher for Minecraft that allows you to easily manage multiple installations of Minecraft at once (Fork of MultiMC)"; nixConfig = { - extra-substituters = [ "https://cache.garnix.io" ]; - extra-trusted-public-keys = [ "cache.garnix.io:CTFPyKSLcx5RMJKfLo5EEPUObbA78b0YQ2DTCJXqr9g=" ]; + extra-substituters = [ "https://prismlauncher.cachix.org" ]; + extra-trusted-public-keys = [ + "prismlauncher.cachix.org-1:9/n/FGyABA2jLUVfY+DEp4hKds/rwO+SCOtbOkDzd+c=" + ]; }; inputs = { @@ -118,5 +120,24 @@ # Only output them if they're available on the current system lib.filterAttrs (_: lib.meta.availableOn pkgs.stdenv.hostPlatform) packages ); + + # We put these under legacyPackages as they are meant for CI, not end user consumption + legacyPackages = forAllSystems ( + system: + let + prismPackages = self.packages.${system}; + legacyPackages = self.legacyPackages.${system}; + in + { + prismlauncher-debug = prismPackages.prismlauncher.override { + prismlauncher-unwrapped = legacyPackages.prismlauncher-unwrapped-debug; + }; + + prismlauncher-unwrapped-debug = prismPackages.prismlauncher-unwrapped.overrideAttrs { + cmakeBuildType = "Debug"; + dontStrip = true; + }; + } + ); }; } diff --git a/garnix.yaml b/garnix.yaml deleted file mode 100644 index a7c1b48a9..000000000 --- a/garnix.yaml +++ /dev/null @@ -1,10 +0,0 @@ -builds: - exclude: - # Currently broken on Garnix's end - - "*.x86_64-darwin.*" - include: - - "checks.x86_64-linux.*" - - "packages.x86_64-linux.*" - - "packages.aarch64-linux.*" - - "packages.x86_64-darwin.*" - - "packages.aarch64-darwin.*" diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 35eb6e91e..b8dcc1099 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -780,6 +780,9 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) // FTBApp instances m_settings->registerSetting("FTBAppInstancesPath", ""); + // Custom Technic Client ID + m_settings->registerSetting("TechnicClientID", ""); + // Init page provider { m_globalSettingsProvider = std::make_shared(tr("Settings")); @@ -1022,7 +1025,8 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) } // notify user if /tmp is mounted with `noexec` (#1693) - { + QString jvmArgs = m_settings->get("JvmArgs").toString(); + if (jvmArgs.indexOf("java.io.tmpdir") == -1) { /* java.io.tmpdir is a valid workaround, so don't annoy */ bool is_tmp_noexec = false; #if defined(Q_OS_LINUX) @@ -1042,7 +1046,11 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv) if (is_tmp_noexec) { auto infoMsg = tr("Your /tmp directory is currently mounted with the 'noexec' flag enabled.\n" - "Some versions of Minecraft may not launch.\n"); + "Some versions of Minecraft may not launch.\n" + "\n" + "You may solve this issue by remounting /tmp as 'exec' or setting " + "the java.io.tmpdir JVM argument to a writeable directory in a " + "filesystem where the 'exec' flag is set (e.g., /home/user/.local/tmp)\n"); auto msgBox = new QMessageBox(QMessageBox::Information, tr("Incompatible system configuration"), infoMsg, QMessageBox::Ok); msgBox->setDefaultButton(QMessageBox::Ok); msgBox->setAttribute(Qt::WA_DeleteOnClose); @@ -1870,6 +1878,7 @@ QUrl Application::normalizeImportUrl(QString const& url) return QUrl::fromUserInput(url); } } + const QString Application::javaPath() { return m_settings->get("JavaDir").toString(); diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 975946740..bd6dfc5b6 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -439,6 +439,8 @@ set(JAVA_SOURCES java/download/ArchiveDownloadTask.h java/download/ManifestDownloadTask.cpp java/download/ManifestDownloadTask.h + java/download/SymlinkTask.cpp + java/download/SymlinkTask.h ui/java/InstallJavaDialog.h ui/java/InstallJavaDialog.cpp diff --git a/launcher/InstanceCopyTask.cpp b/launcher/InstanceCopyTask.cpp index ff2d37723..0220a4144 100644 --- a/launcher/InstanceCopyTask.cpp +++ b/launcher/InstanceCopyTask.cpp @@ -173,7 +173,11 @@ void InstanceCopyTask::copyFinished() allowed_symlinks_file .filePath()); // we dont want to modify the original. also make sure the resulting file is not itself a link. - FS::write(allowed_symlinks_file.filePath(), allowed_symlinks); + try { + FS::write(allowed_symlinks_file.filePath(), allowed_symlinks); + } catch (const FS::FileSystemException& e) { + qCritical() << "Failed to write symlink :" << e.cause(); + } } emitSucceeded(); diff --git a/launcher/LaunchController.cpp b/launcher/LaunchController.cpp index 73800574f..687da1322 100644 --- a/launcher/LaunchController.cpp +++ b/launcher/LaunchController.cpp @@ -53,6 +53,7 @@ #include #include #include +#include #include #include "BuildConfig.h" diff --git a/launcher/VersionProxyModel.cpp b/launcher/VersionProxyModel.cpp index 552900d35..12a82f73d 100644 --- a/launcher/VersionProxyModel.cpp +++ b/launcher/VersionProxyModel.cpp @@ -140,9 +140,9 @@ QVariant VersionProxyModel::headerData(int section, Qt::Orientation orientation, case Path: return tr("Filesystem path to this version"); case JavaName: - return tr("The alternative name of the java version"); + return tr("The alternative name of the Java version"); case JavaMajor: - return tr("The java major version"); + return tr("The Java major version"); case Time: return tr("Release date of this version"); } diff --git a/launcher/java/download/ArchiveDownloadTask.cpp b/launcher/java/download/ArchiveDownloadTask.cpp index ba1c96faf..bb7cc568d 100644 --- a/launcher/java/download/ArchiveDownloadTask.cpp +++ b/launcher/java/download/ArchiveDownloadTask.cpp @@ -65,7 +65,7 @@ void ArchiveDownloadTask::executeTask() void ArchiveDownloadTask::extractJava(QString input) { - setStatus(tr("Extracting java")); + setStatus(tr("Extracting Java")); if (input.endsWith("tar")) { setStatus(tr("Extracting Java (Progress is not reported for tar archives)")); QFile in(input); @@ -95,7 +95,7 @@ void ArchiveDownloadTask::extractJava(QString input) } auto files = zip->getFileNameList(); if (files.isEmpty()) { - emitFailed(tr("No files were found in the supplied zip file,")); + emitFailed(tr("No files were found in the supplied zip file.")); return; } m_task = makeShared(zip, m_final_path, files[0]); diff --git a/launcher/java/download/SymlinkTask.cpp b/launcher/java/download/SymlinkTask.cpp new file mode 100644 index 000000000..843c7caa9 --- /dev/null +++ b/launcher/java/download/SymlinkTask.cpp @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023-2024 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 "java/download/SymlinkTask.h" +#include + +#include "FileSystem.h" + +namespace Java { +SymlinkTask::SymlinkTask(QString final_path) : m_path(final_path) {} + +QString findBinPath(QString root, QString pattern) +{ + auto path = FS::PathCombine(root, pattern); + if (QFileInfo::exists(path)) { + return path; + } + + auto entries = QDir(root).entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot); + for (auto& entry : entries) { + path = FS::PathCombine(entry.absoluteFilePath(), pattern); + if (QFileInfo::exists(path)) { + return path; + } + } + + return {}; +} + +void SymlinkTask::executeTask() +{ + setStatus(tr("Checking for Java binary path")); + const auto binPath = FS::PathCombine("bin", "java"); + const auto wantedPath = FS::PathCombine(m_path, binPath); + if (QFileInfo::exists(wantedPath)) { + emitSucceeded(); + return; + } + + setStatus(tr("Searching for Java binary path")); + const auto contentsPartialPath = FS::PathCombine("Contents", "Home", binPath); + const auto relativePathToBin = findBinPath(m_path, contentsPartialPath); + if (relativePathToBin.isEmpty()) { + emitFailed(tr("Failed to find Java binary path")); + return; + } + const auto folderToLink = relativePathToBin.chopped(binPath.length()); + + setStatus(tr("Collecting folders to symlink")); + auto entries = QDir(folderToLink).entryInfoList(QDir::NoDotAndDotDot | QDir::AllEntries); + QList files; + setProgress(0, entries.length()); + for (auto& entry : entries) { + files.append({ entry.absoluteFilePath(), FS::PathCombine(m_path, entry.fileName()) }); + } + + setStatus(tr("Symlinking Java binary path")); + FS::create_link folderLink(files); + connect(&folderLink, &FS::create_link::fileLinked, [this](QString src, QString dst) { setProgress(m_progress + 1, m_progressTotal); }); + if (!folderLink()) { + emitFailed(folderLink.getOSError().message().c_str()); + } else { + emitSucceeded(); + } +} + +} // namespace Java \ No newline at end of file diff --git a/launcher/java/download/SymlinkTask.h b/launcher/java/download/SymlinkTask.h new file mode 100644 index 000000000..88cb20dd7 --- /dev/null +++ b/launcher/java/download/SymlinkTask.h @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2023-2024 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 "tasks/Task.h" +namespace Java { + +class SymlinkTask : public Task { + Q_OBJECT + public: + SymlinkTask(QString final_path); + virtual ~SymlinkTask() = default; + + void executeTask() override; + + protected: + QString m_path; + Task::Ptr m_task; +}; +} // namespace Java \ No newline at end of file diff --git a/launcher/minecraft/auth/steps/MSAStep.cpp b/launcher/minecraft/auth/steps/MSAStep.cpp index 3db04bf2f..74999414c 100644 --- a/launcher/minecraft/auth/steps/MSAStep.cpp +++ b/launcher/minecraft/auth/steps/MSAStep.cpp @@ -90,15 +90,16 @@ MSAStep::MSAStep(AccountData* data, bool silent) : AuthStep(data), m_silent(sile { auto replyHandler = new QOAuthHttpServerReplyHandler(this); - replyHandler->setCallbackText(R"XXX( + replyHandler->setCallbackText(QString(R"XXX( Login Successful, redirecting... - )XXX"); + )XXX") + .arg(BuildConfig.LOGIN_CALLBACK_URL)); oauth2.setReplyHandler(replyHandler); } else { oauth2.setReplyHandler(new CustomOAuthOobReplyHandler(this)); diff --git a/launcher/minecraft/launch/AutoInstallJava.cpp b/launcher/minecraft/launch/AutoInstallJava.cpp index 5daf89382..4fad6f15f 100644 --- a/launcher/minecraft/launch/AutoInstallJava.cpp +++ b/launcher/minecraft/launch/AutoInstallJava.cpp @@ -41,6 +41,7 @@ #include "Application.h" #include "FileSystem.h" #include "MessageLevel.h" +#include "QObjectPtr.h" #include "SysInfo.h" #include "java/JavaInstall.h" #include "java/JavaInstallList.h" @@ -48,10 +49,12 @@ #include "java/JavaVersion.h" #include "java/download/ArchiveDownloadTask.h" #include "java/download/ManifestDownloadTask.h" +#include "java/download/SymlinkTask.h" #include "meta/Index.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" #include "net/Mode.h" +#include "tasks/SequentialTask.h" AutoInstallJava::AutoInstallJava(LaunchTask* parent) : LaunchStep(parent) @@ -175,6 +178,12 @@ void AutoInstallJava::downloadJava(Meta::Version::Ptr version, QString javaName) emitFailed(tr("Could not determine Java download type!")); return; } +#if defined(Q_OS_MACOS) + auto seq = makeShared(this, tr("Install Java")); + seq->addTask(m_current_task); + seq->addTask(makeShared(final_path)); + m_current_task = seq; +#endif auto deletePath = [final_path] { FS::deletePath(final_path); }; connect(m_current_task.get(), &Task::failed, this, [this, deletePath](QString reason) { deletePath(); diff --git a/launcher/minecraft/mod/tasks/LocalModUpdateTask.h b/launcher/minecraft/mod/tasks/LocalModUpdateTask.h index 080999294..5447083ba 100644 --- a/launcher/minecraft/mod/tasks/LocalModUpdateTask.h +++ b/launcher/minecraft/mod/tasks/LocalModUpdateTask.h @@ -42,6 +42,6 @@ class LocalModUpdateTask : public Task { private: QDir m_index_dir; - ModPlatform::IndexedPack& m_mod; - ModPlatform::IndexedVersion& m_mod_version; + ModPlatform::IndexedPack m_mod; + ModPlatform::IndexedVersion m_mod_version; }; diff --git a/launcher/minecraft/skins/SkinList.cpp b/launcher/minecraft/skins/SkinList.cpp index fd883ad52..017cb8dc2 100644 --- a/launcher/minecraft/skins/SkinList.cpp +++ b/launcher/minecraft/skins/SkinList.cpp @@ -336,7 +336,11 @@ void SkinList::save() arr << s.toJSON(); } doc["skins"] = arr; - Json::write(doc, m_dir.absoluteFilePath("index.json")); + try { + Json::write(doc, m_dir.absoluteFilePath("index.json")); + } catch (const FS::FileSystemException& e) { + qCritical() << "Failed to write skin index file :" << e.cause(); + } } int SkinList::getSelectedAccountSkin() diff --git a/launcher/minecraft/skins/SkinModel.cpp b/launcher/minecraft/skins/SkinModel.cpp index d53b9e762..937864e2c 100644 --- a/launcher/minecraft/skins/SkinModel.cpp +++ b/launcher/minecraft/skins/SkinModel.cpp @@ -41,7 +41,7 @@ SkinModel::SkinModel(QDir skinDir, QJsonObject obj) QString SkinModel::name() const { - return QFileInfo(m_path).baseName(); + return QFileInfo(m_path).completeBaseName(); } bool SkinModel::rename(QString newName) diff --git a/launcher/modplatform/EnsureMetadataTask.cpp b/launcher/modplatform/EnsureMetadataTask.cpp index 43acea1a2..f6f49f38d 100644 --- a/launcher/modplatform/EnsureMetadataTask.cpp +++ b/launcher/modplatform/EnsureMetadataTask.cpp @@ -42,6 +42,9 @@ EnsureMetadataTask::EnsureMetadataTask(QList& mods, QDir dir, ModPlatform: m_hashing_task->addTask(hash_task); } } +EnsureMetadataTask::EnsureMetadataTask(QHash& mods, QDir dir, ModPlatform::ResourceProvider prov) + : Task(nullptr), m_mods(mods), m_index_dir(dir), m_provider(prov), m_current_task(nullptr) +{} Hashing::Hasher::Ptr EnsureMetadataTask::createNewHash(Mod* mod) { diff --git a/launcher/modplatform/EnsureMetadataTask.h b/launcher/modplatform/EnsureMetadataTask.h index 2f276e5a0..631b32ae7 100644 --- a/launcher/modplatform/EnsureMetadataTask.h +++ b/launcher/modplatform/EnsureMetadataTask.h @@ -1,14 +1,14 @@ #pragma once #include "ModIndex.h" -#include "net/NetJob.h" #include "modplatform/helpers/HashUtils.h" #include "tasks/ConcurrentTask.h" +#include + class Mod; -class QDir; class EnsureMetadataTask : public Task { Q_OBJECT @@ -16,6 +16,7 @@ class EnsureMetadataTask : public Task { public: EnsureMetadataTask(Mod*, QDir, ModPlatform::ResourceProvider = ModPlatform::ResourceProvider::MODRINTH); EnsureMetadataTask(QList&, QDir, ModPlatform::ResourceProvider = ModPlatform::ResourceProvider::MODRINTH); + EnsureMetadataTask(QHash&, QDir, ModPlatform::ResourceProvider = ModPlatform::ResourceProvider::MODRINTH); ~EnsureMetadataTask() = default; diff --git a/launcher/modplatform/flame/FileResolvingTask.cpp b/launcher/modplatform/flame/FileResolvingTask.cpp index 39b64f1c3..4c2f3d69e 100644 --- a/launcher/modplatform/flame/FileResolvingTask.cpp +++ b/launcher/modplatform/flame/FileResolvingTask.cpp @@ -1,87 +1,101 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2024 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 "FileResolvingTask.h" +#include #include "Json.h" +#include "QObjectPtr.h" #include "modplatform/ModIndex.h" -#include "net/ApiDownload.h" -#include "net/ApiUpload.h" -#include "net/Upload.h" +#include "modplatform/flame/FlameAPI.h" +#include "modplatform/flame/FlameModIndex.h" +#include "modplatform/modrinth/ModrinthAPI.h" #include "modplatform/modrinth/ModrinthPackIndex.h" +#include "net/NetJob.h" +#include "tasks/Task.h" + +static const FlameAPI flameAPI; +static ModrinthAPI modrinthAPI; Flame::FileResolvingTask::FileResolvingTask(const shared_qobject_ptr& network, Flame::Manifest& toProcess) - : m_network(network), m_toProcess(toProcess) + : m_network(network), m_manifest(toProcess) {} bool Flame::FileResolvingTask::abort() { bool aborted = true; - if (m_dljob) - aborted &= m_dljob->abort(); - if (m_checkJob) - aborted &= m_checkJob->abort(); + if (m_task) { + aborted = m_task->abort(); + } return aborted ? Task::abort() : false; } void Flame::FileResolvingTask::executeTask() { - if (m_toProcess.files.isEmpty()) { // no file to resolve so leave it empty and emit success immediately + if (m_manifest.files.isEmpty()) { // no file to resolve so leave it empty and emit success immediately emitSucceeded(); return; } setStatus(tr("Resolving mod IDs...")); setProgress(0, 3); - m_dljob.reset(new NetJob("Mod id resolver", m_network)); - result.reset(new QByteArray()); - // build json data to send - QJsonObject object; + m_result.reset(new QByteArray()); - object["fileIds"] = QJsonArray::fromVariantList( - std::accumulate(m_toProcess.files.begin(), m_toProcess.files.end(), QVariantList(), [](QVariantList& l, const File& s) { - l.push_back(s.fileId); - return l; - })); - QByteArray data = Json::toText(object); - auto dl = Net::ApiUpload::makeByteArray(QUrl("https://api.curseforge.com/v1/mods/files"), result, data); - m_dljob->addNetAction(dl); + QStringList fileIds; + for (auto file : m_manifest.files) { + fileIds.push_back(QString::number(file.fileId)); + } + m_task = flameAPI.getFiles(fileIds, m_result); auto step_progress = std::make_shared(); - connect(m_dljob.get(), &NetJob::finished, this, [this, step_progress]() { + connect(m_task.get(), &Task::finished, this, [this, step_progress]() { step_progress->state = TaskStepState::Succeeded; stepProgress(*step_progress); netJobFinished(); }); - connect(m_dljob.get(), &NetJob::failed, this, [this, step_progress](QString reason) { + connect(m_task.get(), &Task::failed, this, [this, step_progress](QString reason) { step_progress->state = TaskStepState::Failed; stepProgress(*step_progress); emitFailed(reason); }); - connect(m_dljob.get(), &NetJob::stepProgress, this, &FileResolvingTask::propagateStepProgress); - connect(m_dljob.get(), &NetJob::progress, this, [this, step_progress](qint64 current, qint64 total) { + connect(m_task.get(), &Task::stepProgress, this, &FileResolvingTask::propagateStepProgress); + connect(m_task.get(), &Task::progress, this, [this, step_progress](qint64 current, qint64 total) { qDebug() << "Resolve slug progress" << current << total; step_progress->update(current, total); stepProgress(*step_progress); }); - connect(m_dljob.get(), &NetJob::status, this, [this, step_progress](QString status) { + connect(m_task.get(), &Task::status, this, [this, step_progress](QString status) { step_progress->status = status; stepProgress(*step_progress); }); - m_dljob->start(); + m_task->start(); } void Flame::FileResolvingTask::netJobFinished() { setProgress(1, 3); // job to check modrinth for blocked projects - m_checkJob.reset(new NetJob("Modrinth check", m_network)); - m_checkJob->setAskRetry(false); - blockedProjects = QMap>(); - QJsonDocument doc; QJsonArray array; try { - doc = Json::requireDocument(*result); + doc = Json::requireDocument(*m_result); array = Json::requireArray(doc.object()["data"]); } catch (Json::JsonException& e) { qCritical() << "Non-JSON data returned from the CF API"; @@ -92,125 +106,157 @@ void Flame::FileResolvingTask::netJobFinished() return; } + QStringList hashes; for (QJsonValueRef file : array) { - auto fileid = Json::requireInteger(Json::requireObject(file)["id"]); - auto& out = m_toProcess.files[fileid]; try { - out.parseFromObject(Json::requireObject(file)); - } catch ([[maybe_unused]] const JSONValidationError& e) { - qDebug() << "Blocked mod on curseforge" << out.fileName; - auto hash = out.hash; - if (!hash.isEmpty()) { - auto url = QString("https://api.modrinth.com/v2/version_file/%1?algorithm=sha1").arg(hash); - auto output = std::make_shared(); - auto dl = Net::ApiDownload::makeByteArray(QUrl(url), output); - QObject::connect(dl.get(), &Task::succeeded, [&out]() { out.resolved = true; }); - - m_checkJob->addNetAction(dl); - blockedProjects.insert(&out, output); + auto obj = Json::requireObject(file); + auto version = FlameMod::loadIndexedPackVersion(obj); + auto fileid = version.fileId.toInt(); + m_manifest.files[fileid].version = version; + auto url = QUrl(version.downloadUrl, QUrl::TolerantMode); + if (!url.isValid() && "sha1" == version.hash_type && !version.hash.isEmpty()) { + hashes.push_back(version.hash); } + } catch (Json::JsonException& e) { + qCritical() << "Non-JSON data returned from the CF API"; + qCritical() << e.cause(); + + emitFailed(tr("Invalid data returned from the API.")); + + return; } } + if (hashes.isEmpty()) { + getFlameProjects(); + return; + } + m_result.reset(new QByteArray()); + m_task = modrinthAPI.currentVersions(hashes, "sha1", m_result); + (dynamic_cast(m_task.get()))->setAskRetry(false); auto step_progress = std::make_shared(); - connect(m_checkJob.get(), &NetJob::finished, this, [this, step_progress]() { + connect(m_task.get(), &Task::finished, this, [this, step_progress]() { step_progress->state = TaskStepState::Succeeded; stepProgress(*step_progress); - modrinthCheckFinished(); + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*m_result, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from Modrinth::CurrentVersions at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *m_result; + + failed(parse_error.errorString()); + return; + } + + try { + auto entries = Json::requireObject(doc); + for (auto& out : m_manifest.files) { + auto url = QUrl(out.version.downloadUrl, QUrl::TolerantMode); + if (!url.isValid() && "sha1" == out.version.hash_type && !out.version.hash.isEmpty()) { + try { + auto entry = Json::requireObject(entries, out.version.hash); + + auto file = Modrinth::loadIndexedPackVersion(entry); + + // If there's more than one mod loader for this version, we can't know for sure + // which file is relative to each loader, so it's best to not use any one and + // let the user download it manually. + if (!file.loaders || hasSingleModLoaderSelected(file.loaders)) { + out.version.downloadUrl = file.downloadUrl; + qDebug() << "Found alternative on modrinth " << out.version.fileName; + } + } catch (Json::JsonException& e) { + qDebug() << e.cause(); + qDebug() << entries; + } + } + } + } catch (Json::JsonException& e) { + qDebug() << e.cause(); + qDebug() << doc; + } + getFlameProjects(); }); - connect(m_checkJob.get(), &NetJob::failed, this, [this, step_progress](QString reason) { + connect(m_task.get(), &Task::failed, this, [this, step_progress](QString reason) { step_progress->state = TaskStepState::Failed; stepProgress(*step_progress); }); - connect(m_checkJob.get(), &NetJob::stepProgress, this, &FileResolvingTask::propagateStepProgress); - connect(m_checkJob.get(), &NetJob::progress, this, [this, step_progress](qint64 current, qint64 total) { + connect(m_task.get(), &Task::stepProgress, this, &FileResolvingTask::propagateStepProgress); + connect(m_task.get(), &Task::progress, this, [this, step_progress](qint64 current, qint64 total) { qDebug() << "Resolve slug progress" << current << total; step_progress->update(current, total); stepProgress(*step_progress); }); - connect(m_checkJob.get(), &NetJob::status, this, [this, step_progress](QString status) { + connect(m_task.get(), &Task::status, this, [this, step_progress](QString status) { step_progress->status = status; stepProgress(*step_progress); }); - m_checkJob->start(); + m_task->start(); } -void Flame::FileResolvingTask::modrinthCheckFinished() +void Flame::FileResolvingTask::getFlameProjects() { setProgress(2, 3); - qDebug() << "Finished with blocked mods : " << blockedProjects.size(); - - for (auto it = blockedProjects.keyBegin(); it != blockedProjects.keyEnd(); it++) { - auto& out = *it; - auto bytes = blockedProjects[out]; - if (!out->resolved) { - continue; - } - - QJsonDocument doc = QJsonDocument::fromJson(*bytes); - auto obj = doc.object(); - auto file = Modrinth::loadIndexedPackVersion(obj); - - // If there's more than one mod loader for this version, we can't know for sure - // which file is relative to each loader, so it's best to not use any one and - // let the user download it manually. - if (!file.loaders || hasSingleModLoaderSelected(file.loaders)) { - out->url = file.downloadUrl; - qDebug() << "Found alternative on modrinth " << out->fileName; - } else { - out->resolved = false; - } + m_result.reset(new QByteArray()); + QStringList addonIds; + for (auto file : m_manifest.files) { + addonIds.push_back(QString::number(file.projectId)); } - // copy to an output list and filter out projects found on modrinth - auto block = std::make_shared>(); - auto it = blockedProjects.keys(); - std::copy_if(it.begin(), it.end(), std::back_inserter(*block), [](File* f) { return !f->resolved; }); - // Display not found mods early - if (!block->empty()) { - // blocked mods found, we need the slug for displaying.... we need another job :D ! - m_slugJob.reset(new NetJob("Slug Job", m_network)); - int index = 0; - for (auto mod : *block) { - auto projectId = mod->projectId; - auto output = std::make_shared(); - auto url = QString("https://api.curseforge.com/v1/mods/%1").arg(projectId); - auto dl = Net::ApiDownload::makeByteArray(url, output); - qDebug() << "Fetching url slug for file:" << mod->fileName; - QObject::connect(dl.get(), &Task::succeeded, [block, index, output]() { - auto mod = block->at(index); // use the shared_ptr so it is captured and only freed when we are done - auto json = QJsonDocument::fromJson(*output); - auto base = - Json::requireString(Json::requireObject(Json::requireObject(Json::requireObject(json), "data"), "links"), "websiteUrl"); - auto link = QString("%1/download/%2").arg(base, QString::number(mod->fileId)); - mod->websiteUrl = link; - }); - m_slugJob->addNetAction(dl); - index++; - } - auto step_progress = std::make_shared(); - connect(m_slugJob.get(), &NetJob::succeeded, this, [this, step_progress]() { - step_progress->state = TaskStepState::Succeeded; - stepProgress(*step_progress); - emitSucceeded(); - }); - connect(m_slugJob.get(), &NetJob::failed, this, [this, step_progress](QString reason) { - step_progress->state = TaskStepState::Failed; - stepProgress(*step_progress); - emitFailed(reason); - }); - connect(m_slugJob.get(), &NetJob::stepProgress, this, &FileResolvingTask::propagateStepProgress); - connect(m_slugJob.get(), &NetJob::progress, this, [this, step_progress](qint64 current, qint64 total) { - qDebug() << "Resolve slug progress" << current << total; - step_progress->update(current, total); - stepProgress(*step_progress); - }); - connect(m_slugJob.get(), &NetJob::status, this, [this, step_progress](QString status) { - step_progress->status = status; - stepProgress(*step_progress); - }); - m_slugJob->start(); - } else { + m_task = flameAPI.getProjects(addonIds, m_result); + + auto step_progress = std::make_shared(); + connect(m_task.get(), &Task::succeeded, this, [this, step_progress] { + QJsonParseError parse_error{}; + auto doc = QJsonDocument::fromJson(*m_result, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from Modrinth projects task at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qWarning() << *m_result; + return; + } + + try { + QJsonArray entries; + entries = Json::requireArray(Json::requireObject(doc), "data"); + + for (auto entry : entries) { + auto entry_obj = Json::requireObject(entry); + auto id = Json::requireInteger(entry_obj, "id"); + auto file = std::find_if(m_manifest.files.begin(), m_manifest.files.end(), + [id](const Flame::File& file) { return file.projectId == id; }); + if (file == m_manifest.files.end()) { + continue; + } + + setStatus(tr("Parsing API response from CurseForge for '%1'...").arg(file->version.fileName)); + FlameMod::loadIndexedPack(file->pack, entry_obj); + } + } catch (Json::JsonException& e) { + qDebug() << e.cause(); + qDebug() << doc; + } + step_progress->state = TaskStepState::Succeeded; + stepProgress(*step_progress); emitSucceeded(); - } + }); + + connect(m_task.get(), &Task::failed, this, [this, step_progress](QString reason) { + step_progress->state = TaskStepState::Failed; + stepProgress(*step_progress); + emitFailed(reason); + }); + connect(m_task.get(), &Task::stepProgress, this, &FileResolvingTask::propagateStepProgress); + connect(m_task.get(), &Task::progress, this, [this, step_progress](qint64 current, qint64 total) { + qDebug() << "Resolve slug progress" << current << total; + step_progress->update(current, total); + stepProgress(*step_progress); + }); + connect(m_task.get(), &Task::status, this, [this, step_progress](QString status) { + step_progress->status = status; + stepProgress(*step_progress); + }); + + m_task->start(); } diff --git a/launcher/modplatform/flame/FileResolvingTask.h b/launcher/modplatform/flame/FileResolvingTask.h index cfa53cb22..edd9fce9a 100644 --- a/launcher/modplatform/flame/FileResolvingTask.h +++ b/launcher/modplatform/flame/FileResolvingTask.h @@ -1,7 +1,25 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2024 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 "PackManifest.h" -#include "net/NetJob.h" #include "tasks/Task.h" namespace Flame { @@ -9,12 +27,12 @@ class FileResolvingTask : public Task { Q_OBJECT public: explicit FileResolvingTask(const shared_qobject_ptr& network, Flame::Manifest& toProcess); - virtual ~FileResolvingTask() {}; + virtual ~FileResolvingTask() = default; bool canAbort() const override { return true; } bool abort() override; - const Flame::Manifest& getResults() const { return m_toProcess; } + const Flame::Manifest& getResults() const { return m_manifest; } protected: virtual void executeTask() override; @@ -22,16 +40,13 @@ class FileResolvingTask : public Task { protected slots: void netJobFinished(); + private: + void getFlameProjects(); + private: /* data */ shared_qobject_ptr m_network; - Flame::Manifest m_toProcess; - std::shared_ptr result; - NetJob::Ptr m_dljob; - NetJob::Ptr m_checkJob; - NetJob::Ptr m_slugJob; - - void modrinthCheckFinished(); - - QMap> blockedProjects; + Flame::Manifest m_manifest; + std::shared_ptr m_result; + Task::Ptr m_task; }; } // namespace Flame diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp index a629cc15b..b8c40ee42 100644 --- a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp +++ b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp @@ -35,8 +35,11 @@ #include "FlameInstanceCreationTask.h" +#include "QObjectPtr.h" +#include "minecraft/mod/tasks/LocalModUpdateTask.h" #include "modplatform/flame/FileResolvingTask.h" #include "modplatform/flame/FlameAPI.h" +#include "modplatform/flame/FlameModIndex.h" #include "modplatform/flame/PackManifest.h" #include "Application.h" @@ -51,6 +54,7 @@ #include "settings/INISettingsObject.h" +#include "tasks/ConcurrentTask.h" #include "ui/dialogs/BlockedModsDialog.h" #include "ui/dialogs/CustomMessageBox.h" @@ -58,7 +62,6 @@ #include #include "meta/Index.h" -#include "meta/VersionList.h" #include "minecraft/World.h" #include "minecraft/mod/tasks/LocalResourceParse.h" #include "net/ApiDownload.h" @@ -208,8 +211,7 @@ bool FlameCreationTask::updateInstance() Flame::File file; // We don't care about blocked mods, we just need local data to delete the file - file.parseFromObject(entry_obj, false); - + file.version = FlameMod::loadIndexedPackVersion(entry_obj); auto id = Json::requireInteger(entry_obj, "id"); old_files.insert(id, file); } @@ -219,10 +221,10 @@ bool FlameCreationTask::updateInstance() // Delete the files for (auto& file : old_files) { - if (file.fileName.isEmpty() || file.targetFolder.isEmpty()) + if (file.version.fileName.isEmpty() || file.targetFolder.isEmpty()) continue; - QString relative_path(FS::PathCombine(file.targetFolder, file.fileName)); + QString relative_path(FS::PathCombine(file.targetFolder, file.version.fileName)); qDebug() << "Scheduling" << relative_path << "for removal"; m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(relative_path)); } @@ -471,15 +473,15 @@ void FlameCreationTask::idResolverSucceeded(QEventLoop& loop) QList blocked_mods; auto anyBlocked = false; for (const auto& result : results.files.values()) { - if (result.fileName.endsWith(".zip")) { - m_ZIP_resources.append(std::make_pair(result.fileName, result.targetFolder)); + if (result.version.fileName.endsWith(".zip")) { + m_ZIP_resources.append(std::make_pair(result.version.fileName, result.targetFolder)); } - if (!result.resolved || result.url.isEmpty()) { + if (result.version.downloadUrl.isEmpty()) { BlockedMod blocked_mod; - blocked_mod.name = result.fileName; - blocked_mod.websiteUrl = result.websiteUrl; - blocked_mod.hash = result.hash; + blocked_mod.name = result.version.fileName; + blocked_mod.websiteUrl = QString("%1/download/%2").arg(result.pack.websiteUrl, QString::number(result.fileId)); + blocked_mod.hash = result.version.hash; blocked_mod.matched = false; blocked_mod.localPath = ""; blocked_mod.targetFolder = result.targetFolder; @@ -521,7 +523,7 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop) QStringList optionalFiles; for (auto& result : results) { if (!result.required) { - optionalFiles << FS::PathCombine(result.targetFolder, result.fileName); + optionalFiles << FS::PathCombine(result.targetFolder, result.version.fileName); } } @@ -537,7 +539,7 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop) selectedOptionalMods = optionalModDialog.getResult(); } for (const auto& result : results) { - auto fileName = result.fileName; + auto fileName = result.version.fileName; fileName = FS::RemoveInvalidPathChars(fileName); auto relpath = FS::PathCombine(result.targetFolder, fileName); @@ -548,36 +550,16 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop) relpath = FS::PathCombine("minecraft", relpath); auto path = FS::PathCombine(m_stagingPath, relpath); - switch (result.type) { - case Flame::File::Type::Folder: { - logWarning(tr("This 'Folder' may need extracting: %1").arg(relpath)); - // fallthrough intentional, we treat these as plain old mods and dump them wherever. - } - /* fallthrough */ - case Flame::File::Type::SingleFile: - case Flame::File::Type::Mod: { - if (!result.url.isEmpty()) { - qDebug() << "Will download" << result.url << "to" << path; - auto dl = Net::ApiDownload::makeFile(result.url, path); - m_files_job->addNetAction(dl); - } - break; - } - case Flame::File::Type::Modpack: - logWarning(tr("Nesting modpacks in modpacks is not implemented, nothing was downloaded: %1").arg(relpath)); - break; - case Flame::File::Type::Cmod2: - case Flame::File::Type::Ctoc: - case Flame::File::Type::Unknown: - logWarning(tr("Unrecognized/unhandled PackageType for: %1").arg(relpath)); - break; + if (!result.version.downloadUrl.isEmpty()) { + qDebug() << "Will download" << result.version.downloadUrl << "to" << path; + auto dl = Net::ApiDownload::makeFile(result.version.downloadUrl, path); + m_files_job->addNetAction(dl); } } - m_mod_id_resolver.reset(); - connect(m_files_job.get(), &NetJob::succeeded, this, [&]() { + connect(m_files_job.get(), &NetJob::finished, this, [this, &loop]() { m_files_job.reset(); - validateZIPResources(); + validateZIPResources(loop); }); connect(m_files_job.get(), &NetJob::failed, [&](QString reason) { m_files_job.reset(); @@ -588,7 +570,6 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop) setProgress(current, total); }); connect(m_files_job.get(), &NetJob::stepProgress, this, &FlameCreationTask::propagateStepProgress); - connect(m_files_job.get(), &NetJob::finished, &loop, &QEventLoop::quit); setStatus(tr("Downloading mods...")); m_files_job->start(); @@ -626,9 +607,10 @@ void FlameCreationTask::copyBlockedMods(QList const& blocked_mods) setAbortable(true); } -void FlameCreationTask::validateZIPResources() +void FlameCreationTask::validateZIPResources(QEventLoop& loop) { qDebug() << "Validating whether resources stored as .zip are in the right place"; + QStringList zipMods; for (auto [fileName, targetFolder] : m_ZIP_resources) { qDebug() << "Checking" << fileName << "..."; auto localPath = FS::PathCombine(m_stagingPath, "minecraft", targetFolder, fileName); @@ -668,6 +650,7 @@ void FlameCreationTask::validateZIPResources() switch (type) { case PackedResourceType::Mod: validatePath(fileName, targetFolder, "mods"); + zipMods.push_back(fileName); break; case PackedResourceType::ResourcePack: validatePath(fileName, targetFolder, "resourcepacks"); @@ -693,4 +676,16 @@ void FlameCreationTask::validateZIPResources() break; } } + auto task = makeShared(this, "CreateModMetadata", APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt()); + auto results = m_mod_id_resolver->getResults().files; + auto folder = FS::PathCombine(m_stagingPath, "minecraft", "mods", ".index"); + for (auto file : results) { + if (file.targetFolder != "mods" || (file.version.fileName.endsWith(".zip") && !zipMods.contains(file.version.fileName))) { + continue; + } + task->addTask(makeShared(folder, file.pack, file.version)); + } + connect(task.get(), &Task::finished, &loop, &QEventLoop::quit); + m_process_update_file_info_job = task; + task->start(); } diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.h b/launcher/modplatform/flame/FlameInstanceCreationTask.h index 02ad48f2e..28ab176c2 100644 --- a/launcher/modplatform/flame/FlameInstanceCreationTask.h +++ b/launcher/modplatform/flame/FlameInstanceCreationTask.h @@ -74,7 +74,7 @@ class FlameCreationTask final : public InstanceCreationTask { void idResolverSucceeded(QEventLoop&); void setupDownloadJob(QEventLoop&); void copyBlockedMods(QList const& blocked_mods); - void validateZIPResources(); + void validateZIPResources(QEventLoop& loop); QString getVersionForLoader(QString uid, QString loaderType, QString version, QString mcVersion); private: diff --git a/launcher/modplatform/flame/PackManifest.cpp b/launcher/modplatform/flame/PackManifest.cpp index 40a523d31..e576a6a84 100644 --- a/launcher/modplatform/flame/PackManifest.cpp +++ b/launcher/modplatform/flame/PackManifest.cpp @@ -68,35 +68,3 @@ void Flame::loadManifest(Flame::Manifest& m, const QString& filepath) } loadManifestV1(m, obj); } - -bool Flame::File::parseFromObject(const QJsonObject& obj, bool throw_on_blocked) -{ - fileName = Json::requireString(obj, "fileName"); - // This is a piece of a Flame project JSON pulled out into the file metadata (here) for convenience - // It is also optional - type = File::Type::SingleFile; - - targetFolder = "mods"; - - // get the hash - hash = QString(); - auto hashes = Json::ensureArray(obj, "hashes"); - for (QJsonValueRef item : hashes) { - auto hobj = Json::requireObject(item); - auto algo = Json::requireInteger(hobj, "algo"); - auto value = Json::requireString(hobj, "value"); - if (algo == 1) { - hash = value; - } - } - - // may throw, if the project is blocked - QString rawUrl = Json::ensureString(obj, "downloadUrl"); - url = QUrl(rawUrl, QUrl::TolerantMode); - if (!url.isValid() && throw_on_blocked) { - throw JSONValidationError(QString("Invalid URL: %1").arg(rawUrl)); - } - - resolved = true; - return true; -} diff --git a/launcher/modplatform/flame/PackManifest.h b/launcher/modplatform/flame/PackManifest.h index 4417c2430..49a0b2d68 100644 --- a/launcher/modplatform/flame/PackManifest.h +++ b/launcher/modplatform/flame/PackManifest.h @@ -40,26 +40,20 @@ #include #include #include +#include "modplatform/ModIndex.h" namespace Flame { struct File { - // NOTE: throws JSONValidationError - bool parseFromObject(const QJsonObject& object, bool throw_on_blocked = true); - int projectId = 0; int fileId = 0; // NOTE: the opposite to 'optional' bool required = true; - QString hash; - // NOTE: only set on blocked files ! Empty otherwise. - QString websiteUrl; + + ModPlatform::IndexedPack pack; + ModPlatform::IndexedVersion version; // our - bool resolved = false; - QString fileName; - QUrl url; QString targetFolder = QStringLiteral("mods"); - enum class Type { Unknown, Folder, Ctoc, SingleFile, Cmod2, Modpack, Mod } type = Type::Mod; }; struct Modloader { diff --git a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp index 517864790..ba97c441f 100644 --- a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp +++ b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.cpp @@ -5,8 +5,12 @@ #include "InstanceList.h" #include "Json.h" +#include "QObjectPtr.h" +#include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" +#include "minecraft/mod/Mod.h" +#include "modplatform/EnsureMetadataTask.h" #include "modplatform/helpers/OverrideUtils.h" #include "modplatform/modrinth/ModrinthPackManifest.h" @@ -21,6 +25,7 @@ #include #include +#include #include bool ModrinthCreationTask::abort() @@ -29,8 +34,8 @@ bool ModrinthCreationTask::abort() return false; m_abort = true; - if (m_files_job) - m_files_job->abort(); + if (m_task) + m_task->abort(); return Task::abort(); } @@ -234,11 +239,11 @@ bool ModrinthCreationTask::createInstance() instance.setName(name()); instance.saveNow(); - m_files_job.reset(new NetJob(tr("Mod Download Modrinth"), APPLICATION->network())); + auto downloadMods = makeShared(tr("Mod Download Modrinth"), APPLICATION->network()); auto root_modpack_path = FS::PathCombine(m_stagingPath, m_root_path); auto root_modpack_url = QUrl::fromLocalFile(root_modpack_path); - + QHash mods; for (auto file : m_files) { auto fileName = file.path; fileName = FS::RemoveInvalidPathChars(fileName); @@ -249,20 +254,27 @@ bool ModrinthCreationTask::createInstance() .arg(fileName)); return false; } + if (fileName.startsWith("mods/")) { + auto mod = new Mod(file_path); + ModDetails d; + d.mod_id = file_path; + mod->setDetails(d); + mods[file.hash.toHex()] = mod; + } qDebug() << "Will try to download" << file.downloads.front() << "to" << file_path; auto dl = Net::ApiDownload::makeFile(file.downloads.dequeue(), file_path); dl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash)); - m_files_job->addNetAction(dl); + downloadMods->addNetAction(dl); if (!file.downloads.empty()) { // FIXME: This really needs to be put into a ConcurrentTask of // MultipleOptionsTask's , once those exist :) auto param = dl.toWeakRef(); - connect(dl.get(), &Task::failed, [this, &file, file_path, param] { + connect(dl.get(), &Task::failed, [&file, file_path, param, downloadMods] { auto ndl = Net::ApiDownload::makeFile(file.downloads.dequeue(), file_path); ndl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash)); - m_files_job->addNetAction(ndl); + downloadMods->addNetAction(ndl); if (auto shared = param.lock()) shared->succeeded(); }); @@ -271,23 +283,44 @@ bool ModrinthCreationTask::createInstance() bool ended_well = false; - connect(m_files_job.get(), &NetJob::succeeded, this, [&]() { ended_well = true; }); - connect(m_files_job.get(), &NetJob::failed, [&](const QString& reason) { + connect(downloadMods.get(), &NetJob::succeeded, this, [&]() { ended_well = true; }); + connect(downloadMods.get(), &NetJob::failed, [&](const QString& reason) { ended_well = false; setError(reason); }); - connect(m_files_job.get(), &NetJob::finished, &loop, &QEventLoop::quit); - connect(m_files_job.get(), &NetJob::progress, [&](qint64 current, qint64 total) { + connect(downloadMods.get(), &NetJob::finished, &loop, &QEventLoop::quit); + connect(downloadMods.get(), &NetJob::progress, [&](qint64 current, qint64 total) { setDetails(tr("%1 out of %2 complete").arg(current).arg(total)); setProgress(current, total); }); - connect(m_files_job.get(), &NetJob::stepProgress, this, &ModrinthCreationTask::propagateStepProgress); + connect(downloadMods.get(), &NetJob::stepProgress, this, &ModrinthCreationTask::propagateStepProgress); setStatus(tr("Downloading mods...")); - m_files_job->start(); + downloadMods->start(); + m_task = downloadMods; loop.exec(); + QEventLoop ensureMetaLoop; + QDir folder = FS::PathCombine(instance.modsRoot(), ".index"); + auto ensureMetadataTask = makeShared(mods, folder, ModPlatform::ResourceProvider::MODRINTH); + connect(ensureMetadataTask.get(), &Task::succeeded, this, [&]() { ended_well = true; }); + connect(ensureMetadataTask.get(), &Task::finished, &ensureMetaLoop, &QEventLoop::quit); + connect(ensureMetadataTask.get(), &Task::progress, [&](qint64 current, qint64 total) { + setDetails(tr("%1 out of %2 complete").arg(current).arg(total)); + setProgress(current, total); + }); + connect(ensureMetadataTask.get(), &Task::stepProgress, this, &ModrinthCreationTask::propagateStepProgress); + + ensureMetadataTask->start(); + m_task = ensureMetadataTask; + + ensureMetaLoop.exec(); + for (auto m : mods) { + delete m; + } + mods.clear(); + // Update information of the already installed instance, if any. if (m_instance && ended_well) { setAbortable(false); diff --git a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h index f07734a58..ddfa7ae95 100644 --- a/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h +++ b/launcher/modplatform/modrinth/ModrinthInstanceCreationTask.h @@ -1,15 +1,11 @@ #pragma once +#include +#include "BaseInstance.h" #include "InstanceCreationTask.h" -#include - -#include "minecraft/MinecraftInstance.h" - #include "modplatform/modrinth/ModrinthPackManifest.h" -#include "net/NetJob.h" - class ModrinthCreationTask final : public InstanceCreationTask { Q_OBJECT @@ -43,7 +39,7 @@ class ModrinthCreationTask final : public InstanceCreationTask { QString m_managed_id, m_managed_version_id, m_managed_name; std::vector m_files; - NetJob::Ptr m_files_job; + Task::Ptr m_task; std::optional m_instance; diff --git a/launcher/modplatform/packwiz/Packwiz.cpp b/launcher/modplatform/packwiz/Packwiz.cpp index 77a0935f3..325b0a6e4 100644 --- a/launcher/modplatform/packwiz/Packwiz.cpp +++ b/launcher/modplatform/packwiz/Packwiz.cpp @@ -208,9 +208,9 @@ void V1::updateModIndex(QDir& index_dir, Mod& mod) auto tbl = toml::table{ { "name", mod.name.toStdString() }, { "filename", mod.filename.toStdString() }, { "side", sideToString(mod.side).toStdString() }, - { "loaders", loaders }, - { "mcVersions", mcVersions }, - { "releaseType", mod.releaseType.toString().toStdString() }, + { "x-prismlauncher-loaders", loaders }, + { "x-prismlauncher-mc-versions", mcVersions }, + { "x-prismlauncher-release-type", mod.releaseType.toString().toStdString() }, { "download", toml::table{ { "mode", mod.mode.toStdString() }, @@ -295,15 +295,15 @@ auto V1::getIndexForMod(QDir& index_dir, QString slug) -> Mod mod.name = stringEntry(table, "name"); mod.filename = stringEntry(table, "filename"); mod.side = stringToSide(stringEntry(table, "side")); - mod.releaseType = ModPlatform::IndexedVersionType(stringEntry(table, "releaseType")); - if (auto loaders = table["loaders"]; loaders && loaders.is_array()) { + mod.releaseType = ModPlatform::IndexedVersionType(stringEntry(table, "x-prismlauncher-release-type")); + if (auto loaders = table["x-prismlauncher-loaders"]; loaders && loaders.is_array()) { for (auto&& loader : *loaders.as_array()) { if (loader.is_string()) { mod.loaders |= ModPlatform::getModLoaderFromString(QString::fromStdString(loader.as_string()->value_or(""))); } } } - if (auto versions = table["mcVersions"]; versions && versions.is_array()) { + if (auto versions = table["x-prismlauncher-mc-versions"]; versions && versions.is_array()) { for (auto&& version : *versions.as_array()) { if (version.is_string()) { auto ver = QString::fromStdString(version.as_string()->value_or("")); diff --git a/launcher/translations/TranslationsModel.cpp b/launcher/translations/TranslationsModel.cpp index 63867636d..d03469b78 100644 --- a/launcher/translations/TranslationsModel.cpp +++ b/launcher/translations/TranslationsModel.cpp @@ -550,7 +550,7 @@ void TranslationsModel::downloadIndex() d->m_index_job.reset(new NetJob("Translations Index", APPLICATION->network())); MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("translations", "index_v2.json"); entry->setStale(true); - auto task = Net::Download::makeCached(QUrl(BuildConfig.TRANSLATIONS_BASE_URL + "index_v2.json"), entry); + auto task = Net::Download::makeCached(QUrl(BuildConfig.TRANSLATION_FILES_URL + "index_v2.json"), entry); d->m_index_task = task.get(); d->m_index_job->addNetAction(task); d->m_index_job->setAskRetry(false); @@ -591,7 +591,7 @@ void TranslationsModel::downloadTranslation(QString key) MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("translations", "mmc_" + key + ".qm"); entry->setStale(true); - auto dl = Net::Download::makeCached(QUrl(BuildConfig.TRANSLATIONS_BASE_URL + lang->file_name), entry); + auto dl = Net::Download::makeCached(QUrl(BuildConfig.TRANSLATION_FILES_URL + lang->file_name), entry); dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, lang->file_sha1)); dl->setProgress(dl->getProgress(), lang->file_size); diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index a5ccbc19a..09c47b609 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -233,6 +233,8 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi if (qgetenv("XDG_CURRENT_DESKTOP") == "gamescope") { ui->mainToolBar->addAction(ui->actionCloseWindow); } + + ui->actionViewJavaFolder->setEnabled(BuildConfig.JAVA_DOWNLOADER_ENABLED); } // add the toolbar toggles to the view menu @@ -1223,6 +1225,11 @@ void MainWindow::on_actionViewLogsFolder_triggered() DesktopServices::openPath("logs", true); } +void MainWindow::on_actionViewJavaFolder_triggered() +{ + DesktopServices::openPath(APPLICATION->javaPath(), true); +} + void MainWindow::refreshInstances() { APPLICATION->instances()->loadList(); diff --git a/launcher/ui/MainWindow.h b/launcher/ui/MainWindow.h index 41bef9980..0e692eda7 100644 --- a/launcher/ui/MainWindow.h +++ b/launcher/ui/MainWindow.h @@ -48,7 +48,6 @@ #include "BaseInstance.h" #include "minecraft/auth/MinecraftAccount.h" -#include "net/NetJob.h" class LaunchController; class NewsChecker; @@ -119,6 +118,7 @@ class MainWindow : public QMainWindow { void on_actionViewCatPackFolder_triggered(); void on_actionViewIconsFolder_triggered(); void on_actionViewLogsFolder_triggered(); + void on_actionViewJavaFolder_triggered(); void on_actionViewSkinsFolder_triggered(); diff --git a/launcher/ui/MainWindow.ui b/launcher/ui/MainWindow.ui index bad8762ad..f20c34206 100644 --- a/launcher/ui/MainWindow.ui +++ b/launcher/ui/MainWindow.ui @@ -131,7 +131,7 @@ 0 0 800 - 22 + 27 @@ -192,6 +192,7 @@ + @@ -788,6 +789,18 @@ Open the cat packs folder in a file browser. + + + + .. + + + Java + + + Open the Java folder in a file browser. Only available if the built-in Java downloader is used. + + diff --git a/launcher/ui/dialogs/ExportToModListDialog.cpp b/launcher/ui/dialogs/ExportToModListDialog.cpp index 1e0ae87a3..908743ab0 100644 --- a/launcher/ui/dialogs/ExportToModListDialog.cpp +++ b/launcher/ui/dialogs/ExportToModListDialog.cpp @@ -164,7 +164,12 @@ void ExportToModListDialog::done(int result) if (output.isEmpty()) return; - FS::write(output, ui->finalText->toPlainText().toUtf8()); + + try { + FS::write(output, ui->finalText->toPlainText().toUtf8()); + } catch (const FS::FileSystemException& e) { + qCritical() << "Failed to save mod list file :" << e.cause(); + } } QDialog::done(result); diff --git a/launcher/ui/dialogs/skins/SkinManageDialog.cpp b/launcher/ui/dialogs/skins/SkinManageDialog.cpp index a947af632..6c85ffa96 100644 --- a/launcher/ui/dialogs/skins/SkinManageDialog.cpp +++ b/launcher/ui/dialogs/skins/SkinManageDialog.cpp @@ -116,7 +116,7 @@ void SkinManageDialog::selectionChanged(QItemSelection selected, QItemSelection return; m_selected_skin = key; auto skin = m_list.skin(key); - if (!skin) + if (!skin || !skin->isValid()) return; ui->selectedModel->setPixmap(skin->getTexture().scaled(size() * (1. / 3), Qt::KeepAspectRatio, Qt::FastTransformation)); ui->capeCombo->setCurrentIndex(m_capes_idx.value(skin->getCapeId())); @@ -212,7 +212,10 @@ void SkinManageDialog::setupCapes() void SkinManageDialog::on_capeCombo_currentIndexChanged(int index) { auto id = ui->capeCombo->currentData(); - ui->capeImage->setPixmap(m_capes.value(id.toString(), {}).scaled(size() * (1. / 3), Qt::KeepAspectRatio, Qt::FastTransformation)); + auto cape = m_capes.value(id.toString(), {}); + if (!cape.isNull()) { + ui->capeImage->setPixmap(cape.scaled(size() * (1. / 3), Qt::KeepAspectRatio, Qt::FastTransformation)); + } if (auto skin = m_list.skin(m_selected_skin); skin) { skin->setCapeId(id.toString()); } @@ -505,8 +508,13 @@ void SkinManageDialog::resizeEvent(QResizeEvent* event) QSize s = size() * (1. / 3); if (auto skin = m_list.skin(m_selected_skin); skin) { - ui->selectedModel->setPixmap(skin->getTexture().scaled(s, Qt::KeepAspectRatio, Qt::FastTransformation)); + if (skin->isValid()) { + ui->selectedModel->setPixmap(skin->getTexture().scaled(s, Qt::KeepAspectRatio, Qt::FastTransformation)); + } } auto id = ui->capeCombo->currentData(); - ui->capeImage->setPixmap(m_capes.value(id.toString(), {}).scaled(s, Qt::KeepAspectRatio, Qt::FastTransformation)); + auto cape = m_capes.value(id.toString(), {}); + if (!cape.isNull()) { + ui->capeImage->setPixmap(cape.scaled(s, Qt::KeepAspectRatio, Qt::FastTransformation)); + } } diff --git a/launcher/ui/java/InstallJavaDialog.cpp b/launcher/ui/java/InstallJavaDialog.cpp index 4fb9fc2d2..0ece3220b 100644 --- a/launcher/ui/java/InstallJavaDialog.cpp +++ b/launcher/ui/java/InstallJavaDialog.cpp @@ -31,10 +31,12 @@ #include "Filter.h" #include "java/download/ArchiveDownloadTask.h" #include "java/download/ManifestDownloadTask.h" +#include "java/download/SymlinkTask.h" #include "meta/Index.h" #include "meta/VersionList.h" #include "minecraft/MinecraftInstance.h" #include "minecraft/PackProfile.h" +#include "tasks/SequentialTask.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/ProgressDialog.h" #include "ui/java/VersionList.h" @@ -55,13 +57,13 @@ class InstallJavaPage : public QWidget, public BasePage { majorVersionSelect = new VersionSelectWidget(this); majorVersionSelect->selectCurrent(); - majorVersionSelect->setEmptyString(tr("No java versions are currently available in the meta.")); - majorVersionSelect->setEmptyErrorString(tr("Couldn't load or download the java version lists!")); + majorVersionSelect->setEmptyString(tr("No Java versions are currently available in the meta.")); + majorVersionSelect->setEmptyErrorString(tr("Couldn't load or download the Java version lists!")); horizontalLayout->addWidget(majorVersionSelect, 1); javaVersionSelect = new VersionSelectWidget(this); - javaVersionSelect->setEmptyString(tr("No java versions are currently available for your OS.")); - javaVersionSelect->setEmptyErrorString(tr("Couldn't load or download the java version lists!")); + javaVersionSelect->setEmptyString(tr("No Java versions are currently available for your OS.")); + javaVersionSelect->setEmptyErrorString(tr("Couldn't load or download the Java version lists!")); horizontalLayout->addWidget(javaVersionSelect, 4); connect(majorVersionSelect, &VersionSelectWidget::selectedVersionChanged, this, &InstallJavaPage::setSelectedVersion); connect(majorVersionSelect, &VersionSelectWidget::selectedVersionChanged, this, &InstallJavaPage::selectionChanged); @@ -313,6 +315,12 @@ void InstallDialog::done(int result) CustomMessageBox::selectable(this, tr("Error"), error, QMessageBox::Warning)->show(); deletePath(); } +#if defined(Q_OS_MACOS) + auto seq = makeShared(this, tr("Install Java")); + seq->addTask(task); + seq->addTask(makeShared(final_path)); + task = seq; +#endif connect(task.get(), &Task::failed, this, [this, &deletePath](QString reason) { QString error = QString("Java download failed: %1").arg(reason); CustomMessageBox::selectable(this, tr("Error"), error, QMessageBox::Warning)->show(); diff --git a/launcher/ui/pages/global/APIPage.cpp b/launcher/ui/pages/global/APIPage.cpp index 82aa76a4f..a137c4cde 100644 --- a/launcher/ui/pages/global/APIPage.cpp +++ b/launcher/ui/pages/global/APIPage.cpp @@ -143,6 +143,7 @@ void APIPage::loadSettings() ui->modrinthToken->setText(modrinthToken); QString customUserAgent = s->get("UserAgentOverride").toString(); ui->userAgentLineEdit->setText(customUserAgent); + ui->technicClientID->setText(s->get("TechnicClientID").toString()); } void APIPage::applySettings() @@ -172,6 +173,7 @@ void APIPage::applySettings() QString modrinthToken = ui->modrinthToken->text(); s->set("ModrinthToken", modrinthToken); s->set("UserAgentOverride", ui->userAgentLineEdit->text()); + s->set("TechnicClientID", ui->technicClientID->text()); } bool APIPage::apply() diff --git a/launcher/ui/pages/global/APIPage.ui b/launcher/ui/pages/global/APIPage.ui index a7f3f3f72..9c713aa79 100644 --- a/launcher/ui/pages/global/APIPage.ui +++ b/launcher/ui/pages/global/APIPage.ui @@ -6,8 +6,8 @@ 0 0 - 800 - 600 + 841 + 620 @@ -288,6 +288,36 @@ + + + + Technic Client ID + + + + + + <html><head/><body><p>Note: you only need to set this to access private data.</p></body></html> + + + + + + + (None) + + + + + + + Enter a custom GUID client ID for Technic here. + + + + + + diff --git a/launcher/ui/pages/global/JavaPage.cpp b/launcher/ui/pages/global/JavaPage.cpp index 6699b00c0..0ae296815 100644 --- a/launcher/ui/pages/global/JavaPage.cpp +++ b/launcher/ui/pages/global/JavaPage.cpp @@ -67,8 +67,8 @@ JavaPage::JavaPage(QWidget* parent) : QWidget(parent), ui(new Ui::JavaPage) ui->managedJavaList->initialize(new JavaInstallList(this, true)); ui->managedJavaList->setResizeOn(2); ui->managedJavaList->selectCurrent(); - ui->managedJavaList->setEmptyString(tr("No managed java versions are installed")); - ui->managedJavaList->setEmptyErrorString(tr("Couldn't load the managed java list!")); + ui->managedJavaList->setEmptyString(tr("No managed Java versions are installed")); + ui->managedJavaList->setEmptyErrorString(tr("Couldn't load the managed Java list!")); connect(ui->autodetectJavaCheckBox, &QCheckBox::stateChanged, this, [this] { ui->autodownloadCheckBox->setEnabled(ui->autodetectJavaCheckBox->isChecked()); if (!ui->autodetectJavaCheckBox->isChecked()) diff --git a/launcher/ui/pages/instance/LogPage.cpp b/launcher/ui/pages/instance/LogPage.cpp index 0c22d1de6..6a4888f9c 100644 --- a/launcher/ui/pages/instance/LogPage.cpp +++ b/launcher/ui/pages/instance/LogPage.cpp @@ -234,7 +234,7 @@ bool LogPage::apply() bool LogPage::shouldDisplay() const { - return m_instance->isRunning() || m_proxy->rowCount() > 0; + return true; } void LogPage::on_btnPaste_clicked() diff --git a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp index 4181edab6..f7e7f4433 100644 --- a/launcher/ui/pages/modplatform/technic/TechnicModel.cpp +++ b/launcher/ui/pages/modplatform/technic/TechnicModel.cpp @@ -154,6 +154,10 @@ void Technic::ListModel::performSearch() QString("%1search?build=%2&q=%3").arg(BuildConfig.TECHNIC_API_BASE_URL, BuildConfig.TECHNIC_API_BUILD, currentSearchTerm); searchMode = List; } + auto clientId = APPLICATION->settings()->get("TechnicClientID").toString(); + if (!clientId.isEmpty()) { + searchUrl += "?cid=" + clientId; + } netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(searchUrl), response)); jobPtr = netJob; jobPtr->start(); diff --git a/launcher/ui/setupwizard/AutoJavaWizardPage.ui b/launcher/ui/setupwizard/AutoJavaWizardPage.ui index bd72cf695..a862524b0 100644 --- a/launcher/ui/setupwizard/AutoJavaWizardPage.ui +++ b/launcher/ui/setupwizard/AutoJavaWizardPage.ui @@ -30,7 +30,7 @@ - We've added a feature to automatically download the correct Java version for each version of Minecraft(this can be changed in the Java Settings). Would you like to enable or disable this feature? + We've added a feature to automatically download the correct Java version for each version of Minecraft (this can be changed in the Java Settings). Would you like to enable or disable this feature? true diff --git a/launcher/ui/setupwizard/JavaWizardPage.cpp b/launcher/ui/setupwizard/JavaWizardPage.cpp index 47718d6da..8caae173c 100644 --- a/launcher/ui/setupwizard/JavaWizardPage.cpp +++ b/launcher/ui/setupwizard/JavaWizardPage.cpp @@ -83,6 +83,6 @@ void JavaWizardPage::retranslate() { setTitle(tr("Java")); setSubTitle( - tr("Please select how much memory to allocate to instances and if Prism Launcher should manage java automatically or manually.")); + tr("Please select how much memory to allocate to instances and if Prism Launcher should manage Java automatically or manually.")); m_java_widget->retranslate(); } diff --git a/launcher/ui/setupwizard/LoginWizardPage.cpp b/launcher/ui/setupwizard/LoginWizardPage.cpp index 6be24a2f7..f53e31908 100644 --- a/launcher/ui/setupwizard/LoginWizardPage.cpp +++ b/launcher/ui/setupwizard/LoginWizardPage.cpp @@ -35,11 +35,10 @@ void LoginWizardPage::on_pushButton_clicked() if (account) { APPLICATION->accounts()->addAccount(account); APPLICATION->accounts()->setDefaultAccount(account); - } - - if (wizard()->currentId() == wizard()->pageIds().last()) { - wizard()->accept(); - } else { - wizard()->next(); + if (wizard()->currentId() == wizard()->pageIds().last()) { + wizard()->accept(); + } else { + wizard()->next(); + } } } diff --git a/launcher/ui/themes/ThemeManager.h b/launcher/ui/themes/ThemeManager.h index c7e32bc8c..a9036107c 100644 --- a/launcher/ui/themes/ThemeManager.h +++ b/launcher/ui/themes/ThemeManager.h @@ -18,10 +18,12 @@ */ #pragma once +#include +#include #include +#include #include "IconTheme.h" -#include "ui/MainWindow.h" #include "ui/themes/CatPack.h" #include "ui/themes/ITheme.h" diff --git a/launcher/updater/prismupdater/PrismUpdater.cpp b/launcher/updater/prismupdater/PrismUpdater.cpp index 6b9754864..8bf8cb473 100644 --- a/launcher/updater/prismupdater/PrismUpdater.cpp +++ b/launcher/updater/prismupdater/PrismUpdater.cpp @@ -1210,7 +1210,7 @@ std::optional PrismUpdaterApp::unpackArchive(QFileInfo archive) QProcess proc = QProcess(); proc.start(cmd, args); if (!proc.waitForStarted(5000)) { // wait 5 seconds to start - auto msg = tr("Failed to launcher child process \"%1 %2\".").arg(cmd).arg(args.join(" ")); + auto msg = tr("Failed to launch child process \"%1 %2\".").arg(cmd).arg(args.join(" ")); logUpdate(msg); showFatalErrorMessage(tr("Failed extract archive"), msg); return std::nullopt; @@ -1241,7 +1241,7 @@ bool PrismUpdaterApp::loadPrismVersionFromExe(const QString& exe_path) proc.setReadChannel(QProcess::StandardOutput); proc.start(exe_path, { "--version" }); if (!proc.waitForStarted(5000)) { - showFatalErrorMessage(tr("Failed to Check Version"), tr("Failed to launcher child launcher process to read version.")); + showFatalErrorMessage(tr("Failed to Check Version"), tr("Failed to launch child process to read version.")); return false; } // wait 5 seconds to start if (!proc.waitForFinished(5000)) { diff --git a/nix/README.md b/nix/README.md index 76cb8bf27..8bb658477 100644 --- a/nix/README.md +++ b/nix/README.md @@ -8,8 +8,8 @@ See [Package variants](#package-variants) for a list of available packages. ## Installing a development release (flake) -We use [garnix](https://garnix.io/) to build and cache our development builds. -If you want to avoid rebuilds you may add the garnix cache to your substitutors, or use `--accept-flake-config` +We use [cachix](https://cachix.org/) to cache our development and release builds. +If you want to avoid rebuilds you may add the Cachix bucket to your substitutors, or use `--accept-flake-config` to temporarily enable it when using `nix` commands. Example (NixOS): @@ -17,12 +17,10 @@ Example (NixOS): ```nix { nix.settings = { - trusted-substituters = [ - "https://cache.garnix.io" - ]; + trusted-substituters = [ "https://prismlauncher.cachix.org" ]; trusted-public-keys = [ - "cache.garnix.io:CTFPyKSLcx5RMJKfLo5EEPUObbA78b0YQ2DTCJXqr9g=" + "prismlauncher.cachix.org-1:9/n/FGyABA2jLUVfY+DEp4hKds/rwO+SCOtbOkDzd+c=" ]; }; } @@ -137,20 +135,18 @@ nix profile install github:PrismLauncher/PrismLauncher ## Installing a development release (without flakes) -We use [garnix](https://garnix.io/) to build and cache our development builds. -If you want to avoid rebuilds you may add the garnix cache to your substitutors. +We use [Cachix](https://cachix.org/) to cache our development and release builds. +If you want to avoid rebuilds you may add the Cachix bucket to your substitutors. Example (NixOS): ```nix { nix.settings = { - trusted-substituters = [ - "https://cache.garnix.io" - ]; + trusted-substituters = [ "https://prismlauncher.cachix.org" ]; trusted-public-keys = [ - "cache.garnix.io:CTFPyKSLcx5RMJKfLo5EEPUObbA78b0YQ2DTCJXqr9g=" + "prismlauncher.cachix.org-1:9/n/FGyABA2jLUVfY+DEp4hKds/rwO+SCOtbOkDzd+c=" ]; }; } diff --git a/nix/checks.nix b/nix/checks.nix index 40a2e272f..ec219d6f8 100644 --- a/nix/checks.nix +++ b/nix/checks.nix @@ -23,7 +23,7 @@ cd ${self} echo "Running clang-format...." - clang-format -i --style='file' --Werror */**.{c,cc,cpp,h,hh,hpp} + clang-format --dry-run --style='file' --Werror */**.{c,cc,cpp,h,hh,hpp} echo "Running deadnix..." deadnix --fail