Merge branch 'develop' of https://github.com/PrismLauncher/PrismLauncher into fix_login

This commit is contained in:
Trial97 2024-10-01 08:49:03 +03:00
commit b676a67b3c
No known key found for this signature in database
GPG Key ID: 55EF5DA53DB36318
52 changed files with 673 additions and 321 deletions

View File

@ -2,3 +2,6 @@
# tabs -> spaces # tabs -> spaces
bbb3b3e6f6e3c0f95873f22e6d0a4aaf350f49d9 bbb3b3e6f6e3c0f95873f22e6d0a4aaf350f49d9
# (nix) alejandra -> nixfmt
4c81d8c53d09196426568c4a31a4e752ed05397a

View File

@ -39,6 +39,9 @@ on:
APPLE_NOTARIZE_PASSWORD: APPLE_NOTARIZE_PASSWORD:
description: Password used for notarizing macOS builds description: Password used for notarizing macOS builds
required: false required: false
CACHIX_AUTH_TOKEN:
description: Private token for authenticating against Cachix cache
required: false
GPG_PRIVATE_KEY: GPG_PRIVATE_KEY:
description: Private key for AppImage signing description: Private key for AppImage signing
required: false required: false
@ -631,3 +634,53 @@ jobs:
with: with:
bundle: "Prism Launcher.flatpak" bundle: "Prism Launcher.flatpak"
manifest-path: flatpak/org.prismlauncher.PrismLauncher.yml 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

View File

@ -38,5 +38,6 @@ jobs:
APPLE_NOTARIZE_APPLE_ID: ${{ secrets.APPLE_NOTARIZE_APPLE_ID }} APPLE_NOTARIZE_APPLE_ID: ${{ secrets.APPLE_NOTARIZE_APPLE_ID }}
APPLE_NOTARIZE_TEAM_ID: ${{ secrets.APPLE_NOTARIZE_TEAM_ID }} APPLE_NOTARIZE_TEAM_ID: ${{ secrets.APPLE_NOTARIZE_TEAM_ID }}
APPLE_NOTARIZE_PASSWORD: ${{ secrets.APPLE_NOTARIZE_PASSWORD }} APPLE_NOTARIZE_PASSWORD: ${{ secrets.APPLE_NOTARIZE_PASSWORD }}
CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }}
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
GPG_PRIVATE_KEY_ID: ${{ secrets.GPG_PRIVATE_KEY_ID }} GPG_PRIVATE_KEY_ID: ${{ secrets.GPG_PRIVATE_KEY_ID }}

View File

@ -22,6 +22,7 @@ jobs:
APPLE_NOTARIZE_APPLE_ID: ${{ secrets.APPLE_NOTARIZE_APPLE_ID }} APPLE_NOTARIZE_APPLE_ID: ${{ secrets.APPLE_NOTARIZE_APPLE_ID }}
APPLE_NOTARIZE_TEAM_ID: ${{ secrets.APPLE_NOTARIZE_TEAM_ID }} APPLE_NOTARIZE_TEAM_ID: ${{ secrets.APPLE_NOTARIZE_TEAM_ID }}
APPLE_NOTARIZE_PASSWORD: ${{ secrets.APPLE_NOTARIZE_PASSWORD }} APPLE_NOTARIZE_PASSWORD: ${{ secrets.APPLE_NOTARIZE_PASSWORD }}
CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }}
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
GPG_PRIVATE_KEY_ID: ${{ secrets.GPG_PRIVATE_KEY_ID }} GPG_PRIVATE_KEY_ID: ${{ secrets.GPG_PRIVATE_KEY_ID }}

View File

@ -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_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_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_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 version numbers ########
set(Launcher_VERSION_MAJOR 9) set(Launcher_VERSION_MAJOR 9)
@ -205,6 +207,7 @@ set(Launcher_BUG_TRACKER_URL "https://github.com/PrismLauncher/PrismLauncher/iss
# Translations Platform URL # Translations Platform URL
set(Launcher_TRANSLATIONS_URL "https://hosted.weblate.org/projects/prismlauncher/launcher/" CACHE STRING "URL for the translations platform.") 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 # Matrix Space
set(Launcher_MATRIX_URL "https://prismlauncher.org/matrix" CACHE STRING "URL to the Matrix Space") set(Launcher_MATRIX_URL "https://prismlauncher.org/matrix" CACHE STRING "URL to the Matrix Space")

View File

@ -116,16 +116,19 @@ Config::Config()
NEWS_RSS_URL = "@Launcher_NEWS_RSS_URL@"; NEWS_RSS_URL = "@Launcher_NEWS_RSS_URL@";
NEWS_OPEN_URL = "@Launcher_NEWS_OPEN_URL@"; NEWS_OPEN_URL = "@Launcher_NEWS_OPEN_URL@";
HELP_URL = "@Launcher_HELP_URL@"; HELP_URL = "@Launcher_HELP_URL@";
LOGIN_CALLBACK_URL = "@Launcher_LOGIN_CALLBACK_URL@";
IMGUR_CLIENT_ID = "@Launcher_IMGUR_CLIENT_ID@"; IMGUR_CLIENT_ID = "@Launcher_IMGUR_CLIENT_ID@";
MSA_CLIENT_ID = "@Launcher_MSA_CLIENT_ID@"; MSA_CLIENT_ID = "@Launcher_MSA_CLIENT_ID@";
FLAME_API_KEY = "@Launcher_CURSEFORGE_API_KEY@"; FLAME_API_KEY = "@Launcher_CURSEFORGE_API_KEY@";
META_URL = "@Launcher_META_URL@"; META_URL = "@Launcher_META_URL@";
FMLLIBS_BASE_URL = "@Launcher_FMLLIBS_BASE_URL@";
GLFW_LIBRARY_NAME = "@Launcher_GLFW_LIBRARY_NAME@"; GLFW_LIBRARY_NAME = "@Launcher_GLFW_LIBRARY_NAME@";
OPENAL_LIBRARY_NAME = "@Launcher_OPENAL_LIBRARY_NAME@"; OPENAL_LIBRARY_NAME = "@Launcher_OPENAL_LIBRARY_NAME@";
BUG_TRACKER_URL = "@Launcher_BUG_TRACKER_URL@"; BUG_TRACKER_URL = "@Launcher_BUG_TRACKER_URL@";
TRANSLATIONS_URL = "@Launcher_TRANSLATIONS_URL@"; TRANSLATIONS_URL = "@Launcher_TRANSLATIONS_URL@";
TRANSLATION_FILES_URL = "@Launcher_TRANSLATION_FILES_URL@";
MATRIX_URL = "@Launcher_MATRIX_URL@"; MATRIX_URL = "@Launcher_MATRIX_URL@";
DISCORD_URL = "@Launcher_DISCORD_URL@"; DISCORD_URL = "@Launcher_DISCORD_URL@";
SUBREDDIT_URL = "@Launcher_SUBREDDIT_URL@"; SUBREDDIT_URL = "@Launcher_SUBREDDIT_URL@";

View File

@ -133,6 +133,11 @@ class Config {
*/ */
QString HELP_URL; 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 * 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 RESOURCE_BASE = "https://resources.download.minecraft.net/";
QString LIBRARY_BASE = "https://libraries.minecraft.net/"; QString LIBRARY_BASE = "https://libraries.minecraft.net/";
QString IMGUR_BASE_URL = "https://api.imgur.com/3/"; QString IMGUR_BASE_URL = "https://api.imgur.com/3/";
QString FMLLIBS_BASE_URL = "https://files.prismlauncher.org/fmllibs/"; // FIXME: move into CMakeLists QString FMLLIBS_BASE_URL;
QString TRANSLATIONS_BASE_URL = "https://i18n.prismlauncher.org/"; // FIXME: move into CMakeLists QString TRANSLATION_FILES_URL;
QString MODPACKSCH_API_BASE_URL = "https://api.modpacks.ch/"; QString MODPACKSCH_API_BASE_URL = "https://api.modpacks.ch/";

View File

@ -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)"; description = "A custom launcher for Minecraft that allows you to easily manage multiple installations of Minecraft at once (Fork of MultiMC)";
nixConfig = { nixConfig = {
extra-substituters = [ "https://cache.garnix.io" ]; extra-substituters = [ "https://prismlauncher.cachix.org" ];
extra-trusted-public-keys = [ "cache.garnix.io:CTFPyKSLcx5RMJKfLo5EEPUObbA78b0YQ2DTCJXqr9g=" ]; extra-trusted-public-keys = [
"prismlauncher.cachix.org-1:9/n/FGyABA2jLUVfY+DEp4hKds/rwO+SCOtbOkDzd+c="
];
}; };
inputs = { inputs = {
@ -118,5 +120,24 @@
# Only output them if they're available on the current system # Only output them if they're available on the current system
lib.filterAttrs (_: lib.meta.availableOn pkgs.stdenv.hostPlatform) packages 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;
};
}
);
}; };
} }

