diff options
Diffstat (limited to 'launcher/minecraft')
76 files changed, 2099 insertions, 368 deletions
diff --git a/launcher/minecraft/AssetsUtils.cpp b/launcher/minecraft/AssetsUtils.cpp index 15062c2b..16fdfdb1 100644 --- a/launcher/minecraft/AssetsUtils.cpp +++ b/launcher/minecraft/AssetsUtils.cpp @@ -340,7 +340,7 @@ QString AssetObject::getRelPath() NetJob::Ptr AssetsIndex::getDownloadJob() { - auto job = new NetJob(QObject::tr("Assets for %1").arg(id), APPLICATION->network()); + auto job = makeShared<NetJob>(QObject::tr("Assets for %1").arg(id), APPLICATION->network()); for (auto &object : objects.values()) { auto dl = object.getDownloadAction(); diff --git a/launcher/minecraft/ComponentUpdateTask.cpp b/launcher/minecraft/ComponentUpdateTask.cpp index 6db21622..d55bc17f 100644 --- a/launcher/minecraft/ComponentUpdateTask.cpp +++ b/launcher/minecraft/ComponentUpdateTask.cpp @@ -572,7 +572,7 @@ void ComponentUpdateTask::resolveDependencies(bool checkOnly) // add stuff... for(auto &add: toAdd) { - ComponentPtr component = new Component(d->m_list, add.uid); + auto component = makeShared<Component>(d->m_list, add.uid); if(!add.equalsVersion.isEmpty()) { // exact version diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index 1d37224a..2c624a36 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -192,6 +192,10 @@ void MinecraftInstance::loadSpecificSettings() m_settings->registerSetting("JoinServerOnLaunch", false); m_settings->registerSetting("JoinServerOnLaunchAddress", ""); + // Use account for instance, this does not have a global override + m_settings->registerSetting("UseAccountForInstance", false); + m_settings->registerSetting("InstanceAccountId", ""); + qDebug() << "Instance-type specific settings were loaded!"; setSpecificSettingsLoaded(true); @@ -286,6 +290,11 @@ QString MinecraftInstance::coreModsDir() const return FS::PathCombine(gameRoot(), "coremods"); } +QString MinecraftInstance::nilModsDir() const +{ + return FS::PathCombine(gameRoot(), "nilmods"); +} + QString MinecraftInstance::resourcePacksDir() const { return FS::PathCombine(gameRoot(), "resourcepacks"); @@ -457,8 +466,8 @@ QMap<QString, QString> MinecraftInstance::getVariables() QMap<QString, QString> out; out.insert("INST_NAME", name()); out.insert("INST_ID", id()); - out.insert("INST_DIR", QDir(instanceRoot()).absolutePath()); - out.insert("INST_MC_DIR", QDir(gameRoot()).absolutePath()); + out.insert("INST_DIR", QDir::toNativeSeparators(QDir(instanceRoot()).absolutePath())); + out.insert("INST_MC_DIR", QDir::toNativeSeparators(QDir(gameRoot()).absolutePath())); out.insert("INST_JAVA", settings()->get("JavaPath").toString()); out.insert("INST_JAVA_ARGS", javaArguments().join(' ')); return out; @@ -916,7 +925,10 @@ QString MinecraftInstance::getStatusbarDescription() if(m_settings->get("ShowGameTime").toBool()) { if (lastTimePlayed() > 0) { - description.append(tr(", last played for %1").arg(Time::prettifyDuration(lastTimePlayed()))); + QDateTime lastLaunchTime = QDateTime::fromMSecsSinceEpoch(lastLaunch()); + description.append(tr(", last played on %1 for %2") + .arg(QLocale().toString(lastLaunchTime, QLocale::ShortFormat)) + .arg(Time::prettifyDuration(lastTimePlayed()))); } if (totalTimePlayed() > 0) { @@ -958,12 +970,12 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt // print a header { - process->appendStep(new TextPrint(pptr, "Minecraft folder is:\n" + gameRoot() + "\n\n", MessageLevel::Launcher)); + process->appendStep(makeShared<TextPrint>(pptr, "Minecraft folder is:\n" + gameRoot() + "\n\n", MessageLevel::Launcher)); } // check java { - process->appendStep(new CheckJava(pptr)); + process->appendStep(makeShared<CheckJava>(pptr)); } // check launch method @@ -971,13 +983,13 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt QString method = launchMethod(); if(!validMethods.contains(method)) { - process->appendStep(new TextPrint(pptr, "Selected launch method \"" + method + "\" is not valid.\n", MessageLevel::Fatal)); + process->appendStep(makeShared<TextPrint>(pptr, "Selected launch method \"" + method + "\" is not valid.\n", MessageLevel::Fatal)); return process; } // create the .minecraft folder and server-resource-packs (workaround for Minecraft bug MCL-3732) { - process->appendStep(new CreateGameFolders(pptr)); + process->appendStep(makeShared<CreateGameFolders>(pptr)); } if (!serverToJoin && settings()->get("JoinServerOnLaunch").toBool()) @@ -989,7 +1001,7 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt if(serverToJoin && serverToJoin->port == 25565) { // Resolve server address to join on launch - auto *step = new LookupServerAddress(pptr); + auto step = makeShared<LookupServerAddress>(pptr); step->setLookupAddress(serverToJoin->address); step->setOutputAddressPtr(serverToJoin); process->appendStep(step); @@ -998,7 +1010,7 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt // run pre-launch command if that's needed if(getPreLaunchCommand().size()) { - auto step = new PreLaunchCommand(pptr); + auto step = makeShared<PreLaunchCommand>(pptr); step->setWorkingDirectory(gameRoot()); process->appendStep(step); } @@ -1007,43 +1019,43 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt if(session->status != AuthSession::PlayableOffline) { if(!session->demo) { - process->appendStep(new ClaimAccount(pptr, session)); + process->appendStep(makeShared<ClaimAccount>(pptr, session)); } - process->appendStep(new Update(pptr, Net::Mode::Online)); + process->appendStep(makeShared<Update>(pptr, Net::Mode::Online)); } else { - process->appendStep(new Update(pptr, Net::Mode::Offline)); + process->appendStep(makeShared<Update>(pptr, Net::Mode::Offline)); } // if there are any jar mods { - process->appendStep(new ModMinecraftJar(pptr)); + process->appendStep(makeShared<ModMinecraftJar>(pptr)); } // Scan mods folders for mods { - process->appendStep(new ScanModFolders(pptr)); + process->appendStep(makeShared<ScanModFolders>(pptr)); } // print some instance info here... { - process->appendStep(new PrintInstanceInfo(pptr, session, serverToJoin)); + process->appendStep(makeShared<PrintInstanceInfo>(pptr, session, serverToJoin)); } // extract native jars if needed { - process->appendStep(new ExtractNatives(pptr)); + process->appendStep(makeShared<ExtractNatives>(pptr)); } // reconstruct assets if needed { - process->appendStep(new ReconstructAssets(pptr)); + process->appendStep(makeShared<ReconstructAssets>(pptr)); } // verify that minimum Java requirements are met { - process->appendStep(new VerifyJavaInstall(pptr)); + process->appendStep(makeShared<VerifyJavaInstall>(pptr)); } { @@ -1051,7 +1063,7 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt auto method = launchMethod(); if(method == "LauncherPart") { - auto step = new LauncherPartLaunch(pptr); + auto step = makeShared<LauncherPartLaunch>(pptr); step->setWorkingDirectory(gameRoot()); step->setAuthSession(session); step->setServerToJoin(serverToJoin); @@ -1059,7 +1071,7 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt } else if (method == "DirectJava") { - auto step = new DirectJavaLaunch(pptr); + auto step = makeShared<DirectJavaLaunch>(pptr); step->setWorkingDirectory(gameRoot()); step->setAuthSession(session); step->setServerToJoin(serverToJoin); @@ -1070,7 +1082,7 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt // run post-exit command if that's needed if(getPostExitCommand().size()) { - auto step = new PostLaunchCommand(pptr); + auto step = makeShared<PostLaunchCommand>(pptr); step->setWorkingDirectory(gameRoot()); process->appendStep(step); } @@ -1080,8 +1092,7 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt } if(m_settings->get("QuitAfterGameStop").toBool()) { - auto step = new QuitAfterGameStop(pptr); - process->appendStep(step); + process->appendStep(makeShared<QuitAfterGameStop>(pptr)); } m_launchProcess = process; emit launchTaskChanged(m_launchProcess); @@ -1098,67 +1109,79 @@ JavaVersion MinecraftInstance::getJavaVersion() return JavaVersion(settings()->get("JavaVersion").toString()); } -std::shared_ptr<ModFolderModel> MinecraftInstance::loaderModList() const +std::shared_ptr<ModFolderModel> MinecraftInstance::loaderModList() { if (!m_loader_mod_list) { bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); - m_loader_mod_list.reset(new ModFolderModel(modsRoot(), is_indexed)); + m_loader_mod_list.reset(new ModFolderModel(modsRoot(), this, is_indexed)); m_loader_mod_list->disableInteraction(isRunning()); connect(this, &BaseInstance::runningStatusChanged, m_loader_mod_list.get(), &ModFolderModel::disableInteraction); } return m_loader_mod_list; } -std::shared_ptr<ModFolderModel> MinecraftInstance::coreModList() const +std::shared_ptr<ModFolderModel> MinecraftInstance::coreModList() { if (!m_core_mod_list) { bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); - m_core_mod_list.reset(new ModFolderModel(coreModsDir(), is_indexed)); + m_core_mod_list.reset(new ModFolderModel(coreModsDir(), this, is_indexed)); m_core_mod_list->disableInteraction(isRunning()); connect(this, &BaseInstance::runningStatusChanged, m_core_mod_list.get(), &ModFolderModel::disableInteraction); } return m_core_mod_list; } -std::shared_ptr<ResourcePackFolderModel> MinecraftInstance::resourcePackList() const +std::shared_ptr<ModFolderModel> MinecraftInstance::nilModList() +{ + if (!m_nil_mod_list) + { + bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); + m_nil_mod_list.reset(new ModFolderModel(nilModsDir(), this, is_indexed, false)); + m_nil_mod_list->disableInteraction(isRunning()); + connect(this, &BaseInstance::runningStatusChanged, m_nil_mod_list.get(), &ModFolderModel::disableInteraction); + } + return m_nil_mod_list; +} + +std::shared_ptr<ResourcePackFolderModel> MinecraftInstance::resourcePackList() { if (!m_resource_pack_list) { - m_resource_pack_list.reset(new ResourcePackFolderModel(resourcePacksDir())); + m_resource_pack_list.reset(new ResourcePackFolderModel(resourcePacksDir(), this)); } return m_resource_pack_list; } -std::shared_ptr<TexturePackFolderModel> MinecraftInstance::texturePackList() const +std::shared_ptr<TexturePackFolderModel> MinecraftInstance::texturePackList() { if (!m_texture_pack_list) { - m_texture_pack_list.reset(new TexturePackFolderModel(texturePacksDir())); + m_texture_pack_list.reset(new TexturePackFolderModel(texturePacksDir(), this)); } return m_texture_pack_list; } -std::shared_ptr<ShaderPackFolderModel> MinecraftInstance::shaderPackList() const +std::shared_ptr<ShaderPackFolderModel> MinecraftInstance::shaderPackList() { if (!m_shader_pack_list) { - m_shader_pack_list.reset(new ShaderPackFolderModel(shaderPacksDir())); + m_shader_pack_list.reset(new ShaderPackFolderModel(shaderPacksDir(), this)); } return m_shader_pack_list; } -std::shared_ptr<WorldList> MinecraftInstance::worldList() const +std::shared_ptr<WorldList> MinecraftInstance::worldList() { if (!m_world_list) { - m_world_list.reset(new WorldList(worldDir())); + m_world_list.reset(new WorldList(worldDir(), this)); } return m_world_list; } -std::shared_ptr<GameOptions> MinecraftInstance::gameOptionsModel() const +std::shared_ptr<GameOptions> MinecraftInstance::gameOptionsModel() { if (!m_game_options) { diff --git a/launcher/minecraft/MinecraftInstance.h b/launcher/minecraft/MinecraftInstance.h index 1bbd7b83..068b3008 100644 --- a/launcher/minecraft/MinecraftInstance.h +++ b/launcher/minecraft/MinecraftInstance.h @@ -84,6 +84,7 @@ public: QString shaderPacksDir() const; QString modsRoot() const override; QString coreModsDir() const; + QString nilModsDir() const; QString modsCacheLocation() const; QString libDir() const; QString worldDir() const; @@ -114,13 +115,14 @@ public: std::shared_ptr<PackProfile> getPackProfile() const; ////// Mod Lists ////// - std::shared_ptr<ModFolderModel> loaderModList() const; - std::shared_ptr<ModFolderModel> coreModList() const; - std::shared_ptr<ResourcePackFolderModel> resourcePackList() const; - std::shared_ptr<TexturePackFolderModel> texturePackList() const; - std::shared_ptr<ShaderPackFolderModel> shaderPackList() const; - std::shared_ptr<WorldList> worldList() const; - std::shared_ptr<GameOptions> gameOptionsModel() const; + std::shared_ptr<ModFolderModel> loaderModList(); + std::shared_ptr<ModFolderModel> coreModList(); + std::shared_ptr<ModFolderModel> nilModList(); + std::shared_ptr<ResourcePackFolderModel> resourcePackList(); + std::shared_ptr<TexturePackFolderModel> texturePackList(); + std::shared_ptr<ShaderPackFolderModel> shaderPackList(); + std::shared_ptr<WorldList> worldList(); + std::shared_ptr<GameOptions> gameOptionsModel(); ////// Launch stuff ////// Task::Ptr createUpdateTask(Net::Mode mode) override; @@ -170,6 +172,7 @@ protected: // data std::shared_ptr<PackProfile> m_components; mutable std::shared_ptr<ModFolderModel> m_loader_mod_list; mutable std::shared_ptr<ModFolderModel> m_core_mod_list; + mutable std::shared_ptr<ModFolderModel> m_nil_mod_list; mutable std::shared_ptr<ResourcePackFolderModel> m_resource_pack_list; mutable std::shared_ptr<ShaderPackFolderModel> m_shader_pack_list; mutable std::shared_ptr<TexturePackFolderModel> m_texture_pack_list; diff --git a/launcher/minecraft/MinecraftLoadAndCheck.cpp b/launcher/minecraft/MinecraftLoadAndCheck.cpp index d72bc7be..1c3f6fb7 100644 --- a/launcher/minecraft/MinecraftLoadAndCheck.cpp +++ b/launcher/minecraft/MinecraftLoadAndCheck.cpp @@ -22,6 +22,7 @@ void MinecraftLoadAndCheck::executeTask() connect(m_task.get(), &Task::failed, this, &MinecraftLoadAndCheck::subtaskFailed); connect(m_task.get(), &Task::aborted, this, [this]{ subtaskFailed(tr("Aborted")); }); connect(m_task.get(), &Task::progress, this, &MinecraftLoadAndCheck::progress); + connect(m_task.get(), &Task::stepProgress, this, &MinecraftLoadAndCheck::propogateStepProgress); connect(m_task.get(), &Task::status, this, &MinecraftLoadAndCheck::setStatus); } diff --git a/launcher/minecraft/MinecraftUpdate.cpp b/launcher/minecraft/MinecraftUpdate.cpp index 3a3aa864..35430bb0 100644 --- a/launcher/minecraft/MinecraftUpdate.cpp +++ b/launcher/minecraft/MinecraftUpdate.cpp @@ -43,7 +43,7 @@ void MinecraftUpdate::executeTask() m_tasks.clear(); // create folders { - m_tasks.append(new FoldersTask(m_inst)); + m_tasks.append(makeShared<FoldersTask>(m_inst)); } // add metadata update task if necessary @@ -59,17 +59,17 @@ void MinecraftUpdate::executeTask() // libraries download { - m_tasks.append(new LibrariesTask(m_inst)); + m_tasks.append(makeShared<LibrariesTask>(m_inst)); } // FML libraries download and copy into the instance { - m_tasks.append(new FMLLibrariesTask(m_inst)); + m_tasks.append(makeShared<FMLLibrariesTask>(m_inst)); } // assets update { - m_tasks.append(new AssetUpdateTask(m_inst)); + m_tasks.append(makeShared<AssetUpdateTask>(m_inst)); } if(!m_preFailure.isEmpty()) @@ -100,7 +100,9 @@ void MinecraftUpdate::next() disconnect(task.get(), &Task::failed, this, &MinecraftUpdate::subtaskFailed); disconnect(task.get(), &Task::aborted, this, &Task::abort); disconnect(task.get(), &Task::progress, this, &MinecraftUpdate::progress); + disconnect(task.get(), &Task::stepProgress, this, &MinecraftUpdate::propogateStepProgress); disconnect(task.get(), &Task::status, this, &MinecraftUpdate::setStatus); + disconnect(task.get(), &Task::details, this, &MinecraftUpdate::setDetails); } if(m_currentTask == m_tasks.size()) { @@ -118,7 +120,9 @@ void MinecraftUpdate::next() connect(task.get(), &Task::failed, this, &MinecraftUpdate::subtaskFailed); connect(task.get(), &Task::aborted, this, &Task::abort); connect(task.get(), &Task::progress, this, &MinecraftUpdate::progress); + connect(task.get(), &Task::stepProgress, this, &MinecraftUpdate::propogateStepProgress); connect(task.get(), &Task::status, this, &MinecraftUpdate::setStatus); + connect(task.get(), &Task::details, this, &MinecraftUpdate::setDetails); // if the task is already running, do not start it again if(!task->isRunning()) { diff --git a/launcher/minecraft/OneSixVersionFormat.cpp b/launcher/minecraft/OneSixVersionFormat.cpp index 280f6b26..888b6860 100644 --- a/launcher/minecraft/OneSixVersionFormat.cpp +++ b/launcher/minecraft/OneSixVersionFormat.cpp @@ -39,6 +39,8 @@ #include "minecraft/ParseUtils.h" #include <minecraft/MojangVersionFormat.h> +#include <QRegularExpression> + using namespace Json; static void readString(const QJsonObject &root, const QString &key, QString &variable) @@ -121,6 +123,15 @@ VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument &doc out->uid = root.value("fileId").toString(); } + const QRegularExpression valid_uid_regex{ QRegularExpression::anchoredPattern(QStringLiteral(R"([a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]+)*)")) }; + if (!valid_uid_regex.match(out->uid).hasMatch()) { + qCritical() << "The component's 'uid' contains illegal characters! UID:" << out->uid; + out->addProblem( + ProblemSeverity::Error, + QObject::tr("The component's 'uid' contains illegal characters! This can cause security issues.") + ); + } + out->version = root.value("version").toString(); MojangVersionFormat::readVersionProperties(root, out.get()); diff --git a/launcher/minecraft/PackProfile.cpp b/launcher/minecraft/PackProfile.cpp index 43fa3f8d..aff05dbc 100644 --- a/launcher/minecraft/PackProfile.cpp +++ b/launcher/minecraft/PackProfile.cpp @@ -1,7 +1,10 @@ -// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2022-2023 Sefa Eyeoglu <contact@scrumplex.net> +// +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 + /* * Prism Launcher - Minecraft Launcher - * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * Copyright (C) 2022-2023 Sefa Eyeoglu <contact@scrumplex.net> * Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me> * * This program is free software: you can redistribute it and/or modify @@ -49,18 +52,20 @@ #include "minecraft/OneSixVersionFormat.h" #include "FileSystem.h" #include "minecraft/MinecraftInstance.h" +#include "minecraft/ProfileUtils.h" #include "Json.h" #include "PackProfile.h" #include "PackProfile_p.h" #include "ComponentUpdateTask.h" -#include "modplatform/ModAPI.h" +#include "Application.h" +#include "modplatform/ResourceAPI.h" -static const QMap<QString, ModAPI::ModLoaderType> modloaderMapping{ - {"net.minecraftforge", ModAPI::Forge}, - {"net.fabricmc.fabric-loader", ModAPI::Fabric}, - {"org.quiltmc.quilt-loader", ModAPI::Quilt} +static const QMap<QString, ResourceAPI::ModLoaderType> modloaderMapping{ + {"net.minecraftforge", ResourceAPI::Forge}, + {"net.fabricmc.fabric-loader", ResourceAPI::Fabric}, + {"org.quiltmc.quilt-loader", ResourceAPI::Quilt} }; PackProfile::PackProfile(MinecraftInstance * instance) @@ -129,7 +134,7 @@ static ComponentPtr componentFromJsonV1(PackProfile * parent, const QString & co // critical auto uid = Json::requireString(obj.value("uid")); auto filePath = componentJsonPattern.arg(uid); - auto component = new Component(parent, uid); + auto component = makeShared<Component>(parent, uid); component->m_version = Json::ensureString(obj.value("version")); component->m_dependencyOnly = Json::ensureBoolean(obj.value("dependencyOnly"), false); component->m_important = Json::ensureBoolean(obj.value("important"), false); @@ -517,23 +522,23 @@ bool PackProfile::revertToBase(int index) return true; } -Component * PackProfile::getComponent(const QString &id) +ComponentPtr PackProfile::getComponent(const QString &id) { auto iter = d->componentIndex.find(id); if (iter == d->componentIndex.end()) { return nullptr; } - return (*iter).get(); + return (*iter); } -Component * PackProfile::getComponent(int index) +ComponentPtr PackProfile::getComponent(int index) { if(index < 0 || index >= d->components.size()) { return nullptr; } - return d->components[index].get(); + return d->components[index]; } QVariant PackProfile::data(const QModelIndex &index, int role) const @@ -729,16 +734,47 @@ void PackProfile::invalidateLaunchProfile() void PackProfile::installJarMods(QStringList selectedFiles) { + // FIXME: get rid of _internal installJarMods_internal(selectedFiles); } void PackProfile::installCustomJar(QString selectedFile) { + // FIXME: get rid of _internal installCustomJar_internal(selectedFile); } +bool PackProfile::installComponents(QStringList selectedFiles) +{ + const QString patchDir = FS::PathCombine(d->m_instance->instanceRoot(), "patches"); + if (!FS::ensureFolderPathExists(patchDir)) + return false; + + bool result = true; + for (const QString& source : selectedFiles) { + const QFileInfo sourceInfo(source); + + auto versionFile = ProfileUtils::parseJsonFile(sourceInfo, false); + const QString target = FS::PathCombine(patchDir, versionFile->uid + ".json"); + + if (!QFile::copy(source, target)) { + qWarning() << "Component" << source << "could not be copied to target" << target; + result = false; + continue; + } + + appendComponent(makeShared<Component>(this, versionFile->uid, versionFile)); + } + + scheduleSave(); + invalidateLaunchProfile(); + + return result; +} + void PackProfile::installAgents(QStringList selectedFiles) { + // FIXME: get rid of _internal installAgents_internal(selectedFiles); } @@ -764,7 +800,7 @@ bool PackProfile::installEmpty(const QString& uid, const QString& name) file.write(OneSixVersionFormat::versionFileToJson(f).toJson()); file.close(); - appendComponent(new Component(this, f->uid, f)); + appendComponent(makeShared<Component>(this, f->uid, f)); scheduleSave(); invalidateLaunchProfile(); return true; @@ -871,7 +907,7 @@ bool PackProfile::installJarMods_internal(QStringList filepaths) file.write(OneSixVersionFormat::versionFileToJson(f).toJson()); file.close(); - appendComponent(new Component(this, f->uid, f)); + appendComponent(makeShared<Component>(this, f->uid, f)); } scheduleSave(); invalidateLaunchProfile(); @@ -932,7 +968,7 @@ bool PackProfile::installCustomJar_internal(QString filepath) file.write(OneSixVersionFormat::versionFileToJson(f).toJson()); file.close(); - appendComponent(new Component(this, f->uid, f)); + appendComponent(makeShared<Component>(this, f->uid, f)); scheduleSave(); invalidateLaunchProfile(); @@ -988,7 +1024,7 @@ bool PackProfile::installAgents_internal(QStringList filepaths) patchFile.write(OneSixVersionFormat::versionFileToJson(versionFile).toJson()); patchFile.close(); - appendComponent(new Component(this, versionFile->uid, versionFile)); + appendComponent(makeShared<Component>(this, versionFile->uid, versionFile)); } scheduleSave(); @@ -1037,7 +1073,7 @@ bool PackProfile::setComponentVersion(const QString& uid, const QString& version else { // add new - auto component = new Component(this, uid); + auto component = makeShared<Component>(this, uid); component->m_version = version; component->m_important = important; appendComponent(component); @@ -1066,19 +1102,22 @@ void PackProfile::disableInteraction(bool disable) } } -ModAPI::ModLoaderTypes PackProfile::getModLoaders() +std::optional<ResourceAPI::ModLoaderTypes> PackProfile::getModLoaders() { - ModAPI::ModLoaderTypes result = ModAPI::Unspecified; + ResourceAPI::ModLoaderTypes result; + bool has_any_loader = false; - QMapIterator<QString, ModAPI::ModLoaderType> i(modloaderMapping); + QMapIterator<QString, ResourceAPI::ModLoaderType> i(modloaderMapping); - while (i.hasNext()) - { + while (i.hasNext()) { i.next(); - Component* c = getComponent(i.key()); - if (c != nullptr && c->isEnabled()) { + if (auto c = getComponent(i.key()); c != nullptr && c->isEnabled()) { result |= i.value(); + has_any_loader = true; } } + + if (!has_any_loader) + return {}; return result; } diff --git a/launcher/minecraft/PackProfile.h b/launcher/minecraft/PackProfile.h index 2330cca1..d144d875 100644 --- a/launcher/minecraft/PackProfile.h +++ b/launcher/minecraft/PackProfile.h @@ -1,7 +1,10 @@ -// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2022-2023 Sefa Eyeoglu <contact@scrumplex.net> +// +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 + /* * Prism Launcher - Minecraft Launcher - * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * Copyright (C) 2022-2023 Sefa Eyeoglu <contact@scrumplex.net> * Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me> * * This program is free software: you can redistribute it and/or modify @@ -49,7 +52,7 @@ #include "BaseVersion.h" #include "MojangDownloadInfo.h" #include "net/Mode.h" -#include "modplatform/ModAPI.h" +#include "modplatform/ResourceAPI.h" class MinecraftInstance; struct PackProfileData; @@ -86,6 +89,9 @@ public: /// install a jar/zip as a replacement for the main jar void installCustomJar(QString selectedFile); + /// install MMC/Prism component files + bool installComponents(QStringList selectedFiles); + /// install Java agent files void installAgents(QStringList selectedFiles); @@ -136,16 +142,16 @@ signals: public: /// get the profile component by id - Component * getComponent(const QString &id); + ComponentPtr getComponent(const QString &id); /// get the profile component by index - Component * getComponent(int index); + ComponentPtr getComponent(int index); /// Add the component to the internal list of patches // todo(merged): is this the best approach void appendComponent(ComponentPtr component); - ModAPI::ModLoaderTypes getModLoaders(); + std::optional<ResourceAPI::ModLoaderTypes> getModLoaders(); private: void scheduleSave(); diff --git a/launcher/minecraft/World.cpp b/launcher/minecraft/World.cpp index 90fcf337..54fb9434 100644 --- a/launcher/minecraft/World.cpp +++ b/launcher/minecraft/World.cpp @@ -1,7 +1,8 @@ // SPDX-License-Identifier: GPL-3.0-only /* - * PolyMC - Minecraft Launcher + * Prism Launcher - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -55,6 +56,8 @@ #include <optional> +#include "FileSystem.h" + using std::optional; using std::nullopt; @@ -545,6 +548,10 @@ bool World::replace(World &with) bool World::destroy() { if(!is_valid) return false; + + if (FS::trash(m_containerFile.filePath())) + return true; + if (m_containerFile.isDir()) { QDir d(m_containerFile.filePath()); @@ -562,3 +569,25 @@ bool World::operator==(const World &other) const { return is_valid == other.is_valid && folderName() == other.folderName(); } + +bool World::isSymLinkUnder(const QString& instPath) const +{ + if (isSymLink()) + return true; + + auto instDir = QDir(instPath); + + auto relAbsPath = instDir.relativeFilePath(m_containerFile.absoluteFilePath()); + auto relCanonPath = instDir.relativeFilePath(m_containerFile.canonicalFilePath()); + + return relAbsPath != relCanonPath; +} + +bool World::isMoreThanOneHardLink() const +{ + if (m_containerFile.isDir()) + { + return FS::hardLinkCount(QDir(m_containerFile.absoluteFilePath()).filePath("level.dat")) > 1; + } + return FS::hardLinkCount(m_containerFile.absoluteFilePath()) > 1; +} diff --git a/launcher/minecraft/World.h b/launcher/minecraft/World.h index 8327253a..10328cce 100644 --- a/launcher/minecraft/World.h +++ b/launcher/minecraft/World.h @@ -95,6 +95,21 @@ public: // WEAK compare operator - used for replacing worlds bool operator==(const World &other) const; + [[nodiscard]] auto isSymLink() const -> bool{ return m_containerFile.isSymLink(); } + + /** + * @brief Take a instance path, checks if the file pointed to by the resource is a symlink or under a symlink in that instance + * + * @param instPath path to an instance directory + * @return true + * @return false + */ + [[nodiscard]] bool isSymLinkUnder(const QString& instPath) const; + + [[nodiscard]] bool isMoreThanOneHardLink() const; + + QString canonicalFilePath() const { return m_containerFile.canonicalFilePath(); } + private: void readFromZip(const QFileInfo &file); void readFromFS(const QFileInfo &file); diff --git a/launcher/minecraft/WorldList.cpp b/launcher/minecraft/WorldList.cpp index ae29a972..0feee299 100644 --- a/launcher/minecraft/WorldList.cpp +++ b/launcher/minecraft/WorldList.cpp @@ -45,16 +45,15 @@ #include <QFileSystemWatcher> #include <QDebug> -WorldList::WorldList(const QString &dir) - : QAbstractListModel(), m_dir(dir) +WorldList::WorldList(const QString &dir, BaseInstance* instance) + : QAbstractListModel(), m_instance(instance), m_dir(dir) { FS::ensureFolderPathExists(m_dir.absolutePath()); m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs); m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); m_watcher = new QFileSystemWatcher(this); is_watching = false; - connect(m_watcher, SIGNAL(directoryChanged(QString)), this, - SLOT(directoryChanged(QString))); + connect(m_watcher, &QFileSystemWatcher::directoryChanged, this, &WorldList::directoryChanged); } void WorldList::startWatching() @@ -128,6 +127,10 @@ bool WorldList::isValid() return m_dir.exists() && m_dir.isReadable(); } +QString WorldList::instDirPath() const { + return QFileInfo(m_instance->instanceRoot()).absoluteFilePath(); +} + bool WorldList::deleteWorld(int index) { if (index >= worlds.size() || index < 0) @@ -173,7 +176,7 @@ bool WorldList::resetIcon(int row) int WorldList::columnCount(const QModelIndex &parent) const { - return parent.isValid()? 0 : 4; + return parent.isValid()? 0 : 5; } QVariant WorldList::data(const QModelIndex &index, int role) const @@ -207,6 +210,14 @@ QVariant WorldList::data(const QModelIndex &index, int role) const case SizeColumn: return locale.formattedDataSize(world.bytes()); + case InfoColumn: + if (world.isSymLinkUnder(instDirPath())) { + return tr("This world is symbolically linked from elsewhere."); + } + if (world.isMoreThanOneHardLink()) { + return tr("\nThis world is hard linked elsewhere."); + } + return ""; default: return QVariant(); } @@ -222,7 +233,16 @@ QVariant WorldList::data(const QModelIndex &index, int role) const } case Qt::ToolTipRole: - { + { + if (column == InfoColumn) { + if (world.isSymLinkUnder(instDirPath())) { + return tr("Warning: This world is symbolically linked from elsewhere. Editing it will also change the original." + "\nCanonical Path: %1").arg(world.canonicalFilePath()); + } + if (world.isMoreThanOneHardLink()) { + return tr("Warning: This world is hard linked elsewhere. Editing it will also change the original."); + } + } return world.folderName(); } case ObjectRole: @@ -274,6 +294,9 @@ QVariant WorldList::headerData(int section, Qt::Orientation orientation, int rol case SizeColumn: //: World size on disk return tr("Size"); + case InfoColumn: + //: special warnings? + return tr("Info"); default: return QVariant(); } @@ -289,6 +312,8 @@ QVariant WorldList::headerData(int section, Qt::Orientation orientation, int rol return tr("Date and time the world was last played."); case SizeColumn: return tr("Size of the world on disk."); + case InfoColumn: + return tr("Information and warnings about the world."); default: return QVariant(); } diff --git a/launcher/minecraft/WorldList.h b/launcher/minecraft/WorldList.h index 08294755..96b64193 100644 --- a/launcher/minecraft/WorldList.h +++ b/launcher/minecraft/WorldList.h @@ -21,6 +21,7 @@ #include <QAbstractListModel> #include <QMimeData> #include "minecraft/World.h" +#include "BaseInstance.h" class QFileSystemWatcher; @@ -33,7 +34,8 @@ public: NameColumn, GameModeColumn, LastPlayedColumn, - SizeColumn + SizeColumn, + InfoColumn }; enum Roles @@ -48,7 +50,7 @@ public: IconFileRole }; - WorldList(const QString &dir); + WorldList(const QString &dir, BaseInstance* instance); virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; @@ -112,6 +114,8 @@ public: return m_dir; } + QString instDirPath() const; + const QList<World> &allWorlds() const { return worlds; @@ -124,6 +128,7 @@ signals: void changed(); protected: + BaseInstance* m_instance; QFileSystemWatcher *m_watcher; bool is_watching; QDir m_dir; diff --git a/launcher/minecraft/auth/AuthRequest.cpp b/launcher/minecraft/auth/AuthRequest.cpp index bb82e1e2..a21634b7 100644 --- a/launcher/minecraft/auth/AuthRequest.cpp +++ b/launcher/minecraft/auth/AuthRequest.cpp @@ -55,12 +55,12 @@ void AuthRequest::get(const QNetworkRequest &req, int timeout/* = 60*1000*/) { reply_ = APPLICATION->network()->get(request_); status_ = Requesting; timedReplies_.add(new Katabasis::Reply(reply_, timeout)); -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - connect(reply_, SIGNAL(errorOccurred(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError))); -#else - connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError))); +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15 + connect(reply_, &QNetworkReply::errorOccurred, this, &AuthRequest::onRequestError); +#else // &QNetworkReply::error SIGNAL depricated + connect(reply_, QOverload<QNetworkReply::NetworkError>::of(&QNetworkReply::error), this, &AuthRequest::onRequestError); #endif - connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished())); + connect(reply_, &QNetworkReply::finished, this, &AuthRequest::onRequestFinished); connect(reply_, &QNetworkReply::sslErrors, this, &AuthRequest::onSslErrors); } @@ -70,14 +70,14 @@ void AuthRequest::post(const QNetworkRequest &req, const QByteArray &data, int t status_ = Requesting; reply_ = APPLICATION->network()->post(request_, data_); timedReplies_.add(new Katabasis::Reply(reply_, timeout)); -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - connect(reply_, SIGNAL(errorOccurred(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError))); -#else - connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError))); +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15 + connect(reply_, &QNetworkReply::errorOccurred, this, &AuthRequest::onRequestError); +#else // &QNetworkReply::error SIGNAL depricated + connect(reply_, QOverload<QNetworkReply::NetworkError>::of(&QNetworkReply::error), this, &AuthRequest::onRequestError); #endif - connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished())); + connect(reply_, &QNetworkReply::finished, this, &AuthRequest::onRequestFinished); connect(reply_, &QNetworkReply::sslErrors, this, &AuthRequest::onSslErrors); - connect(reply_, SIGNAL(uploadProgress(qint64,qint64)), this, SLOT(onUploadProgress(qint64,qint64))); + connect(reply_, &QNetworkReply::uploadProgress, this, &AuthRequest::onUploadProgress); } void AuthRequest::onRequestFinished() { diff --git a/launcher/minecraft/auth/MinecraftAccount.cpp b/launcher/minecraft/auth/MinecraftAccount.cpp index 73d570f1..3b050ac0 100644 --- a/launcher/minecraft/auth/MinecraftAccount.cpp +++ b/launcher/minecraft/auth/MinecraftAccount.cpp @@ -75,7 +75,7 @@ MinecraftAccountPtr MinecraftAccount::loadFromJsonV3(const QJsonObject& json) { MinecraftAccountPtr MinecraftAccount::createFromUsername(const QString &username) { - MinecraftAccountPtr account = new MinecraftAccount(); + auto account = makeShared<MinecraftAccount>(); account->data.type = AccountType::Mojang; account->data.yggdrasilToken.extra["userName"] = username; account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]")); @@ -91,7 +91,7 @@ MinecraftAccountPtr MinecraftAccount::createBlankMSA() MinecraftAccountPtr MinecraftAccount::createOffline(const QString &username) { - MinecraftAccountPtr account = new MinecraftAccount(); + auto account = makeShared<MinecraftAccount>(); account->data.type = AccountType::Offline; account->data.yggdrasilToken.token = "offline"; account->data.yggdrasilToken.validity = Katabasis::Validity::Certain; @@ -133,8 +133,8 @@ shared_qobject_ptr<AccountTask> MinecraftAccount::login(QString password) { Q_ASSERT(m_currentTask.get() == nullptr); m_currentTask.reset(new MojangLogin(&data, password)); - connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded())); - connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString))); + connect(m_currentTask.get(), &Task::succeeded, this, &MinecraftAccount::authSucceeded); + connect(m_currentTask.get(), &Task::failed, this, &MinecraftAccount::authFailed); connect(m_currentTask.get(), &Task::aborted, this, [this]{ authFailed(tr("Aborted")); }); emit activityChanged(true); return m_currentTask; @@ -144,8 +144,8 @@ shared_qobject_ptr<AccountTask> MinecraftAccount::loginMSA() { Q_ASSERT(m_currentTask.get() == nullptr); m_currentTask.reset(new MSAInteractive(&data)); - connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded())); - connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString))); + connect(m_currentTask.get(), &Task::succeeded, this, &MinecraftAccount::authSucceeded); + connect(m_currentTask.get(), &Task::failed, this, &MinecraftAccount::authFailed); connect(m_currentTask.get(), &Task::aborted, this, [this]{ authFailed(tr("Aborted")); }); emit activityChanged(true); return m_currentTask; @@ -155,8 +155,8 @@ shared_qobject_ptr<AccountTask> MinecraftAccount::loginOffline() { Q_ASSERT(m_currentTask.get() == nullptr); m_currentTask.reset(new OfflineLogin(&data)); - connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded())); - connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString))); + connect(m_currentTask.get(), &Task::succeeded, this, &MinecraftAccount::authSucceeded); + connect(m_currentTask.get(), &Task::failed, this, &MinecraftAccount::authFailed); connect(m_currentTask.get(), &Task::aborted, this, [this]{ authFailed(tr("Aborted")); }); emit activityChanged(true); return m_currentTask; @@ -177,8 +177,8 @@ shared_qobject_ptr<AccountTask> MinecraftAccount::refresh() { m_currentTask.reset(new MojangRefresh(&data)); } - connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded())); - connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString))); + connect(m_currentTask.get(), &Task::succeeded, this, &MinecraftAccount::authSucceeded); + connect(m_currentTask.get(), &Task::failed, this, &MinecraftAccount::authFailed); connect(m_currentTask.get(), &Task::aborted, this, [this]{ authFailed(tr("Aborted")); }); emit activityChanged(true); return m_currentTask; diff --git a/launcher/minecraft/auth/Parsers.cpp b/launcher/minecraft/auth/Parsers.cpp index 47473899..f3d9ad56 100644 --- a/launcher/minecraft/auth/Parsers.cpp +++ b/launcher/minecraft/auth/Parsers.cpp @@ -1,5 +1,6 @@ #include "Parsers.h" #include "Json.h" +#include "Logging.h" #include <QJsonDocument> #include <QJsonArray> @@ -75,9 +76,7 @@ bool getBool(QJsonValue value, bool & out) { bool parseXTokenResponse(QByteArray & data, Katabasis::Token &output, QString name) { qDebug() << "Parsing" << name <<":"; -#ifndef NDEBUG - qDebug() << data; -#endif + qCDebug(authCredentials()) << data; QJsonParseError jsonError; QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); if(jsonError.error) { @@ -137,9 +136,7 @@ bool parseXTokenResponse(QByteArray & data, Katabasis::Token &output, QString na bool parseMinecraftProfile(QByteArray & data, MinecraftProfile &output) { qDebug() << "Parsing Minecraft profile..."; -#ifndef NDEBUG - qDebug() << data; -#endif + qCDebug(authCredentials()) << data; QJsonParseError jsonError; QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); @@ -275,9 +272,7 @@ decoded base64 "value": bool parseMinecraftProfileMojang(QByteArray & data, MinecraftProfile &output) { qDebug() << "Parsing Minecraft profile..."; -#ifndef NDEBUG - qDebug() << data; -#endif + qCDebug(authCredentials()) << data; QJsonParseError jsonError; QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); @@ -389,9 +384,7 @@ bool parseMinecraftProfileMojang(QByteArray & data, MinecraftProfile &output) { bool parseMinecraftEntitlements(QByteArray & data, MinecraftEntitlement &output) { qDebug() << "Parsing Minecraft entitlements..."; -#ifndef NDEBUG - qDebug() << data; -#endif + qCDebug(authCredentials()) << data; QJsonParseError jsonError; QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); @@ -424,9 +417,7 @@ bool parseMinecraftEntitlements(QByteArray & data, MinecraftEntitlement &output) bool parseRolloutResponse(QByteArray & data, bool& result) { qDebug() << "Parsing Rollout response..."; -#ifndef NDEBUG - qDebug() << data; -#endif + qCDebug(authCredentials()) << data; QJsonParseError jsonError; QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); @@ -455,9 +446,7 @@ bool parseRolloutResponse(QByteArray & data, bool& result) { bool parseMojangResponse(QByteArray & data, Katabasis::Token &output) { QJsonParseError jsonError; qDebug() << "Parsing Mojang response..."; -#ifndef NDEBUG - qDebug() << data; -#endif + qCDebug(authCredentials()) << data; QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); if(jsonError.error) { qWarning() << "Failed to parse response from api.minecraftservices.com/launcher/login as JSON: " << jsonError.errorString(); diff --git a/launcher/minecraft/auth/flows/MSA.cpp b/launcher/minecraft/auth/flows/MSA.cpp index 416b8f2c..f1987e0c 100644 --- a/launcher/minecraft/auth/flows/MSA.cpp +++ b/launcher/minecraft/auth/flows/MSA.cpp @@ -10,28 +10,28 @@ #include "minecraft/auth/steps/GetSkinStep.h" MSASilent::MSASilent(AccountData* data, QObject* parent) : AuthFlow(data, parent) { - m_steps.append(new MSAStep(m_data, MSAStep::Action::Refresh)); - m_steps.append(new XboxUserStep(m_data)); - m_steps.append(new XboxAuthorizationStep(m_data, &m_data->xboxApiToken, "http://xboxlive.com", "Xbox")); - m_steps.append(new XboxAuthorizationStep(m_data, &m_data->mojangservicesToken, "rp://api.minecraftservices.com/", "Mojang")); - m_steps.append(new LauncherLoginStep(m_data)); - m_steps.append(new XboxProfileStep(m_data)); - m_steps.append(new EntitlementsStep(m_data)); - m_steps.append(new MinecraftProfileStep(m_data)); - m_steps.append(new GetSkinStep(m_data)); + m_steps.append(makeShared<MSAStep>(m_data, MSAStep::Action::Refresh)); + m_steps.append(makeShared<XboxUserStep>(m_data)); + m_steps.append(makeShared<XboxAuthorizationStep>(m_data, &m_data->xboxApiToken, "http://xboxlive.com", "Xbox")); + m_steps.append(makeShared<XboxAuthorizationStep>(m_data, &m_data->mojangservicesToken, "rp://api.minecraftservices.com/", "Mojang")); + m_steps.append(makeShared<LauncherLoginStep>(m_data)); + m_steps.append(makeShared<XboxProfileStep>(m_data)); + m_steps.append(makeShared<EntitlementsStep>(m_data)); + m_steps.append(makeShared<MinecraftProfileStep>(m_data)); + m_steps.append(makeShared<GetSkinStep>(m_data)); } MSAInteractive::MSAInteractive( AccountData* data, QObject* parent ) : AuthFlow(data, parent) { - m_steps.append(new MSAStep(m_data, MSAStep::Action::Login)); - m_steps.append(new XboxUserStep(m_data)); - m_steps.append(new XboxAuthorizationStep(m_data, &m_data->xboxApiToken, "http://xboxlive.com", "Xbox")); - m_steps.append(new XboxAuthorizationStep(m_data, &m_data->mojangservicesToken, "rp://api.minecraftservices.com/", "Mojang")); - m_steps.append(new LauncherLoginStep(m_data)); - m_steps.append(new XboxProfileStep(m_data)); - m_steps.append(new EntitlementsStep(m_data)); - m_steps.append(new MinecraftProfileStep(m_data)); - m_steps.append(new GetSkinStep(m_data)); + m_steps.append(makeShared<MSAStep>(m_data, MSAStep::Action::Login)); + m_steps.append(makeShared<XboxUserStep>(m_data)); + m_steps.append(makeShared<XboxAuthorizationStep>(m_data, &m_data->xboxApiToken, "http://xboxlive.com", "Xbox")); + m_steps.append(makeShared<XboxAuthorizationStep>(m_data, &m_data->mojangservicesToken, "rp://api.minecraftservices.com/", "Mojang")); + m_steps.append(makeShared<LauncherLoginStep>(m_data)); + m_steps.append(makeShared<XboxProfileStep>(m_data)); + m_steps.append(makeShared<EntitlementsStep>(m_data)); + m_steps.append(makeShared<MinecraftProfileStep>(m_data)); + m_steps.append(makeShared<GetSkinStep>(m_data)); } diff --git a/launcher/minecraft/auth/flows/Mojang.cpp b/launcher/minecraft/auth/flows/Mojang.cpp index b86b0936..5900ea98 100644 --- a/launcher/minecraft/auth/flows/Mojang.cpp +++ b/launcher/minecraft/auth/flows/Mojang.cpp @@ -9,10 +9,10 @@ MojangRefresh::MojangRefresh( AccountData *data, QObject *parent ) : AuthFlow(data, parent) { - m_steps.append(new YggdrasilStep(m_data, QString())); - m_steps.append(new MinecraftProfileStepMojang(m_data)); - m_steps.append(new MigrationEligibilityStep(m_data)); - m_steps.append(new GetSkinStep(m_data)); + m_steps.append(makeShared<YggdrasilStep>(m_data, QString())); + m_steps.append(makeShared<MinecraftProfileStepMojang>(m_data)); + m_steps.append(makeShared<MigrationEligibilityStep>(m_data)); + m_steps.append(makeShared<GetSkinStep>(m_data)); } MojangLogin::MojangLogin( @@ -20,8 +20,8 @@ MojangLogin::MojangLogin( QString password, QObject *parent ): AuthFlow(data, parent), m_password(password) { - m_steps.append(new YggdrasilStep(m_data, m_password)); - m_steps.append(new MinecraftProfileStepMojang(m_data)); - m_steps.append(new MigrationEligibilityStep(m_data)); - m_steps.append(new GetSkinStep(m_data)); + m_steps.append(makeShared<YggdrasilStep>(m_data, m_password)); + m_steps.append(makeShared<MinecraftProfileStepMojang>(m_data)); + m_steps.append(makeShared<MigrationEligibilityStep>(m_data)); + m_steps.append(makeShared<GetSkinStep>(m_data)); } diff --git a/launcher/minecraft/auth/flows/Offline.cpp b/launcher/minecraft/auth/flows/Offline.cpp index fc614a8c..d5c63271 100644 --- a/launcher/minecraft/auth/flows/Offline.cpp +++ b/launcher/minecraft/auth/flows/Offline.cpp @@ -6,12 +6,12 @@ OfflineRefresh::OfflineRefresh( AccountData *data, QObject *parent ) : AuthFlow(data, parent) { - m_steps.append(new OfflineStep(m_data)); + m_steps.append(makeShared<OfflineStep>(m_data)); } OfflineLogin::OfflineLogin( AccountData *data, QObject *parent ) : AuthFlow(data, parent) { - m_steps.append(new OfflineStep(m_data)); + m_steps.append(makeShared<OfflineStep>(m_data)); } diff --git a/launcher/minecraft/auth/steps/EntitlementsStep.cpp b/launcher/minecraft/auth/steps/EntitlementsStep.cpp index f726244f..bd604292 100644 --- a/launcher/minecraft/auth/steps/EntitlementsStep.cpp +++ b/launcher/minecraft/auth/steps/EntitlementsStep.cpp @@ -3,6 +3,7 @@ #include <QNetworkRequest> #include <QUuid> +#include "Logging.h" #include "minecraft/auth/AuthRequest.h" #include "minecraft/auth/Parsers.h" @@ -41,9 +42,7 @@ void EntitlementsStep::onRequestDone( auto requestor = qobject_cast<AuthRequest *>(QObject::sender()); requestor->deleteLater(); -#ifndef NDEBUG - qDebug() << data; -#endif + qCDebug(authCredentials()) << data; // TODO: check presence of same entitlementsRequestId? // TODO: validate JWTs? diff --git a/launcher/minecraft/auth/steps/LauncherLoginStep.cpp b/launcher/minecraft/auth/steps/LauncherLoginStep.cpp index 8c53f037..8a26cbe7 100644 --- a/launcher/minecraft/auth/steps/LauncherLoginStep.cpp +++ b/launcher/minecraft/auth/steps/LauncherLoginStep.cpp @@ -2,9 +2,10 @@ #include <QNetworkRequest> +#include "Logging.h" +#include "minecraft/auth/AccountTask.h" #include "minecraft/auth/AuthRequest.h" #include "minecraft/auth/Parsers.h" -#include "minecraft/auth/AccountTask.h" #include "net/NetUtils.h" LauncherLoginStep::LauncherLoginStep(AccountData* data) : AuthStep(data) { @@ -51,14 +52,10 @@ void LauncherLoginStep::onRequestDone( auto requestor = qobject_cast<AuthRequest *>(QObject::sender()); requestor->deleteLater(); -#ifndef NDEBUG - qDebug() << data; -#endif + qCDebug(authCredentials()) << data; if (error != QNetworkReply::NoError) { qWarning() << "Reply error:" << error; -#ifndef NDEBUG - qDebug() << data; -#endif + qCDebug(authCredentials()) << data; if (Net::isApplicationError(error)) { emit finished( AccountTaskState::STATE_FAILED_SOFT, @@ -76,9 +73,7 @@ void LauncherLoginStep::onRequestDone( if(!Parsers::parseMojangResponse(data, m_data->yggdrasilToken)) { qWarning() << "Could not parse login_with_xbox response..."; -#ifndef NDEBUG - qDebug() << data; -#endif + qCDebug(authCredentials()) << data; emit finished( AccountTaskState::STATE_FAILED_SOFT, tr("Failed to parse the Minecraft access token response.") diff --git a/launcher/minecraft/auth/steps/MSAStep.cpp b/launcher/minecraft/auth/steps/MSAStep.cpp index 16afcb42..6fc8d468 100644 --- a/launcher/minecraft/auth/steps/MSAStep.cpp +++ b/launcher/minecraft/auth/steps/MSAStep.cpp @@ -42,6 +42,7 @@ #include "minecraft/auth/Parsers.h" #include "Application.h" +#include "Logging.h" using OAuth2 = Katabasis::DeviceFlow; using Activity = Katabasis::Activity; @@ -117,14 +118,12 @@ void MSAStep::onOAuthActivityChanged(Katabasis::Activity activity) { // Succeeded or did not invalidate tokens emit hideVerificationUriAndCode(); QVariantMap extraTokens = m_oauth2->extraTokens(); -#ifndef NDEBUG if (!extraTokens.isEmpty()) { - qDebug() << "Extra tokens in response:"; + qCDebug(authCredentials()) << "Extra tokens in response:"; foreach (QString key, extraTokens.keys()) { - qDebug() << "\t" << key << ":" << extraTokens.value(key); + qCDebug(authCredentials()) << "\t" << key << ":" << extraTokens.value(key); } } -#endif emit finished(AccountTaskState::STATE_WORKING, tr("Got ")); return; } diff --git a/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp b/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp index b39b9326..6cfa7c1c 100644 --- a/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp +++ b/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp @@ -2,6 +2,7 @@ #include <QNetworkRequest> +#include "Logging.h" #include "minecraft/auth/AuthRequest.h" #include "minecraft/auth/Parsers.h" #include "net/NetUtils.h" @@ -40,9 +41,7 @@ void MinecraftProfileStep::onRequestDone( auto requestor = qobject_cast<AuthRequest *>(QObject::sender()); requestor->deleteLater(); -#ifndef NDEBUG - qDebug() << data; -#endif + qCDebug(authCredentials()) << data; if (error == QNetworkReply::ContentNotFoundError) { // NOTE: Succeed even if we do not have a profile. This is a valid account state. if(m_data->type == AccountType::Mojang) { diff --git a/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp b/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp index 6a1eb7a0..8c378588 100644 --- a/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp +++ b/launcher/minecraft/auth/steps/MinecraftProfileStepMojang.cpp @@ -2,6 +2,7 @@ #include <QNetworkRequest> +#include "Logging.h" #include "minecraft/auth/AuthRequest.h" #include "minecraft/auth/Parsers.h" #include "net/NetUtils.h" @@ -43,9 +44,7 @@ void MinecraftProfileStepMojang::onRequestDone( auto requestor = qobject_cast<AuthRequest *>(QObject::sender()); requestor->deleteLater(); -#ifndef NDEBUG - qDebug() << data; -#endif + qCDebug(authCredentials()) << data; if (error == QNetworkReply::ContentNotFoundError) { // NOTE: Succeed even if we do not have a profile. This is a valid account state. if(m_data->type == AccountType::Mojang) { diff --git a/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp b/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp index 14bde47e..b397b734 100644 --- a/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp +++ b/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp @@ -4,6 +4,7 @@ #include <QJsonParseError> #include <QJsonDocument> +#include "Logging.h" #include "minecraft/auth/AuthRequest.h" #include "minecraft/auth/Parsers.h" #include "net/NetUtils.h" @@ -58,9 +59,7 @@ void XboxAuthorizationStep::onRequestDone( auto requestor = qobject_cast<AuthRequest *>(QObject::sender()); requestor->deleteLater(); -#ifndef NDEBUG - qDebug() << data; -#endif + qCDebug(authCredentials()) << data; if (error != QNetworkReply::NoError) { qWarning() << "Reply error:" << error; if (Net::isApplicationError(error)) { diff --git a/launcher/minecraft/auth/steps/XboxProfileStep.cpp b/launcher/minecraft/auth/steps/XboxProfileStep.cpp index 738fe1db..644c419b 100644 --- a/launcher/minecraft/auth/steps/XboxProfileStep.cpp +++ b/launcher/minecraft/auth/steps/XboxProfileStep.cpp @@ -3,7 +3,7 @@ #include <QNetworkRequest> #include <QUrlQuery> - +#include "Logging.h" #include "minecraft/auth/AuthRequest.h" #include "minecraft/auth/Parsers.h" #include "net/NetUtils.h" @@ -56,9 +56,7 @@ void XboxProfileStep::onRequestDone( if (error != QNetworkReply::NoError) { qWarning() << "Reply error:" << error; -#ifndef NDEBUG - qDebug() << data; -#endif + qCDebug(authCredentials()) << data; if (Net::isApplicationError(error)) { emit finished( AccountTaskState::STATE_FAILED_SOFT, @@ -74,9 +72,7 @@ void XboxProfileStep::onRequestDone( return; } -#ifndef NDEBUG - qDebug() << "XBox profile: " << data; -#endif + qCDebug(authCredentials()) << "XBox profile: " << data; emit finished(AccountTaskState::STATE_WORKING, tr("Got Xbox profile")); } diff --git a/launcher/minecraft/auth/steps/XboxUserStep.cpp b/launcher/minecraft/auth/steps/XboxUserStep.cpp index 53069597..842eb60f 100644 --- a/launcher/minecraft/auth/steps/XboxUserStep.cpp +++ b/launcher/minecraft/auth/steps/XboxUserStep.cpp @@ -38,6 +38,10 @@ void XboxUserStep::perform() { QNetworkRequest request = QNetworkRequest(QUrl("https://user.auth.xboxlive.com/user/authenticate")); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); request.setRawHeader("Accept", "application/json"); + // set contract-verison header (prevent err 400 bad-request?) + // https://learn.microsoft.com/en-us/gaming/gdk/_content/gc/reference/live/rest/additional/httpstandardheaders + request.setRawHeader("x-xbl-contract-version", "1"); + auto *requestor = new AuthRequest(this); connect(requestor, &AuthRequest::finished, this, &XboxUserStep::onRequestDone); requestor->post(request, xbox_auth_data.toUtf8()); diff --git a/launcher/minecraft/launch/LauncherPartLaunch.cpp b/launcher/minecraft/launch/LauncherPartLaunch.cpp index 1d8d7083..8ecf715d 100644 --- a/launcher/minecraft/launch/LauncherPartLaunch.cpp +++ b/launcher/minecraft/launch/LauncherPartLaunch.cpp @@ -36,6 +36,7 @@ #include "LauncherPartLaunch.h" #include <QStandardPaths> +#include <QRegularExpression> #include "launch/LaunchTask.h" #include "minecraft/MinecraftInstance.h" diff --git a/launcher/minecraft/launch/ScanModFolders.cpp b/launcher/minecraft/launch/ScanModFolders.cpp index bdffeadd..71e7638c 100644 --- a/launcher/minecraft/launch/ScanModFolders.cpp +++ b/launcher/minecraft/launch/ScanModFolders.cpp @@ -55,6 +55,12 @@ void ScanModFolders::executeTask() if(!cores->update()) { m_coreModsDone = true; } + + auto nils = m_inst->nilModList(); + connect(nils.get(), &ModFolderModel::updateFinished, this, &ScanModFolders::nilModsDone); + if(!nils->update()) { + m_nilModsDone = true; + } checkDone(); } @@ -70,9 +76,15 @@ void ScanModFolders::coreModsDone() checkDone(); } +void ScanModFolders::nilModsDone() +{ + m_nilModsDone = true; + checkDone(); +} + void ScanModFolders::checkDone() { - if(m_modsDone && m_coreModsDone) { + if(m_modsDone && m_coreModsDone && m_nilModsDone) { emitSucceeded(); } } diff --git a/launcher/minecraft/launch/ScanModFolders.h b/launcher/minecraft/launch/ScanModFolders.h index d5989170..111a5850 100644 --- a/launcher/minecraft/launch/ScanModFolders.h +++ b/launcher/minecraft/launch/ScanModFolders.h @@ -33,10 +33,12 @@ public: private slots: void coreModsDone(); void modsDone(); + void nilModsDone(); private: void checkDone(); private: // DATA bool m_modsDone = false; + bool m_nilModsDone = false; bool m_coreModsDone = false; }; diff --git a/launcher/minecraft/mod/DataPack.cpp b/launcher/minecraft/mod/DataPack.cpp new file mode 100644 index 00000000..5c58f6b2 --- /dev/null +++ b/launcher/minecraft/mod/DataPack.cpp @@ -0,0 +1,108 @@ +// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include "DataPack.h" + +#include <QDebug> +#include <QMap> +#include <QRegularExpression> + +#include "Version.h" + +// Values taken from: +// https://minecraft.fandom.com/wiki/Tutorials/Creating_a_data_pack#%22pack_format%22 +static const QMap<int, std::pair<Version, Version>> s_pack_format_versions = { + { 4, { Version("1.13"), Version("1.14.4") } }, { 5, { Version("1.15"), Version("1.16.1") } }, + { 6, { Version("1.16.2"), Version("1.16.5") } }, { 7, { Version("1.17"), Version("1.17.1") } }, + { 8, { Version("1.18"), Version("1.18.1") } }, { 9, { Version("1.18.2"), Version("1.18.2") } }, + { 10, { Version("1.19"), Version("1.19.3") } }, +}; + +void DataPack::setPackFormat(int new_format_id) +{ + QMutexLocker locker(&m_data_lock); + + if (!s_pack_format_versions.contains(new_format_id)) { + qWarning() << "Pack format '" << new_format_id << "' is not a recognized data pack id!"; + } + + m_pack_format = new_format_id; +} + +void DataPack::setDescription(QString new_description) +{ + QMutexLocker locker(&m_data_lock); + + m_description = new_description; +} + +std::pair<Version, Version> DataPack::compatibleVersions() const +{ + if (!s_pack_format_versions.contains(m_pack_format)) { + return { {}, {} }; + } + + return s_pack_format_versions.constFind(m_pack_format).value(); +} + +std::pair<int, bool> DataPack::compare(const Resource& other, SortType type) const +{ + auto const& cast_other = static_cast<DataPack const&>(other); + + switch (type) { + default: { + auto res = Resource::compare(other, type); + if (res.first != 0) + return res; + } + case SortType::PACK_FORMAT: { + auto this_ver = packFormat(); + auto other_ver = cast_other.packFormat(); + + if (this_ver > other_ver) + return { 1, type == SortType::PACK_FORMAT }; + if (this_ver < other_ver) + return { -1, type == SortType::PACK_FORMAT }; + } + } + return { 0, false }; +} + +bool DataPack::applyFilter(QRegularExpression filter) const +{ + if (filter.match(description()).hasMatch()) + return true; + + if (filter.match(QString::number(packFormat())).hasMatch()) + return true; + + if (filter.match(compatibleVersions().first.toString()).hasMatch()) + return true; + if (filter.match(compatibleVersions().second.toString()).hasMatch()) + return true; + + return Resource::applyFilter(filter); +} + +bool DataPack::valid() const +{ + return m_pack_format != 0; +} diff --git a/launcher/minecraft/mod/DataPack.h b/launcher/minecraft/mod/DataPack.h new file mode 100644 index 00000000..fc2703c7 --- /dev/null +++ b/launcher/minecraft/mod/DataPack.h @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "Resource.h" + +#include <QMutex> + +class Version; + +/* TODO: + * + * Store localized descriptions + * */ + +class DataPack : public Resource { + Q_OBJECT + public: + using Ptr = shared_qobject_ptr<Resource>; + + DataPack(QObject* parent = nullptr) : Resource(parent) {} + DataPack(QFileInfo file_info) : Resource(file_info) {} + + /** Gets the numerical ID of the pack format. */ + [[nodiscard]] int packFormat() const { return m_pack_format; } + /** Gets, respectively, the lower and upper versions supported by the set pack format. */ + [[nodiscard]] std::pair<Version, Version> compatibleVersions() const; + + /** Gets the description of the data pack. */ + [[nodiscard]] QString description() const { return m_description; } + + /** Thread-safe. */ + void setPackFormat(int new_format_id); + + /** Thread-safe. */ + void setDescription(QString new_description); + + bool valid() const override; + + [[nodiscard]] auto compare(Resource const& other, SortType type) const -> std::pair<int, bool> override; + [[nodiscard]] bool applyFilter(QRegularExpression filter) const override; + + protected: + mutable QMutex m_data_lock; + + /* The 'version' of a data pack, as defined in the pack.mcmeta file. + * See https://minecraft.fandom.com/wiki/Data_pack#pack.mcmeta + */ + int m_pack_format = 0; + + /** The data pack's description, as defined in the pack.mcmeta file. + */ + QString m_description; +}; diff --git a/launcher/minecraft/mod/Mod.cpp b/launcher/minecraft/mod/Mod.cpp index 39023f69..c495cd47 100644 --- a/launcher/minecraft/mod/Mod.cpp +++ b/launcher/minecraft/mod/Mod.cpp @@ -43,6 +43,9 @@ #include "MetadataHandler.h" #include "Version.h" +#include "minecraft/mod/ModDetails.h" + +static ModPlatform::ProviderCapabilities ProviderCaps; Mod::Mod(const QFileInfo& file) : Resource(file), m_local_details() { @@ -68,6 +71,10 @@ void Mod::setMetadata(std::shared_ptr<Metadata::ModStruct>&& metadata) m_local_details.metadata = metadata; } +void Mod::setDetails(const ModDetails& details) { + m_local_details = details; +} + std::pair<int, bool> Mod::compare(const Resource& other, SortType type) const { auto cast_other = dynamic_cast<Mod const*>(&other); @@ -91,6 +98,11 @@ std::pair<int, bool> Mod::compare(const Resource& other, SortType type) const if (this_ver < other_ver) return { -1, type == SortType::VERSION }; } + case SortType::PROVIDER: { + auto compare_result = QString::compare(provider().value_or("Unknown"), cast_other->provider().value_or("Unknown"), Qt::CaseInsensitive); + if (compare_result != 0) + return { compare_result, type == SortType::PROVIDER }; + } } return { 0, false }; } @@ -189,4 +201,16 @@ void Mod::finishResolvingWithDetails(ModDetails&& details) m_local_details = std::move(details); if (metadata) setMetadata(std::move(metadata)); +}; + +auto Mod::provider() const -> std::optional<QString> +{ + if (metadata()) + return ProviderCaps.readableName(metadata()->provider); + return {}; +} + +bool Mod::valid() const +{ + return !m_local_details.mod_id.isEmpty(); } diff --git a/launcher/minecraft/mod/Mod.h b/launcher/minecraft/mod/Mod.h index f336bec4..c4032538 100644 --- a/launcher/minecraft/mod/Mod.h +++ b/launcher/minecraft/mod/Mod.h @@ -39,6 +39,8 @@ #include <QFileInfo> #include <QList> +#include <optional> + #include "Resource.h" #include "ModDetails.h" @@ -61,6 +63,7 @@ public: auto description() const -> QString; auto authors() const -> QStringList; auto status() const -> ModStatus; + auto provider() const -> std::optional<QString>; auto metadata() -> std::shared_ptr<Metadata::ModStruct>; auto metadata() const -> const std::shared_ptr<Metadata::ModStruct>; @@ -68,6 +71,9 @@ public: void setStatus(ModStatus status); void setMetadata(std::shared_ptr<Metadata::ModStruct>&& metadata); void setMetadata(const Metadata::ModStruct& metadata) { setMetadata(std::make_shared<Metadata::ModStruct>(metadata)); } + void setDetails(const ModDetails& details); + + bool valid() const override; [[nodiscard]] auto compare(Resource const& other, SortType type) const -> std::pair<int, bool> override; [[nodiscard]] bool applyFilter(QRegularExpression filter) const override; diff --git a/launcher/minecraft/mod/ModDetails.h b/launcher/minecraft/mod/ModDetails.h index dd84b0a3..176e4fc1 100644 --- a/launcher/minecraft/mod/ModDetails.h +++ b/launcher/minecraft/mod/ModDetails.h @@ -81,7 +81,7 @@ struct ModDetails ModDetails() = default; /** Metadata should be handled manually to properly set the mod status. */ - ModDetails(ModDetails& other) + ModDetails(const ModDetails& other) : mod_id(other.mod_id) , name(other.name) , version(other.version) @@ -92,7 +92,7 @@ struct ModDetails , status(other.status) {} - ModDetails& operator=(ModDetails& other) + ModDetails& operator=(const ModDetails& other) { this->mod_id = other.mod_id; this->name = other.name; @@ -106,7 +106,7 @@ struct ModDetails return *this; } - ModDetails& operator=(ModDetails&& other) + ModDetails& operator=(const ModDetails&& other) { this->mod_id = other.mod_id; this->name = other.name; diff --git a/launcher/minecraft/mod/ModFolderModel.cpp b/launcher/minecraft/mod/ModFolderModel.cpp index 4ccc5d4d..5e3b31e0 100644 --- a/launcher/minecraft/mod/ModFolderModel.cpp +++ b/launcher/minecraft/mod/ModFolderModel.cpp @@ -39,19 +39,25 @@ #include <FileSystem.h> #include <QDebug> #include <QFileSystemWatcher> +#include <QIcon> #include <QMimeData> #include <QString> +#include <QStyle> #include <QThreadPool> #include <QUrl> #include <QUuid> #include <algorithm> +#include "Application.h" + #include "minecraft/mod/tasks/LocalModParseTask.h" #include "minecraft/mod/tasks/ModFolderLoadTask.h" +#include "modplatform/ModIndex.h" -ModFolderModel::ModFolderModel(const QString &dir, bool is_indexed) : ResourceFolderModel(QDir(dir)), m_is_indexed(is_indexed) +ModFolderModel::ModFolderModel(const QString& dir, BaseInstance* instance, bool is_indexed, bool create_dir) + : ResourceFolderModel(QDir(dir), instance, nullptr, create_dir), m_is_indexed(is_indexed) { - m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::VERSION, SortType::DATE }; + m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::VERSION, SortType::DATE, SortType::PROVIDER }; } QVariant ModFolderModel::data(const QModelIndex &index, int role) const @@ -82,14 +88,39 @@ QVariant ModFolderModel::data(const QModelIndex &index, int role) const } case DateColumn: return m_resources[row]->dateTimeChanged(); + case ProviderColumn: { + auto provider = at(row)->provider(); + if (!provider.has_value()) { + //: Unknown mod provider (i.e. not Modrinth, CurseForge, etc...) + return tr("Unknown"); + } + return provider.value(); + } default: return QVariant(); } case Qt::ToolTipRole: + if (column == NAME_COLUMN) { + if (at(row)->isSymLinkUnder(instDirPath())) { + return m_resources[row]->internal_id() + + tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original." + "\nCanonical Path: %1") + .arg(at(row)->fileinfo().canonicalFilePath()); + } + if (at(row)->isMoreThanOneHardLink()) { + return m_resources[row]->internal_id() + + tr("\nWarning: This resource is hard linked elsewhere. Editing it will also change the original."); + } + } return m_resources[row]->internal_id(); + case Qt::DecorationRole: { + if (column == NAME_COLUMN && (at(row)->isSymLinkUnder(instDirPath()) || at(row)->isMoreThanOneHardLink())) + return APPLICATION->getThemedIcon("status-yellow"); + return {}; + } case Qt::CheckStateRole: switch (column) { @@ -118,6 +149,8 @@ QVariant ModFolderModel::headerData(int section, Qt::Orientation orientation, in return tr("Version"); case DateColumn: return tr("Last changed"); + case ProviderColumn: + return tr("Provider"); default: return QVariant(); } @@ -133,6 +166,8 @@ QVariant ModFolderModel::headerData(int section, Qt::Orientation orientation, in return tr("The version of the mod."); case DateColumn: return tr("The date and time this mod was last changed (or added)."); + case ProviderColumn: + return tr("Where the mod was downloaded from."); default: return QVariant(); } diff --git a/launcher/minecraft/mod/ModFolderModel.h b/launcher/minecraft/mod/ModFolderModel.h index 93980319..d337fe29 100644 --- a/launcher/minecraft/mod/ModFolderModel.h +++ b/launcher/minecraft/mod/ModFolderModel.h @@ -67,6 +67,7 @@ public: NameColumn, VersionColumn, DateColumn, + ProviderColumn, NUM_COLUMNS }; enum ModStatusAction { @@ -74,7 +75,7 @@ public: Enable, Toggle }; - ModFolderModel(const QString &dir, bool is_indexed = false); + ModFolderModel(const QString &dir, BaseInstance* instance, bool is_indexed = false, bool create_dir = true); QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; diff --git a/launcher/minecraft/mod/Resource.cpp b/launcher/minecraft/mod/Resource.cpp index 0fbcfd7c..a0b8a4bb 100644 --- a/launcher/minecraft/mod/Resource.cpp +++ b/launcher/minecraft/mod/Resource.cpp @@ -1,6 +1,8 @@ #include "Resource.h" + #include <QRegularExpression> +#include <QFileInfo> #include "FileSystem.h" @@ -37,6 +39,9 @@ void Resource::parseFile() if (file_name.endsWith(".zip") || file_name.endsWith(".jar")) { m_type = ResourceType::ZIPFILE; file_name.chop(4); + } else if (file_name.endsWith(".nilmod")) { + m_type = ResourceType::ZIPFILE; + file_name.chop(7); } else if (file_name.endsWith(".litemod")) { m_type = ResourceType::LITEMOD; file_name.chop(8); @@ -143,5 +148,27 @@ bool Resource::enable(EnableAction action) bool Resource::destroy() { m_type = ResourceType::UNKNOWN; + + if (FS::trash(m_file_info.filePath())) + return true; + return FS::deletePath(m_file_info.filePath()); } + +bool Resource::isSymLinkUnder(const QString& instPath) const +{ + if (isSymLink()) + return true; + + auto instDir = QDir(instPath); + + auto relAbsPath = instDir.relativeFilePath(m_file_info.absoluteFilePath()); + auto relCanonPath = instDir.relativeFilePath(m_file_info.canonicalFilePath()); + + return relAbsPath != relCanonPath; +} + +bool Resource::isMoreThanOneHardLink() const +{ + return FS::hardLinkCount(m_file_info.absoluteFilePath()) > 1; +} diff --git a/launcher/minecraft/mod/Resource.h b/launcher/minecraft/mod/Resource.h index f9bd811e..a5e9ae91 100644 --- a/launcher/minecraft/mod/Resource.h +++ b/launcher/minecraft/mod/Resource.h @@ -20,7 +20,8 @@ enum class SortType { DATE, VERSION, ENABLED, - PACK_FORMAT + PACK_FORMAT, + PROVIDER }; enum class EnableAction { @@ -93,6 +94,19 @@ class Resource : public QObject { // Delete all files of this resource. bool destroy(); + [[nodiscard]] auto isSymLink() const -> bool { return m_file_info.isSymLink(); } + + /** + * @brief Take a instance path, checks if the file pointed to by the resource is a symlink or under a symlink in that instance + * + * @param instPath path to an instance directory + * @return true + * @return false + */ + [[nodiscard]] bool isSymLinkUnder(const QString& instPath) const; + + [[nodiscard]] bool isMoreThanOneHardLink() const; + protected: /* The file corresponding to this resource. */ QFileInfo m_file_info; diff --git a/launcher/minecraft/mod/ResourceFolderModel.cpp b/launcher/minecraft/mod/ResourceFolderModel.cpp index a52c5db3..d2d875e4 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.cpp +++ b/launcher/minecraft/mod/ResourceFolderModel.cpp @@ -2,25 +2,32 @@ #include <QCoreApplication> #include <QDebug> +#include <QFileInfo> +#include <QIcon> #include <QMimeData> +#include <QStyle> #include <QThreadPool> #include <QUrl> +#include "Application.h" #include "FileSystem.h" #include "minecraft/mod/tasks/BasicFolderLoadTask.h" #include "tasks/Task.h" -ResourceFolderModel::ResourceFolderModel(QDir dir, QObject* parent) : QAbstractListModel(parent), m_dir(dir), m_watcher(this) +ResourceFolderModel::ResourceFolderModel(QDir dir, BaseInstance* instance, QObject* parent, bool create_dir) + : QAbstractListModel(parent), m_dir(dir), m_instance(instance), m_watcher(this) { - FS::ensureFolderPathExists(m_dir.absolutePath()); + if (create_dir) { + FS::ensureFolderPathExists(m_dir.absolutePath()); + } m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs); m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &ResourceFolderModel::directoryChanged); - connect(&m_helper_thread_task, &ConcurrentTask::finished, this, [this]{ m_helper_thread_task.clear(); }); + connect(&m_helper_thread_task, &ConcurrentTask::finished, this, [this] { m_helper_thread_task.clear(); }); } ResourceFolderModel::~ResourceFolderModel() @@ -260,7 +267,7 @@ void ResourceFolderModel::resolveResource(Resource* res) return; } - auto task = createParseTask(*res); + Task::Ptr task{ createParseTask(*res) }; if (!task) return; @@ -270,11 +277,11 @@ void ResourceFolderModel::resolveResource(Resource* res) m_active_parse_tasks.insert(ticket, task); connect( - task, &Task::succeeded, this, [=] { onParseSucceeded(ticket, res->internal_id()); }, Qt::ConnectionType::QueuedConnection); + task.get(), &Task::succeeded, this, [=] { onParseSucceeded(ticket, res->internal_id()); }, Qt::ConnectionType::QueuedConnection); connect( - task, &Task::failed, this, [=] { onParseFailed(ticket, res->internal_id()); }, Qt::ConnectionType::QueuedConnection); + task.get(), &Task::failed, this, [=] { onParseFailed(ticket, res->internal_id()); }, Qt::ConnectionType::QueuedConnection); connect( - task, &Task::finished, this, [=] { m_active_parse_tasks.remove(ticket); }, Qt::ConnectionType::QueuedConnection); + task.get(), &Task::finished, this, [=] { m_active_parse_tasks.remove(ticket); }, Qt::ConnectionType::QueuedConnection); m_helper_thread_task.addTask(task); @@ -415,7 +422,26 @@ QVariant ResourceFolderModel::data(const QModelIndex& index, int role) const return {}; } case Qt::ToolTipRole: + if (column == NAME_COLUMN) { + if (at(row).isSymLinkUnder(instDirPath())) { + return m_resources[row]->internal_id() + + tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original." + "\nCanonical Path: %1") + .arg(at(row).fileinfo().canonicalFilePath());; + } + if (at(row).isMoreThanOneHardLink()) { + return m_resources[row]->internal_id() + + tr("\nWarning: This resource is hard linked elsewhere. Editing it will also change the original."); + } + } + return m_resources[row]->internal_id(); + case Qt::DecorationRole: { + if (column == NAME_COLUMN && (at(row).isSymLinkUnder(instDirPath()) || at(row).isMoreThanOneHardLink())) + return APPLICATION->getThemedIcon("status-yellow"); + + return {}; + } case Qt::CheckStateRole: switch (column) { case ACTIVE_COLUMN: @@ -529,3 +555,7 @@ void ResourceFolderModel::enableInteraction(bool enabled) return (compare_result.first < 0); return (compare_result.first > 0); } + +QString ResourceFolderModel::instDirPath() const { + return QFileInfo(m_instance->instanceRoot()).absoluteFilePath(); +} diff --git a/launcher/minecraft/mod/ResourceFolderModel.h b/launcher/minecraft/mod/ResourceFolderModel.h index f1bc2dd7..0a35e1bc 100644 --- a/launcher/minecraft/mod/ResourceFolderModel.h +++ b/launcher/minecraft/mod/ResourceFolderModel.h @@ -9,6 +9,8 @@ #include "Resource.h" +#include "BaseInstance.h" + #include "tasks/Task.h" #include "tasks/ConcurrentTask.h" @@ -24,7 +26,7 @@ class QSortFilterProxyModel; class ResourceFolderModel : public QAbstractListModel { Q_OBJECT public: - ResourceFolderModel(QDir, QObject* parent = nullptr); + ResourceFolderModel(QDir, BaseInstance* instance, QObject* parent = nullptr, bool create_dir = true); ~ResourceFolderModel() override; /** Starts watching the paths for changes. @@ -125,6 +127,8 @@ class ResourceFolderModel : public QAbstractListModel { [[nodiscard]] bool lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const override; }; + QString instDirPath() const; + public slots: void enableInteraction(bool enabled); void disableInteraction(bool disabled) { enableInteraction(!disabled); } @@ -187,6 +191,7 @@ class ResourceFolderModel : public QAbstractListModel { bool m_can_interact = true; QDir m_dir; + BaseInstance* m_instance; QFileSystemWatcher m_watcher; bool m_is_watching = false; diff --git a/launcher/minecraft/mod/ResourcePack.cpp b/launcher/minecraft/mod/ResourcePack.cpp index 3a2fd771..876d5c3e 100644 --- a/launcher/minecraft/mod/ResourcePack.cpp +++ b/launcher/minecraft/mod/ResourcePack.cpp @@ -13,11 +13,12 @@ // Values taken from: // https://minecraft.fandom.com/wiki/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta static const QMap<int, std::pair<Version, Version>> s_pack_format_versions = { - { 1, { Version("1.6.1"), Version("1.8.9") } }, { 2, { Version("1.9"), Version("1.10.2") } }, - { 3, { Version("1.11"), Version("1.12.2") } }, { 4, { Version("1.13"), Version("1.14.4") } }, - { 5, { Version("1.15"), Version("1.16.1") } }, { 6, { Version("1.16.2"), Version("1.16.5") } }, - { 7, { Version("1.17"), Version("1.17.1") } }, { 8, { Version("1.18"), Version("1.18.2") } }, - { 9, { Version("1.19"), Version("1.19.2") } }, { 11, { Version("1.19.3"), Version("1.19.3") } }, + { 1, { Version("1.6.1"), Version("1.8.9") } }, { 2, { Version("1.9"), Version("1.10.2") } }, + { 3, { Version("1.11"), Version("1.12.2") } }, { 4, { Version("1.13"), Version("1.14.4") } }, + { 5, { Version("1.15"), Version("1.16.1") } }, { 6, { Version("1.16.2"), Version("1.16.5") } }, + { 7, { Version("1.17"), Version("1.17.1") } }, { 8, { Version("1.18"), Version("1.18.2") } }, + { 9, { Version("1.19"), Version("1.19.2") } }, { 11, { Version("22w42a"), Version("22w44a") } }, + { 12, { Version("1.19.3"), Version("1.19.3") } }, }; void ResourcePack::setPackFormat(int new_format_id) @@ -25,7 +26,7 @@ void ResourcePack::setPackFormat(int new_format_id) QMutexLocker locker(&m_data_lock); if (!s_pack_format_versions.contains(new_format_id)) { - qWarning() << "Pack format '%1' is not a recognized resource pack id!"; + qWarning() << "Pack format '" << new_format_id << "' is not a recognized resource pack id!"; } m_pack_format = new_format_id; diff --git a/launcher/minecraft/mod/ResourcePackFolderModel.cpp b/launcher/minecraft/mod/ResourcePackFolderModel.cpp index ebac707d..c12d1f23 100644 --- a/launcher/minecraft/mod/ResourcePackFolderModel.cpp +++ b/launcher/minecraft/mod/ResourcePackFolderModel.cpp @@ -36,12 +36,17 @@ #include "ResourcePackFolderModel.h" +#include <QIcon> +#include <QStyle> + +#include "Application.h" #include "Version.h" #include "minecraft/mod/tasks/BasicFolderLoadTask.h" #include "minecraft/mod/tasks/LocalResourcePackParseTask.h" -ResourcePackFolderModel::ResourcePackFolderModel(const QString& dir) : ResourceFolderModel(QDir(dir)) +ResourcePackFolderModel::ResourcePackFolderModel(const QString& dir, BaseInstance* instance) + : ResourceFolderModel(QDir(dir), instance) { m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::PACK_FORMAT, SortType::DATE }; } @@ -78,12 +83,29 @@ QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const default: return {}; } + case Qt::DecorationRole: { + if (column == NAME_COLUMN && (at(row)->isSymLinkUnder(instDirPath()) || at(row)->isMoreThanOneHardLink())) + return APPLICATION->getThemedIcon("status-yellow"); + return {}; + } case Qt::ToolTipRole: { if (column == PackFormatColumn) { //: The string being explained by this is in the format: ID (Lower version - Upper version) return tr("The resource pack format ID, as well as the Minecraft versions it was designed for."); } + if (column == NAME_COLUMN) { + if (at(row)->isSymLinkUnder(instDirPath())) { + return m_resources[row]->internal_id() + + tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also change the original." + "\nCanonical Path: %1") + .arg(at(row)->fileinfo().canonicalFilePath());; + } + if (at(row)->isMoreThanOneHardLink()) { + return m_resources[row]->internal_id() + + tr("\nWarning: This resource is hard linked elsewhere. Editing it will also change the original."); + } + } return m_resources[row]->internal_id(); } case Qt::CheckStateRole: @@ -142,7 +164,7 @@ int ResourcePackFolderModel::columnCount(const QModelIndex& parent) const Task* ResourcePackFolderModel::createUpdateTask() { - return new BasicFolderLoadTask(m_dir, [](QFileInfo const& entry) { return new ResourcePack(entry); }); + return new BasicFolderLoadTask(m_dir, [](QFileInfo const& entry) { return makeShared<ResourcePack>(entry); }); } Task* ResourcePackFolderModel::createParseTask(Resource& resource) diff --git a/launcher/minecraft/mod/ResourcePackFolderModel.h b/launcher/minecraft/mod/ResourcePackFolderModel.h index cb620ce2..db4b14fb 100644 --- a/launcher/minecraft/mod/ResourcePackFolderModel.h +++ b/launcher/minecraft/mod/ResourcePackFolderModel.h @@ -17,7 +17,7 @@ public: NUM_COLUMNS }; - explicit ResourcePackFolderModel(const QString &dir); + explicit ResourcePackFolderModel(const QString &dir, BaseInstance* instance); [[nodiscard]] QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; diff --git a/launcher/minecraft/mod/ShaderPack.cpp b/launcher/minecraft/mod/ShaderPack.cpp new file mode 100644 index 00000000..6a9641de --- /dev/null +++ b/launcher/minecraft/mod/ShaderPack.cpp @@ -0,0 +1,37 @@ + +// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include "ShaderPack.h" + +#include "minecraft/mod/tasks/LocalShaderPackParseTask.h" + +void ShaderPack::setPackFormat(ShaderPackFormat new_format) +{ + QMutexLocker locker(&m_data_lock); + + m_pack_format = new_format; +} + +bool ShaderPack::valid() const +{ + return m_pack_format != ShaderPackFormat::INVALID; +} diff --git a/launcher/minecraft/mod/ShaderPack.h b/launcher/minecraft/mod/ShaderPack.h new file mode 100644 index 00000000..ec0f9404 --- /dev/null +++ b/launcher/minecraft/mod/ShaderPack.h @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "Resource.h" + +/* Info: + * Currently For Optifine / Iris shader packs, + * could be expanded to support others should they exist? + * + * This class and enum are mostly here as placeholders for validating + * that a shaderpack exists and is in the right format, + * namely that they contain a folder named 'shaders'. + * + * In the technical sense it would be possible to parse files like `shaders/shaders.properties` + * to get information like the available profiles but this is not all that useful without more knowledge of the + * shader mod used to be able to change settings. + */ + +#include <QMutex> + +enum class ShaderPackFormat { VALID, INVALID }; + +class ShaderPack : public Resource { + Q_OBJECT + public: + using Ptr = shared_qobject_ptr<Resource>; + + [[nodiscard]] ShaderPackFormat packFormat() const { return m_pack_format; } + + ShaderPack(QObject* parent = nullptr) : Resource(parent) {} + ShaderPack(QFileInfo file_info) : Resource(file_info) {} + + /** Thread-safe. */ + void setPackFormat(ShaderPackFormat new_format); + + bool valid() const override; + + protected: + mutable QMutex m_data_lock; + + ShaderPackFormat m_pack_format = ShaderPackFormat::INVALID; +}; diff --git a/launcher/minecraft/mod/ShaderPackFolderModel.h b/launcher/minecraft/mod/ShaderPackFolderModel.h index a3aa958f..dc5acf80 100644 --- a/launcher/minecraft/mod/ShaderPackFolderModel.h +++ b/launcher/minecraft/mod/ShaderPackFolderModel.h @@ -6,5 +6,7 @@ class ShaderPackFolderModel : public ResourceFolderModel { Q_OBJECT public: - explicit ShaderPackFolderModel(const QString& dir) : ResourceFolderModel(QDir(dir)) {} + explicit ShaderPackFolderModel(const QString& dir, BaseInstance* instance) + : ResourceFolderModel(QDir(dir), instance) + {} }; diff --git a/launcher/minecraft/mod/TexturePackFolderModel.cpp b/launcher/minecraft/mod/TexturePackFolderModel.cpp index 561f6202..c6609ed1 100644 --- a/launcher/minecraft/mod/TexturePackFolderModel.cpp +++ b/launcher/minecraft/mod/TexturePackFolderModel.cpp @@ -39,11 +39,13 @@ #include "minecraft/mod/tasks/BasicFolderLoadTask.h" #include "minecraft/mod/tasks/LocalTexturePackParseTask.h" -TexturePackFolderModel::TexturePackFolderModel(const QString &dir) : ResourceFolderModel(QDir(dir)) {} +TexturePackFolderModel::TexturePackFolderModel(const QString& dir, BaseInstance* instance) + : ResourceFolderModel(QDir(dir), instance) +{} Task* TexturePackFolderModel::createUpdateTask() { - return new BasicFolderLoadTask(m_dir, [](QFileInfo const& entry) { return new TexturePack(entry); }); + return new BasicFolderLoadTask(m_dir, [](QFileInfo const& entry) { return makeShared<TexturePack>(entry); }); } Task* TexturePackFolderModel::createParseTask(Resource& resource) diff --git a/launcher/minecraft/mod/TexturePackFolderModel.h b/launcher/minecraft/mod/TexturePackFolderModel.h index 261f83b4..425a71e4 100644 --- a/launcher/minecraft/mod/TexturePackFolderModel.h +++ b/launcher/minecraft/mod/TexturePackFolderModel.h @@ -43,7 +43,7 @@ class TexturePackFolderModel : public ResourceFolderModel Q_OBJECT public: - explicit TexturePackFolderModel(const QString &dir); + explicit TexturePackFolderModel(const QString &dir, BaseInstance* instance); [[nodiscard]] Task* createUpdateTask() override; [[nodiscard]] Task* createParseTask(Resource&) override; }; diff --git a/launcher/minecraft/mod/WorldSave.cpp b/launcher/minecraft/mod/WorldSave.cpp new file mode 100644 index 00000000..7123f512 --- /dev/null +++ b/launcher/minecraft/mod/WorldSave.cpp @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include "WorldSave.h" + +#include "minecraft/mod/tasks/LocalWorldSaveParseTask.h" + +void WorldSave::setSaveFormat(WorldSaveFormat new_save_format) +{ + QMutexLocker locker(&m_data_lock); + + m_save_format = new_save_format; +} + +void WorldSave::setSaveDirName(QString dir_name) +{ + QMutexLocker locker(&m_data_lock); + + m_save_dir_name = dir_name; +} + +bool WorldSave::valid() const +{ + return m_save_format != WorldSaveFormat::INVALID; +} diff --git a/launcher/minecraft/mod/WorldSave.h b/launcher/minecraft/mod/WorldSave.h new file mode 100644 index 00000000..5985fc8a --- /dev/null +++ b/launcher/minecraft/mod/WorldSave.h @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "Resource.h" + +#include <QMutex> + +class Version; + +enum class WorldSaveFormat { SINGLE, MULTI, INVALID }; + +class WorldSave : public Resource { + Q_OBJECT + public: + using Ptr = shared_qobject_ptr<Resource>; + + WorldSave(QObject* parent = nullptr) : Resource(parent) {} + WorldSave(QFileInfo file_info) : Resource(file_info) {} + + /** Gets the format of the save. */ + [[nodiscard]] WorldSaveFormat saveFormat() const { return m_save_format; } + /** Gets the name of the save dir (first found in multi mode). */ + [[nodiscard]] QString saveDirName() const { return m_save_dir_name; } + + /** Thread-safe. */ + void setSaveFormat(WorldSaveFormat new_save_format); + /** Thread-safe. */ + void setSaveDirName(QString dir_name); + + bool valid() const override; + + protected: + mutable QMutex m_data_lock; + + /** The format in which the save file is in. + * Since saves can be distributed in various slightly different ways, this allows us to treat them separately. + */ + WorldSaveFormat m_save_format = WorldSaveFormat::INVALID; + + QString m_save_dir_name; +}; diff --git a/launcher/minecraft/mod/tasks/BasicFolderLoadTask.h b/launcher/minecraft/mod/tasks/BasicFolderLoadTask.h index 2fce2942..3ee7e2e0 100644 --- a/launcher/minecraft/mod/tasks/BasicFolderLoadTask.h +++ b/launcher/minecraft/mod/tasks/BasicFolderLoadTask.h @@ -26,11 +26,11 @@ class BasicFolderLoadTask : public Task { public: BasicFolderLoadTask(QDir dir) : Task(nullptr, false), m_dir(dir), m_result(new Result), m_thread_to_spawn_into(thread()) { - m_create_func = [](QFileInfo const& entry) -> Resource* { - return new Resource(entry); + m_create_func = [](QFileInfo const& entry) -> Resource::Ptr { + return makeShared<Resource>(entry); }; } - BasicFolderLoadTask(QDir dir, std::function<Resource*(QFileInfo const&)> create_function) + BasicFolderLoadTask(QDir dir, std::function<Resource::Ptr(QFileInfo const&)> create_function) : Task(nullptr, false), m_dir(dir), m_result(new Result), m_create_func(std::move(create_function)), m_thread_to_spawn_into(thread()) {} @@ -65,7 +65,7 @@ private: std::atomic<bool> m_aborted = false; - std::function<Resource*(QFileInfo const&)> m_create_func; + std::function<Resource::Ptr(QFileInfo const&)> m_create_func; /** This is the thread in which we should put new mod objects */ QThread* m_thread_to_spawn_into; diff --git a/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp new file mode 100644 index 00000000..5bb44877 --- /dev/null +++ b/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp @@ -0,0 +1,177 @@ +// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include "LocalDataPackParseTask.h" + +#include "FileSystem.h" +#include "Json.h" + +#include <quazip/quazip.h> +#include <quazip/quazipdir.h> +#include <quazip/quazipfile.h> + +#include <QCryptographicHash> + +namespace DataPackUtils { + +bool process(DataPack& pack, ProcessingLevel level) +{ + switch (pack.type()) { + case ResourceType::FOLDER: + return DataPackUtils::processFolder(pack, level); + case ResourceType::ZIPFILE: + return DataPackUtils::processZIP(pack, level); + default: + qWarning() << "Invalid type for data pack parse task!"; + return false; + } +} + +bool processFolder(DataPack& pack, ProcessingLevel level) +{ + Q_ASSERT(pack.type() == ResourceType::FOLDER); + + auto mcmeta_invalid = [&pack]() { + qWarning() << "Data pack at" << pack.fileinfo().filePath() << "does not have a valid pack.mcmeta"; + return false; // the mcmeta is not optional + }; + + QFileInfo mcmeta_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.mcmeta")); + if (mcmeta_file_info.exists() && mcmeta_file_info.isFile()) { + QFile mcmeta_file(mcmeta_file_info.filePath()); + if (!mcmeta_file.open(QIODevice::ReadOnly)) + return mcmeta_invalid(); // can't open mcmeta file + + auto data = mcmeta_file.readAll(); + + bool mcmeta_result = DataPackUtils::processMCMeta(pack, std::move(data)); + + mcmeta_file.close(); + if (!mcmeta_result) { + return mcmeta_invalid(); // mcmeta invalid + } + } else { + return mcmeta_invalid(); // mcmeta file isn't a valid file + } + + QFileInfo data_dir_info(FS::PathCombine(pack.fileinfo().filePath(), "data")); + if (!data_dir_info.exists() || !data_dir_info.isDir()) { + return false; // data dir does not exists or isn't valid + } + + if (level == ProcessingLevel::BasicInfoOnly) { + return true; // only need basic info already checked + } + + return true; // all tests passed +} + +bool processZIP(DataPack& pack, ProcessingLevel level) +{ + Q_ASSERT(pack.type() == ResourceType::ZIPFILE); + + QuaZip zip(pack.fileinfo().filePath()); + if (!zip.open(QuaZip::mdUnzip)) + return false; // can't open zip file + + QuaZipFile file(&zip); + + auto mcmeta_invalid = [&pack]() { + qWarning() << "Data pack at" << pack.fileinfo().filePath() << "does not have a valid pack.mcmeta"; + return false; // the mcmeta is not optional + }; + + if (zip.setCurrentFile("pack.mcmeta")) { + if (!file.open(QIODevice::ReadOnly)) { + qCritical() << "Failed to open file in zip."; + zip.close(); + return mcmeta_invalid(); + } + + auto data = file.readAll(); + + bool mcmeta_result = DataPackUtils::processMCMeta(pack, std::move(data)); + + file.close(); + if (!mcmeta_result) { + return mcmeta_invalid(); // mcmeta invalid + } + } else { + return mcmeta_invalid(); // could not set pack.mcmeta as current file. + } + + QuaZipDir zipDir(&zip); + if (!zipDir.exists("/data")) { + return false; // data dir does not exists at zip root + } + + if (level == ProcessingLevel::BasicInfoOnly) { + zip.close(); + return true; // only need basic info already checked + } + + zip.close(); + + return true; +} + +// https://minecraft.fandom.com/wiki/Data_pack#pack.mcmeta +bool processMCMeta(DataPack& pack, QByteArray&& raw_data) +{ + try { + auto json_doc = QJsonDocument::fromJson(raw_data); + auto pack_obj = Json::requireObject(json_doc.object(), "pack", {}); + + pack.setPackFormat(Json::ensureInteger(pack_obj, "pack_format", 0)); + pack.setDescription(Json::ensureString(pack_obj, "description", "")); + } catch (Json::JsonException& e) { + qWarning() << "JsonException: " << e.what() << e.cause(); + return false; + } + return true; +} + +bool validate(QFileInfo file) +{ + DataPack dp{ file }; + return DataPackUtils::process(dp, ProcessingLevel::BasicInfoOnly) && dp.valid(); +} + +} // namespace DataPackUtils + +LocalDataPackParseTask::LocalDataPackParseTask(int token, DataPack& dp) : Task(nullptr, false), m_token(token), m_data_pack(dp) {} + +bool LocalDataPackParseTask::abort() +{ + m_aborted = true; + return true; +} + +void LocalDataPackParseTask::executeTask() +{ + if (!DataPackUtils::process(m_data_pack)) + return; + + if (m_aborted) + emitAborted(); + else + emitSucceeded(); +} diff --git a/launcher/minecraft/mod/tasks/LocalDataPackParseTask.h b/launcher/minecraft/mod/tasks/LocalDataPackParseTask.h new file mode 100644 index 00000000..12fd8c82 --- /dev/null +++ b/launcher/minecraft/mod/tasks/LocalDataPackParseTask.h @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <QDebug> +#include <QObject> + +#include "minecraft/mod/DataPack.h" + +#include "tasks/Task.h" + +namespace DataPackUtils { + +enum class ProcessingLevel { Full, BasicInfoOnly }; + +bool process(DataPack& pack, ProcessingLevel level = ProcessingLevel::Full); + +bool processZIP(DataPack& pack, ProcessingLevel level = ProcessingLevel::Full); +bool processFolder(DataPack& pack, ProcessingLevel level = ProcessingLevel::Full); + +bool processMCMeta(DataPack& pack, QByteArray&& raw_data); + +/** Checks whether a file is valid as a data pack or not. */ +bool validate(QFileInfo file); + +} // namespace DataPackUtils + +class LocalDataPackParseTask : public Task { + Q_OBJECT + public: + LocalDataPackParseTask(int token, DataPack& dp); + + [[nodiscard]] bool canAbort() const override { return true; } + bool abort() override; + + void executeTask() override; + + [[nodiscard]] int token() const { return m_token; } + + private: + int m_token; + + DataPack& m_data_pack; + + bool m_aborted = false; +}; diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp index 774f6114..5342d693 100644 --- a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp @@ -3,6 +3,7 @@ #include <quazip/quazip.h> #include <quazip/quazipfile.h> #include <toml++/toml.h> +#include <qdcss.h> #include <QJsonArray> #include <QJsonDocument> #include <QJsonObject> @@ -11,12 +12,13 @@ #include "FileSystem.h" #include "Json.h" +#include "minecraft/mod/ModDetails.h" #include "settings/INIFile.h" -namespace { +namespace ModUtils { // NEW format -// https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/6f62b37cea040daf350dc253eae6326dd9c822c3 +// https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/c8d8f1929aff9979e322af79a59ce81f3e02db6a // OLD format: // https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/5bf6a2d05145ec79387acc0d45c958642fb049fc @@ -73,10 +75,11 @@ ModDetails ReadMCModInfo(QByteArray contents) version = Json::ensureString(val, "").toInt(); if (version != 2) { - qCritical() << "BAD stuff happened to mod json:"; - qCritical() << contents; - return {}; + qWarning() << QString(R"(The value of 'modListVersion' is "%1" (expected "2")! The file may be corrupted.)").arg(version); + qWarning() << "The contents of 'mcmod.info' are as follows:"; + qWarning() << contents; } + auto arrVal = jsonDoc.object().value("modlist"); if (arrVal.isUndefined()) { arrVal = jsonDoc.object().value("modList"); @@ -239,7 +242,7 @@ ModDetails ReadQuiltModInfo(QByteArray contents) return details; } -ModDetails ReadForgeInfo(QByteArray contents) +ModDetails ReadForgeInfo(QString fileName) { ModDetails details; // Read the data @@ -247,7 +250,7 @@ ModDetails ReadForgeInfo(QByteArray contents) details.mod_id = "Forge"; details.homeurl = "http://www.minecraftforge.net/forum/"; INIFile ini; - if (!ini.loadFile(contents)) + if (!ini.loadFile(fileName)) return details; QString major = ini.get("forge.major.number", "0").toString(); @@ -283,35 +286,72 @@ ModDetails ReadLiteModInfo(QByteArray contents) return details; } -} // namespace +// https://git.sleeping.town/unascribed/NilLoader/src/commit/d7fc87b255fc31019ff90f80d45894927fac6efc/src/main/java/nilloader/api/NilMetadata.java#L64 +ModDetails ReadNilModInfo(QByteArray contents, QString fname) +{ + ModDetails details; + + QDCSS cssData = QDCSS(contents); + auto name = cssData.get("@nilmod.name"); + auto desc = cssData.get("@nilmod.description"); + auto authors = cssData.get("@nilmod.authors"); -LocalModParseTask::LocalModParseTask(int token, ResourceType type, const QFileInfo& modFile) - : Task(nullptr, false), m_token(token), m_type(type), m_modFile(modFile), m_result(new Result()) -{} + if (name->has_value()) { + details.name = name->value(); + } + if (desc->has_value()) { + details.description = desc->value(); + } + if (authors->has_value()) { + details.authors.append(authors->value()); + } + details.version = cssData.get("@nilmod.version")->value_or("?"); + + details.mod_id = fname.remove(".nilmod.css"); + + return details; +} -void LocalModParseTask::processAsZip() +bool process(Mod& mod, ProcessingLevel level) { - QuaZip zip(m_modFile.filePath()); + switch (mod.type()) { + case ResourceType::FOLDER: + return processFolder(mod, level); + case ResourceType::ZIPFILE: + return processZIP(mod, level); + case ResourceType::LITEMOD: + return processLitemod(mod); + default: + qWarning() << "Invalid type for mod parse task!"; + return false; + } +} + +bool processZIP(Mod& mod, ProcessingLevel level) +{ + ModDetails details; + + QuaZip zip(mod.fileinfo().filePath()); if (!zip.open(QuaZip::mdUnzip)) - return; + return false; QuaZipFile file(&zip); if (zip.setCurrentFile("META-INF/mods.toml")) { if (!file.open(QIODevice::ReadOnly)) { zip.close(); - return; + return false; } - m_result->details = ReadMCModTOML(file.readAll()); + details = ReadMCModTOML(file.readAll()); file.close(); // to replace ${file.jarVersion} with the actual version, as needed - if (m_result->details.version == "${file.jarVersion}") { + if (details.version == "${file.jarVersion}") { if (zip.setCurrentFile("META-INF/MANIFEST.MF")) { if (!file.open(QIODevice::ReadOnly)) { zip.close(); - return; + return false; } // quick and dirty line-by-line parser @@ -330,93 +370,157 @@ void LocalModParseTask::processAsZip() manifestVersion = "NONE"; } - m_result->details.version = manifestVersion; + details.version = manifestVersion; file.close(); } } zip.close(); - return; + mod.setDetails(details); + + return true; } else if (zip.setCurrentFile("mcmod.info")) { if (!file.open(QIODevice::ReadOnly)) { zip.close(); - return; + return false; } - m_result->details = ReadMCModInfo(file.readAll()); + details = ReadMCModInfo(file.readAll()); file.close(); zip.close(); - return; + + mod.setDetails(details); + return true; } else if (zip.setCurrentFile("quilt.mod.json")) { if (!file.open(QIODevice::ReadOnly)) { zip.close(); - return; + return false; } - m_result->details = ReadQuiltModInfo(file.readAll()); + details = ReadQuiltModInfo(file.readAll()); file.close(); zip.close(); - return; + + mod.setDetails(details); + return true; } else if (zip.setCurrentFile("fabric.mod.json")) { if (!file.open(QIODevice::ReadOnly)) { zip.close(); - return; + return false; } - m_result->details = ReadFabricModInfo(file.readAll()); + details = ReadFabricModInfo(file.readAll()); file.close(); zip.close(); - return; + + mod.setDetails(details); + return true; } else if (zip.setCurrentFile("forgeversion.properties")) { if (!file.open(QIODevice::ReadOnly)) { zip.close(); - return; + return false; } - m_result->details = ReadForgeInfo(file.readAll()); + details = ReadForgeInfo(file.getFileName()); file.close(); zip.close(); - return; + + mod.setDetails(details); + return true; + } else if (zip.setCurrentFile("META-INF/nil/mappings.json")) { + // nilloader uses the filename of the metadata file for the modid, so we can't know the exact filename + // thankfully, there is a good file to use as a canary so we don't look for nil meta all the time + + QString foundNilMeta; + for (auto& fname : zip.getFileNameList()) { + // nilmods can shade nilloader to be able to run as a standalone agent - which includes nilloader's own meta file + if (fname.endsWith(".nilmod.css") && fname != "nilloader.nilmod.css") { + foundNilMeta = fname; + break; + } + } + + if (zip.setCurrentFile(foundNilMeta)) { + if (!file.open(QIODevice::ReadOnly)) { + zip.close(); + return false; + } + + details = ReadNilModInfo(file.readAll(), foundNilMeta); + file.close(); + zip.close(); + + mod.setDetails(details); + return true; + } } zip.close(); + return false; // no valid mod found in archive } -void LocalModParseTask::processAsFolder() +bool processFolder(Mod& mod, ProcessingLevel level) { - QFileInfo mcmod_info(FS::PathCombine(m_modFile.filePath(), "mcmod.info")); - if (mcmod_info.isFile()) { + ModDetails details; + + QFileInfo mcmod_info(FS::PathCombine(mod.fileinfo().filePath(), "mcmod.info")); + if (mcmod_info.exists() && mcmod_info.isFile()) { QFile mcmod(mcmod_info.filePath()); if (!mcmod.open(QIODevice::ReadOnly)) - return; + return false; auto data = mcmod.readAll(); if (data.isEmpty() || data.isNull()) - return; - m_result->details = ReadMCModInfo(data); + return false; + details = ReadMCModInfo(data); + + mod.setDetails(details); + return true; } + + return false; // no valid mcmod.info file found } -void LocalModParseTask::processAsLitemod() +bool processLitemod(Mod& mod, ProcessingLevel level) { - QuaZip zip(m_modFile.filePath()); + ModDetails details; + + QuaZip zip(mod.fileinfo().filePath()); if (!zip.open(QuaZip::mdUnzip)) - return; + return false; QuaZipFile file(&zip); if (zip.setCurrentFile("litemod.json")) { if (!file.open(QIODevice::ReadOnly)) { zip.close(); - return; + return false; } - m_result->details = ReadLiteModInfo(file.readAll()); + details = ReadLiteModInfo(file.readAll()); file.close(); + + mod.setDetails(details); + return true; } zip.close(); + + return false; // no valid litemod.json found in archive +} + +/** Checks whether a file is valid as a mod or not. */ +bool validate(QFileInfo file) +{ + Mod mod{ file }; + return ModUtils::process(mod, ProcessingLevel::BasicInfoOnly) && mod.valid(); } +} // namespace ModUtils + +LocalModParseTask::LocalModParseTask(int token, ResourceType type, const QFileInfo& modFile) + : Task(nullptr, false), m_token(token), m_type(type), m_modFile(modFile), m_result(new Result()) +{} + bool LocalModParseTask::abort() { m_aborted.store(true); @@ -425,19 +529,10 @@ bool LocalModParseTask::abort() void LocalModParseTask::executeTask() { - switch (m_type) { - case ResourceType::ZIPFILE: - processAsZip(); - break; - case ResourceType::FOLDER: - processAsFolder(); - break; - case ResourceType::LITEMOD: - processAsLitemod(); - break; - default: - break; - } + Mod mod{ m_modFile }; + ModUtils::process(mod, ModUtils::ProcessingLevel::Full); + + m_result->details = mod.details(); if (m_aborted) emit finished(); diff --git a/launcher/minecraft/mod/tasks/LocalModParseTask.h b/launcher/minecraft/mod/tasks/LocalModParseTask.h index 413eb2d1..38dae135 100644 --- a/launcher/minecraft/mod/tasks/LocalModParseTask.h +++ b/launcher/minecraft/mod/tasks/LocalModParseTask.h @@ -8,32 +8,48 @@ #include "tasks/Task.h" -class LocalModParseTask : public Task -{ +namespace ModUtils { + +ModDetails ReadFabricModInfo(QByteArray contents); +ModDetails ReadQuiltModInfo(QByteArray contents); +ModDetails ReadForgeInfo(QByteArray contents); +ModDetails ReadLiteModInfo(QByteArray contents); + +enum class ProcessingLevel { Full, BasicInfoOnly }; + +bool process(Mod& mod, ProcessingLevel level = ProcessingLevel::Full); + +bool processZIP(Mod& mod, ProcessingLevel level = ProcessingLevel::Full); +bool processFolder(Mod& mod, ProcessingLevel level = ProcessingLevel::Full); +bool processLitemod(Mod& mod, ProcessingLevel level = ProcessingLevel::Full); + +/** Checks whether a file is valid as a mod or not. */ +bool validate(QFileInfo file); +} // namespace ModUtils + +class LocalModParseTask : public Task { Q_OBJECT -public: + public: struct Result { ModDetails details; }; using ResultPtr = std::shared_ptr<Result>; - ResultPtr result() const { - return m_result; - } + ResultPtr result() const { return m_result; } [[nodiscard]] bool canAbort() const override { return true; } bool abort() override; - LocalModParseTask(int token, ResourceType type, const QFileInfo & modFile); + LocalModParseTask(int token, ResourceType type, const QFileInfo& modFile); void executeTask() override; [[nodiscard]] int token() const { return m_token; } -private: + private: void processAsZip(); void processAsFolder(); void processAsLitemod(); -private: + private: int m_token; ResourceType m_type; QFileInfo m_modFile; diff --git a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp index 6fd4b024..4bf0b80d 100644 --- a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.cpp @@ -22,6 +22,7 @@ #include "Json.h" #include <quazip/quazip.h> +#include <quazip/quazipdir.h> #include <quazip/quazipfile.h> #include <QCryptographicHash> @@ -32,99 +33,152 @@ bool process(ResourcePack& pack, ProcessingLevel level) { switch (pack.type()) { case ResourceType::FOLDER: - ResourcePackUtils::processFolder(pack, level); - return true; + return ResourcePackUtils::processFolder(pack, level); case ResourceType::ZIPFILE: - ResourcePackUtils::processZIP(pack, level); - return true; + return ResourcePackUtils::processZIP(pack, level); default: qWarning() << "Invalid type for resource pack parse task!"; return false; } } -void processFolder(ResourcePack& pack, ProcessingLevel level) +bool processFolder(ResourcePack& pack, ProcessingLevel level) { Q_ASSERT(pack.type() == ResourceType::FOLDER); + auto mcmeta_invalid = [&pack]() { + qWarning() << "Resource pack at" << pack.fileinfo().filePath() << "does not have a valid pack.mcmeta"; + return false; // the mcmeta is not optional + }; + QFileInfo mcmeta_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.mcmeta")); - if (mcmeta_file_info.isFile()) { + if (mcmeta_file_info.exists() && mcmeta_file_info.isFile()) { QFile mcmeta_file(mcmeta_file_info.filePath()); if (!mcmeta_file.open(QIODevice::ReadOnly)) - return; + return mcmeta_invalid(); // can't open mcmeta file auto data = mcmeta_file.readAll(); - ResourcePackUtils::processMCMeta(pack, std::move(data)); + bool mcmeta_result = ResourcePackUtils::processMCMeta(pack, std::move(data)); mcmeta_file.close(); + if (!mcmeta_result) { + return mcmeta_invalid(); // mcmeta invalid + } + } else { + return mcmeta_invalid(); // mcmeta file isn't a valid file } - if (level == ProcessingLevel::BasicInfoOnly) - return; + QFileInfo assets_dir_info(FS::PathCombine(pack.fileinfo().filePath(), "assets")); + if (!assets_dir_info.exists() || !assets_dir_info.isDir()) { + return false; // assets dir does not exists or isn't valid + } + + if (level == ProcessingLevel::BasicInfoOnly) { + return true; // only need basic info already checked + } + + auto png_invalid = [&pack]() { + qWarning() << "Resource pack at" << pack.fileinfo().filePath() << "does not have a valid pack.png"; + return true; // the png is optional + }; QFileInfo image_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.png")); - if (image_file_info.isFile()) { - QFile mcmeta_file(image_file_info.filePath()); - if (!mcmeta_file.open(QIODevice::ReadOnly)) - return; + if (image_file_info.exists() && image_file_info.isFile()) { + QFile pack_png_file(image_file_info.filePath()); + if (!pack_png_file.open(QIODevice::ReadOnly)) + return png_invalid(); // can't open pack.png file - auto data = mcmeta_file.readAll(); + auto data = pack_png_file.readAll(); - ResourcePackUtils::processPackPNG(pack, std::move(data)); + bool pack_png_result = ResourcePackUtils::processPackPNG(pack, std::move(data)); - mcmeta_file.close(); + pack_png_file.close(); + if (!pack_png_result) { + return png_invalid(); // pack.png invalid + } + } else { + return png_invalid(); // pack.png does not exists or is not a valid file. } + + return true; // all tests passed } -void processZIP(ResourcePack& pack, ProcessingLevel level) +bool processZIP(ResourcePack& pack, ProcessingLevel level) { Q_ASSERT(pack.type() == ResourceType::ZIPFILE); QuaZip zip(pack.fileinfo().filePath()); if (!zip.open(QuaZip::mdUnzip)) - return; + return false; // can't open zip file QuaZipFile file(&zip); + auto mcmeta_invalid = [&pack]() { + qWarning() << "Resource pack at" << pack.fileinfo().filePath() << "does not have a valid pack.mcmeta"; + return false; // the mcmeta is not optional + }; + if (zip.setCurrentFile("pack.mcmeta")) { if (!file.open(QIODevice::ReadOnly)) { qCritical() << "Failed to open file in zip."; zip.close(); - return; + return mcmeta_invalid(); } auto data = file.readAll(); - ResourcePackUtils::processMCMeta(pack, std::move(data)); + bool mcmeta_result = ResourcePackUtils::processMCMeta(pack, std::move(data)); file.close(); + if (!mcmeta_result) { + return mcmeta_invalid(); // mcmeta invalid + } + } else { + return mcmeta_invalid(); // could not set pack.mcmeta as current file. + } + + QuaZipDir zipDir(&zip); + if (!zipDir.exists("/assets")) { + return false; // assets dir does not exists at zip root } if (level == ProcessingLevel::BasicInfoOnly) { zip.close(); - return; + return true; // only need basic info already checked } + auto png_invalid = [&pack]() { + qWarning() << "Resource pack at" << pack.fileinfo().filePath() << "does not have a valid pack.png"; + return true; // the png is optional + }; + if (zip.setCurrentFile("pack.png")) { if (!file.open(QIODevice::ReadOnly)) { qCritical() << "Failed to open file in zip."; zip.close(); - return; + return png_invalid(); } auto data = file.readAll(); - ResourcePackUtils::processPackPNG(pack, std::move(data)); + bool pack_png_result = ResourcePackUtils::processPackPNG(pack, std::move(data)); file.close(); + if (!pack_png_result) { + return png_invalid(); // pack.png invalid + } + } else { + return png_invalid(); // could not set pack.mcmeta as current file. } zip.close(); + + return true; } // https://minecraft.fandom.com/wiki/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta -void processMCMeta(ResourcePack& pack, QByteArray&& raw_data) +bool processMCMeta(ResourcePack& pack, QByteArray&& raw_data) { try { auto json_doc = QJsonDocument::fromJson(raw_data); @@ -134,17 +188,21 @@ void processMCMeta(ResourcePack& pack, QByteArray&& raw_data) pack.setDescription(Json::ensureString(pack_obj, "description", "")); } catch (Json::JsonException& e) { qWarning() << "JsonException: " << e.what() << e.cause(); + return false; } + return true; } -void processPackPNG(ResourcePack& pack, QByteArray&& raw_data) +bool processPackPNG(ResourcePack& pack, QByteArray&& raw_data) { auto img = QImage::fromData(raw_data); if (!img.isNull()) { pack.setImage(img); } else { qWarning() << "Failed to parse pack.png."; + return false; } + return true; } bool validate(QFileInfo file) diff --git a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h index 69dbd6ad..d0c24c2b 100644 --- a/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h +++ b/launcher/minecraft/mod/tasks/LocalResourcePackParseTask.h @@ -31,11 +31,11 @@ enum class ProcessingLevel { Full, BasicInfoOnly }; bool process(ResourcePack& pack, ProcessingLevel level = ProcessingLevel::Full); -void processZIP(ResourcePack& pack, ProcessingLevel level = ProcessingLevel::Full); -void processFolder(ResourcePack& pack, ProcessingLevel level = ProcessingLevel::Full); +bool processZIP(ResourcePack& pack, ProcessingLevel level = ProcessingLevel::Full); +bool processFolder(ResourcePack& pack, ProcessingLevel level = ProcessingLevel::Full); -void processMCMeta(ResourcePack& pack, QByteArray&& raw_data); -void processPackPNG(ResourcePack& pack, QByteArray&& raw_data); +bool processMCMeta(ResourcePack& pack, QByteArray&& raw_data); +bool processPackPNG(ResourcePack& pack, QByteArray&& raw_data); /** Checks whether a file is valid as a resource pack or not. */ bool validate(QFileInfo file); diff --git a/launcher/minecraft/mod/tasks/LocalResourceParse.cpp b/launcher/minecraft/mod/tasks/LocalResourceParse.cpp new file mode 100644 index 00000000..4d760df2 --- /dev/null +++ b/launcher/minecraft/mod/tasks/LocalResourceParse.cpp @@ -0,0 +1,78 @@ +// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <QObject> + +#include "LocalResourceParse.h" + +#include "LocalDataPackParseTask.h" +#include "LocalModParseTask.h" +#include "LocalResourcePackParseTask.h" +#include "LocalShaderPackParseTask.h" +#include "LocalTexturePackParseTask.h" +#include "LocalWorldSaveParseTask.h" + + +static const QMap<PackedResourceType, QString> s_packed_type_names = { + {PackedResourceType::ResourcePack, QObject::tr("resource pack")}, + {PackedResourceType::TexturePack, QObject::tr("texture pack")}, + {PackedResourceType::DataPack, QObject::tr("data pack")}, + {PackedResourceType::ShaderPack, QObject::tr("shader pack")}, + {PackedResourceType::WorldSave, QObject::tr("world save")}, + {PackedResourceType::Mod , QObject::tr("mod")}, + {PackedResourceType::UNKNOWN, QObject::tr("unknown")} +}; + +namespace ResourceUtils { +PackedResourceType identify(QFileInfo file){ + if (file.exists() && file.isFile()) { + if (ResourcePackUtils::validate(file)) { + qDebug() << file.fileName() << "is a resource pack"; + return PackedResourceType::ResourcePack; + } else if (TexturePackUtils::validate(file)) { + qDebug() << file.fileName() << "is a pre 1.6 texture pack"; + return PackedResourceType::TexturePack; + } else if (DataPackUtils::validate(file)) { + qDebug() << file.fileName() << "is a data pack"; + return PackedResourceType::DataPack; + } else if (ModUtils::validate(file)) { + qDebug() << file.fileName() << "is a mod"; + return PackedResourceType::Mod; + } else if (WorldSaveUtils::validate(file)) { + qDebug() << file.fileName() << "is a world save"; + return PackedResourceType::WorldSave; + } else if (ShaderPackUtils::validate(file)) { + qDebug() << file.fileName() << "is a shader pack"; + return PackedResourceType::ShaderPack; + } else { + qDebug() << "Can't Identify" << file.fileName() ; + } + } else { + qDebug() << "Can't find" << file.absolutePath(); + } + return PackedResourceType::UNKNOWN; +} + +QString getPackedTypeName(PackedResourceType type) { + return s_packed_type_names.constFind(type).value(); +} + +} diff --git a/launcher/minecraft/mod/tasks/LocalResourceParse.h b/launcher/minecraft/mod/tasks/LocalResourceParse.h new file mode 100644 index 00000000..7385d24b --- /dev/null +++ b/launcher/minecraft/mod/tasks/LocalResourceParse.h @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <set> + +#include <QDebug> +#include <QFileInfo> +#include <QObject> + +enum class PackedResourceType { DataPack, ResourcePack, TexturePack, ShaderPack, WorldSave, Mod, UNKNOWN }; +namespace ResourceUtils { +static const std::set<PackedResourceType> ValidResourceTypes = { PackedResourceType::DataPack, PackedResourceType::ResourcePack, + PackedResourceType::TexturePack, PackedResourceType::ShaderPack, + PackedResourceType::WorldSave, PackedResourceType::Mod }; +PackedResourceType identify(QFileInfo file); +QString getPackedTypeName(PackedResourceType type); +} // namespace ResourceUtils diff --git a/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.cpp new file mode 100644 index 00000000..a9949735 --- /dev/null +++ b/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.cpp @@ -0,0 +1,113 @@ +// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include "LocalShaderPackParseTask.h" + +#include "FileSystem.h" + +#include <quazip/quazip.h> +#include <quazip/quazipdir.h> +#include <quazip/quazipfile.h> + +namespace ShaderPackUtils { + +bool process(ShaderPack& pack, ProcessingLevel level) +{ + switch (pack.type()) { + case ResourceType::FOLDER: + return ShaderPackUtils::processFolder(pack, level); + case ResourceType::ZIPFILE: + return ShaderPackUtils::processZIP(pack, level); + default: + qWarning() << "Invalid type for shader pack parse task!"; + return false; + } +} + +bool processFolder(ShaderPack& pack, ProcessingLevel level) +{ + Q_ASSERT(pack.type() == ResourceType::FOLDER); + + QFileInfo shaders_dir_info(FS::PathCombine(pack.fileinfo().filePath(), "shaders")); + if (!shaders_dir_info.exists() || !shaders_dir_info.isDir()) { + return false; // assets dir does not exists or isn't valid + } + pack.setPackFormat(ShaderPackFormat::VALID); + + if (level == ProcessingLevel::BasicInfoOnly) { + return true; // only need basic info already checked + } + + return true; // all tests passed +} + +bool processZIP(ShaderPack& pack, ProcessingLevel level) +{ + Q_ASSERT(pack.type() == ResourceType::ZIPFILE); + + QuaZip zip(pack.fileinfo().filePath()); + if (!zip.open(QuaZip::mdUnzip)) + return false; // can't open zip file + + QuaZipFile file(&zip); + + QuaZipDir zipDir(&zip); + if (!zipDir.exists("/shaders")) { + return false; // assets dir does not exists at zip root + } + pack.setPackFormat(ShaderPackFormat::VALID); + + if (level == ProcessingLevel::BasicInfoOnly) { + zip.close(); + return true; // only need basic info already checked + } + + zip.close(); + + return true; +} + +bool validate(QFileInfo file) +{ + ShaderPack sp{ file }; + return ShaderPackUtils::process(sp, ProcessingLevel::BasicInfoOnly) && sp.valid(); +} + +} // namespace ShaderPackUtils + +LocalShaderPackParseTask::LocalShaderPackParseTask(int token, ShaderPack& sp) : Task(nullptr, false), m_token(token), m_shader_pack(sp) {} + +bool LocalShaderPackParseTask::abort() +{ + m_aborted = true; + return true; +} + +void LocalShaderPackParseTask::executeTask() +{ + if (!ShaderPackUtils::process(m_shader_pack)) + return; + + if (m_aborted) + emitAborted(); + else + emitSucceeded(); +} diff --git a/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.h b/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.h new file mode 100644 index 00000000..6be2183c --- /dev/null +++ b/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.h @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <QDebug> +#include <QObject> + +#include "minecraft/mod/ShaderPack.h" + +#include "tasks/Task.h" + +namespace ShaderPackUtils { + +enum class ProcessingLevel { Full, BasicInfoOnly }; + +bool process(ShaderPack& pack, ProcessingLevel level = ProcessingLevel::Full); + +bool processZIP(ShaderPack& pack, ProcessingLevel level = ProcessingLevel::Full); +bool processFolder(ShaderPack& pack, ProcessingLevel level = ProcessingLevel::Full); + +/** Checks whether a file is valid as a shader pack or not. */ +bool validate(QFileInfo file); +} // namespace ShaderPackUtils + +class LocalShaderPackParseTask : public Task { + Q_OBJECT + public: + LocalShaderPackParseTask(int token, ShaderPack& sp); + + [[nodiscard]] bool canAbort() const override { return true; } + bool abort() override; + + void executeTask() override; + + [[nodiscard]] int token() const { return m_token; } + + private: + int m_token; + + ShaderPack& m_shader_pack; + + bool m_aborted = false; +}; diff --git a/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp index adb19aca..38f1d7c1 100644 --- a/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp @@ -32,18 +32,16 @@ bool process(TexturePack& pack, ProcessingLevel level) { switch (pack.type()) { case ResourceType::FOLDER: - TexturePackUtils::processFolder(pack, level); - return true; + return TexturePackUtils::processFolder(pack, level); case ResourceType::ZIPFILE: - TexturePackUtils::processZIP(pack, level); - return true; + return TexturePackUtils::processZIP(pack, level); default: qWarning() << "Invalid type for resource pack parse task!"; return false; } } -void processFolder(TexturePack& pack, ProcessingLevel level) +bool processFolder(TexturePack& pack, ProcessingLevel level) { Q_ASSERT(pack.type() == ResourceType::FOLDER); @@ -51,39 +49,51 @@ void processFolder(TexturePack& pack, ProcessingLevel level) if (mcmeta_file_info.isFile()) { QFile mcmeta_file(mcmeta_file_info.filePath()); if (!mcmeta_file.open(QIODevice::ReadOnly)) - return; + return false; auto data = mcmeta_file.readAll(); - TexturePackUtils::processPackTXT(pack, std::move(data)); + bool packTXT_result = TexturePackUtils::processPackTXT(pack, std::move(data)); mcmeta_file.close(); + if (!packTXT_result) { + return false; + } + } else { + return false; } if (level == ProcessingLevel::BasicInfoOnly) - return; + return true; QFileInfo image_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.png")); if (image_file_info.isFile()) { QFile mcmeta_file(image_file_info.filePath()); if (!mcmeta_file.open(QIODevice::ReadOnly)) - return; + return false; auto data = mcmeta_file.readAll(); - TexturePackUtils::processPackPNG(pack, std::move(data)); + bool packPNG_result = TexturePackUtils::processPackPNG(pack, std::move(data)); mcmeta_file.close(); + if (!packPNG_result) { + return false; + } + } else { + return false; } + + return true; } -void processZIP(TexturePack& pack, ProcessingLevel level) +bool processZIP(TexturePack& pack, ProcessingLevel level) { Q_ASSERT(pack.type() == ResourceType::ZIPFILE); QuaZip zip(pack.fileinfo().filePath()); if (!zip.open(QuaZip::mdUnzip)) - return; + return false; QuaZipFile file(&zip); @@ -91,51 +101,62 @@ void processZIP(TexturePack& pack, ProcessingLevel level) if (!file.open(QIODevice::ReadOnly)) { qCritical() << "Failed to open file in zip."; zip.close(); - return; + return false; } auto data = file.readAll(); - TexturePackUtils::processPackTXT(pack, std::move(data)); + bool packTXT_result = TexturePackUtils::processPackTXT(pack, std::move(data)); file.close(); + if (!packTXT_result) { + return false; + } } if (level == ProcessingLevel::BasicInfoOnly) { zip.close(); - return; + return true; } if (zip.setCurrentFile("pack.png")) { if (!file.open(QIODevice::ReadOnly)) { qCritical() << "Failed to open file in zip."; zip.close(); - return; + return false; } auto data = file.readAll(); - TexturePackUtils::processPackPNG(pack, std::move(data)); + bool packPNG_result = TexturePackUtils::processPackPNG(pack, std::move(data)); file.close(); + if (!packPNG_result) { + return false; + } } zip.close(); + + return true; } -void processPackTXT(TexturePack& pack, QByteArray&& raw_data) +bool processPackTXT(TexturePack& pack, QByteArray&& raw_data) { pack.setDescription(QString(raw_data)); + return true; } -void processPackPNG(TexturePack& pack, QByteArray&& raw_data) +bool processPackPNG(TexturePack& pack, QByteArray&& raw_data) { auto img = QImage::fromData(raw_data); if (!img.isNull()) { pack.setImage(img); } else { qWarning() << "Failed to parse pack.png."; + return false; } + return true; } bool validate(QFileInfo file) diff --git a/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.h b/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.h index 9f7aab75..1589f8cb 100644 --- a/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.h +++ b/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.h @@ -32,11 +32,11 @@ enum class ProcessingLevel { Full, BasicInfoOnly }; bool process(TexturePack& pack, ProcessingLevel level = ProcessingLevel::Full); -void processZIP(TexturePack& pack, ProcessingLevel level = ProcessingLevel::Full); -void processFolder(TexturePack& pack, ProcessingLevel level = ProcessingLevel::Full); +bool processZIP(TexturePack& pack, ProcessingLevel level = ProcessingLevel::Full); +bool processFolder(TexturePack& pack, ProcessingLevel level = ProcessingLevel::Full); -void processPackTXT(TexturePack& pack, QByteArray&& raw_data); -void processPackPNG(TexturePack& pack, QByteArray&& raw_data); +bool processPackTXT(TexturePack& pack, QByteArray&& raw_data); +bool processPackPNG(TexturePack& pack, QByteArray&& raw_data); /** Checks whether a file is valid as a texture pack or not. */ bool validate(QFileInfo file); diff --git a/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.cpp b/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.cpp new file mode 100644 index 00000000..cbc8f8ce --- /dev/null +++ b/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.cpp @@ -0,0 +1,190 @@ + +// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include "LocalWorldSaveParseTask.h" + +#include "FileSystem.h" + +#include <quazip/quazip.h> +#include <quazip/quazipdir.h> +#include <quazip/quazipfile.h> + +#include <QDir> +#include <QFileInfo> + +namespace WorldSaveUtils { + +bool process(WorldSave& pack, ProcessingLevel level) +{ + switch (pack.type()) { + case ResourceType::FOLDER: + return WorldSaveUtils::processFolder(pack, level); + case ResourceType::ZIPFILE: + return WorldSaveUtils::processZIP(pack, level); + default: + qWarning() << "Invalid type for world save parse task!"; + return false; + } +} + +/// @brief checks a folder structure to see if it contains a level.dat +/// @param dir the path to check +/// @param saves used in recursive call if a "saves" dir was found +/// @return std::tuple of ( +/// bool <found level.dat>, +/// QString <name of folder containing level.dat>, +/// bool <saves folder found> +/// ) +static std::tuple<bool, QString, bool> contains_level_dat(QDir dir, bool saves = false) +{ + for (auto const& entry : dir.entryInfoList()) { + if (!entry.isDir()) { + continue; + } + if (!saves && entry.fileName() == "saves") { + return contains_level_dat(QDir(entry.filePath()), true); + } + QFileInfo level_dat(FS::PathCombine(entry.filePath(), "level.dat")); + if (level_dat.exists() && level_dat.isFile()) { + return std::make_tuple(true, entry.fileName(), saves); + } + } + return std::make_tuple(false, "", saves); +} + +bool processFolder(WorldSave& save, ProcessingLevel level) +{ + Q_ASSERT(save.type() == ResourceType::FOLDER); + + auto [found, save_dir_name, found_saves_dir] = contains_level_dat(QDir(save.fileinfo().filePath())); + + if (!found) { + return false; + } + + save.setSaveDirName(save_dir_name); + + if (found_saves_dir) { + save.setSaveFormat(WorldSaveFormat::MULTI); + } else { + save.setSaveFormat(WorldSaveFormat::SINGLE); + } + + if (level == ProcessingLevel::BasicInfoOnly) { + return true; // only need basic info already checked + } + + // reserved for more intensive processing + + return true; // all tests passed +} + +/// @brief checks a folder structure to see if it contains a level.dat +/// @param zip the zip file to check +/// @return std::tuple of ( +/// bool <found level.dat>, +/// QString <name of folder containing level.dat>, +/// bool <saves folder found> +/// ) +static std::tuple<bool, QString, bool> contains_level_dat(QuaZip& zip) +{ + bool saves = false; + QuaZipDir zipDir(&zip); + if (zipDir.exists("/saves")) { + saves = true; + zipDir.cd("/saves"); + } + + for (auto const& entry : zipDir.entryList()) { + zipDir.cd(entry); + if (zipDir.exists("level.dat")) { + return std::make_tuple(true, entry, saves); + } + zipDir.cd(".."); + } + return std::make_tuple(false, "", saves); +} + +bool processZIP(WorldSave& save, ProcessingLevel level) +{ + Q_ASSERT(save.type() == ResourceType::ZIPFILE); + + QuaZip zip(save.fileinfo().filePath()); + if (!zip.open(QuaZip::mdUnzip)) + return false; // can't open zip file + + auto [found, save_dir_name, found_saves_dir] = contains_level_dat(zip); + + if (save_dir_name.endsWith("/")) { + save_dir_name.chop(1); + } + + if (!found) { + return false; + } + + save.setSaveDirName(save_dir_name); + + if (found_saves_dir) { + save.setSaveFormat(WorldSaveFormat::MULTI); + } else { + save.setSaveFormat(WorldSaveFormat::SINGLE); + } + + if (level == ProcessingLevel::BasicInfoOnly) { + zip.close(); + return true; // only need basic info already checked + } + + // reserved for more intensive processing + + zip.close(); + + return true; +} + +bool validate(QFileInfo file) +{ + WorldSave sp{ file }; + return WorldSaveUtils::process(sp, ProcessingLevel::BasicInfoOnly) && sp.valid(); +} + +} // namespace WorldSaveUtils + +LocalWorldSaveParseTask::LocalWorldSaveParseTask(int token, WorldSave& save) : Task(nullptr, false), m_token(token), m_save(save) {} + +bool LocalWorldSaveParseTask::abort() +{ + m_aborted = true; + return true; +} + +void LocalWorldSaveParseTask::executeTask() +{ + if (!WorldSaveUtils::process(m_save)) + return; + + if (m_aborted) + emitAborted(); + else + emitSucceeded(); +} diff --git a/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.h b/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.h new file mode 100644 index 00000000..9dcdca2b --- /dev/null +++ b/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.h @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> +// +// SPDX-License-Identifier: GPL-3.0-only + +/* + * Prism Launcher - Minecraft Launcher + * Copyright (C) 2022 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <QDebug> +#include <QObject> + +#include "minecraft/mod/WorldSave.h" + +#include "tasks/Task.h" + +namespace WorldSaveUtils { + +enum class ProcessingLevel { Full, BasicInfoOnly }; + +bool process(WorldSave& save, ProcessingLevel level = ProcessingLevel::Full); + +bool processZIP(WorldSave& pack, ProcessingLevel level = ProcessingLevel::Full); +bool processFolder(WorldSave& pack, ProcessingLevel level = ProcessingLevel::Full); + +bool validate(QFileInfo file); + +} // namespace WorldSaveUtils + +class LocalWorldSaveParseTask : public Task { + Q_OBJECT + public: + LocalWorldSaveParseTask(int token, WorldSave& save); + + [[nodiscard]] bool canAbort() const override { return true; } + bool abort() override; + + void executeTask() override; + + [[nodiscard]] int token() const { return m_token; } + + private: + int m_token; + + WorldSave& m_save; + + bool m_aborted = false; +}; diff --git a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp index 78ef4386..3677a1dc 100644 --- a/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp +++ b/launcher/minecraft/mod/tasks/ModFolderLoadTask.cpp @@ -72,14 +72,14 @@ void ModFolderLoadTask::executeTask() delete mod; } else { - m_result->mods[mod->internal_id()] = mod; + m_result->mods[mod->internal_id()].reset(std::move(mod)); m_result->mods[mod->internal_id()]->setStatus(ModStatus::NoMetadata); } } else { QString chopped_id = mod->internal_id().chopped(9); if (m_result->mods.contains(chopped_id)) { - m_result->mods[mod->internal_id()] = mod; + m_result->mods[mod->internal_id()].reset(std::move(mod)); auto metadata = m_result->mods[chopped_id]->metadata(); if (metadata) { @@ -90,7 +90,7 @@ void ModFolderLoadTask::executeTask() } } else { - m_result->mods[mod->internal_id()] = mod; + m_result->mods[mod->internal_id()].reset(std::move(mod)); m_result->mods[mod->internal_id()]->setStatus(ModStatus::NoMetadata); } } @@ -130,6 +130,6 @@ void ModFolderLoadTask::getFromMetadata() auto* mod = new Mod(m_mods_dir, metadata); mod->setStatus(ModStatus::NotInstalled); - m_result->mods[mod->internal_id()] = mod; + m_result->mods[mod->internal_id()].reset(std::move(mod)); } } diff --git a/launcher/minecraft/services/CapeChange.cpp b/launcher/minecraft/services/CapeChange.cpp index c73a11b6..1d5ea36d 100644 --- a/launcher/minecraft/services/CapeChange.cpp +++ b/launcher/minecraft/services/CapeChange.cpp @@ -54,9 +54,14 @@ void CapeChange::setCape(QString& cape) { setStatus(tr("Equipping cape")); m_reply = shared_qobject_ptr<QNetworkReply>(rep); - connect(rep, &QNetworkReply::uploadProgress, this, &Task::setProgress); - connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(downloadError(QNetworkReply::NetworkError))); - connect(rep, SIGNAL(finished()), this, SLOT(downloadFinished())); + connect(rep, &QNetworkReply::uploadProgress, this, &CapeChange::setProgress); +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15 + connect(rep, &QNetworkReply::errorOccurred, this, &CapeChange::downloadError); +#else + connect(rep, QOverload<QNetworkReply::NetworkError>::of(&QNetworkReply::error), this, &CapeChange::downloadError); +#endif + connect(rep, &QNetworkReply::sslErrors, this, &CapeChange::sslErrors); + connect(rep, &QNetworkReply::finished, this, &CapeChange::downloadFinished); } void CapeChange::clearCape() { @@ -68,13 +73,14 @@ void CapeChange::clearCape() { setStatus(tr("Removing cape")); m_reply = shared_qobject_ptr<QNetworkReply>(rep); - connect(rep, &QNetworkReply::uploadProgress, this, &Task::setProgress); -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - connect(rep, SIGNAL(errorOccurred(QNetworkReply::NetworkError)), this, SLOT(downloadError(QNetworkReply::NetworkError))); + connect(rep, &QNetworkReply::uploadProgress, this, &CapeChange::setProgress); +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15 + connect(rep, &QNetworkReply::errorOccurred, this, &CapeChange::downloadError); #else - connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(downloadError(QNetworkReply::NetworkError))); + connect(rep, QOverload<QNetworkReply::NetworkError>::of(&QNetworkReply::error), this, &CapeChange::downloadError); #endif - connect(rep, SIGNAL(finished()), this, SLOT(downloadFinished())); + connect(rep, &QNetworkReply::sslErrors, this, &CapeChange::sslErrors); + connect(rep, &QNetworkReply::finished, this, &CapeChange::downloadFinished); } @@ -95,6 +101,17 @@ void CapeChange::downloadError(QNetworkReply::NetworkError error) emitFailed(m_reply->errorString()); } +void CapeChange::sslErrors(const QList<QSslError>& errors) +{ + int i = 1; + for (auto error : errors) { + qCritical() << "Cape change SSL Error #" << i << " : " << error.errorString(); + auto cert = error.certificate(); + qCritical() << "Certificate in question:\n" << cert.toText(); + i++; + } +} + void CapeChange::downloadFinished() { // if the download failed diff --git a/launcher/minecraft/services/CapeChange.h b/launcher/minecraft/services/CapeChange.h index 185d69b6..38069f90 100644 --- a/launcher/minecraft/services/CapeChange.h +++ b/launcher/minecraft/services/CapeChange.h @@ -27,6 +27,7 @@ protected: public slots: void downloadError(QNetworkReply::NetworkError); + void sslErrors(const QList<QSslError>& errors); void downloadFinished(); }; diff --git a/launcher/minecraft/services/SkinDelete.cpp b/launcher/minecraft/services/SkinDelete.cpp index 921bd094..fbaaeacb 100644 --- a/launcher/minecraft/services/SkinDelete.cpp +++ b/launcher/minecraft/services/SkinDelete.cpp @@ -53,13 +53,14 @@ void SkinDelete::executeTask() m_reply = shared_qobject_ptr<QNetworkReply>(rep); setStatus(tr("Deleting skin")); - connect(rep, &QNetworkReply::uploadProgress, this, &Task::setProgress); -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - connect(rep, SIGNAL(errorOccurred(QNetworkReply::NetworkError)), this, SLOT(downloadError(QNetworkReply::NetworkError))); + connect(rep, &QNetworkReply::uploadProgress, this, &SkinDelete::setProgress); +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15 + connect(rep, &QNetworkReply::errorOccurred, this, &SkinDelete::downloadError); #else - connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(downloadError(QNetworkReply::NetworkError))); + connect(rep, QOverload<QNetworkReply::NetworkError>::of(&QNetworkReply::error), this, &SkinDelete::downloadError); #endif - connect(rep, SIGNAL(finished()), this, SLOT(downloadFinished())); + connect(rep, &QNetworkReply::sslErrors, this, &SkinDelete::sslErrors); + connect(rep, &QNetworkReply::finished, this, &SkinDelete::downloadFinished); } void SkinDelete::downloadError(QNetworkReply::NetworkError error) @@ -69,6 +70,17 @@ void SkinDelete::downloadError(QNetworkReply::NetworkError error) emitFailed(m_reply->errorString()); } +void SkinDelete::sslErrors(const QList<QSslError>& errors) +{ + int i = 1; + for (auto error : errors) { + qCritical() << "Skin Delete SSL Error #" << i << " : " << error.errorString(); + auto cert = error.certificate(); + qCritical() << "Certificate in question:\n" << cert.toText(); + i++; + } +} + void SkinDelete::downloadFinished() { // if the download failed diff --git a/launcher/minecraft/services/SkinDelete.h b/launcher/minecraft/services/SkinDelete.h index 83a84685..b9a1c9d3 100644 --- a/launcher/minecraft/services/SkinDelete.h +++ b/launcher/minecraft/services/SkinDelete.h @@ -22,5 +22,6 @@ protected: public slots: void downloadError(QNetworkReply::NetworkError); + void sslErrors(const QList<QSslError>& errors); void downloadFinished(); }; diff --git a/launcher/minecraft/services/SkinUpload.cpp b/launcher/minecraft/services/SkinUpload.cpp index c7987875..711f8739 100644 --- a/launcher/minecraft/services/SkinUpload.cpp +++ b/launcher/minecraft/services/SkinUpload.cpp @@ -78,13 +78,14 @@ void SkinUpload::executeTask() m_reply = shared_qobject_ptr<QNetworkReply>(rep); setStatus(tr("Uploading skin")); - connect(rep, &QNetworkReply::uploadProgress, this, &Task::setProgress); -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - connect(rep, SIGNAL(errorOccurred(QNetworkReply::NetworkError)), this, SLOT(downloadError(QNetworkReply::NetworkError))); + connect(rep, &QNetworkReply::uploadProgress, this, &SkinUpload::setProgress); +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) // QNetworkReply::errorOccurred added in 5.15 + connect(rep, &QNetworkReply::errorOccurred, this, &SkinUpload::downloadError); #else - connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(downloadError(QNetworkReply::NetworkError))); + connect(rep, QOverload<QNetworkReply::NetworkError>::of(&QNetworkReply::error), this, &SkinUpload::downloadError); #endif - connect(rep, SIGNAL(finished()), this, SLOT(downloadFinished())); + connect(rep, &QNetworkReply::sslErrors, this, &SkinUpload::sslErrors); + connect(rep, &QNetworkReply::finished, this, &SkinUpload::downloadFinished); } void SkinUpload::downloadError(QNetworkReply::NetworkError error) @@ -94,6 +95,17 @@ void SkinUpload::downloadError(QNetworkReply::NetworkError error) emitFailed(m_reply->errorString()); } +void SkinUpload::sslErrors(const QList<QSslError>& errors) +{ + int i = 1; + for (auto error : errors) { + qCritical() << "Skin Upload SSL Error #" << i << " : " << error.errorString(); + auto cert = error.certificate(); + qCritical() << "Certificate in question:\n" << cert.toText(); + i++; + } +} + void SkinUpload::downloadFinished() { // if the download failed diff --git a/launcher/minecraft/services/SkinUpload.h b/launcher/minecraft/services/SkinUpload.h index 2c1f0a2e..ac8c5b36 100644 --- a/launcher/minecraft/services/SkinUpload.h +++ b/launcher/minecraft/services/SkinUpload.h @@ -32,6 +32,7 @@ protected: public slots: void downloadError(QNetworkReply::NetworkError); + void sslErrors(const QList<QSslError>& errors); void downloadFinished(); }; diff --git a/launcher/minecraft/update/AssetUpdateTask.cpp b/launcher/minecraft/update/AssetUpdateTask.cpp index dd246665..31fd5eb1 100644 --- a/launcher/minecraft/update/AssetUpdateTask.cpp +++ b/launcher/minecraft/update/AssetUpdateTask.cpp @@ -24,7 +24,7 @@ void AssetUpdateTask::executeTask() auto assets = profile->getMinecraftAssets(); QUrl indexUrl = assets->url; QString localPath = assets->id + ".json"; - auto job = new NetJob( + auto job = makeShared<NetJob>( tr("Asset index for %1").arg(m_inst->name()), APPLICATION->network() ); @@ -45,6 +45,7 @@ void AssetUpdateTask::executeTask() connect(downloadJob.get(), &NetJob::failed, this, &AssetUpdateTask::assetIndexFailed); connect(downloadJob.get(), &NetJob::aborted, this, [this]{ emitFailed(tr("Aborted")); }); connect(downloadJob.get(), &NetJob::progress, this, &AssetUpdateTask::progress); + connect(downloadJob.get(), &NetJob::stepProgress, this, &AssetUpdateTask::propogateStepProgress); qDebug() << m_inst->name() << ": Starting asset index download"; downloadJob->start(); @@ -83,6 +84,7 @@ void AssetUpdateTask::assetIndexFinished() connect(downloadJob.get(), &NetJob::failed, this, &AssetUpdateTask::assetsFailed); connect(downloadJob.get(), &NetJob::aborted, this, [this]{ emitFailed(tr("Aborted")); }); connect(downloadJob.get(), &NetJob::progress, this, &AssetUpdateTask::progress); + connect(downloadJob.get(), &NetJob::stepProgress, this, &AssetUpdateTask::propogateStepProgress); downloadJob->start(); return; } diff --git a/launcher/minecraft/update/FMLLibrariesTask.cpp b/launcher/minecraft/update/FMLLibrariesTask.cpp index 7a0bd2f3..75e5c572 100644 --- a/launcher/minecraft/update/FMLLibrariesTask.cpp +++ b/launcher/minecraft/update/FMLLibrariesTask.cpp @@ -61,7 +61,7 @@ void FMLLibrariesTask::executeTask() // download missing libs to our place setStatus(tr("Downloading FML libraries...")); - auto dljob = new NetJob("FML libraries", APPLICATION->network()); + NetJob::Ptr dljob{ new NetJob("FML libraries", APPLICATION->network()) }; auto metacache = APPLICATION->metacache(); Net::Download::Options options = Net::Download::Option::MakeEternal; for (auto &lib : fmlLibsToProcess) @@ -71,10 +71,11 @@ void FMLLibrariesTask::executeTask() dljob->addNetAction(Net::Download::makeCached(QUrl(urlString), entry, options)); } - connect(dljob, &NetJob::succeeded, this, &FMLLibrariesTask::fmllibsFinished); - connect(dljob, &NetJob::failed, this, &FMLLibrariesTask::fmllibsFailed); - connect(dljob, &NetJob::aborted, this, [this]{ emitFailed(tr("Aborted")); }); - connect(dljob, &NetJob::progress, this, &FMLLibrariesTask::progress); + connect(dljob.get(), &NetJob::succeeded, this, &FMLLibrariesTask::fmllibsFinished); + connect(dljob.get(), &NetJob::failed, this, &FMLLibrariesTask::fmllibsFailed); + connect(dljob.get(), &NetJob::aborted, this, [this]{ emitFailed(tr("Aborted")); }); + connect(dljob.get(), &NetJob::progress, this, &FMLLibrariesTask::progress); + connect(dljob.get(), &NetJob::stepProgress, this, &FMLLibrariesTask::propogateStepProgress); downloadJob.reset(dljob); downloadJob->start(); } diff --git a/launcher/minecraft/update/LibrariesTask.cpp b/launcher/minecraft/update/LibrariesTask.cpp index 33a575c2..415b9a66 100644 --- a/launcher/minecraft/update/LibrariesTask.cpp +++ b/launcher/minecraft/update/LibrariesTask.cpp @@ -20,7 +20,7 @@ void LibrariesTask::executeTask() auto components = inst->getPackProfile(); auto profile = components->getProfile(); - auto job = new NetJob(tr("Libraries for instance %1").arg(inst->name()), APPLICATION->network()); + NetJob::Ptr job{ new NetJob(tr("Libraries for instance %1").arg(inst->name()), APPLICATION->network()) }; downloadJob.reset(job); auto metacache = APPLICATION->metacache(); @@ -70,6 +70,8 @@ void LibrariesTask::executeTask() connect(downloadJob.get(), &NetJob::failed, this, &LibrariesTask::jarlibFailed); connect(downloadJob.get(), &NetJob::aborted, this, [this]{ emitFailed(tr("Aborted")); }); connect(downloadJob.get(), &NetJob::progress, this, &LibrariesTask::progress); + connect(downloadJob.get(), &NetJob::stepProgress, this, &LibrariesTask::propogateStepProgress); + downloadJob->start(); } |