diff options
author | Aaron Sonin <10217842+byteduck@users.noreply.github.com> | 2023-01-12 20:11:46 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-01-12 20:11:46 -0800 |
commit | b2f48eaeb8cd6dbdf9b8bbbf1a163339c88173a7 (patch) | |
tree | 80773415691e538d437bc03c42f73a8abd01f126 /launcher/minecraft/mod/tasks | |
parent | 160dd09fc2788fea17c8e9e332c2877586640971 (diff) | |
parent | 5e89ba70797df863c7eeae9b147080e7dbdf6d3b (diff) | |
download | PrismLauncher-b2f48eaeb8cd6dbdf9b8bbbf1a163339c88173a7.tar.gz PrismLauncher-b2f48eaeb8cd6dbdf9b8bbbf1a163339c88173a7.tar.bz2 PrismLauncher-b2f48eaeb8cd6dbdf9b8bbbf1a163339c88173a7.zip |
Merge branch 'PrismLauncher:develop' into instance-accounts
Diffstat (limited to 'launcher/minecraft/mod/tasks')
14 files changed, 1009 insertions, 113 deletions
diff --git a/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp b/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp new file mode 100644 index 00000000..3fcb2110 --- /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() << "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.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() << "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 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..8bfe2c84 100644 --- a/launcher/minecraft/mod/tasks/LocalModParseTask.cpp +++ b/launcher/minecraft/mod/tasks/LocalModParseTask.cpp @@ -11,9 +11,10 @@ #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 @@ -283,35 +284,46 @@ ModDetails ReadLiteModInfo(QByteArray contents) return details; } -} // namespace - -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 process(Mod& mod, ProcessingLevel level) +{ + 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; + } +} -void LocalModParseTask::processAsZip() +bool processZIP(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("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 +342,131 @@ 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.readAll()); file.close(); zip.close(); - return; + + 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 +475,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..19ddc899 --- /dev/null +++ b/launcher/minecraft/mod/tasks/LocalResourceParse.cpp @@ -0,0 +1,60 @@ +// 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 "LocalResourceParse.h" + +#include "LocalDataPackParseTask.h" +#include "LocalModParseTask.h" +#include "LocalResourcePackParseTask.h" +#include "LocalShaderPackParseTask.h" +#include "LocalTexturePackParseTask.h" +#include "LocalWorldSaveParseTask.h" + +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; +} +} diff --git a/launcher/minecraft/mod/tasks/LocalResourceParse.h b/launcher/minecraft/mod/tasks/LocalResourceParse.h new file mode 100644 index 00000000..b07a874c --- /dev/null +++ b/launcher/minecraft/mod/tasks/LocalResourceParse.h @@ -0,0 +1,31 @@ +// 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 <QFileInfo> +#include <QObject> + +enum class PackedResourceType { DataPack, ResourcePack, TexturePack, ShaderPack, WorldSave, Mod, UNKNOWN }; +namespace ResourceUtils { +PackedResourceType identify(QFileInfo file); +} // 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; +}; |