View File

@ -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.*"

View File

@ -780,6 +780,9 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
// FTBApp instances // FTBApp instances
m_settings->registerSetting("FTBAppInstancesPath", ""); m_settings->registerSetting("FTBAppInstancesPath", "");
// Custom Technic Client ID
m_settings->registerSetting("TechnicClientID", "");
// Init page provider // Init page provider
{ {
m_globalSettingsProvider = std::make_shared<GenericPageProvider>(tr("Settings")); m_globalSettingsProvider = std::make_shared<GenericPageProvider>(tr("Settings"));
@ -1022,7 +1025,8 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
} }
// notify user if /tmp is mounted with `noexec` (#1693) // 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; bool is_tmp_noexec = false;
#if defined(Q_OS_LINUX) #if defined(Q_OS_LINUX)
@ -1042,7 +1046,11 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
if (is_tmp_noexec) { if (is_tmp_noexec) {
auto infoMsg = auto infoMsg =
tr("Your /tmp directory is currently mounted with the 'noexec' flag enabled.\n" 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); auto msgBox = new QMessageBox(QMessageBox::Information, tr("Incompatible system configuration"), infoMsg, QMessageBox::Ok);
msgBox->setDefaultButton(QMessageBox::Ok); msgBox->setDefaultButton(QMessageBox::Ok);
msgBox->setAttribute(Qt::WA_DeleteOnClose); msgBox->setAttribute(Qt::WA_DeleteOnClose);
@ -1870,6 +1878,7 @@ QUrl Application::normalizeImportUrl(QString const& url)
return QUrl::fromUserInput(url); return QUrl::fromUserInput(url);
} }
} }
const QString Application::javaPath() const QString Application::javaPath()
{ {
return m_settings->get("JavaDir").toString(); return m_settings->get("JavaDir").toString();

View File

@ -439,6 +439,8 @@ set(JAVA_SOURCES
java/download/ArchiveDownloadTask.h java/download/ArchiveDownloadTask.h
java/download/ManifestDownloadTask.cpp java/download/ManifestDownloadTask.cpp
java/download/ManifestDownloadTask.h java/download/ManifestDownloadTask.h
java/download/SymlinkTask.cpp
java/download/SymlinkTask.h
ui/java/InstallJavaDialog.h ui/java/InstallJavaDialog.h
ui/java/InstallJavaDialog.cpp ui/java/InstallJavaDialog.cpp

View File

@ -173,7 +173,11 @@ void InstanceCopyTask::copyFinished()
allowed_symlinks_file allowed_symlinks_file
.filePath()); // we dont want to modify the original. also make sure the resulting file is not itself a link. .filePath()); // we dont want to modify the original. also make sure the resulting file is not itself a link.
try {
FS::write(allowed_symlinks_file.filePath(), allowed_symlinks); FS::write(allowed_symlinks_file.filePath(), allowed_symlinks);
} catch (const FS::FileSystemException& e) {
qCritical() << "Failed to write symlink :" << e.cause();
}
} }
emitSucceeded(); emitSucceeded();

View File

@ -53,6 +53,7 @@
#include <QLineEdit> #include <QLineEdit>
#include <QList> #include <QList>
#include <QPushButton> #include <QPushButton>
#include <QRegularExpression>
#include <QStringList> #include <QStringList>
#include "BuildConfig.h" #include "BuildConfig.h"

View File

@ -140,9 +140,9 @@ QVariant VersionProxyModel::headerData(int section, Qt::Orientation orientation,
case Path: case Path:
return tr("Filesystem path to this version"); return tr("Filesystem path to this version");
case JavaName: case JavaName:
return tr("The alternative name of the java version"); return tr("The alternative name of the Java version");
case JavaMajor: case JavaMajor:
return tr("The java major version"); return tr("The Java major version");
case Time: case Time:
return tr("Release date of this version"); return tr("Release date of this version");
} }

View File

