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();  } | 
