diff --git a/launcher/minecraft/mod/WorldSave.cpp b/launcher/minecraft/mod/WorldSave.cpp
new file mode 100644
index 00000000..9a626fc1
--- /dev/null
+++ b/launcher/minecraft/mod/WorldSave.cpp
@@ -0,0 +1,45 @@
+// 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 .
+ */
+
+#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;
+}
\ No newline at end of file
diff --git a/launcher/minecraft/mod/WorldSave.h b/launcher/minecraft/mod/WorldSave.h
new file mode 100644
index 00000000..f48f42b9
--- /dev/null
+++ b/launcher/minecraft/mod/WorldSave.h
@@ -0,0 +1,67 @@
+// 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 .
+ */
+
+#pragma once
+
+#include "Resource.h"
+
+#include
+
+class Version;
+
+enum WorldSaveFormat {
+ SINGLE,
+ MULTI,
+ INVALID
+};
+
+class WorldSave : public Resource {
+ Q_OBJECT
+ public:
+ using Ptr = shared_qobject_ptr;
+
+ 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 'version' of a resource pack, as defined in the pack.mcmeta file.
+ * See https://minecraft.fandom.com/wiki/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta
+ */
+ WorldSaveFormat m_save_format = WorldSaveFormat::INVALID;
+
+ QString m_save_dir_name;
+
+};
diff --git a/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.cpp b/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.cpp
new file mode 100644
index 00000000..5405d308
--- /dev/null
+++ b/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.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 .
+ */
+
+#include "LocalWorldSaveParseTask.h"
+
+#include "FileSystem.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+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 shader pack parse task!";
+ return false;
+ }
+}
+
+
+static std::tuple 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
+ }
+
+ // resurved for more intensive processing
+
+ return true; // all tests passed
+}
+
+static std::tuple 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 (!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
+ }
+
+ // resurved 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..44153735
--- /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 .
+ */
+
+#pragma once
+
+#include
+#include
+
+#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;
+};
\ No newline at end of file