@ -65,7 +65,7 @@ void ArchiveDownloadTask::executeTask()
void ArchiveDownloadTask::extractJava(QString input) void ArchiveDownloadTask::extractJava(QString input)
{ {
setStatus(tr("Extracting java")); setStatus(tr("Extracting Java"));
if (input.endsWith("tar")) { if (input.endsWith("tar")) {
setStatus(tr("Extracting Java (Progress is not reported for tar archives)")); setStatus(tr("Extracting Java (Progress is not reported for tar archives)"));
QFile in(input); QFile in(input);
@ -95,7 +95,7 @@ void ArchiveDownloadTask::extractJava(QString input)
} }
auto files = zip->getFileNameList(); auto files = zip->getFileNameList();
if (files.isEmpty()) { 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; return;
} }
m_task = makeShared<MMCZip::ExtractZipTask>(zip, m_final_path, files[0]); m_task = makeShared<MMCZip::ExtractZipTask>(zip, m_final_path, files[0]);

View File

@ -0,0 +1,81 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2023-2024 Trial97 <alexandru.tripon97@gmail.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 <https://www.gnu.org/licenses/>.
*/
#include "java/download/SymlinkTask.h"
#include <QFileInfo>
#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<FS::LinkPair> 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

View File

@ -0,0 +1,36 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2023-2024 Trial97 <alexandru.tripon97@gmail.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 <https://www.gnu.org/licenses/>.
*/
#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

View File

@ -90,15 +90,16 @@ MSAStep::MSAStep(AccountData* data, bool silent) : AuthStep(data), m_silent(sile
{ {
auto replyHandler = new QOAuthHttpServerReplyHandler(this); auto replyHandler = new QOAuthHttpServerReplyHandler(this);
replyHandler->setCallbackText(R"XXX( replyHandler->setCallbackText(QString(R"XXX(
<noscript> <noscript>
<meta http-equiv="Refresh" content="0; URL=https://prismlauncher.org/successful-login" /> <meta http-equiv="Refresh" content="0; URL=%1" />
</noscript> </noscript>
Login Successful, redirecting... Login Successful, redirecting...
<script> <script>
window.location.replace("https://prismlauncher.org/successful-login"); window.location.replace("%1");
</script> </script>
)XXX"); )XXX")
.arg(BuildConfig.LOGIN_CALLBACK_URL));
oauth2.setReplyHandler(replyHandler); oauth2.setReplyHandler(replyHandler);
} else { } else {
oauth2.setReplyHandler(new CustomOAuthOobReplyHandler(this)); oauth2.setReplyHandler(new CustomOAuthOobReplyHandler(this));

View File

@ -41,6 +41,7 @@
#include "Application.h" #include "Application.h"
#include "FileSystem.h" #include "FileSystem.h"
#include "MessageLevel.h" #include "MessageLevel.h"
#include "QObjectPtr.h"
#include "SysInfo.h" #include "SysInfo.h"
#include "java/JavaInstall.h" #include "java/JavaInstall.h"
#include "java/JavaInstallList.h" #include "java/JavaInstallList.h"
@ -48,10 +49,12 @@
#include "java/JavaVersion.h" #include "java/JavaVersion.h"
#include "java/download/ArchiveDownloadTask.h" #include "java/download/ArchiveDownloadTask.h"
#include "java/download/ManifestDownloadTask.h" #include "java/download/ManifestDownloadTask.h"
#include "java/download/SymlinkTask.h"
#include "meta/Index.h" #include "meta/Index.h"
#include "minecraft/MinecraftInstance.h" #include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h" #include "minecraft/PackProfile.h"
#include "net/Mode.h" #include "net/Mode.h"
#include "tasks/SequentialTask.h"
AutoInstallJava::AutoInstallJava(LaunchTask* parent) AutoInstallJava::AutoInstallJava(LaunchTask* parent)
: LaunchStep(parent) : LaunchStep(parent)
@ -175,6 +178,12 @@ void AutoInstallJava::downloadJava(Meta::Version::Ptr version, QString javaName)
emitFailed(tr("Could not determine Java download type!")); emitFailed(tr("Could not determine Java download type!"));
return; return;
} }
#if defined(Q_OS_MACOS)
auto seq = makeShared<SequentialTask>(this, tr("Install Java"));
seq->addTask(m_current_task);
seq->addTask(makeShared<Java::SymlinkTask>(final_path));
m_current_task = seq;
#endif
auto deletePath = [final_path] { FS::deletePath(final_path); }; auto deletePath = [final_path] { FS::deletePath(final_path); };
connect(m_current_task.get(), &Task::failed, this, [this, deletePath](QString reason) { connect(m_current_task.get(), &Task::failed, this, [this, deletePath](QString reason) {
deletePath(); deletePath();

View File

@ -42,6 +42,6 @@ class LocalModUpdateTask : public Task {
private: private:
QDir m_index_dir; QDir m_index_dir;
ModPlatform::IndexedPack& m_mod; ModPlatform::IndexedPack m_mod;
ModPlatform::IndexedVersion& m_mod_version; ModPlatform::IndexedVersion m_mod_version;
}; };

View File

@ -336,7 +336,11 @@ void SkinList::save()
arr << s.toJSON(); arr << s.toJSON();
} }
doc["skins"] = arr; doc["skins"] = arr;
try {
Json::write(doc, m_dir.absoluteFilePath("index.json")); 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() int SkinList::getSelectedAccountSkin()

View File

@ -41,7 +41,7 @@ SkinModel::SkinModel(QDir skinDir, QJsonObject obj)
QString SkinModel::name() const QString SkinModel::name() const
{ {
return QFileInfo(m_path).baseName(); return QFileInfo(m_path).completeBaseName();
} }
bool SkinModel::rename(QString newName) bool SkinModel::rename(QString newName)

View File

@ -42,6 +42,9 @@ EnsureMetadataTask::EnsureMetadataTask(QList<Mod*>& mods, QDir dir, ModPlatform:
m_hashing_task->addTask(hash_task); m_hashing_task->addTask(hash_task);
} }
} }
EnsureMetadataTask::EnsureMetadataTask(QHash<QString, Mod*>& 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) Hashing::Hasher::Ptr EnsureMetadataTask::createNewHash(Mod* mod)
{ {

View File

@ -1,14 +1,14 @@
#pragma once #pragma once
#include "ModIndex.h" #include "ModIndex.h"
#include "net/NetJob.h"
#include "modplatform/helpers/HashUtils.h" #include "modplatform/helpers/HashUtils.h"
#include "tasks/ConcurrentTask.h" #include "tasks/ConcurrentTask.h"
#include <QDir>
class Mod; class Mod;
class QDir;
class EnsureMetadataTask : public Task { class EnsureMetadataTask : public Task {
Q_OBJECT Q_OBJECT
@ -16,6 +16,7 @@ class EnsureMetadataTask : public Task {
public: public:
EnsureMetadataTask(Mod*, QDir, ModPlatform::ResourceProvider = ModPlatform::ResourceProvider::MODRINTH); EnsureMetadataTask(Mod*, QDir, ModPlatform::ResourceProvider = ModPlatform::ResourceProvider::MODRINTH);
EnsureMetadataTask(QList<Mod*>&, QDir, ModPlatform::ResourceProvider = ModPlatform::ResourceProvider::MODRINTH); EnsureMetadataTask(QList<Mod*>&, QDir, ModPlatform::ResourceProvider = ModPlatform::ResourceProvider::MODRINTH);
EnsureMetadataTask(QHash<QString, Mod*>&, QDir, ModPlatform::ResourceProvider = ModPlatform::ResourceProvider::MODRINTH);
~EnsureMetadataTask() = default; ~EnsureMetadataTask() = default;

View File

@ -1,87 +1,101 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2024 Trial97 <alexandru.tripon97@gmail.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 <https://www.gnu.org/licenses/>.
*/
#include "FileResolvingTask.h" #include "FileResolvingTask.h"
#include <algorithm>
#include "Json.h" #include "Json.h"
#include "QObjectPtr.h"
#include "modplatform/ModIndex.h" #include "modplatform/ModIndex.h"
#include "net/ApiDownload.h" #include "modplatform/flame/FlameAPI.h"
#include "net/ApiUpload.h" #include "modplatform/flame/FlameModIndex.h"
#include "net/Upload.h" #include "modplatform/modrinth/ModrinthAPI.h"
#include "modplatform/modrinth/ModrinthPackIndex.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<QNetworkAccessManager>& network, Flame::Manifest& toProcess) Flame::FileResolvingTask::FileResolvingTask(const shared_qobject_ptr<QNetworkAccessManager>& network, Flame::Manifest& toProcess)
: m_network(network), m_toProcess(toProcess) : m_network(network), m_manifest(toProcess)
{} {}
bool Flame::FileResolvingTask::abort() bool Flame::FileResolvingTask::abort()
{ {
bool aborted = true; bool aborted = true;
if (m_dljob) if (m_task) {
aborted &= m_dljob->abort(); aborted = m_task->abort();
if (m_checkJob) }
aborted &= m_checkJob->abort();
return aborted ? Task::abort() : false; return aborted ? Task::abort() : false;
} }
void Flame::FileResolvingTask::executeTask() 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(); emitSucceeded();
return; return;
} }
setStatus(tr("Resolving mod IDs...")); setStatus(tr("Resolving mod IDs..."));
setProgress(0, 3); setProgress(0, 3);
m_dljob.reset(new NetJob("Mod id resolver", m_network)); m_result.reset(new QByteArray());
result.reset(new QByteArray());
// build json data to send
QJsonObject object;
object["fileIds"] = QJsonArray::fromVariantList( QStringList fileIds;
std::accumulate(m_toProcess.files.begin(), m_toProcess.files.end(), QVariantList(), [](QVariantList& l, const File& s) { for (auto file : m_manifest.files) {
l.push_back(s.fileId); fileIds.push_back(QString::number(file.fileId));
return l; }
})); m_task = flameAPI.getFiles(fileIds, m_result);
QByteArray data = Json::toText(object);
auto dl = Net::ApiUpload::makeByteArray(QUrl("https://api.curseforge.com/v1/mods/files"), result, data);
m_dljob->addNetAction(dl);
auto step_progress = std::make_shared<TaskStepProgress>(); auto step_progress = std::make_shared<TaskStepProgress>();
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; step_progress->state = TaskStepState::Succeeded;
stepProgress(*step_progress); stepProgress(*step_progress);
netJobFinished(); 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; step_progress->state = TaskStepState::Failed;
stepProgress(*step_progress); stepProgress(*step_progress);
emitFailed(reason); emitFailed(reason);
}); });
connect(m_dljob.get(), &NetJob::stepProgress, this, &FileResolvingTask::propagateStepProgress); connect(m_task.get(), &Task::stepProgress, this, &FileResolvingTask::propagateStepProgress);
connect(m_dljob.get(), &NetJob::progress, this, [this, step_progress](qint64 current, qint64 total) { connect(m_task.get(), &Task::progress, this, [this, step_progress](qint64 current, qint64 total) {
qDebug() << "Resolve slug progress" << current << total; qDebug() << "Resolve slug progress" << current << total;
step_progress->update(current, total); step_progress->update(current, total);
stepProgress(*step_progress); 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; step_progress->status = status;
stepProgress(*step_progress); stepProgress(*step_progress);
}); });
m_dljob->start(); m_task->start();
} }
void Flame::FileResolvingTask::netJobFinished() void Flame::FileResolvingTask::netJobFinished()
{ {
setProgress(1, 3); setProgress(1, 3);
// job to check modrinth for blocked projects // job to check modrinth for blocked projects
m_checkJob.reset(new NetJob("Modrinth check", m_network));
m_checkJob->setAskRetry(false);
blockedProjects = QMap<File*, std::shared_ptr<QByteArray>>();
QJsonDocument doc; QJsonDocument doc;
QJsonArray array; QJsonArray array;
try { try {
doc = Json::requireDocument(*result); doc = Json::requireDocument(*m_result);
array = Json::requireArray(doc.object()["data"]); array = Json::requireArray(doc.object()["data"]);
} catch (Json::JsonException& e) { } catch (Json::JsonException& e) {
qCritical() << "Non-JSON data returned from the CF API"; qCritical() << "Non-JSON data returned from the CF API";
@ -92,125 +106,157 @@ void Flame::FileResolvingTask::netJobFinished()
return; return;
} }
QStringList hashes;
for (QJsonValueRef file : array) { for (QJsonValueRef file : array) {
auto fileid = Json::requireInteger(Json::requireObject(file)["id"]);
auto& out = m_toProcess.files[fileid];
try { try {
out.parseFromObject(Json::requireObject(file)); auto obj = Json::requireObject(file);
} catch ([[maybe_unused]] const JSONValidationError& e) { auto version = FlameMod::loadIndexedPackVersion(obj);
qDebug() << "Blocked mod on curseforge" << out.fileName; auto fileid = version.fileId.toInt();
auto hash = out.hash; m_manifest.files[fileid].version = version;
if (!hash.isEmpty()) { auto url = QUrl(version.downloadUrl, QUrl::TolerantMode);
auto url = QString("https://api.modrinth.com/v2/version_file/%1?algorithm=sha1").arg(hash); if (!url.isValid() && "sha1" == version.hash_type && !version.hash.isEmpty()) {
auto output = std::make_shared<QByteArray>(); hashes.push_back(version.hash);
auto dl = Net::ApiDownload::makeByteArray(QUrl(url), output); }
QObject::connect(dl.get(), &Task::succeeded, [&out]() { out.resolved = true; }); } catch (Json::JsonException& e) {
qCritical() << "Non-JSON data returned from the CF API";
qCritical() << e.cause();
m_checkJob->addNetAction(dl); emitFailed(tr("Invalid data returned from the API."));
blockedProjects.insert(&out, output);
return;
} }
} }
if (hashes.isEmpty()) {
getFlameProjects();
return;
} }
m_result.reset(new QByteArray());
m_task = modrinthAPI.currentVersions(hashes, "sha1", m_result);
(dynamic_cast<NetJob*>(m_task.get()))->setAskRetry(false);
auto step_progress = std::make_shared<TaskStepProgress>(); auto step_progress = std::make_shared<TaskStepProgress>();
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; step_progress->state = TaskStepState::Succeeded;
stepProgress(*step_progress); stepProgress(*step_progress);
modrinthCheckFinished(); QJsonParseError parse_error{};
}); QJsonDocument doc = QJsonDocument::fromJson(*m_result, &parse_error);
connect(m_checkJob.get(), &NetJob::failed, this, [this, step_progress](QString reason) { if (parse_error.error != QJsonParseError::NoError) {
step_progress->state = TaskStepState::Failed; qWarning() << "Error while parsing JSON response from Modrinth::CurrentVersions at " << parse_error.offset
stepProgress(*step_progress); << " reason: " << parse_error.errorString();
}); qWarning() << *m_result;
connect(m_checkJob.get(), &NetJob::stepProgress, this, &FileResolvingTask::propagateStepProgress);
connect(m_checkJob.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_checkJob.get(), &NetJob::status, this, [this, step_progress](QString status) {
step_progress->status = status;
stepProgress(*step_progress);
});
m_checkJob->start(); failed(parse_error.errorString());
} return;
void Flame::FileResolvingTask::modrinthCheckFinished()
{
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); try {
auto obj = doc.object(); auto entries = Json::requireObject(doc);
auto file = Modrinth::loadIndexedPackVersion(obj); 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 // 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 // which file is relative to each loader, so it's best to not use any one and
// let the user download it manually. // let the user download it manually.
if (!file.loaders || hasSingleModLoaderSelected(file.loaders)) { if (!file.loaders || hasSingleModLoaderSelected(file.loaders)) {
out->url = file.downloadUrl; out.version.downloadUrl = file.downloadUrl;
qDebug() << "Found alternative on modrinth " << out->fileName; qDebug() << "Found alternative on modrinth " << out.version.fileName;
} else { }
out->resolved = false; } catch (Json::JsonException& e) {
qDebug() << e.cause();
qDebug() << entries;
} }
} }
// copy to an output list and filter out projects found on modrinth }
auto block = std::make_shared<QList<File*>>(); } catch (Json::JsonException& e) {
auto it = blockedProjects.keys(); qDebug() << e.cause();
std::copy_if(it.begin(), it.end(), std::back_inserter(*block), [](File* f) { return !f->resolved; }); qDebug() << doc;
// Display not found mods early }
if (!block->empty()) { getFlameProjects();
// 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<QByteArray>();
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); connect(m_task.get(), &Task::failed, this, [this, step_progress](QString reason) {
index++;
}
auto step_progress = std::make_shared<TaskStepProgress>();
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; step_progress->state = TaskStepState::Failed;
stepProgress(*step_progress); stepProgress(*step_progress);
emitFailed(reason);
}); });
connect(m_slugJob.get(), &NetJob::stepProgress, this, &FileResolvingTask::propagateStepProgress); connect(m_task.get(), &Task::stepProgress, this, &FileResolvingTask::propagateStepProgress);
connect(m_slugJob.get(), &NetJob::progress, this, [this, step_progress](qint64 current, qint64 total) { connect(m_task.get(), &Task::progress, this, [this, step_progress](qint64 current, qint64 total) {
qDebug() << "Resolve slug progress" << current << total; qDebug() << "Resolve slug progress" << current << total;
step_progress->update(current, total); step_progress->update(current, total);
stepProgress(*step_progress); stepProgress(*step_progress);
}); });
connect(m_slugJob.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; step_progress->status = status;
stepProgress(*step_progress); stepProgress(*step_progress);
}); });
m_slugJob->start(); m_task->start();
} else { }
emitSucceeded();
} void Flame::FileResolvingTask::getFlameProjects()
{
setProgress(2, 3);
m_result.reset(new QByteArray());
QStringList addonIds;
for (auto file : m_manifest.files) {
addonIds.push_back(QString::number(file.projectId));
}
m_task = flameAPI.getProjects(addonIds, m_result);
auto step_progress = std::make_shared<TaskStepProgress>();
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();
} }

View File

@ -1,7 +1,25 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2024 Trial97 <alexandru.tripon97@gmail.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 <https://www.gnu.org/licenses/>.
*/
#pragma once #pragma once
#include <QNetworkAccessManager>
#include "PackManifest.h" #include "PackManifest.h"
#include "net/NetJob.h"
#include "tasks/Task.h" #include "tasks/Task.h"
namespace Flame { namespace Flame {
@ -9,12 +27,12 @@ class FileResolvingTask : public Task {
Q_OBJECT Q_OBJECT
public: public:
explicit FileResolvingTask(const shared_qobject_ptr<QNetworkAccessManager>& network, Flame::Manifest& toProcess); explicit FileResolvingTask(const shared_qobject_ptr<QNetworkAccessManager>& network, Flame::Manifest& toProcess);
virtual ~FileResolvingTask() {}; virtual ~FileResolvingTask() = default;
bool canAbort() const override { return true; } bool canAbort() const override { return true; }
bool abort() override; bool abort() override;
const Flame::Manifest& getResults() const { return m_toProcess; } const Flame::Manifest& getResults() const { return m_manifest; }
protected: protected:
virtual void executeTask() override; virtual void executeTask() override;
@ -22,16 +40,13 @@ class FileResolvingTask : public Task {
protected slots: protected slots:
void netJobFinished(); void netJobFinished();
private:
void getFlameProjects();
private: /* data */ private: /* data */
shared_qobject_ptr<QNetworkAccessManager> m_network; shared_qobject_ptr<QNetworkAccessManager> m_network;
Flame::Manifest m_toProcess; Flame::Manifest m_manifest;
std::shared_ptr<QByteArray> result; std::shared_ptr<QByteArray> m_result;
NetJob::Ptr m_dljob; Task::Ptr m_task;
NetJob::Ptr m_checkJob;
NetJob::Ptr m_slugJob;
void modrinthCheckFinished();
QMap<File*, std::shared_ptr<QByteArray>> blockedProjects;
}; };
} // namespace Flame } // namespace Flame

View File

@ -35,8 +35,11 @@
#include "FlameInstanceCreationTask.h" #include "FlameInstanceCreationTask.h"
#include "QObjectPtr.h"
#include "minecraft/mod/tasks/LocalModUpdateTask.h"
#include "modplatform/flame/FileResolvingTask.h" #include "modplatform/flame/FileResolvingTask.h"
#include "modplatform/flame/FlameAPI.h" #include "modplatform/flame/FlameAPI.h"
#include "modplatform/flame/FlameModIndex.h"
#include "modplatform/flame/PackManifest.h" #include "modplatform/flame/PackManifest.h"
#include "Application.h" #include "Application.h"
@ -51,6 +54,7 @@
#include "settings/INISettingsObject.h" #include "settings/INISettingsObject.h"
#include "tasks/ConcurrentTask.h"
#include "ui/dialogs/BlockedModsDialog.h" #include "ui/dialogs/BlockedModsDialog.h"
#include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/CustomMessageBox.h"
@ -58,7 +62,6 @@
#include <QFileInfo> #include <QFileInfo>
#include "meta/Index.h" #include "meta/Index.h"
#include "meta/VersionList.h"
#include "minecraft/World.h" #include "minecraft/World.h"
#include "minecraft/mod/tasks/LocalResourceParse.h" #include "minecraft/mod/tasks/LocalResourceParse.h"
#include "net/ApiDownload.h" #include "net/ApiDownload.h"
@ -208,8 +211,7 @@ bool FlameCreationTask::updateInstance()
Flame::File file; Flame::File file;
// We don't care about blocked mods, we just need local data to delete the 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"); auto id = Json::requireInteger(entry_obj, "id");
old_files.insert(id, file); old_files.insert(id, file);
} }
@ -219,10 +221,10 @@ bool FlameCreationTask::updateInstance()
// Delete the files // Delete the files
for (auto& file : old_files) { for (auto& file : old_files) {
if (file.fileName.isEmpty() || file.targetFolder.isEmpty()) if (file.version.fileName.isEmpty() || file.targetFolder.isEmpty())
continue; 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"; qDebug() << "Scheduling" << relative_path << "for removal";
m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(relative_path)); m_files_to_remove.append(old_minecraft_dir.absoluteFilePath(relative_path));
} }
@ -471,15 +473,15 @@ void FlameCreationTask::idResolverSucceeded(QEventLoop& loop)
QList<BlockedMod> blocked_mods; QList<BlockedMod> blocked_mods;
auto anyBlocked = false; auto anyBlocked = false;
for (const auto& result : results.files.values()) { for (const auto& result : results.files.values()) {
if (result.fileName.endsWith(".zip")) { if (result.version.fileName.endsWith(".zip")) {
m_ZIP_resources.append(std::make_pair(result.fileName, result.targetFolder)); 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; BlockedMod blocked_mod;
blocked_mod.name = result.fileName; blocked_mod.name = result.version.fileName;
blocked_mod.websiteUrl = result.websiteUrl; blocked_mod.websiteUrl = QString("%1/download/%2").arg(result.pack.websiteUrl, QString::number(result.fileId));
blocked_mod.hash = result.hash; blocked_mod.hash = result.version.hash;
blocked_mod.matched = false; blocked_mod.matched = false;
blocked_mod.localPath = ""; blocked_mod.localPath = "";
blocked_mod.targetFolder = result.targetFolder; blocked_mod.targetFolder = result.targetFolder;
@ -521,7 +523,7 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop)
QStringList optionalFiles; QStringList optionalFiles;
for (auto& result : results) { for (auto& result : results) {
if (!result.required) { 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(); selectedOptionalMods = optionalModDialog.getResult();
} }
for (const auto& result : results) { for (const auto& result : results) {
auto fileName = result.fileName; auto fileName = result.version.fileName;
fileName = FS::RemoveInvalidPathChars(fileName); fileName = FS::RemoveInvalidPathChars(fileName);
auto relpath = FS::PathCombine(result.targetFolder, fileName); auto relpath = FS::PathCombine(result.targetFolder, fileName);
@ -548,36 +550,16 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop)
relpath = FS::PathCombine("minecraft", relpath); relpath = FS::PathCombine("minecraft", relpath);
auto path = FS::PathCombine(m_stagingPath, relpath); auto path = FS::PathCombine(m_stagingPath, relpath);
switch (result.type) { if (!result.version.downloadUrl.isEmpty()) {
case Flame::File::Type::Folder: { qDebug() << "Will download" << result.version.downloadUrl << "to" << path;
logWarning(tr("This 'Folder' may need extracting: %1").arg(relpath)); auto dl = Net::ApiDownload::makeFile(result.version.downloadUrl, path);
// 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); 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;
}
} }
m_mod_id_resolver.reset(); connect(m_files_job.get(), &NetJob::finished, this, [this, &loop]() {
connect(m_files_job.get(), &NetJob::succeeded, this, [&]() {
m_files_job.reset(); m_files_job.reset();
validateZIPResources(); validateZIPResources(loop);
}); });
connect(m_files_job.get(), &NetJob::failed, [&](QString reason) { connect(m_files_job.get(), &NetJob::failed, [&](QString reason) {
m_files_job.reset(); m_files_job.reset();
@ -588,7 +570,6 @@ void FlameCreationTask::setupDownloadJob(QEventLoop& loop)
setProgress(current, total); setProgress(current, total);
}); });
connect(m_files_job.get(), &NetJob::stepProgress, this, &FlameCreationTask::propagateStepProgress); connect(m_files_job.get(), &NetJob::stepProgress, this, &FlameCreationTask::propagateStepProgress);
connect(m_files_job.get(), &NetJob::finished, &loop, &QEventLoop::quit);
setStatus(tr("Downloading mods...")); setStatus(tr("Downloading mods..."));
m_files_job->start(); m_files_job->start();
@ -626,9 +607,10 @@ void FlameCreationTask::copyBlockedMods(QList<BlockedMod> const& blocked_mods)
setAbortable(true); setAbortable(true);
} }
void FlameCreationTask::validateZIPResources() void FlameCreationTask::validateZIPResources(QEventLoop& loop)
{ {
qDebug() << "Validating whether resources stored as .zip are in the right place"; qDebug() << "Validating whether resources stored as .zip are in the right place";
QStringList zipMods;
for (auto [fileName, targetFolder] : m_ZIP_resources) { for (auto [fileName, targetFolder] : m_ZIP_resources) {
qDebug() << "Checking" << fileName << "..."; qDebug() << "Checking" << fileName << "...";
auto localPath = FS::PathCombine(m_stagingPath, "minecraft", targetFolder, fileName); auto localPath = FS::PathCombine(m_stagingPath, "minecraft", targetFolder, fileName);
@ -668,6 +650,7 @@ void FlameCreationTask::validateZIPResources()
switch (type) { switch (type) {
case PackedResourceType::Mod: case PackedResourceType::Mod:
validatePath(fileName, targetFolder, "mods"); validatePath(fileName, targetFolder, "mods");
zipMods.push_back(fileName);
break; break;
case PackedResourceType::ResourcePack: case PackedResourceType::ResourcePack:
validatePath(fileName, targetFolder, "resourcepacks"); validatePath(fileName, targetFolder, "resourcepacks");
@ -693,4 +676,16 @@ void FlameCreationTask::validateZIPResources()
break; break;
} }
} }
auto task = makeShared<ConcurrentTask>(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<LocalModUpdateTask>(folder, file.pack, file.version));
}
connect(task.get(), &Task::finished, &loop, &QEventLoop::quit);
m_process_update_file_info_job = task;
task->start();
} }

View File

@ -74,7 +74,7 @@ class FlameCreationTask final : public InstanceCreationTask {
void idResolverSucceeded(QEventLoop&); void idResolverSucceeded(QEventLoop&);
void setupDownloadJob(QEventLoop&); void setupDownloadJob(QEventLoop&);
void copyBlockedMods(QList<BlockedMod> const& blocked_mods); void copyBlockedMods(QList<BlockedMod> const& blocked_mods);
void validateZIPResources(); void validateZIPResources(QEventLoop& loop);
QString getVersionForLoader(QString uid, QString loaderType, QString version, QString mcVersion); QString getVersionForLoader(QString uid, QString loaderType, QString version, QString mcVersion);
private: private:

View File

@ -68,35 +68,3 @@ void Flame::loadManifest(Flame::Manifest& m, const QString& filepath)
} }
loadManifestV1(m, obj); 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;
}

View File

@ -40,26 +40,20 @@
#include <QString> #include <QString>
#include <QUrl> #include <QUrl>
#include <QVector> #include <QVector>
#include "modplatform/ModIndex.h"
namespace Flame { namespace Flame {
struct File { struct File {
// NOTE: throws JSONValidationError
bool parseFromObject(const QJsonObject& object, bool throw_on_blocked = true);
int projectId = 0; int projectId = 0;
int fileId = 0; int fileId = 0;
// NOTE: the opposite to 'optional' // NOTE: the opposite to 'optional'
bool required = true; bool required = true;
QString hash;
// NOTE: only set on blocked files ! Empty otherwise. ModPlatform::IndexedPack pack;
QString websiteUrl; ModPlatform::IndexedVersion version;
// our // our
bool resolved = false;
QString fileName;
QUrl url;
QString targetFolder = QStringLiteral("mods"); QString targetFolder = QStringLiteral("mods");
enum class Type { Unknown, Folder, Ctoc, SingleFile, Cmod2, Modpack, Mod } type = Type::Mod;
}; };
struct Modloader { struct Modloader {

View File

@ -5,8 +5,12 @@
#include "InstanceList.h" #include "InstanceList.h"
#include "Json.h" #include "Json.h"
#include "QObjectPtr.h"
#include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h" #include "minecraft/PackProfile.h"
#include "minecraft/mod/Mod.h"
#include "modplatform/EnsureMetadataTask.h"
#include "modplatform/helpers/OverrideUtils.h" #include "modplatform/helpers/OverrideUtils.h"
#include "modplatform/modrinth/ModrinthPackManifest.h" #include "modplatform/modrinth/ModrinthPackManifest.h"
@ -21,6 +25,7 @@
#include <QAbstractButton> #include <QAbstractButton>
#include <QFileInfo> #include <QFileInfo>
#include <QHash>
#include <vector> #include <vector>
bool ModrinthCreationTask::abort() bool ModrinthCreationTask::abort()
@ -29,8 +34,8 @@ bool ModrinthCreationTask::abort()
return false; return false;
m_abort = true; m_abort = true;
if (m_files_job) if (m_task)
m_files_job->abort(); m_task->abort();
return Task::abort(); return Task::abort();
} }
@ -234,11 +239,11 @@ bool ModrinthCreationTask::createInstance()
instance.setName(name()); instance.setName(name());
instance.saveNow(); instance.saveNow();
m_files_job.reset(new NetJob(tr("Mod Download Modrinth"), APPLICATION->network())); auto downloadMods = makeShared<NetJob>(tr("Mod Download Modrinth"), APPLICATION->network());
auto root_modpack_path = FS::PathCombine(m_stagingPath, m_root_path); auto root_modpack_path = FS::PathCombine(m_stagingPath, m_root_path);
auto root_modpack_url = QUrl::fromLocalFile(root_modpack_path); auto root_modpack_url = QUrl::fromLocalFile(root_modpack_path);
QHash<QString, Mod*> mods;
for (auto file : m_files) { for (auto file : m_files) {
auto fileName = file.path; auto fileName = file.path;
fileName = FS::RemoveInvalidPathChars(fileName); fileName = FS::RemoveInvalidPathChars(fileName);
@ -249,20 +254,27 @@ bool ModrinthCreationTask::createInstance()
.arg(fileName)); .arg(fileName));
return false; 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; qDebug() << "Will try to download" << file.downloads.front() << "to" << file_path;
auto dl = Net::ApiDownload::makeFile(file.downloads.dequeue(), file_path); auto dl = Net::ApiDownload::makeFile(file.downloads.dequeue(), file_path);
dl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash)); dl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash));
m_files_job->addNetAction(dl); downloadMods->addNetAction(dl);
if (!file.downloads.empty()) { if (!file.downloads.empty()) {
// FIXME: This really needs to be put into a ConcurrentTask of // FIXME: This really needs to be put into a ConcurrentTask of
// MultipleOptionsTask's , once those exist :) // MultipleOptionsTask's , once those exist :)
auto param = dl.toWeakRef(); 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); auto ndl = Net::ApiDownload::makeFile(file.downloads.dequeue(), file_path);
ndl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash)); ndl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash));
m_files_job->addNetAction(ndl); downloadMods->addNetAction(ndl);
if (auto shared = param.lock()) if (auto shared = param.lock())
shared->succeeded(); shared->succeeded();
}); });
@ -271,23 +283,44 @@ bool ModrinthCreationTask::createInstance()
bool ended_well = false; bool ended_well = false;
connect(m_files_job.get(), &NetJob::succeeded, this, [&]() { ended_well = true; }); connect(downloadMods.get(), &NetJob::succeeded, this, [&]() { ended_well = true; });
connect(m_files_job.get(), &NetJob::failed, [&](const QString& reason) { connect(downloadMods.get(), &NetJob::failed, [&](const QString& reason) {
ended_well = false; ended_well = false;
setError(reason); setError(reason);
}); });
connect(m_files_job.get(), &NetJob::finished, &loop, &QEventLoop::quit); connect(downloadMods.get(), &NetJob::finished, &loop, &QEventLoop::quit);
connect(m_files_job.get(), &NetJob::progress, [&](qint64 current, qint64 total) { connect(downloadMods.get(), &NetJob::progress, [&](qint64 current, qint64 total) {
setDetails(tr("%1 out of %2 complete").arg(current).arg(total)); setDetails(tr("%1 out of %2 complete").arg(current).arg(total));
setProgress(current, 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...")); setStatus(tr("Downloading mods..."));
m_files_job->start(); downloadMods->start();
m_task = downloadMods;
loop.exec(); loop.exec();
QEventLoop ensureMetaLoop;
QDir folder = FS::PathCombine(instance.modsRoot(), ".index");
auto ensureMetadataTask = makeShared<EnsureMetadataTask>(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. // Update information of the already installed instance, if any.
if (m_instance && ended_well) { if (m_instance && ended_well) {
setAbortable(false); setAbortable(false);

View File

@ -1,15 +1,11 @@
#pragma once #pragma once
#include <optional>
#include "BaseInstance.h"
#include "InstanceCreationTask.h" #include "InstanceCreationTask.h"
#include <optional>
#include "minecraft/MinecraftInstance.h"
#include "modplatform/modrinth/ModrinthPackManifest.h" #include "modplatform/modrinth/ModrinthPackManifest.h"
#include "net/NetJob.h"
class ModrinthCreationTask final : public InstanceCreationTask { class ModrinthCreationTask final : public InstanceCreationTask {
Q_OBJECT Q_OBJECT
@ -43,7 +39,7 @@ class ModrinthCreationTask final : public InstanceCreationTask {
QString m_managed_id, m_managed_version_id, m_managed_name; QString m_managed_id, m_managed_version_id, m_managed_name;
std::vector<Modrinth::File> m_files; std::vector<Modrinth::File> m_files;
NetJob::Ptr m_files_job; Task::Ptr m_task;
std::optional<InstancePtr> m_instance; std::optional<InstancePtr> m_instance;

View File

@ -208,9 +208,9 @@ void V1::updateModIndex(QDir& index_dir, Mod& mod)
auto tbl = toml::table{ { "name", mod.name.toStdString() }, auto tbl = toml::table{ { "name", mod.name.toStdString() },
{ "filename", mod.filename.toStdString() }, { "filename", mod.filename.toStdString() },
{ "side", sideToString(mod.side).toStdString() }, { "side", sideToString(mod.side).toStdString() },
{ "loaders", loaders }, { "x-prismlauncher-loaders", loaders },
{ "mcVersions", mcVersions }, { "x-prismlauncher-mc-versions", mcVersions },
{ "releaseType", mod.releaseType.toString().toStdString() }, { "x-prismlauncher-release-type", mod.releaseType.toString().toStdString() },
{ "download", { "download",
toml::table{ toml::table{
{ "mode", mod.mode.toStdString() }, { "mode", mod.mode.toStdString() },
@ -295,15 +295,15 @@ auto V1::getIndexForMod(QDir& index_dir, QString slug) -> Mod
mod.name = stringEntry(table, "name"); mod.name = stringEntry(table, "name");
mod.filename = stringEntry(table, "filename"); mod.filename = stringEntry(table, "filename");
mod.side = stringToSide(stringEntry(table, "side")); mod.side = stringToSide(stringEntry(table, "side"));
mod.releaseType = ModPlatform::IndexedVersionType(stringEntry(table, "releaseType")); mod.releaseType = ModPlatform::IndexedVersionType(stringEntry(table, "x-prismlauncher-release-type"));
if (auto loaders = table["loaders"]; loaders && loaders.is_array()) { if (auto loaders = table["x-prismlauncher-loaders"]; loaders && loaders.is_array()) {
for (auto&& loader : *loaders.as_array()) { for (auto&& loader : *loaders.as_array()) {
if (loader.is_string()) { if (loader.is_string()) {
mod.loaders |= ModPlatform::getModLoaderFromString(QString::fromStdString(loader.as_string()->value_or(""))); 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()) { for (auto&& version : *versions.as_array()) {
if (version.is_string()) { if (version.is_string()) {
auto ver = QString::fromStdString(version.as_string()->value_or("")); auto ver = QString::fromStdString(version.as_string()->value_or(""));

View File

@ -550,7 +550,7 @@ void TranslationsModel::downloadIndex()
d->m_index_job.reset(new NetJob("Translations Index", APPLICATION->network())); d->m_index_job.reset(new NetJob("Translations Index", APPLICATION->network()));
MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("translations", "index_v2.json"); MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("translations", "index_v2.json");
entry->setStale(true); 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_task = task.get();
d->m_index_job->addNetAction(task); d->m_index_job->addNetAction(task);
d->m_index_job->setAskRetry(false); d->m_index_job->setAskRetry(false);
@ -591,7 +591,7 @@ void TranslationsModel::downloadTranslation(QString key)
MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("translations", "mmc_" + key + ".qm"); MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("translations", "mmc_" + key + ".qm");
entry->setStale(true); 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->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, lang->file_sha1));
dl->setProgress(dl->getProgress(), lang->file_size); dl->setProgress(dl->getProgress(), lang->file_size);

View File

@ -233,6 +233,8 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi
if (qgetenv("XDG_CURRENT_DESKTOP") == "gamescope") { if (qgetenv("XDG_CURRENT_DESKTOP") == "gamescope") {
ui->mainToolBar->addAction(ui->actionCloseWindow); ui->mainToolBar->addAction(ui->actionCloseWindow);
} }
ui->actionViewJavaFolder->setEnabled(BuildConfig.JAVA_DOWNLOADER_ENABLED);
} }
// add the toolbar toggles to the view menu // add the toolbar toggles to the view menu
@ -1223,6 +1225,11 @@ void MainWindow::on_actionViewLogsFolder_triggered()
DesktopServices::openPath("logs", true); DesktopServices::openPath("logs", true);
} }
void MainWindow::on_actionViewJavaFolder_triggered()
{
DesktopServices::openPath(APPLICATION->javaPath(), true);
}
void MainWindow::refreshInstances() void MainWindow::refreshInstances()
{ {
APPLICATION->instances()->loadList(); APPLICATION->instances()->loadList();

View File

@ -48,7 +48,6 @@
#include "BaseInstance.h" #include "BaseInstance.h"
#include "minecraft/auth/MinecraftAccount.h" #include "minecraft/auth/MinecraftAccount.h"
#include "net/NetJob.h"
class LaunchController; class LaunchController;
class NewsChecker; class NewsChecker;
@ -119,6 +118,7 @@ class MainWindow : public QMainWindow {
void on_actionViewCatPackFolder_triggered(); void on_actionViewCatPackFolder_triggered();
void on_actionViewIconsFolder_triggered(); void on_actionViewIconsFolder_triggered();
void on_actionViewLogsFolder_triggered(); void on_actionViewLogsFolder_triggered();
void on_actionViewJavaFolder_triggered();
void on_actionViewSkinsFolder_triggered(); void on_actionViewSkinsFolder_triggered();

View File

@ -131,7 +131,7 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>800</width> <width>800</width>
<height>22</height> <height>27</height>
</rect> </rect>
</property> </property>
<widget class="QMenu" name="fileMenu"> <widget class="QMenu" name="fileMenu">
@ -192,6 +192,7 @@
<addaction name="actionViewInstanceFolder"/> <addaction name="actionViewInstanceFolder"/>
<addaction name="actionViewCentralModsFolder"/> <addaction name="actionViewCentralModsFolder"/>
<addaction name="actionViewSkinsFolder"/> <addaction name="actionViewSkinsFolder"/>
<addaction name="actionViewJavaFolder"/>
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="actionViewIconThemeFolder"/> <addaction name="actionViewIconThemeFolder"/>
<addaction name="actionViewWidgetThemeFolder"/> <addaction name="actionViewWidgetThemeFolder"/>
@ -788,6 +789,18 @@
<string>Open the cat packs folder in a file browser.</string> <string>Open the cat packs folder in a file browser.</string>
</property> </property>
</action> </action>
<action name="actionViewJavaFolder">
<property name="icon">
<iconset theme="viewfolder">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="text">
<string>Java</string>
</property>
<property name="toolTip">
<string>Open the Java folder in a file browser. Only available if the built-in Java downloader is used.</string>
</property>
</action>
</widget> </widget>
<customwidgets> <customwidgets>
<customwidget> <customwidget>

View File

@ -164,7 +164,12 @@ void ExportToModListDialog::done(int result)
if (output.isEmpty()) if (output.isEmpty())
return; return;
try {
FS::write(output, ui->finalText->toPlainText().toUtf8()); FS::write(output, ui->finalText->toPlainText().toUtf8());
} catch (const FS::FileSystemException& e) {
qCritical() << "Failed to save mod list file :" << e.cause();
}
} }
QDialog::done(result); QDialog::done(result);

View File

@ -116,7 +116,7 @@ void SkinManageDialog::selectionChanged(QItemSelection selected, QItemSelection
return; return;
m_selected_skin = key; m_selected_skin = key;
auto skin = m_list.skin(key); auto skin = m_list.skin(key);
if (!skin) if (!skin || !skin->isValid())
return; return;
ui->selectedModel->setPixmap(skin->getTexture().scaled(size() * (1. / 3), Qt::KeepAspectRatio, Qt::FastTransformation)); ui->selectedModel->setPixmap(skin->getTexture().scaled(size() * (1. / 3), Qt::KeepAspectRatio, Qt::FastTransformation));
ui->capeCombo->setCurrentIndex(m_capes_idx.value(skin->getCapeId())); ui->capeCombo->setCurrentIndex(m_capes_idx.value(skin->getCapeId()));
@ -212,7 +212,10 @@ void SkinManageDialog::setupCapes()
void SkinManageDialog::on_capeCombo_currentIndexChanged(int index) void SkinManageDialog::on_capeCombo_currentIndexChanged(int index)
{ {
auto id = ui->capeCombo->currentData(); 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) { if (auto skin = m_list.skin(m_selected_skin); skin) {
skin->setCapeId(id.toString()); skin->setCapeId(id.toString());
} }
@ -505,8 +508,13 @@ void SkinManageDialog::resizeEvent(QResizeEvent* event)
QSize s = size() * (1. / 3); QSize s = size() * (1. / 3);
if (auto skin = m_list.skin(m_selected_skin); skin) { if (auto skin = m_list.skin(m_selected_skin); skin) {
if (skin->isValid()) {
ui->selectedModel->setPixmap(skin->getTexture().scaled(s, Qt::KeepAspectRatio, Qt::FastTransformation)); ui->selectedModel->setPixmap(skin->getTexture().scaled(s, Qt::KeepAspectRatio, Qt::FastTransformation));
} }
}
auto id = ui->capeCombo->currentData(); 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));
}
} }

View File

@ -31,10 +31,12 @@
#include "Filter.h" #include "Filter.h"
#include "java/download/ArchiveDownloadTask.h" #include "java/download/ArchiveDownloadTask.h"
#include "java/download/ManifestDownloadTask.h" #include "java/download/ManifestDownloadTask.h"
#include "java/download/SymlinkTask.h"
#include "meta/Index.h" #include "meta/Index.h"
#include "meta/VersionList.h" #include "meta/VersionList.h"
#include "minecraft/MinecraftInstance.h" #include "minecraft/MinecraftInstance.h"
#include "minecraft/PackProfile.h" #include "minecraft/PackProfile.h"
#include "tasks/SequentialTask.h"
#include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/CustomMessageBox.h"
#include "ui/dialogs/ProgressDialog.h" #include "ui/dialogs/ProgressDialog.h"
#include "ui/java/VersionList.h" #include "ui/java/VersionList.h"
@ -55,13 +57,13 @@ class InstallJavaPage : public QWidget, public BasePage {
majorVersionSelect = new VersionSelectWidget(this); majorVersionSelect = new VersionSelectWidget(this);
majorVersionSelect->selectCurrent(); majorVersionSelect->selectCurrent();
majorVersionSelect->setEmptyString(tr("No java versions are currently available in the meta.")); 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->setEmptyErrorString(tr("Couldn't load or download the Java version lists!"));
horizontalLayout->addWidget(majorVersionSelect, 1); horizontalLayout->addWidget(majorVersionSelect, 1);
javaVersionSelect = new VersionSelectWidget(this); javaVersionSelect = new VersionSelectWidget(this);
javaVersionSelect->setEmptyString(tr("No java versions are currently available for your OS.")); 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->setEmptyErrorString(tr("Couldn't load or download the Java version lists!"));
horizontalLayout->addWidget(javaVersionSelect, 4); horizontalLayout->addWidget(javaVersionSelect, 4);
connect(majorVersionSelect, &VersionSelectWidget::selectedVersionChanged, this, &InstallJavaPage::setSelectedVersion); connect(majorVersionSelect, &VersionSelectWidget::selectedVersionChanged, this, &InstallJavaPage::setSelectedVersion);
connect(majorVersionSelect, &VersionSelectWidget::selectedVersionChanged, this, &InstallJavaPage::selectionChanged); 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(); CustomMessageBox::selectable(this, tr("Error"), error, QMessageBox::Warning)->show();
deletePath(); deletePath();
} }
#if defined(Q_OS_MACOS)
auto seq = makeShared<SequentialTask>(this, tr("Install Java"));
seq->addTask(task);
seq->addTask(makeShared<Java::SymlinkTask>(final_path));
task = seq;
#endif
connect(task.get(), &Task::failed, this, [this, &deletePath](QString reason) { connect(task.get(), &Task::failed, this, [this, &deletePath](QString reason) {
QString error = QString("Java download failed: %1").arg(reason); QString error = QString("Java download failed: %1").arg(reason);
CustomMessageBox::selectable(this, tr("Error"), error, QMessageBox::Warning)->show(); CustomMessageBox::selectable(this, tr("Error"), error, QMessageBox::Warning)->show();

View File

@ -143,6 +143,7 @@ void APIPage::loadSettings()
ui->modrinthToken->setText(modrinthToken); ui->modrinthToken->setText(modrinthToken);
QString customUserAgent = s->get("UserAgentOverride").toString(); QString customUserAgent = s->get("UserAgentOverride").toString();
ui->userAgentLineEdit->setText(customUserAgent); ui->userAgentLineEdit->setText(customUserAgent);
ui->technicClientID->setText(s->get("TechnicClientID").toString());
} }
void APIPage::applySettings() void APIPage::applySettings()
@ -172,6 +173,7 @@ void APIPage::applySettings()
QString modrinthToken = ui->modrinthToken->text(); QString modrinthToken = ui->modrinthToken->text();
s->set("ModrinthToken", modrinthToken); s->set("ModrinthToken", modrinthToken);
s->set("UserAgentOverride", ui->userAgentLineEdit->text()); s->set("UserAgentOverride", ui->userAgentLineEdit->text());
s->set("TechnicClientID", ui->technicClientID->text());
} }
bool APIPage::apply() bool APIPage::apply()

View File

@ -6,8 +6,8 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>800</width> <width>841</width>
<height>600</height> <height>620</height>
</rect> </rect>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout"> <layout class="QVBoxLayout" name="verticalLayout">
@ -288,6 +288,36 @@
</layout> </layout>
</widget> </widget>
</item> </item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Technic Client ID</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_9">
<item>
<widget class="QLabel" name="label_11">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Note: you only need to set this to access private data.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="technicClientID">
<property name="placeholderText">
<string>(None)</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_12">
<property name="text">
<string>Enter a custom GUID client ID for Technic here.</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item> <item>
<spacer name="verticalSpacer"> <spacer name="verticalSpacer">
<property name="orientation"> <property name="orientation">

View File

@ -67,8 +67,8 @@ JavaPage::JavaPage(QWidget* parent) : QWidget(parent), ui(new Ui::JavaPage)
ui->managedJavaList->initialize(new JavaInstallList(this, true)); ui->managedJavaList->initialize(new JavaInstallList(this, true));
ui->managedJavaList->setResizeOn(2); ui->managedJavaList->setResizeOn(2);
ui->managedJavaList->selectCurrent(); ui->managedJavaList->selectCurrent();
ui->managedJavaList->setEmptyString(tr("No managed java versions are installed")); ui->managedJavaList->setEmptyString(tr("No managed Java versions are installed"));
ui->managedJavaList->setEmptyErrorString(tr("Couldn't load the managed java list!")); ui->managedJavaList->setEmptyErrorString(tr("Couldn't load the managed Java list!"));
connect(ui->autodetectJavaCheckBox, &QCheckBox::stateChanged, this, [this] { connect(ui->autodetectJavaCheckBox, &QCheckBox::stateChanged, this, [this] {
ui->autodownloadCheckBox->setEnabled(ui->autodetectJavaCheckBox->isChecked()); ui->autodownloadCheckBox->setEnabled(ui->autodetectJavaCheckBox->isChecked());
if (!ui->autodetectJavaCheckBox->isChecked()) if (!ui->autodetectJavaCheckBox->isChecked())

View File

@ -234,7 +234,7 @@ bool LogPage::apply()
bool LogPage::shouldDisplay() const bool LogPage::shouldDisplay() const
{ {
return m_instance->isRunning() || m_proxy->rowCount() > 0; return true;
} }
void LogPage::on_btnPaste_clicked() void LogPage::on_btnPaste_clicked()

View File

@ -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); QString("%1search?build=%2&q=%3").arg(BuildConfig.TECHNIC_API_BASE_URL, BuildConfig.TECHNIC_API_BUILD, currentSearchTerm);
searchMode = List; searchMode = List;
} }
auto clientId = APPLICATION->settings()->get("TechnicClientID").toString();
if (!clientId.isEmpty()) {
searchUrl += "?cid=" + clientId;
}
netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(searchUrl), response)); netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(searchUrl), response));
jobPtr = netJob; jobPtr = netJob;
jobPtr->start(); jobPtr->start();

View File

@ -30,7 +30,7 @@
<item> <item>
<widget class="QLabel" name="label_2"> <widget class="QLabel" name="label_2">
<property name="text"> <property name="text">
<string>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?</string> <string>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?</string>
</property> </property>
<property name="wordWrap"> <property name="wordWrap">
<bool>true</bool> <bool>true</bool>

View File

@ -83,6 +83,6 @@ void JavaWizardPage::retranslate()
{ {
setTitle(tr("Java")); setTitle(tr("Java"));
setSubTitle( 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(); m_java_widget->retranslate();
} }

View File

@ -35,11 +35,10 @@ void LoginWizardPage::on_pushButton_clicked()
if (account) { if (account) {
APPLICATION->accounts()->addAccount(account); APPLICATION->accounts()->addAccount(account);
APPLICATION->accounts()->setDefaultAccount(account); APPLICATION->accounts()->setDefaultAccount(account);
}
if (wizard()->currentId() == wizard()->pageIds().last()) { if (wizard()->currentId() == wizard()->pageIds().last()) {
wizard()->accept(); wizard()->accept();
} else { } else {
wizard()->next(); wizard()->next();
} }
}
} }

View File

@ -18,10 +18,12 @@
*/ */
#pragma once #pragma once
#include <QDir>
#include <QLoggingCategory>
#include <QString> #include <QString>
#include <memory>
#include "IconTheme.h" #include "IconTheme.h"
#include "ui/MainWindow.h"
#include "ui/themes/CatPack.h" #include "ui/themes/CatPack.h"
#include "ui/themes/ITheme.h" #include "ui/themes/ITheme.h"

View File

@ -1210,7 +1210,7 @@ std::optional<QDir> PrismUpdaterApp::unpackArchive(QFileInfo archive)
QProcess proc = QProcess(); QProcess proc = QProcess();
proc.start(cmd, args); proc.start(cmd, args);
if (!proc.waitForStarted(5000)) { // wait 5 seconds to start 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); logUpdate(msg);
showFatalErrorMessage(tr("Failed extract archive"), msg); showFatalErrorMessage(tr("Failed extract archive"), msg);
return std::nullopt; return std::nullopt;
@ -1241,7 +1241,7 @@ bool PrismUpdaterApp::loadPrismVersionFromExe(const QString& exe_path)
proc.setReadChannel(QProcess::StandardOutput); proc.setReadChannel(QProcess::StandardOutput);
proc.start(exe_path, { "--version" }); proc.start(exe_path, { "--version" });
if (!proc.waitForStarted(5000)) { 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; return false;
} // wait 5 seconds to start } // wait 5 seconds to start
if (!proc.waitForFinished(5000)) { if (!proc.waitForFinished(5000)) {

View File

@ -8,8 +8,8 @@ See [Package variants](#package-variants) for a list of available packages.
## Installing a development release (flake) ## Installing a development release (flake)
We use [garnix](https://garnix.io/) to build and cache our development builds. We use [cachix](https://cachix.org/) to cache our development and release builds.
If you want to avoid rebuilds you may add the garnix cache to your substitutors, or use `--accept-flake-config` 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. to temporarily enable it when using `nix` commands.
Example (NixOS): Example (NixOS):
@ -17,12 +17,10 @@ Example (NixOS):
```nix ```nix
{ {
nix.settings = { nix.settings = {
trusted-substituters = [ trusted-substituters = [ "https://prismlauncher.cachix.org" ];
"https://cache.garnix.io"
];
trusted-public-keys = [ 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) ## Installing a development release (without flakes)
We use [garnix](https://garnix.io/) to build and cache our development builds. We use [Cachix](https://cachix.org/) to cache our development and release builds.
If you want to avoid rebuilds you may add the garnix cache to your substitutors. If you want to avoid rebuilds you may add the Cachix bucket to your substitutors.
Example (NixOS): Example (NixOS):
```nix ```nix
{ {
nix.settings = { nix.settings = {
trusted-substituters = [ trusted-substituters = [ "https://prismlauncher.cachix.org" ];
"https://cache.garnix.io"
];
trusted-public-keys = [ trusted-public-keys = [
"cache.garnix.io:CTFPyKSLcx5RMJKfLo5EEPUObbA78b0YQ2DTCJXqr9g=" "prismlauncher.cachix.org-1:9/n/FGyABA2jLUVfY+DEp4hKds/rwO+SCOtbOkDzd+c="
]; ];
}; };
} }

View File

@ -23,7 +23,7 @@
cd ${self} cd ${self}
echo "Running clang-format...." 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..." echo "Running deadnix..."
deadnix --fail deadnix --fail