summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/actions/setup-dependencies/linux/action.yml2
-rw-r--r--.github/actions/setup-dependencies/macos/action.yml2
-rw-r--r--.github/actions/setup-dependencies/windows/action.yml2
-rw-r--r--CMakeLists.txt3
-rw-r--r--launcher/Application.cpp25
-rw-r--r--launcher/CMakeLists.txt5
-rw-r--r--launcher/InstanceImportTask.cpp90
-rw-r--r--launcher/InstanceImportTask.h2
-rw-r--r--launcher/MMCZip.cpp633
-rw-r--r--launcher/MMCZip.h71
-rw-r--r--launcher/launch/steps/Update.cpp4
-rw-r--r--launcher/minecraft/MinecraftLoadAndCheck.h1
-rw-r--r--launcher/minecraft/MinecraftUpdate.h1
-rw-r--r--launcher/minecraft/World.cpp42
-rw-r--r--launcher/minecraft/launch/ExtractNatives.cpp29
-rw-r--r--launcher/minecraft/launch/VerifyJavaInstall.cpp94
-rw-r--r--launcher/minecraft/mod/LocalModParseTask.cpp100
-rw-r--r--launcher/modplatform/atlauncher/ATLPackInstallTask.cpp18
-rw-r--r--launcher/modplatform/flame/FlamePackIndex.cpp1
-rw-r--r--launcher/modplatform/flame/FlamePackIndex.h1
-rw-r--r--launcher/modplatform/flame/PackManifest.cpp8
-rw-r--r--launcher/modplatform/legacy_ftb/PackInstallTask.cpp7
-rw-r--r--launcher/modplatform/legacy_ftb/PackInstallTask.h3
-rw-r--r--launcher/modplatform/modpacksch/FTBPackInstallTask.cpp9
-rw-r--r--launcher/modplatform/technic/SingleZipPackInstallTask.cpp14
-rw-r--r--launcher/modplatform/technic/SingleZipPackInstallTask.h3
-rw-r--r--launcher/modplatform/technic/TechnicPackProcessor.cpp38
-rw-r--r--launcher/tasks/Task.cpp13
-rw-r--r--launcher/ui/dialogs/BlockedModsDialog.cpp171
-rw-r--r--launcher/ui/dialogs/BlockedModsDialog.h70
-rw-r--r--launcher/ui/pages/modplatform/flame/FlamePage.cpp22
-rw-r--r--launcher/ui/pages/modplatform/flame/FlamePage.h1
-rw-r--r--libraries/classparser/CMakeLists.txt2
-rw-r--r--libraries/classparser/src/classparser.cpp53
-rw-r--r--nix/unwrapped.nix1
-rwxr-xr-xscripts/checkpatch.pl3
-rw-r--r--vcpkg.json4
37 files changed, 1045 insertions, 503 deletions
diff --git a/.github/actions/setup-dependencies/linux/action.yml b/.github/actions/setup-dependencies/linux/action.yml
index 1f89c85b07..53b6284d68 100644
--- a/.github/actions/setup-dependencies/linux/action.yml
+++ b/.github/actions/setup-dependencies/linux/action.yml
@@ -18,7 +18,7 @@ runs:
dpkg-dev \
ninja-build extra-cmake-modules pkg-config scdoc \
cmark gamemode-dev libarchive-dev libcmark-dev libqrencode-dev zlib1g-dev \
- libxcb-cursor-dev libtomlplusplus-dev libvulkan-dev libquazip1-qt6-dev
+ libxcb-cursor-dev libtomlplusplus-dev libvulkan-dev libarchive
- name: Setup AppImage tooling
shell: bash
diff --git a/.github/actions/setup-dependencies/macos/action.yml b/.github/actions/setup-dependencies/macos/action.yml
index f37f131411..3eb5b37fa6 100644
--- a/.github/actions/setup-dependencies/macos/action.yml
+++ b/.github/actions/setup-dependencies/macos/action.yml
@@ -17,7 +17,7 @@ runs:
shell: bash
run: |
brew update
- brew install ninja extra-cmake-modules temurin@17 mono quazip autoconf
+ brew install ninja extra-cmake-modules temurin@17 mono autoconf libarchive
- name: Set JAVA_HOME
shell: bash
diff --git a/.github/actions/setup-dependencies/windows/action.yml b/.github/actions/setup-dependencies/windows/action.yml
index 9434d7929e..a06b38d389 100644
--- a/.github/actions/setup-dependencies/windows/action.yml
+++ b/.github/actions/setup-dependencies/windows/action.yml
@@ -83,7 +83,7 @@ runs:
qt6-svg:p
qt6-imageformats:p
qt6-networkauth:p
- quazip-qt6:p
+ qt6-5compat:p
cmark:p
qrencode:p
tomlplusplus:p
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 442756bf8c..21a7378f59 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -113,7 +113,7 @@ find_package(Qt6 REQUIRED COMPONENTS
Xml
)
-find_package(QuaZip-Qt6 REQUIRED)
+find_package(LibArchive REQUIRED)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
####################################### Branding #######################################
@@ -143,6 +143,7 @@ set(MeshMC_Git "https://github.com/Project-Tick/MeshMC")
set(MeshMC_SVGFileName "${MeshMC_AppID}.svg")
set(MeshMC_Branding_ICNS "branding/${MeshMC_AppID}.icns")
+set(MeshMC_Branding_ICO "${MeshMC_AppID}.ico")
set(MeshMC_Branding_WindowsRC "branding/${MeshMC_AppBinaryName}.rc")
set(MeshMC_Branding_LogoQRC "branding/${MeshMC_AppBinaryName}.qrc")
diff --git a/launcher/Application.cpp b/launcher/Application.cpp
index 9159fa8e37..c762f8211c 100644
--- a/launcher/Application.cpp
+++ b/launcher/Application.cpp
@@ -807,9 +807,28 @@ void Application::initSettings()
m_settings->registerSetting({"ProxyUser", "ProxyUsername"}, "");
m_settings->registerSetting({"ProxyPass", "ProxyPassword"}, "");
- // Memory
- m_settings->registerSetting({"MinMemAlloc", "MinMemoryAlloc"}, 512);
- m_settings->registerSetting({"MaxMemAlloc", "MaxMemoryAlloc"}, 1024);
+ // Memory — compute reasonable defaults based on system RAM
+ int defaultMinMem = 512;
+ int defaultMaxMem = 1024;
+ {
+ uint64_t systemRamMiB = Sys::getSystemRam() / Sys::mebibyte;
+ if (systemRamMiB >= 32768) { // 32+ GB
+ defaultMinMem = 1024;
+ defaultMaxMem = 8192;
+ } else if (systemRamMiB >= 16384) { // 16-32 GB
+ defaultMinMem = 1024;
+ defaultMaxMem = 6144;
+ } else if (systemRamMiB >= 8192) { // 8-16 GB
+ defaultMinMem = 512;
+ defaultMaxMem = 4096;
+ } else if (systemRamMiB >= 4096) { // 4-8 GB
+ defaultMinMem = 512;
+ defaultMaxMem = 2048;
+ }
+ // <4 GB: keep 512/1024 defaults
+ }
+ m_settings->registerSetting({"MinMemAlloc", "MinMemoryAlloc"}, defaultMinMem);
+ m_settings->registerSetting({"MaxMemAlloc", "MaxMemoryAlloc"}, defaultMaxMem);
m_settings->registerSetting("PermGen", 128);
// Java Settings
diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt
index 026fc34ae9..99b61b9ea5 100644
--- a/launcher/CMakeLists.txt
+++ b/launcher/CMakeLists.txt
@@ -773,6 +773,8 @@ SET(MESHMC_SOURCES
# GUI - dialogs
ui/dialogs/AboutDialog.cpp
ui/dialogs/AboutDialog.h
+ ui/dialogs/BlockedModsDialog.cpp
+ ui/dialogs/BlockedModsDialog.h
ui/dialogs/ProfileSelectDialog.cpp
ui/dialogs/ProfileSelectDialog.h
ui/dialogs/ProfileSetupDialog.cpp
@@ -958,11 +960,10 @@ target_link_libraries(MeshMC_logic
Qt6::NetworkAuth
Qt6::Concurrent
Qt6::Gui
- QuaZip::QuaZip
+ LibArchive::LibArchive
)
target_link_libraries(MeshMC_logic
MeshMC_iconfix
- ${QUAZIP_LIBRARIES}
hoedown
MeshMC_rainbow
LocalPeer
diff --git a/launcher/InstanceImportTask.cpp b/launcher/InstanceImportTask.cpp
index bcf205f6dd..72d38d1a51 100644
--- a/launcher/InstanceImportTask.cpp
+++ b/launcher/InstanceImportTask.cpp
@@ -54,11 +54,14 @@
#include "modplatform/flame/PackManifest.h"
#include "modplatform/modrinth/ModrinthPackManifest.h"
#include "Json.h"
-#include <quazipdir.h>
#include "modplatform/technic/TechnicPackProcessor.h"
#include "icons/IconList.h"
#include "Application.h"
+#include "ui/dialogs/BlockedModsDialog.h"
+
+#include <QDir>
+#include <QStandardPaths>
InstanceImportTask::InstanceImportTask(const QUrl sourceUrl)
{
@@ -114,20 +117,13 @@ void InstanceImportTask::processZipPack()
QDir extractDir(m_stagingPath);
qDebug() << "Attempting to create instance from" << m_archivePath;
- // open the zip and find relevant files in it
- m_packZip.reset(new QuaZip(m_archivePath));
- if (!m_packZip->open(QuaZip::mdUnzip))
- {
- emitFailed(tr("Unable to open supplied modpack zip file."));
- return;
- }
-
+ // find relevant files in the zip
QStringList blacklist = {"instance.cfg", "manifest.json", "modrinth.index.json"};
- QString mmcFound = MMCZip::findFolderOfFileInZip(m_packZip.get(), "instance.cfg");
- bool technicFound = QuaZipDir(m_packZip.get()).exists("/bin/modpack.jar") ||
- QuaZipDir(m_packZip.get()).exists("/bin/version.json");
- QString flameFound = MMCZip::findFolderOfFileInZip(m_packZip.get(), "manifest.json");
- QString modrinthFound = MMCZip::findFolderOfFileInZip(m_packZip.get(), "modrinth.index.json");
+ QString mmcFound = MMCZip::findFolderOfFileInZip(m_archivePath, "instance.cfg");
+ bool technicFound = MMCZip::entryExists(m_archivePath, "bin/modpack.jar") ||
+ MMCZip::entryExists(m_archivePath, "bin/version.json");
+ QString flameFound = MMCZip::findFolderOfFileInZip(m_archivePath, "manifest.json");
+ QString modrinthFound = MMCZip::findFolderOfFileInZip(m_archivePath, "modrinth.index.json");
QString root;
if(!mmcFound.isNull())
{
@@ -165,9 +161,10 @@ void InstanceImportTask::processZipPack()
}
// make sure we extract just the pack
+ QString archivePath = m_archivePath;
m_extractFuture = QtConcurrent::run(
QThreadPool::globalInstance(), MMCZip::extractSubDir,
- m_packZip.get(), root, extractDir.absolutePath());
+ archivePath, root, extractDir.absolutePath());
connect(&m_extractFutureWatcher,
&QFutureWatcher<QStringList>::finished,
this, &InstanceImportTask::extractFinished);
@@ -179,7 +176,6 @@ void InstanceImportTask::processZipPack()
void InstanceImportTask::extractFinished()
{
- m_packZip.reset();
if (!m_extractFuture.result())
{
emitFailed(tr("Failed to extract modpack"));
@@ -419,6 +415,10 @@ void InstanceImportTask::onFlameFileResolutionSucceeded()
{
auto results = m_modIdResolver->getResults();
m_filesNetJob = new NetJob(tr("Mod download"), APPLICATION->network());
+
+ // Collect restricted mods that need browser download
+ QList<BlockedMod> blockedMods;
+
for(auto result: results.files)
{
QString filename = result.fileName;
@@ -441,9 +441,16 @@ void InstanceImportTask::onFlameFileResolutionSucceeded()
[[fallthrough]];
case Flame::File::Type::Mod:
{
- if(!result.resolved || !result.url.isValid() || result.url.isEmpty())
+ bool isBlocked = !result.resolved || !result.url.isValid() || result.url.isEmpty();
+ if (isBlocked && !result.fileName.isEmpty())
+ {
+ blockedMods.append({result.projectId, result.fileId, result.fileName, path, false});
+ break;
+ }
+ if (isBlocked)
{
- logWarning(tr("Skipping %1 - no download URL available (mod may have restricted downloads)").arg(result.fileName));
+ logWarning(tr("Skipping mod %1 (project %2) - no download URL and no filename available")
+ .arg(result.fileId).arg(result.projectId));
break;
}
qDebug() << "Will download" << result.url << "to" << path;
@@ -463,17 +470,56 @@ void InstanceImportTask::onFlameFileResolutionSucceeded()
break;
}
}
+
+ // Handle restricted mods via dialog
+ if (!blockedMods.isEmpty())
+ {
+ BlockedModsDialog dlg(nullptr,
+ tr("Restricted Mods"),
+ tr("The following mods have restricted downloads and are not available through the API.\n"
+ "Click the Download button next to each mod to open its download page in your browser.\n"
+ "Once all files appear in your Downloads folder, click Continue."),
+ blockedMods);
+
+ if (dlg.exec() == QDialog::Accepted)
+ {
+ QString downloadDir = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation);
+ for (const auto &mod : blockedMods)
+ {
+ if (mod.found)
+ {
+ QString srcPath = FS::PathCombine(downloadDir, mod.fileName);
+ QFileInfo targetInfo(mod.targetPath);
+ QDir().mkpath(targetInfo.absolutePath());
+
+ if (QFile::copy(srcPath, mod.targetPath))
+ {
+ qDebug() << "Copied restricted mod:" << mod.fileName;
+ }
+ else
+ {
+ logWarning(tr("Failed to copy %1 from downloads folder").arg(mod.fileName));
+ }
+ }
+ }
+ }
+ else
+ {
+ logWarning(tr("User cancelled restricted mod downloads - %1 mod(s) will be missing").arg(blockedMods.size()));
+ }
+ }
+
m_modIdResolver.reset();
connect(m_filesNetJob.get(), &NetJob::succeeded, this, [&]()
{
- m_filesNetJob.reset();
emitSucceeded();
+ m_filesNetJob.reset();
}
);
connect(m_filesNetJob.get(), &NetJob::failed, [&](QString reason)
{
- m_filesNetJob.reset();
emitFailed(reason);
+ m_filesNetJob.reset();
});
connect(m_filesNetJob.get(), &NetJob::progress, [&](qint64 current, qint64 total)
{
@@ -610,13 +656,13 @@ void InstanceImportTask::processModrinth()
connect(m_filesNetJob.get(), &NetJob::succeeded, this, [&]()
{
- m_filesNetJob.reset();
emitSucceeded();
+ m_filesNetJob.reset();
});
connect(m_filesNetJob.get(), &NetJob::failed, [&](QString reason)
{
- m_filesNetJob.reset();
emitFailed(reason);
+ m_filesNetJob.reset();
});
connect(m_filesNetJob.get(), &NetJob::progress, [&](qint64 current, qint64 total)
{
diff --git a/launcher/InstanceImportTask.h b/launcher/InstanceImportTask.h
index 9748c6dbea..052070ea2c 100644
--- a/launcher/InstanceImportTask.h
+++ b/launcher/InstanceImportTask.h
@@ -48,7 +48,6 @@
#include <nonstd/optional>
-class QuaZip;
namespace Flame
{
class FileResolvingTask;
@@ -87,7 +86,6 @@ private: /* data */
QUrl m_sourceUrl;
QString m_archivePath;
bool m_downloadRequired = false;
- std::unique_ptr<QuaZip> m_packZip;
QFuture<nonstd::optional<QStringList>> m_extractFuture;
QFutureWatcher<nonstd::optional<QStringList>> m_extractFutureWatcher;
enum class ModpackType{
diff --git a/launcher/MMCZip.cpp b/launcher/MMCZip.cpp
index 7870386518..b5d19ba177 100644
--- a/launcher/MMCZip.cpp
+++ b/launcher/MMCZip.cpp
@@ -36,29 +36,121 @@
* limitations under the License.
*/
-#include <quazip.h>
-#include <quazipdir.h>
-#include <quazipfile.h>
-#include <JlCompress.h>
#include "MMCZip.h"
#include "FileSystem.h"
+#include <archive.h>
+#include <archive_entry.h>
+
#include <QDebug>
+#include <QDir>
#include <QDirIterator>
+#include <QFile>
-bool MMCZip::compressDir(QString zipFile, QString dir, FilterFunction excludeFilter)
+#include <memory>
+
+// RAII helpers for libarchive
+struct ArchiveReadDeleter {
+ void operator()(struct archive *a) const { archive_read_free(a); } // NOLINT(readability-identifier-naming)
+};
+struct ArchiveWriteDeleter {
+ void operator()(struct archive *a) const { archive_write_free(a); } // NOLINT(readability-identifier-naming)
+};
+using ArchiveReadPtr = std::unique_ptr<struct archive, ArchiveReadDeleter>; // NOLINT(readability-identifier-naming)
+using ArchiveWritePtr = std::unique_ptr<struct archive, ArchiveWriteDeleter>; // NOLINT(readability-identifier-naming)
+
+static ArchiveReadPtr openZipForReading(const QString &path)
{
- QuaZip zip(zipFile);
- if (!zip.open(QuaZip::mdCreate))
- {
+ ArchiveReadPtr a(archive_read_new());
+ archive_read_support_format_zip(a.get());
+ if (archive_read_open_filename(a.get(), path.toUtf8().constData(), 10240) != ARCHIVE_OK) {
+ qWarning() << "Could not open archive:" << path << archive_error_string(a.get());
+ return nullptr;
+ }
+ return a;
+}
+
+static ArchiveWritePtr createZipForWriting(const QString &path)
+{
+ ArchiveWritePtr a(archive_write_new());
+ archive_write_set_format_zip(a.get());
+ if (archive_write_open_filename(a.get(), path.toUtf8().constData()) != ARCHIVE_OK) {
+ qWarning() << "Could not create archive:" << path << archive_error_string(a.get());
+ return nullptr;
+ }
+ return a;
+}
+
+static bool copyArchiveData(struct archive *ar, struct archive *aw) // NOLINT(readability-identifier-naming)
+{
+ const void *buff;
+ size_t size;
+ la_int64_t offset;
+ int r;
+ while ((r = archive_read_data_block(ar, &buff, &size, &offset)) == ARCHIVE_OK) {
+ if (archive_write_data_block(aw, buff, size, offset) != ARCHIVE_OK)
+ return false;
+ }
+ return r == ARCHIVE_EOF;
+}
+
+static bool writeFileToArchive(struct archive *aw, const QString &entryName, // NOLINT(readability-identifier-naming)
+ const QByteArray &data)
+{
+ struct archive_entry *entry = archive_entry_new();
+ archive_entry_set_pathname(entry, entryName.toUtf8().constData());
+ archive_entry_set_size(entry, data.size());
+ archive_entry_set_filetype(entry, AE_IFREG);
+ archive_entry_set_perm(entry, 0644);
+
+ if (archive_write_header(aw, entry) != ARCHIVE_OK) {
+ archive_entry_free(entry);
return false;
}
+ if (data.size() > 0) {
+ archive_write_data(aw, data.constData(), data.size());
+ }
+ archive_entry_free(entry);
+ return true;
+}
+
+static bool writeDiskEntry(struct archive *ar, const QString &absFilePath)
+{
+ // Ensure parent directory exists
+ QFileInfo fi(absFilePath);
+ QDir().mkpath(fi.absolutePath());
+
+ if (absFilePath.endsWith('/')) {
+ // Directory entry
+ QDir().mkpath(absFilePath);
+ return true;
+ }
+
+ QFile outFile(absFilePath);
+ if (!outFile.open(QIODevice::WriteOnly)) {
+ qWarning() << "Failed to open for writing:" << absFilePath;
+ return false;
+ }
+
+ const void *buff;
+ size_t size;
+ la_int64_t offset;
+ while (archive_read_data_block(ar, &buff, &size, &offset) == ARCHIVE_OK) {
+ outFile.write(static_cast<const char*>(buff), size);
+ }
+ outFile.close();
+ return true;
+}
+
+bool MMCZip::compressDir(QString zipFile, QString dir, FilterFunction excludeFilter)
+{
+ auto aw = createZipForWriting(zipFile);
+ if (!aw)
+ return false;
QDir directory(dir);
if (!directory.exists())
- {
return false;
- }
QDirIterator it(dir, QDir::Files | QDir::Hidden, QDirIterator::Subdirectories);
bool success = true;
@@ -67,309 +159,490 @@ bool MMCZip::compressDir(QString zipFile, QString dir, FilterFunction excludeFil
it.next();
QString relPath = directory.relativeFilePath(it.filePath());
if (excludeFilter && excludeFilter(relPath))
- {
continue;
+
+ QFile file(it.filePath());
+ if (!file.open(QIODevice::ReadOnly)) {
+ success = false;
+ break;
}
- if (!JlCompress::compressFile(&zip, it.filePath(), relPath))
- {
+ QByteArray data = file.readAll();
+ file.close();
+
+ if (!writeFileToArchive(aw.get(), relPath, data)) {
success = false;
break;
}
}
- zip.close();
- if (zip.getZipError() != 0)
- {
- return false;
- }
+ archive_write_close(aw.get());
return success;
}
-// ours
-bool MMCZip::mergeZipFiles(QuaZip *into, QFileInfo from, QSet<QString> &contained, const FilterFunction filter)
+bool MMCZip::mergeZipFiles(const QString &intoPath, QFileInfo from,
+ QSet<QString> &contained, const FilterFunction filter)
{
- QuaZip modZip(from.filePath());
- modZip.open(QuaZip::mdUnzip);
+ // Read all entries from source zip
+ auto ar = openZipForReading(from.filePath());
+ if (!ar)
+ return false;
- QuaZipFile fileInsideMod(&modZip);
- QuaZipFile zipOutFile(into);
- for (bool more = modZip.goToFirstFile(); more; more = modZip.goToNextFile())
- {
- QString filename = modZip.getCurrentFileName();
- if (filter && !filter(filename))
- {
- qDebug() << "Skipping file " << filename << " from "
- << from.fileName() << " - filtered";
+ // Read existing entries from target if it exists, then append new ones
+ // We build a list of entries to write
+ struct EntryData {
+ QString name;
+ QByteArray data;
+ };
+ QList<EntryData> newEntries;
+
+ struct archive_entry *entry;
+ while (archive_read_next_header(ar.get(), &entry) == ARCHIVE_OK) {
+ QString filename = QString::fromUtf8(archive_entry_pathname(entry));
+ if (filter && !filter(filename)) {
+ qDebug() << "Skipping file " << filename << " from " << from.fileName() << " - filtered";
+ archive_read_data_skip(ar.get());
continue;
}
- if (contained.contains(filename))
- {
- qDebug() << "Skipping already contained file " << filename << " from "
- << from.fileName();
+ if (contained.contains(filename)) {
+ qDebug() << "Skipping already contained file " << filename << " from " << from.fileName();
+ archive_read_data_skip(ar.get());
continue;
}
contained.insert(filename);
- if (!fileInsideMod.open(QIODevice::ReadOnly))
- {
- qCritical() << "Failed to open " << filename << " from " << from.fileName();
- return false;
+ // Read data
+ la_int64_t entrySize = archive_entry_size(entry);
+ QByteArray data;
+ if (entrySize > 0) {
+ data.resize(entrySize);
+ la_ssize_t bytesRead = archive_read_data(ar.get(), data.data(), entrySize);
+ if (bytesRead < 0) {
+ qCritical() << "Failed to read " << filename << " from " << from.fileName();
+ return false;
+ }
+ data.resize(bytesRead);
}
+ newEntries.append({filename, data});
+ }
- QuaZipNewInfo info_out(fileInsideMod.getActualFileName());
+ // Now append to existing zip (or create new one)
+ // libarchive doesn't support append, so we need to read existing + write all
+ QList<EntryData> existingEntries;
+ if (QFile::exists(intoPath)) {
+ auto existingAr = openZipForReading(intoPath);
+ if (existingAr) {
+ while (archive_read_next_header(existingAr.get(), &entry) == ARCHIVE_OK) {
+ QString name = QString::fromUtf8(archive_entry_pathname(entry));
+ la_int64_t sz = archive_entry_size(entry);
+ QByteArray data;
+ if (sz > 0) {
+ data.resize(sz);
+ archive_read_data(existingAr.get(), data.data(), sz);
+ }
+ existingEntries.append({name, data});
+ }
+ }
+ }
- if (!zipOutFile.open(QIODevice::WriteOnly, info_out))
- {
- qCritical() << "Failed to open " << filename << " in the jar";
- fileInsideMod.close();
+ auto aw = createZipForWriting(intoPath);
+ if (!aw)
+ return false;
+
+ // Write existing entries
+ for (const auto &e : existingEntries) {
+ if (!writeFileToArchive(aw.get(), e.name, e.data)) {
+ qCritical() << "Failed to write existing entry " << e.name;
return false;
}
- if (!JlCompress::copyData(fileInsideMod, zipOutFile))
- {
- zipOutFile.close();
- fileInsideMod.close();
- qCritical() << "Failed to copy data of " << filename << " into the jar";
+ }
+
+ // Write new entries
+ for (const auto &e : newEntries) {
+ if (!writeFileToArchive(aw.get(), e.name, e.data)) {
+ qCritical() << "Failed to write " << e.name << " into the jar";
return false;
}
- zipOutFile.close();
- fileInsideMod.close();
}
+
+ archive_write_close(aw.get());
return true;
}
-// ours
bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<Mod>& mods)
{
- QuaZip zipOut(targetJarPath);
- if (!zipOut.open(QuaZip::mdCreate))
- {
- QFile::remove(targetJarPath);
- qCritical() << "Failed to open the minecraft.jar for modding";
- return false;
- }
- // Files already added to the jar.
- // These files will be skipped.
+ // Files already added to the jar. These files will be skipped.
QSet<QString> addedFiles;
- // Modify the jar
+ // We collect all entries first, then write them all at once.
+ struct EntryData {
+ QString name;
+ QByteArray data;
+ };
+ QList<EntryData> allEntries;
+
+ // Modify the jar - process mods in reverse order
QListIterator<Mod> i(mods);
i.toBack();
while (i.hasPrevious())
{
const Mod &mod = i.previous();
- // do not merge disabled mods.
if (!mod.enabled())
continue;
+
if (mod.type() == Mod::MOD_ZIPFILE)
{
- if (!mergeZipFiles(&zipOut, mod.filename(), addedFiles))
- {
- zipOut.close();
+ auto ar = openZipForReading(mod.filename().absoluteFilePath());
+ if (!ar) {
QFile::remove(targetJarPath);
qCritical() << "Failed to add" << mod.filename().fileName() << "to the jar.";
return false;
}
+ struct archive_entry *entry;
+ while (archive_read_next_header(ar.get(), &entry) == ARCHIVE_OK) {
+ QString filename = QString::fromUtf8(archive_entry_pathname(entry));
+ if (addedFiles.contains(filename)) {
+ archive_read_data_skip(ar.get());
+ continue;
+ }
+ addedFiles.insert(filename);
+ la_int64_t sz = archive_entry_size(entry);
+ QByteArray data;
+ if (sz > 0) {
+ data.resize(sz);
+ archive_read_data(ar.get(), data.data(), sz);
+ }
+ allEntries.append({filename, data});
+ }
}
else if (mod.type() == Mod::MOD_SINGLEFILE)
{
- // FIXME: buggy - does not work with addedFiles
auto filename = mod.filename();
- if (!JlCompress::compressFile(&zipOut, filename.absoluteFilePath(), filename.fileName()))
- {
- zipOut.close();
+ QFile file(filename.absoluteFilePath());
+ if (!file.open(QIODevice::ReadOnly)) {
QFile::remove(targetJarPath);
- qCritical() << "Failed to add" << mod.filename().fileName() << "to the jar.";
+ qCritical() << "Failed to add" << filename.fileName() << "to the jar.";
return false;
}
+ allEntries.append({filename.fileName(), file.readAll()});
addedFiles.insert(filename.fileName());
}
else if (mod.type() == Mod::MOD_FOLDER)
{
- // FIXME: buggy - does not work with addedFiles
auto filename = mod.filename();
QString what_to_zip = filename.absoluteFilePath();
QDir dir(what_to_zip);
dir.cdUp();
QString parent_dir = dir.absolutePath();
- if (!JlCompress::compressSubDir(&zipOut, what_to_zip, parent_dir, true, QDir::NoFilter))
- {
- zipOut.close();
- QFile::remove(targetJarPath);
- qCritical() << "Failed to add" << mod.filename().fileName() << "to the jar.";
- return false;
+ QDirIterator it(what_to_zip, QDir::Files | QDir::Hidden, QDirIterator::Subdirectories);
+ while (it.hasNext()) {
+ it.next();
+ QString relPath = QDir(parent_dir).relativeFilePath(it.filePath());
+ QFile f(it.filePath());
+ if (f.open(QIODevice::ReadOnly)) {
+ allEntries.append({relPath, f.readAll()});
+ }
}
- qDebug() << "Adding folder " << filename.fileName() << " from "
- << filename.absoluteFilePath();
+ qDebug() << "Adding folder " << filename.fileName() << " from " << filename.absoluteFilePath();
}
else
{
- // Make sure we do not continue launching when something is missing or undefined...
- zipOut.close();
QFile::remove(targetJarPath);
qCritical() << "Failed to add unknown mod type" << mod.filename().fileName() << "to the jar.";
return false;
}
}
- if (!mergeZipFiles(&zipOut, QFileInfo(sourceJarPath), addedFiles, [](const QString key){return !key.contains("META-INF");}))
- {
- zipOut.close();
+ // Add source jar contents (skip META-INF and already added files)
+ auto ar = openZipForReading(sourceJarPath);
+ if (!ar) {
QFile::remove(targetJarPath);
qCritical() << "Failed to insert minecraft.jar contents.";
return false;
}
+ struct archive_entry *entry;
+ while (archive_read_next_header(ar.get(), &entry) == ARCHIVE_OK) {
+ QString filename = QString::fromUtf8(archive_entry_pathname(entry));
+ if (filename.contains("META-INF")) {
+ archive_read_data_skip(ar.get());
+ continue;
+ }
+ if (addedFiles.contains(filename)) {
+ archive_read_data_skip(ar.get());
+ continue;
+ }
+ la_int64_t sz = archive_entry_size(entry);
+ QByteArray data;
+ if (sz > 0) {
+ data.resize(sz);
+ archive_read_data(ar.get(), data.data(), sz);
+ }
+ allEntries.append({filename, data});
+ }
- // Recompress the jar
- zipOut.close();
- if (zipOut.getZipError() != 0)
- {
+ // Write the final jar
+ auto aw = createZipForWriting(targetJarPath);
+ if (!aw) {
QFile::remove(targetJarPath);
- qCritical() << "Failed to finalize minecraft.jar!";
+ qCritical() << "Failed to open the minecraft.jar for modding";
return false;
}
+ for (const auto &e : allEntries) {
+ if (!writeFileToArchive(aw.get(), e.name, e.data)) {
+ archive_write_close(aw.get());
+ (void)QFile::remove(targetJarPath);
+ qCritical() << "Failed to finalize minecraft.jar!";
+ return false;
+ }
+ }
+ archive_write_close(aw.get());
return true;
}
-// ours
-QString MMCZip::findFolderOfFileInZip(QuaZip * zip, const QString & what, const QString &root)
+QString MMCZip::findFolderOfFileInZip(const QString &zipPath, const QString & what, const QString &root)
{
- QuaZipDir rootDir(zip, root);
- for(auto fileName: rootDir.entryList(QDir::Files))
- {
- if(fileName == what)
+ auto entries = listEntries(zipPath, root, QDir::Files);
+ for (const auto &fileName : entries) {
+ if (fileName == what)
return root;
}
- for(auto fileName: rootDir.entryList(QDir::Dirs))
- {
- QString result = findFolderOfFileInZip(zip, what, root + fileName);
- if(!result.isEmpty())
- {
+ auto dirs = listEntries(zipPath, root, QDir::Dirs);
+ for (const auto &dirName : dirs) {
+ QString result = findFolderOfFileInZip(zipPath, what, root + dirName);
+ if (!result.isEmpty())
return result;
- }
}
return QString();
}
-// ours
-bool MMCZip::findFilesInZip(QuaZip * zip, const QString & what, QStringList & result, const QString &root)
+bool MMCZip::findFilesInZip(const QString &zipPath, const QString & what, QStringList & result, const QString &root)
{
- QuaZipDir rootDir(zip, root);
- for(auto fileName: rootDir.entryList(QDir::Files))
- {
- if(fileName == what)
- {
+ auto entries = listEntries(zipPath, root, QDir::Files);
+ for (const auto &fileName : entries) {
+ if (fileName == what) {
result.append(root);
return true;
}
}
- for(auto fileName: rootDir.entryList(QDir::Dirs))
- {
- findFilesInZip(zip, what, result, root + fileName);
+ auto dirs = listEntries(zipPath, root, QDir::Dirs);
+ for (const auto &dirName : dirs) {
+ (void)findFilesInZip(zipPath, what, result, root + dirName);
}
return !result.isEmpty();
}
-
-// ours
-nonstd::optional<QStringList> MMCZip::extractSubDir(QuaZip *zip, const QString & subdir, const QString &target)
+nonstd::optional<QStringList> MMCZip::extractSubDir(const QString &zipPath, const QString & subdir, const QString &target)
{
QDir directory(target);
QStringList extracted;
- qDebug() << "Extracting subdir" << subdir << "from" << zip->getZipName() << "to" << target;
- auto numEntries = zip->getEntriesCount();
- if(numEntries < 0) {
- qWarning() << "Failed to enumerate files in archive";
- return nonstd::nullopt;
- }
- else if(numEntries == 0) {
- qDebug() << "Extracting empty archives seems odd...";
- return extracted;
- }
- else if (!zip->goToFirstFile())
- {
- qWarning() << "Failed to seek to first file in zip";
+ qDebug() << "Extracting subdir" << subdir << "from" << zipPath << "to" << target;
+
+ auto ar = openZipForReading(zipPath);
+ if (!ar) {
+ // check if this is a minimum size empty zip file...
+ QFileInfo fileInfo(zipPath);
+ if (fileInfo.size() == 22) {
+ return extracted;
+ }
+ qWarning() << "Failed to open archive:" << zipPath;
return nonstd::nullopt;
}
- do
- {
- QString name = zip->getCurrentFileName();
- if(!name.startsWith(subdir))
- {
+ struct archive_entry *entry;
+ bool hasEntries = false;
+ while (archive_read_next_header(ar.get(), &entry) == ARCHIVE_OK) {
+ hasEntries = true;
+ QString name = QString::fromUtf8(archive_entry_pathname(entry));
+ if (!name.startsWith(subdir)) {
+ archive_read_data_skip(ar.get());
continue;
}
- name.remove(0, subdir.size());
- QString absFilePath = directory.absoluteFilePath(name);
- if(name.isEmpty())
- {
+ QString relName = name.mid(subdir.size());
+ QString absFilePath = directory.absoluteFilePath(relName);
+ if (relName.isEmpty()) {
absFilePath += "/";
}
- if (!JlCompress::extractFile(zip, "", absFilePath))
- {
+
+ if (!writeDiskEntry(ar.get(), absFilePath)) {
qWarning() << "Failed to extract file" << name << "to" << absFilePath;
- JlCompress::removeFile(extracted);
+ // Clean up extracted files
+ for (const auto &f : extracted)
+ (void)QFile::remove(f);
return nonstd::nullopt;
}
extracted.append(absFilePath);
- qDebug() << "Extracted file" << name;
- } while (zip->goToNextFile());
+ qDebug() << "Extracted file" << relName;
+ }
+
+ if (!hasEntries) {
+ qDebug() << "Extracting empty archives seems odd...";
+ }
return extracted;
}
-// ours
-bool MMCZip::extractRelFile(QuaZip *zip, const QString &file, const QString &target)
+bool MMCZip::extractRelFile(const QString &zipPath, const QString &file, const QString &target)
{
- return JlCompress::extractFile(zip, file, target);
+ auto ar = openZipForReading(zipPath);
+ if (!ar)
+ return false;
+
+ struct archive_entry *entry;
+ while (archive_read_next_header(ar.get(), &entry) == ARCHIVE_OK) {
+ QString name = QString::fromUtf8(archive_entry_pathname(entry));
+ if (name == file) {
+ return writeDiskEntry(ar.get(), target);
+ }
+ archive_read_data_skip(ar.get());
+ }
+ return false;
}
-// ours
nonstd::optional<QStringList> MMCZip::extractDir(QString fileCompressed, QString dir)
{
- QuaZip zip(fileCompressed);
- if (!zip.open(QuaZip::mdUnzip))
- {
- // check if this is a minimum size empty zip file...
- QFileInfo fileInfo(fileCompressed);
- if(fileInfo.size() == 22) {
- return QStringList();
- }
- qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError();;
- return nonstd::nullopt;
- }
- return MMCZip::extractSubDir(&zip, "", dir);
+ return MMCZip::extractSubDir(fileCompressed, "", dir);
}
-// ours
nonstd::optional<QStringList> MMCZip::extractDir(QString fileCompressed, QString subdir, QString dir)
{
- QuaZip zip(fileCompressed);
- if (!zip.open(QuaZip::mdUnzip))
- {
- // check if this is a minimum size empty zip file...
- QFileInfo fileInfo(fileCompressed);
- if(fileInfo.size() == 22) {
- return QStringList();
+ return MMCZip::extractSubDir(fileCompressed, subdir, dir);
+}
+
+bool MMCZip::extractFile(QString fileCompressed, QString file, QString target)
+{
+ // check if this is a minimum size empty zip file...
+ QFileInfo fileInfo(fileCompressed);
+ if (fileInfo.size() == 22) {
+ return true;
+ }
+ return MMCZip::extractRelFile(fileCompressed, file, target);
+}
+
+QByteArray MMCZip::readFileFromZip(const QString &zipPath, const QString &entryName)
+{
+ auto ar = openZipForReading(zipPath);
+ if (!ar)
+ return {};
+
+ struct archive_entry *entry;
+ while (archive_read_next_header(ar.get(), &entry) == ARCHIVE_OK) {
+ QString name = QString::fromUtf8(archive_entry_pathname(entry));
+ if (name == entryName) {
+ la_int64_t sz = archive_entry_size(entry);
+ if (sz <= 0) {
+ // Size may be unknown; read in chunks
+ QByteArray result;
+ char buf[8192];
+ la_ssize_t r;
+ while ((r = archive_read_data(ar.get(), buf, sizeof(buf))) > 0)
+ result.append(buf, r);
+ return result;
+ }
+ QByteArray data(sz, Qt::Uninitialized);
+ archive_read_data(ar.get(), data.data(), sz);
+ return data;
}
- qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError();;
- return nonstd::nullopt;
+ archive_read_data_skip(ar.get());
}
- return MMCZip::extractSubDir(&zip, subdir, dir);
+ return {};
}
-// ours
-bool MMCZip::extractFile(QString fileCompressed, QString file, QString target)
+bool MMCZip::entryExists(const QString &zipPath, const QString &entryName)
{
- QuaZip zip(fileCompressed);
- if (!zip.open(QuaZip::mdUnzip))
- {
- // check if this is a minimum size empty zip file...
- QFileInfo fileInfo(fileCompressed);
- if(fileInfo.size() == 22) {
+ auto ar = openZipForReading(zipPath);
+ if (!ar)
+ return false;
+
+ struct archive_entry *entry;
+ while (archive_read_next_header(ar.get(), &entry) == ARCHIVE_OK) {
+ QString name = QString::fromUtf8(archive_entry_pathname(entry));
+ if (name == entryName || name == entryName + "/")
return true;
+ archive_read_data_skip(ar.get());
+ }
+ return false;
+}
+
+QStringList MMCZip::listEntries(const QString &zipPath)
+{
+ QStringList result;
+ auto ar = openZipForReading(zipPath);
+ if (!ar)
+ return result;
+
+ struct archive_entry *entry;
+ while (archive_read_next_header(ar.get(), &entry) == ARCHIVE_OK) {
+ result.append(QString::fromUtf8(archive_entry_pathname(entry)));
+ archive_read_data_skip(ar.get());
+ }
+ return result;
+}
+
+QStringList MMCZip::listEntries(const QString &zipPath, const QString &dirPath, QDir::Filters type)
+{
+ QStringList result;
+ auto ar = openZipForReading(zipPath);
+ if (!ar)
+ return result;
+
+ QSet<QString> seen;
+ struct archive_entry *entry;
+ while (archive_read_next_header(ar.get(), &entry) == ARCHIVE_OK) {
+ QString name = QString::fromUtf8(archive_entry_pathname(entry));
+
+ // Must start with dirPath
+ if (!name.startsWith(dirPath)) {
+ archive_read_data_skip(ar.get());
+ continue;
}
- qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError();
- return false;
+
+ QString relative = name.mid(dirPath.size());
+ // Remove leading slashes
+ while (relative.startsWith('/'))
+ relative = relative.mid(1);
+
+ if (relative.isEmpty()) {
+ archive_read_data_skip(ar.get());
+ continue;
+ }
+
+ int slashIdx = relative.indexOf('/');
+ if (slashIdx < 0) {
+ // It's a file directly in dirPath
+ if (type & QDir::Files) {
+ if (!seen.contains(relative)) {
+ seen.insert(relative);
+ result.append(relative);
+ }
+ }
+ } else {
+ // It's inside a subdirectory
+ if (type & QDir::Dirs) {
+ QString dirName = relative.left(slashIdx + 1); // include trailing /
+ if (!seen.contains(dirName)) {
+ seen.insert(dirName);
+ result.append(dirName);
+ }
+ }
+ }
+ archive_read_data_skip(ar.get());
+ }
+ return result;
+}
+
+QDateTime MMCZip::getEntryModTime(const QString &zipPath, const QString &entryName)
+{
+ auto ar = openZipForReading(zipPath);
+ if (!ar)
+ return {};
+
+ struct archive_entry *entry;
+ while (archive_read_next_header(ar.get(), &entry) == ARCHIVE_OK) {
+ QString name = QString::fromUtf8(archive_entry_pathname(entry));
+ if (name == entryName) {
+ time_t mtime = archive_entry_mtime(entry);
+ return QDateTime::fromSecsSinceEpoch(mtime);
+ }
+ archive_read_data_skip(ar.get());
}
- return MMCZip::extractRelFile(&zip, file, target);
+ return {};
}
diff --git a/launcher/MMCZip.h b/launcher/MMCZip.h
index d29cac13c9..a490793073 100644
--- a/launcher/MMCZip.h
+++ b/launcher/MMCZip.h
@@ -40,11 +40,12 @@
#include <QString>
#include <QFileInfo>
+#include <QDir>
#include <QSet>
+#include <QDateTime>
#include "minecraft/mod/Mod.h"
#include <functional>
-#include <JlCompress.h>
#include <nonstd/optional>
namespace MMCZip
@@ -52,43 +53,46 @@ namespace MMCZip
using FilterFunction = std::function<bool(const QString &)>;
/**
- * Merge two zip files, using a filter function
+ * Merge two zip files, using a filter function.
+ * Reads entries from 'from' and writes them into the zip at 'intoPath'.
+ * 'contained' tracks already-added filenames to avoid duplicates.
*/
- bool mergeZipFiles(QuaZip *into, QFileInfo from, QSet<QString> &contained,
- const FilterFunction filter = nullptr);
+ bool mergeZipFiles(const QString &intoPath, QFileInfo from, QSet<QString> &contained,
+ const FilterFunction filter = nullptr);
/**
- * take a source jar, add mods to it, resulting in target jar
+ * Take a source jar, add mods to it, resulting in target jar.
*/
bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList<Mod>& mods);
/**
- * Find a single file in archive by file name (not path)
- *
+ * Find a single file in archive by file name (not path).
* \return the path prefix where the file is
*/
- QString findFolderOfFileInZip(QuaZip * zip, const QString & what, const QString &root = QString(""));
+ QString findFolderOfFileInZip(const QString &zipPath, const QString & what, const QString &root = QString(""));
/**
- * Find a multiple files of the same name in archive by file name
- * If a file is found in a path, no deeper paths are searched
- *
+ * Find multiple files of the same name in archive by file name.
+ * If a file is found in a path, no deeper paths are searched.
* \return true if anything was found
*/
- bool findFilesInZip(QuaZip * zip, const QString & what, QStringList & result, const QString &root = QString());
+ bool findFilesInZip(const QString &zipPath, const QString &what,
+ QStringList &result, const QString &root = QString());
/**
- * Extract a subdirectory from an archive
+ * Compress a directory into a zip, using a filter function to exclude entries.
*/
+ bool compressDir(QString zipFile, QString dir, FilterFunction excludeFilter);
/**
- * Compress a directory, using a filter function to exclude entries
+ * Extract a subdirectory from an archive.
*/
- bool compressDir(QString zipFile, QString dir, FilterFunction excludeFilter);
-
- nonstd::optional<QStringList> extractSubDir(QuaZip *zip, const QString & subdir, const QString &target);
+ nonstd::optional<QStringList> extractSubDir(const QString &zipPath, const QString & subdir, const QString &target);
- bool extractRelFile(QuaZip *zip, const QString & file, const QString &target);
+ /**
+ * Extract a single file relative to the zip root.
+ */
+ bool extractRelFile(const QString &zipPath, const QString & file, const QString &target);
/**
* Extract a whole archive.
@@ -100,7 +104,7 @@ namespace MMCZip
nonstd::optional<QStringList> extractDir(QString fileCompressed, QString dir);
/**
- * Extract a subdirectory from an archive
+ * Extract a subdirectory from an archive.
*
* \param fileCompressed The name of the archive.
* \param subdir The directory within the archive to extract
@@ -110,7 +114,7 @@ namespace MMCZip
nonstd::optional<QStringList> extractDir(QString fileCompressed, QString subdir, QString dir);
/**
- * Extract a single file from an archive into a directory
+ * Extract a single file from an archive into a directory.
*
* \param fileCompressed The name of the archive.
* \param file The file within the archive to extract
@@ -119,4 +123,31 @@ namespace MMCZip
*/
bool extractFile(QString fileCompressed, QString file, QString dir);
+ /**
+ * Read a file's contents from inside a zip archive.
+ * \return the file data, or empty QByteArray on failure
+ */
+ QByteArray readFileFromZip(const QString &zipPath, const QString &entryName);
+
+ /**
+ * Check if a given entry path exists in a zip archive.
+ */
+ bool entryExists(const QString &zipPath, const QString &entryName);
+
+ /**
+ * List all entry names in a zip archive.
+ */
+ QStringList listEntries(const QString &zipPath);
+
+ /**
+ * List entries under a specific directory in a zip archive.
+ * \param type QDir::Files, QDir::Dirs, or both
+ */
+ QStringList listEntries(const QString &zipPath, const QString &dirPath,
+ QDir::Filters type = QDir::Files | QDir::Dirs);
+
+ /**
+ * Get the modification time of a specific entry in a zip archive.
+ */
+ QDateTime getEntryModTime(const QString &zipPath, const QString &entryName);
}
diff --git a/launcher/launch/steps/Update.cpp b/launcher/launch/steps/Update.cpp
index c9b9e3cee6..aa544c6c16 100644
--- a/launcher/launch/steps/Update.cpp
+++ b/launcher/launch/steps/Update.cpp
@@ -67,15 +67,15 @@ void Update::updateFinished()
{
if(m_updateTask->wasSuccessful())
{
- m_updateTask.reset();
emitSucceeded();
+ m_updateTask.reset();
}
else
{
QString reason = tr("Instance update failed because: %1\n\n").arg(m_updateTask->failReason());
- m_updateTask.reset();
emit logLine(reason, MessageLevel::Fatal);
emitFailed(reason);
+ m_updateTask.reset();
}
}
diff --git a/launcher/minecraft/MinecraftLoadAndCheck.h b/launcher/minecraft/MinecraftLoadAndCheck.h
index 8a6db9963e..281dc16f08 100644
--- a/launcher/minecraft/MinecraftLoadAndCheck.h
+++ b/launcher/minecraft/MinecraftLoadAndCheck.h
@@ -43,7 +43,6 @@
#include <QUrl>
#include "tasks/Task.h"
-#include <quazip.h>
#include "QObjectPtr.h"
diff --git a/launcher/minecraft/MinecraftUpdate.h b/launcher/minecraft/MinecraftUpdate.h
index 5ff5593db7..24b5c0957d 100644
--- a/launcher/minecraft/MinecraftUpdate.h
+++ b/launcher/minecraft/MinecraftUpdate.h
@@ -45,7 +45,6 @@
#include "net/NetJob.h"
#include "tasks/Task.h"
#include "minecraft/VersionFilterData.h"
-#include <quazip.h>
class MinecraftVersion;
class MinecraftInstance;
diff --git a/launcher/minecraft/World.cpp b/launcher/minecraft/World.cpp
index a6e84325a8..104fba5cf7 100644
--- a/launcher/minecraft/World.cpp
+++ b/launcher/minecraft/World.cpp
@@ -49,9 +49,6 @@
#include <io/stream_reader.h>
#include <tag_string.h>
#include <tag_primitive.h>
-#include <quazip.h>
-#include <quazipfile.h>
-#include <quazipdir.h>
#include <QCoreApplication>
@@ -260,41 +257,22 @@ void World::readFromFS(const QFileInfo &file)
void World::readFromZip(const QFileInfo &file)
{
- QuaZip zip(file.absoluteFilePath());
- is_valid = zip.open(QuaZip::mdUnzip);
- if (!is_valid)
- {
- return;
- }
- auto location = MMCZip::findFolderOfFileInZip(&zip, "level.dat");
+ QString zipPath = file.absoluteFilePath();
+ auto location = MMCZip::findFolderOfFileInZip(zipPath, "level.dat");
is_valid = !location.isEmpty();
if (!is_valid)
{
return;
}
m_containerOffsetPath = location;
- QuaZipFile zippedFile(&zip);
- // read the install profile
- is_valid = zip.setCurrentFile(location + "level.dat");
- if (!is_valid)
- {
- return;
- }
- is_valid = zippedFile.open(QIODevice::ReadOnly);
- QuaZipFileInfo64 levelDatInfo;
- zippedFile.getFileInfo(&levelDatInfo);
- auto modTime = levelDatInfo.getNTFSmTime();
- if(!modTime.isValid())
- {
- modTime = levelDatInfo.dateTime;
- }
- levelDatTime = modTime;
+ QByteArray levelDatData = MMCZip::readFileFromZip(zipPath, location + "level.dat");
+ is_valid = !levelDatData.isEmpty();
if (!is_valid)
{
return;
}
- loadFromLevelDat(zippedFile.readAll());
- zippedFile.close();
+ levelDatTime = MMCZip::getEntryModTime(zipPath, location + "level.dat");
+ loadFromLevelDat(levelDatData);
}
bool World::install(const QString &to, const QString &name)
@@ -307,12 +285,8 @@ bool World::install(const QString &to, const QString &name)
bool ok = false;
if(m_containerFile.isFile())
{
- QuaZip zip(m_containerFile.absoluteFilePath());
- if (!zip.open(QuaZip::mdUnzip))
- {
- return false;
- }
- ok = !MMCZip::extractSubDir(&zip, m_containerOffsetPath, finalPath);
+ auto result = MMCZip::extractSubDir(m_containerFile.absoluteFilePath(), m_containerOffsetPath, finalPath);
+ ok = result.has_value();
}
else if(m_containerFile.isDir())
{
diff --git a/launcher/minecraft/launch/ExtractNatives.cpp b/launcher/minecraft/launch/ExtractNatives.cpp
index b72b7f0484..7a74ebd34f 100644
--- a/launcher/minecraft/launch/ExtractNatives.cpp
+++ b/launcher/minecraft/launch/ExtractNatives.cpp
@@ -40,8 +40,6 @@
#include <minecraft/MinecraftInstance.h>
#include <launch/LaunchTask.h>
-#include <quazip.h>
-#include <quazipdir.h>
#include "MMCZip.h"
#include "FileSystem.h"
#include <QDir>
@@ -65,19 +63,14 @@ static QString replaceSuffix (QString target, const QString &suffix, const QStri
static bool unzipNatives(QString source, QString targetFolder, bool applyJnilibHack, bool nativeOpenAL, bool nativeGLFW)
{
- QuaZip zip(source);
- if(!zip.open(QuaZip::mdUnzip))
- {
- return false;
- }
QDir directory(targetFolder);
- if (!zip.goToFirstFile())
+ QStringList entries = MMCZip::listEntries(source);
+ if (entries.isEmpty())
{
return false;
}
- do
+ for (const auto &name : entries)
{
- QString name = zip.getCurrentFileName();
auto lowercase = name.toLower();
if (nativeGLFW && name.contains("glfw")) {
continue;
@@ -85,20 +78,20 @@ static bool unzipNatives(QString source, QString targetFolder, bool applyJnilibH
if (nativeOpenAL && name.contains("openal")) {
continue;
}
+ // Skip directories
+ if (name.endsWith('/'))
+ continue;
+
+ QString outName = name;
if(applyJnilibHack)
{
- name = replaceSuffix(name, ".jnilib", ".dylib");
+ outName = replaceSuffix(outName, ".jnilib", ".dylib");
}
- QString absFilePath = directory.absoluteFilePath(name);
- if (!JlCompress::extractFile(&zip, "", absFilePath))
+ QString absFilePath = directory.absoluteFilePath(outName);
+ if (!MMCZip::extractRelFile(source, name, absFilePath))
{
return false;
}
- } while (zip.goToNextFile());
- zip.close();
- if(zip.getZipError()!=0)
- {
- return false;
}
return true;
}
diff --git a/launcher/minecraft/launch/VerifyJavaInstall.cpp b/launcher/minecraft/launch/VerifyJavaInstall.cpp
index b1c47e206d..3368bfa8c1 100644
--- a/launcher/minecraft/launch/VerifyJavaInstall.cpp
+++ b/launcher/minecraft/launch/VerifyJavaInstall.cpp
@@ -35,6 +35,7 @@
#include "FileSystem.h"
#include "Json.h"
+#include "java/JavaUtils.h"
#ifndef MeshMC_DISABLE_JAVA_DOWNLOADER
#include "Application.h"
@@ -74,48 +75,74 @@ QString VerifyJavaInstall::javaInstallDir() const
QString VerifyJavaInstall::findInstalledJava(int requiredMajor) const
{
- QString javaBaseDir = javaInstallDir();
- QDir baseDir(javaBaseDir);
- if (!baseDir.exists())
- return {};
-
#if defined(Q_OS_WIN)
QString binaryName = "javaw.exe";
#else
QString binaryName = "java";
#endif
- // Scan java/{vendor}/{version}/bin/java
- QDirIterator vendorIt(javaBaseDir, QDir::Dirs | QDir::NoDotAndDotDot);
- while (vendorIt.hasNext()) {
- vendorIt.next();
- QString vendorPath = vendorIt.filePath();
-
- QDirIterator versionIt(vendorPath, QDir::Dirs | QDir::NoDotAndDotDot);
- while (versionIt.hasNext()) {
- versionIt.next();
- QString versionPath = versionIt.filePath();
-
- QDirIterator binIt(versionPath, QStringList() << binaryName,
- QDir::Files, QDirIterator::Subdirectories);
- while (binIt.hasNext()) {
- binIt.next();
- QString javaPath = binIt.filePath();
- if (javaPath.contains("/bin/")) {
- // Check if the directory name contains the required major version
- // Version dirs are named like "zulu25.30.31-ca-jdk25.0.0-linux_x64"
- // or "jdk-25+36" etc. We look for the major version number.
+ // Pattern matching Java major version in directory names
+ // Matches: "java-8-openjdk", "jdk-17", "java-21-openjdk-amd64", "zulu25.30.31-ca-jdk25.0.0-linux_x64"
+ QRegularExpression re(QString("(?:jdk|java|jre)[-.]?%1(?:[^0-9]|$)").arg(requiredMajor));
+
+ // 1. Scan MeshMC's managed java/{vendor}/{version}/bin/java directory
+ QString javaBaseDir = javaInstallDir();
+ QDir baseDir(javaBaseDir);
+ if (baseDir.exists()) {
+ QDirIterator vendorIt(javaBaseDir, QDir::Dirs | QDir::NoDotAndDotDot);
+ while (vendorIt.hasNext()) {
+ vendorIt.next();
+ QString vendorPath = vendorIt.filePath();
+
+ QDirIterator versionIt(vendorPath, QDir::Dirs | QDir::NoDotAndDotDot);
+ while (versionIt.hasNext()) {
+ versionIt.next();
+ QString versionPath = versionIt.filePath();
+
+ QDirIterator binIt(versionPath, QStringList() << binaryName,
+ QDir::Files, QDirIterator::Subdirectories);
+ while (binIt.hasNext()) {
+ binIt.next();
+ QString javaPath = binIt.filePath();
+ if (!javaPath.contains("/bin/"))
+ continue;
QString dirName = versionIt.fileName().toLower();
- // Match patterns like "jdk25", "java25", "-25.", "-25+", "-25-"
- QRegularExpression re(QString("(?:jdk|java|jre)[-.]?%1(?:[^0-9]|$)").arg(requiredMajor));
- if (re.match(dirName).hasMatch()) {
+ if (re.match(dirName).hasMatch())
return javaPath;
- }
}
}
}
}
+ // 2. Scan system-installed Java paths
+ // Use JavaUtils to discover all Java installations on the system, then
+ // check if any match the required major version by parsing the path.
+ JavaUtils javaUtils;
+ QList<QString> systemJavas = javaUtils.FindJavaPaths();
+ for (const QString &javaPath : systemJavas) {
+ // Resolve to absolute path and verify it exists
+ QString resolved = FS::ResolveExecutable(javaPath);
+ if (resolved.isEmpty())
+ continue;
+
+ // Extract the parent directory components to check for version info
+ // Typical paths:
+ // /usr/lib/jvm/java-8-openjdk/bin/java
+ // /usr/lib/jvm/java-8-openjdk/jre/bin/java
+ // /usr/lib64/jvm/java-17-openjdk/bin/java
+ // /opt/jdk/jdk-21/bin/java
+ QFileInfo fi(resolved);
+ QString fullPath = fi.absoluteFilePath().toLower();
+
+ // Check if any component in the path matches the required Java version
+ QStringList parts = fullPath.split('/');
+ for (const QString &part : parts) {
+ if (re.match(part).hasMatch()) {
+ return resolved;
+ }
+ }
+ }
+
return {};
}
@@ -210,9 +237,9 @@ void VerifyJavaInstall::fetchVersionList(int requiredMajor)
});
connect(m_fetchJob.get(), &NetJob::failed, this, [this, requiredMajor](QString reason) {
- m_fetchJob.reset();
emitFailed(tr("Failed to fetch Java version list: %1. Please install Java %2 manually.")
.arg(reason).arg(requiredMajor));
+ m_fetchJob.reset();
});
m_fetchJob->start();
@@ -229,7 +256,7 @@ void VerifyJavaInstall::fetchRuntimes(const QString &versionId, int requiredMajo
m_fetchJob->addNetAction(dl);
connect(m_fetchJob.get(), &NetJob::succeeded, this, [this, requiredMajor]() {
- m_fetchJob.reset();
+ auto fetchJob = std::move(m_fetchJob);
QJsonDocument doc;
try {
@@ -261,8 +288,8 @@ void VerifyJavaInstall::fetchRuntimes(const QString &versionId, int requiredMajo
});
connect(m_fetchJob.get(), &NetJob::failed, this, [this](QString reason) {
- m_fetchJob.reset();
emitFailed(tr("Failed to fetch Java runtime details: %1").arg(reason));
+ m_fetchJob.reset();
});
m_fetchJob->start();
@@ -277,7 +304,6 @@ void VerifyJavaInstall::startDownload(const JavaDownload::RuntimeEntry &runtime,
connect(m_downloadTask.get(), &Task::succeeded, this, [this]() {
QString javaPath = m_downloadTask->installedJavaPath();
- m_downloadTask.reset();
if (javaPath.isEmpty()) {
emitFailed(tr("Java was downloaded but the binary could not be found."));
@@ -289,8 +315,8 @@ void VerifyJavaInstall::startDownload(const JavaDownload::RuntimeEntry &runtime,
});
connect(m_downloadTask.get(), &Task::failed, this, [this, requiredMajor](const QString &reason) {
- m_downloadTask.reset();
emitFailed(tr("Failed to download Java %1: %2").arg(requiredMajor).arg(reason));
+ m_downloadTask.reset();
});
connect(m_downloadTask.get(), &Task::status, this, [this](const QString &status) {
diff --git a/launcher/minecraft/mod/LocalModParseTask.cpp b/launcher/minecraft/mod/LocalModParseTask.cpp
index 39daaf4700..d68deb0cc6 100644
--- a/launcher/minecraft/mod/LocalModParseTask.cpp
+++ b/launcher/minecraft/mod/LocalModParseTask.cpp
@@ -25,10 +25,10 @@
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonValue>
-#include <quazip.h>
-#include <quazipfile.h>
#include <toml.h>
+#include "MMCZip.h"
+
#include "settings/INIFile.h"
#include "FileSystem.h"
@@ -343,36 +343,21 @@ LocalModParseTask::LocalModParseTask(int token, Mod::ModType type, const QFileIn
void LocalModParseTask::processAsZip()
{
- QuaZip zip(m_modFile.filePath());
- if (!zip.open(QuaZip::mdUnzip))
- return;
-
- QuaZipFile file(&zip);
+ QString zipPath = m_modFile.filePath();
- if (zip.setCurrentFile("META-INF/mods.toml"))
+ QByteArray modsToml = MMCZip::readFileFromZip(zipPath, "META-INF/mods.toml");
+ if (!modsToml.isEmpty())
{
- if (!file.open(QIODevice::ReadOnly))
- {
- zip.close();
- return;
- }
-
- m_result->details = ReadMCModTOML(file.readAll());
- file.close();
+ m_result->details = ReadMCModTOML(modsToml);
// to replace ${file.jarVersion} with the actual version, as needed
if (m_result->details && m_result->details->version == "${file.jarVersion}")
{
- if (zip.setCurrentFile("META-INF/MANIFEST.MF"))
+ QByteArray manifestData = MMCZip::readFileFromZip(zipPath, "META-INF/MANIFEST.MF");
+ if (!manifestData.isEmpty())
{
- if (!file.open(QIODevice::ReadOnly))
- {
- zip.close();
- return;
- }
-
// quick and dirty line-by-line parser
- auto manifestLines = file.readAll().split('\n');
+ auto manifestLines = manifestData.split('\n');
QString manifestVersion = "";
for (auto &line : manifestLines)
{
@@ -391,55 +376,31 @@ void LocalModParseTask::processAsZip()
}
m_result->details->version = manifestVersion;
-
- file.close();
}
}
-
- zip.close();
return;
}
- else if (zip.setCurrentFile("mcmod.info"))
- {
- if (!file.open(QIODevice::ReadOnly))
- {
- zip.close();
- return;
- }
- m_result->details = ReadMCModInfo(file.readAll());
- file.close();
- zip.close();
+ QByteArray mcmodInfo = MMCZip::readFileFromZip(zipPath, "mcmod.info");
+ if (!mcmodInfo.isEmpty())
+ {
+ m_result->details = ReadMCModInfo(mcmodInfo);
return;
}
- else if (zip.setCurrentFile("fabric.mod.json"))
- {
- if (!file.open(QIODevice::ReadOnly))
- {
- zip.close();
- return;
- }
- m_result->details = ReadFabricModInfo(file.readAll());
- file.close();
- zip.close();
+ QByteArray fabricModJson = MMCZip::readFileFromZip(zipPath, "fabric.mod.json");
+ if (!fabricModJson.isEmpty())
+ {
+ m_result->details = ReadFabricModInfo(fabricModJson);
return;
}
- else if (zip.setCurrentFile("forgeversion.properties"))
- {
- if (!file.open(QIODevice::ReadOnly))
- {
- zip.close();
- return;
- }
- m_result->details = ReadForgeInfo(file.readAll());
- file.close();
- zip.close();
+ QByteArray forgeVersionProps = MMCZip::readFileFromZip(zipPath, "forgeversion.properties");
+ if (!forgeVersionProps.isEmpty())
+ {
+ m_result->details = ReadForgeInfo(forgeVersionProps);
return;
}
-
- zip.close();
}
void LocalModParseTask::processAsFolder()
@@ -459,24 +420,11 @@ void LocalModParseTask::processAsFolder()
void LocalModParseTask::processAsLitemod()
{
- QuaZip zip(m_modFile.filePath());
- if (!zip.open(QuaZip::mdUnzip))
- return;
-
- QuaZipFile file(&zip);
-
- if (zip.setCurrentFile("litemod.json"))
+ QByteArray litemodJson = MMCZip::readFileFromZip(m_modFile.filePath(), "litemod.json");
+ if (!litemodJson.isEmpty())
{
- if (!file.open(QIODevice::ReadOnly))
- {
- zip.close();
- return;
- }
-
- m_result->details = ReadLiteModInfo(file.readAll());
- file.close();
+ m_result->details = ReadLiteModInfo(litemodJson);
}
- zip.close();
}
void LocalModParseTask::run()
diff --git a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp
index 38d99a186b..10c9bdcd7d 100644
--- a/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp
+++ b/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp
@@ -42,8 +42,6 @@
#include <QtConcurrent/QtConcurrent>
#include <QRegularExpression>
-#include <quazip.h>
-
#include "MMCZip.h"
#include "minecraft/OneSixVersionFormat.h"
#include "Version.h"
@@ -145,8 +143,8 @@ void PackInstallTask::onDownloadSucceeded()
void PackInstallTask::onDownloadFailed(QString reason)
{
qDebug() << "PackInstallTask::onDownloadFailed: " << QThread::currentThreadId();
- jobPtr.reset();
emitFailed(reason);
+ jobPtr.reset();
}
QString PackInstallTask::getDirForModType(ModType type, QString raw)
@@ -466,14 +464,14 @@ void PackInstallTask::installConfigs()
connect(jobPtr.get(), &NetJob::succeeded, this, [&]()
{
abortable = false;
- jobPtr.reset();
extractConfigs();
+ jobPtr.reset();
});
connect(jobPtr.get(), &NetJob::failed, [&](QString reason)
{
abortable = false;
- jobPtr.reset();
emitFailed(reason);
+ jobPtr.reset();
});
connect(jobPtr.get(), &NetJob::progress, [&](qint64 current, qint64 total)
{
@@ -491,13 +489,6 @@ void PackInstallTask::extractConfigs()
QDir extractDir(m_stagingPath);
- QuaZip packZip(archivePath);
- if(!packZip.open(QuaZip::mdUnzip))
- {
- emitFailed(tr("Failed to open pack configs %1!").arg(archivePath));
- return;
- }
-
QString extractPath = extractDir.absolutePath() + "/minecraft";
QString archivePathCopy = archivePath;
m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), [archivePathCopy, extractPath]() {
@@ -631,8 +622,8 @@ void PackInstallTask::downloadMods()
connect(jobPtr.get(), &NetJob::failed, [&](QString reason)
{
abortable = false;
- jobPtr.reset();
emitFailed(reason);
+ jobPtr.reset();
});
connect(jobPtr.get(), &NetJob::progress, [&](qint64 current, qint64 total)
{
@@ -647,7 +638,6 @@ void PackInstallTask::onModsDownloaded() {
abortable = false;
qDebug() << "PackInstallTask::onModsDownloaded: " << QThread::currentThreadId();
- jobPtr.reset();
if(!modsToExtract.empty() || !modsToDecomp.empty() || !modsToCopy.empty()) {
auto modsToExtractCopy = modsToExtract;
diff --git a/launcher/modplatform/flame/FlamePackIndex.cpp b/launcher/modplatform/flame/FlamePackIndex.cpp
index 645c011152..f802a6409d 100644
--- a/launcher/modplatform/flame/FlamePackIndex.cpp
+++ b/launcher/modplatform/flame/FlamePackIndex.cpp
@@ -133,6 +133,7 @@ void Flame::loadIndexedPackVersions(Flame::IndexedPack & pack, QJsonArray & arr)
file.mcVersion = versionArray[0].toString();
file.version = Json::requireString(version, "displayName");
file.downloadUrl = Json::ensureString(version, "downloadUrl", "");
+ file.fileName = Json::ensureString(version, "fileName", "");
unsortedVersions.append(file);
}
diff --git a/launcher/modplatform/flame/FlamePackIndex.h b/launcher/modplatform/flame/FlamePackIndex.h
index 965df57c93..087db078b5 100644
--- a/launcher/modplatform/flame/FlamePackIndex.h
+++ b/launcher/modplatform/flame/FlamePackIndex.h
@@ -39,6 +39,7 @@ struct IndexedVersion {
QString version;
QString mcVersion;
QString downloadUrl;
+ QString fileName;
};
struct IndexedPack
diff --git a/launcher/modplatform/flame/PackManifest.cpp b/launcher/modplatform/flame/PackManifest.cpp
index 1565868806..e03bf77c77 100644
--- a/launcher/modplatform/flame/PackManifest.cpp
+++ b/launcher/modplatform/flame/PackManifest.cpp
@@ -115,8 +115,12 @@ bool Flame::File::parseFromBytes(const QByteArray& bytes)
}
if(rawUrl.isEmpty())
{
- qCritical() << "Resolving of" << projectId << fileId << "failed: no download URL provided (mod may have disabled third-party downloads)";
- return false;
+ // Mod has disabled third-party downloads — will be handled via browser download
+ qWarning() << "Mod" << projectId << fileId
+ << "(" << fileName << ") has no download URL (restricted)."
+ << "Will require browser download.";
+ resolved = false;
+ return true;
}
url = QUrl(rawUrl, QUrl::TolerantMode);
if(!url.isValid())
diff --git a/launcher/modplatform/legacy_ftb/PackInstallTask.cpp b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp
index 5f3d8d219b..d5aec541b1 100644
--- a/launcher/modplatform/legacy_ftb/PackInstallTask.cpp
+++ b/launcher/modplatform/legacy_ftb/PackInstallTask.cpp
@@ -102,13 +102,6 @@ void PackInstallTask::unzip()
setStatus(tr("Extracting modpack"));
QDir extractDir(m_stagingPath);
- m_packZip.reset(new QuaZip(archivePath));
- if(!m_packZip->open(QuaZip::mdUnzip))
- {
- emitFailed(tr("Failed to open modpack file %1!").arg(archivePath));
- return;
- }
-
QString extractPath = extractDir.absolutePath() + "/unzip";
QString archivePathCopy = archivePath;
m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), [archivePathCopy, extractPath]() {
diff --git a/launcher/modplatform/legacy_ftb/PackInstallTask.h b/launcher/modplatform/legacy_ftb/PackInstallTask.h
index 5d473af867..1ce3f41d27 100644
--- a/launcher/modplatform/legacy_ftb/PackInstallTask.h
+++ b/launcher/modplatform/legacy_ftb/PackInstallTask.h
@@ -22,8 +22,6 @@
#pragma once
#include "InstanceTask.h"
#include "net/NetJob.h"
-#include "quazip.h"
-#include "quazipdir.h"
#include "meta/Index.h"
#include "meta/Version.h"
#include "meta/VersionList.h"
@@ -66,7 +64,6 @@ private slots:
private: /* data */
shared_qobject_ptr<QNetworkAccessManager> m_network;
bool abortable = false;
- std::unique_ptr<QuaZip> m_packZip;
QFuture<nonstd::optional<QStringList>> m_extractFuture;
QFutureWatcher<nonstd::optional<QStringList>> m_extractFutureWatcher;
NetJob::Ptr netJobContainer;
diff --git a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp
index 3d4758a336..a2c95b5e5e 100644
--- a/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp
+++ b/launcher/modplatform/modpacksch/FTBPackInstallTask.cpp
@@ -97,8 +97,6 @@ void PackInstallTask::executeTask()
void PackInstallTask::onDownloadSucceeded()
{
- jobPtr.reset();
-
QJsonParseError parse_error;
QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error);
if(parse_error.error != QJsonParseError::NoError) {
@@ -117,6 +115,7 @@ void PackInstallTask::onDownloadSucceeded()
catch (const JSONValidationError &e)
{
emitFailed(tr("Could not understand pack manifest:\n") + e.cause());
+ jobPtr.reset();
return;
}
m_version = version;
@@ -126,8 +125,8 @@ void PackInstallTask::onDownloadSucceeded()
void PackInstallTask::onDownloadFailed(QString reason)
{
- jobPtr.reset();
emitFailed(reason);
+ jobPtr.reset();
}
void PackInstallTask::downloadPack()
@@ -169,14 +168,14 @@ void PackInstallTask::downloadPack()
connect(jobPtr.get(), &NetJob::succeeded, this, [&]()
{
abortable = false;
- jobPtr.reset();
install();
+ jobPtr.reset();
});
connect(jobPtr.get(), &NetJob::failed, [&](QString reason)
{
abortable = false;
- jobPtr.reset();
emitFailed(reason);
+ jobPtr.reset();
});
connect(jobPtr.get(), &NetJob::progress, [&](qint64 current, qint64 total)
{
diff --git a/launcher/modplatform/technic/SingleZipPackInstallTask.cpp b/launcher/modplatform/technic/SingleZipPackInstallTask.cpp
index 24e73d6a6a..eda32e34fd 100644
--- a/launcher/modplatform/technic/SingleZipPackInstallTask.cpp
+++ b/launcher/modplatform/technic/SingleZipPackInstallTask.cpp
@@ -85,14 +85,11 @@ void Technic::SingleZipPackInstallTask::downloadSucceeded()
QDir extractDir(FS::PathCombine(m_stagingPath, ".minecraft"));
qDebug() << "Attempting to create instance from" << m_archivePath;
- // open the zip and find relevant files in it
- m_packZip.reset(new QuaZip(m_archivePath));
- if (!m_packZip->open(QuaZip::mdUnzip))
- {
- emitFailed(tr("Unable to open supplied modpack zip file."));
- return;
- }
- m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractSubDir, m_packZip.get(), QString(""), extractDir.absolutePath());
+ QString archivePath = m_archivePath;
+ QString extractPath = extractDir.absolutePath();
+ m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), [archivePath, extractPath]() {
+ return MMCZip::extractSubDir(archivePath, QString(""), extractPath);
+ });
connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::finished, this, &Technic::SingleZipPackInstallTask::extractFinished);
connect(&m_extractFutureWatcher, &QFutureWatcher<QStringList>::canceled, this, &Technic::SingleZipPackInstallTask::extractAborted);
m_extractFutureWatcher.setFuture(m_extractFuture);
@@ -114,7 +111,6 @@ void Technic::SingleZipPackInstallTask::downloadProgressChanged(qint64 current,
void Technic::SingleZipPackInstallTask::extractFinished()
{
- m_packZip.reset();
if (!m_extractFuture.result())
{
emitFailed(tr("Failed to extract modpack"));
diff --git a/launcher/modplatform/technic/SingleZipPackInstallTask.h b/launcher/modplatform/technic/SingleZipPackInstallTask.h
index 67fe6a92be..07ef793012 100644
--- a/launcher/modplatform/technic/SingleZipPackInstallTask.h
+++ b/launcher/modplatform/technic/SingleZipPackInstallTask.h
@@ -41,8 +41,6 @@
#include "InstanceTask.h"
#include "net/NetJob.h"
-#include "quazip.h"
-
#include <QFutureWatcher>
#include <QStringList>
#include <QUrl>
@@ -79,7 +77,6 @@ private:
QString m_minecraftVersion;
QString m_archivePath;
NetJob::Ptr m_filesNetJob;
- std::unique_ptr<QuaZip> m_packZip;
QFuture<nonstd::optional<QStringList>> m_extractFuture;
QFutureWatcher<nonstd::optional<QStringList>> m_extractFutureWatcher;
};
diff --git a/launcher/modplatform/technic/TechnicPackProcessor.cpp b/launcher/modplatform/technic/TechnicPackProcessor.cpp
index 94d161de0d..ac1ef4e90e 100644
--- a/launcher/modplatform/technic/TechnicPackProcessor.cpp
+++ b/launcher/modplatform/technic/TechnicPackProcessor.cpp
@@ -42,9 +42,7 @@
#include <Json.h>
#include <minecraft/MinecraftInstance.h>
#include <minecraft/PackProfile.h>
-#include <quazip.h>
-#include <quazipdir.h>
-#include <quazipfile.h>
+#include <MMCZip.h>
#include <settings/INISettingsObject.h>
#include <memory>
@@ -75,40 +73,27 @@ void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings, const
QString fmlMinecraftVersion;
if (QFile::exists(modpackJar))
{
- QuaZip zipFile(modpackJar);
- if (!zipFile.open(QuaZip::mdUnzip))
+ if (MMCZip::entryExists(modpackJar, "version.json"))
{
- emit failed(tr("Unable to open \"bin/modpack.jar\" file!"));
- return;
- }
- QuaZipDir zipFileRoot(&zipFile, "/");
- if (zipFileRoot.exists("/version.json"))
- {
- if (zipFileRoot.exists("/fmlversion.properties"))
+ if (MMCZip::entryExists(modpackJar, "fmlversion.properties"))
{
- zipFile.setCurrentFile("fmlversion.properties");
- QuaZipFile file(&zipFile);
- if (!file.open(QIODevice::ReadOnly))
+ QByteArray fmlVersionData = MMCZip::readFileFromZip(modpackJar, "fmlversion.properties");
+ if (fmlVersionData.isEmpty())
{
emit failed(tr("Unable to open \"fmlversion.properties\"!"));
return;
}
- QByteArray fmlVersionData = file.readAll();
- file.close();
INIFile iniFile;
iniFile.loadFile(fmlVersionData);
// If not present, this evaluates to a null string
fmlMinecraftVersion = iniFile["fmlbuild.mcversion"].toString();
}
- zipFile.setCurrentFile("version.json", QuaZip::csSensitive);
- QuaZipFile file(&zipFile);
- if (!file.open(QIODevice::ReadOnly))
+ data = MMCZip::readFileFromZip(modpackJar, "version.json");
+ if (data.isEmpty())
{
emit failed(tr("Unable to open \"version.json\"!"));
return;
}
- data = file.readAll();
- file.close();
}
else
{
@@ -120,18 +105,15 @@ void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings, const
// Forge for 1.4.7 and for 1.5.2 require extra libraries.
// Figure out the forge version and add it as a component
// (the code still comes from the jar mod installed above)
- if (zipFileRoot.exists("/forgeversion.properties"))
+ if (MMCZip::entryExists(modpackJar, "forgeversion.properties"))
{
- zipFile.setCurrentFile("forgeversion.properties", QuaZip::csSensitive);
- QuaZipFile file(&zipFile);
- if (!file.open(QIODevice::ReadOnly))
+ QByteArray forgeVersionData = MMCZip::readFileFromZip(modpackJar, "forgeversion.properties");
+ if (forgeVersionData.isEmpty())
{
// Really shouldn't happen, but error handling shall not be forgotten
emit failed(tr("Unable to open \"forgeversion.properties\""));
return;
}
- QByteArray forgeVersionData = file.readAll();
- file.close();
INIFile iniFile;
iniFile.loadFile(forgeVersionData);
QString major, minor, revision, build;
diff --git a/launcher/tasks/Task.cpp b/launcher/tasks/Task.cpp
index 627b3d5079..e7fb0cdd8e 100644
--- a/launcher/tasks/Task.cpp
+++ b/launcher/tasks/Task.cpp
@@ -39,6 +39,7 @@
#include "Task.h"
#include <QDebug>
+#include <QPointer>
Task::Task(QObject *parent) : QObject(parent)
{
@@ -107,8 +108,10 @@ void Task::emitFailed(QString reason)
m_state = State::Failed;
m_failReason = reason;
qCritical() << "Task" << describe() << "failed: " << reason;
+ QPointer<Task> guard(this);
emit failed(reason);
- emit finished();
+ if (guard)
+ emit finished();
}
void Task::emitAborted()
@@ -122,8 +125,10 @@ void Task::emitAborted()
m_state = State::AbortedByUser;
m_failReason = "Aborted.";
qDebug() << "Task" << describe() << "aborted.";
+ QPointer<Task> guard(this);
emit failed(m_failReason);
- emit finished();
+ if (guard)
+ emit finished();
}
void Task::emitSucceeded()
@@ -136,8 +141,10 @@ void Task::emitSucceeded()
}
m_state = State::Succeeded;
qDebug() << "Task" << describe() << "succeeded";
+ QPointer<Task> guard(this);
emit succeeded();
- emit finished();
+ if (guard)
+ emit finished();
}
QString Task::describe()
diff --git a/launcher/ui/dialogs/BlockedModsDialog.cpp b/launcher/ui/dialogs/BlockedModsDialog.cpp
new file mode 100644
index 0000000000..9ea56b1383
--- /dev/null
+++ b/launcher/ui/dialogs/BlockedModsDialog.cpp
@@ -0,0 +1,171 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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 "BlockedModsDialog.h"
+
+#include <QDesktopServices>
+#include <QDir>
+#include <QFont>
+#include <QGridLayout>
+#include <QScrollArea>
+#include <QStandardPaths>
+#include <QUrl>
+
+BlockedModsDialog::BlockedModsDialog(QWidget *parent, const QString &title, const QString &text,
+ QList<BlockedMod> &mods)
+ : QDialog(parent), m_mods(mods)
+{
+ setWindowTitle(title);
+ setMinimumSize(550, 300);
+ resize(620, 420);
+ setWindowModality(Qt::WindowModal);
+
+ auto *mainLayout = new QVBoxLayout(this);
+
+ // Description label at top
+ auto *descLabel = new QLabel(text, this);
+ descLabel->setWordWrap(true);
+ mainLayout->addWidget(descLabel);
+
+ // Scrollable area for mod list
+ auto *scrollArea = new QScrollArea(this);
+ scrollArea->setWidgetResizable(true);
+
+ auto *scrollWidget = new QWidget();
+ auto *grid = new QGridLayout(scrollWidget);
+ grid->setColumnStretch(0, 3); // mod name
+ grid->setColumnStretch(1, 1); // status
+ grid->setColumnStretch(2, 0); // button
+
+ // Header row
+ auto *headerName = new QLabel(tr("<b>Mod</b>"), scrollWidget);
+ auto *headerStatus = new QLabel(tr("<b>Status</b>"), scrollWidget);
+ grid->addWidget(headerName, 0, 0);
+ grid->addWidget(headerStatus, 0, 1);
+
+ for (int i = 0; i < m_mods.size(); i++) {
+ int row = i + 1;
+
+ auto *nameLabel = new QLabel(m_mods[i].fileName, scrollWidget);
+ nameLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
+
+ auto *statusLabel = new QLabel(tr("Missing"), scrollWidget);
+ statusLabel->setStyleSheet("color: #cc3333; font-weight: bold;");
+
+ auto *downloadBtn = new QPushButton(tr("Download"), scrollWidget);
+ connect(downloadBtn, &QPushButton::clicked, this, [this, i]() {
+ openModDownload(i);
+ });
+
+ grid->addWidget(nameLabel, row, 0);
+ grid->addWidget(statusLabel, row, 1);
+ grid->addWidget(downloadBtn, row, 2);
+
+ m_rows.append({nameLabel, statusLabel, downloadBtn});
+ }
+
+ // Add stretch at bottom of grid
+ grid->setRowStretch(m_mods.size() + 1, 1);
+
+ scrollWidget->setLayout(grid);
+ scrollArea->setWidget(scrollWidget);
+ mainLayout->addWidget(scrollArea, 1);
+
+ // Button box at bottom
+ m_buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
+ m_buttons->button(QDialogButtonBox::Ok)->setText(tr("Continue"));
+ m_buttons->button(QDialogButtonBox::Ok)->setEnabled(false);
+ connect(m_buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
+ connect(m_buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
+ mainLayout->addWidget(m_buttons);
+
+ setLayout(mainLayout);
+
+ // Set up Downloads folder watching
+ setupWatch();
+
+ // Initial scan
+ scanDownloadsFolder();
+}
+
+void BlockedModsDialog::setupWatch()
+{
+ m_downloadDir = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation);
+ if (!m_downloadDir.isEmpty() && QDir(m_downloadDir).exists()) {
+ m_watcher.addPath(m_downloadDir);
+ connect(&m_watcher, &QFileSystemWatcher::directoryChanged,
+ this, &BlockedModsDialog::onDownloadDirChanged);
+ }
+}
+
+void BlockedModsDialog::onDownloadDirChanged(const QString &path)
+{
+ Q_UNUSED(path);
+ scanDownloadsFolder();
+}
+
+void BlockedModsDialog::scanDownloadsFolder()
+{
+ if (m_downloadDir.isEmpty())
+ return;
+
+ QDir dir(m_downloadDir);
+ QStringList files = dir.entryList(QDir::Files);
+
+ for (int i = 0; i < m_mods.size(); i++) {
+ if (!m_mods[i].found && files.contains(m_mods[i].fileName)) {
+ m_mods[i].found = true;
+ }
+ }
+
+ updateModStatus();
+}
+
+void BlockedModsDialog::updateModStatus()
+{
+ bool allFound = true;
+
+ for (int i = 0; i < m_mods.size(); i++) {
+ if (m_mods[i].found) {
+ m_rows[i].statusLabel->setText(QString::fromUtf8("\u2714 ") + tr("Found"));
+ m_rows[i].statusLabel->setStyleSheet("color: #33aa33; font-weight: bold;");
+ m_rows[i].downloadButton->setEnabled(false);
+ } else {
+ m_rows[i].statusLabel->setText(tr("Missing"));
+ m_rows[i].statusLabel->setStyleSheet("color: #cc3333; font-weight: bold;");
+ m_rows[i].downloadButton->setEnabled(true);
+ allFound = false;
+ }
+ }
+
+ m_buttons->button(QDialogButtonBox::Ok)->setEnabled(allFound);
+}
+
+void BlockedModsDialog::openModDownload(int index)
+{
+ if (index < 0 || index >= m_mods.size())
+ return;
+
+ const auto &mod = m_mods[index];
+ QString url = QString("https://www.curseforge.com/api/v1/mods/%1/files/%2/download")
+ .arg(mod.projectId).arg(mod.fileId);
+ QDesktopServices::openUrl(QUrl(url));
+}
diff --git a/launcher/ui/dialogs/BlockedModsDialog.h b/launcher/ui/dialogs/BlockedModsDialog.h
new file mode 100644
index 0000000000..31cf4d169c
--- /dev/null
+++ b/launcher/ui/dialogs/BlockedModsDialog.h
@@ -0,0 +1,70 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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 <QDialog>
+#include <QFileSystemWatcher>
+#include <QLabel>
+#include <QPushButton>
+#include <QDialogButtonBox>
+#include <QVBoxLayout>
+
+struct BlockedMod {
+ int projectId;
+ int fileId;
+ QString fileName;
+ QString targetPath;
+ bool found = false;
+};
+
+class BlockedModsDialog : public QDialog {
+ Q_OBJECT
+
+public:
+ explicit BlockedModsDialog(QWidget *parent, const QString &title, const QString &text,
+ QList<BlockedMod> &mods);
+
+ /// Returns the list of mods with updated `found` status
+ QList<BlockedMod> &resultMods() { return m_mods; }
+
+private slots:
+ void onDownloadDirChanged(const QString &path);
+ void openModDownload(int index);
+
+private:
+ void scanDownloadsFolder();
+ void updateModStatus();
+ void setupWatch();
+
+ QList<BlockedMod> &m_mods;
+ QString m_downloadDir;
+ QFileSystemWatcher m_watcher;
+
+ struct ModRow {
+ QLabel *nameLabel;
+ QLabel *statusLabel;
+ QPushButton *downloadButton;
+ };
+ QList<ModRow> m_rows;
+
+ QDialogButtonBox *m_buttons;
+};
diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.cpp b/launcher/ui/pages/modplatform/flame/FlamePage.cpp
index cd2a9112f4..fc9a190845 100644
--- a/launcher/ui/pages/modplatform/flame/FlamePage.cpp
+++ b/launcher/ui/pages/modplatform/flame/FlamePage.cpp
@@ -190,13 +190,29 @@ void FlamePage::suggestCurrent()
return;
}
- if (selectedVersion.isEmpty())
+ if (selectedVersionIndex < 0 || selectedVersionIndex >= current.versions.size())
{
dialog->setSuggestedPack();
return;
}
- dialog->setSuggestedPack(current.name, new InstanceImportTask(selectedVersion));
+ auto &version = current.versions[selectedVersionIndex];
+
+ if (!version.downloadUrl.isEmpty())
+ {
+ // Normal download — direct URL available
+ dialog->setSuggestedPack(current.name, new InstanceImportTask(version.downloadUrl));
+ }
+ else
+ {
+ // Restricted download — construct CurseForge browser download URL
+ // This URL triggers a browser download when opened, respecting ToS
+ QString browserUrl = QString("https://www.curseforge.com/api/v1/mods/%1/files/%2/download")
+ .arg(version.addonId).arg(version.fileId);
+ dialog->setSuggestedPack(current.name, new InstanceImportTask(browserUrl));
+ qDebug() << "Pack has no API download URL, using browser download URL:" << browserUrl;
+ }
+
QString editedLogoName;
editedLogoName = "curseforge_" + current.logoName.section(".", 0, 0);
listModel->getLogo(current.logoName, current.logoUrl, [this, editedLogoName](QString logo)
@@ -210,8 +226,10 @@ void FlamePage::onVersionSelectionChanged(QString data)
if(data.isNull() || data.isEmpty())
{
selectedVersion = "";
+ selectedVersionIndex = -1;
return;
}
selectedVersion = ui->versionSelectionBox->currentData().toString();
+ selectedVersionIndex = ui->versionSelectionBox->currentIndex();
suggestCurrent();
}
diff --git a/launcher/ui/pages/modplatform/flame/FlamePage.h b/launcher/ui/pages/modplatform/flame/FlamePage.h
index 55648e6dd7..09afbcba12 100644
--- a/launcher/ui/pages/modplatform/flame/FlamePage.h
+++ b/launcher/ui/pages/modplatform/flame/FlamePage.h
@@ -100,4 +100,5 @@ private:
Flame::IndexedPack current;
QString selectedVersion;
+ int selectedVersionIndex = -1;
};
diff --git a/libraries/classparser/CMakeLists.txt b/libraries/classparser/CMakeLists.txt
index 400f7dea02..f4776902cf 100644
--- a/libraries/classparser/CMakeLists.txt
+++ b/libraries/classparser/CMakeLists.txt
@@ -38,4 +38,4 @@ add_definitions(-DCLASSPARSER_LIBRARY)
add_library(MeshMC_classparser STATIC ${CLASSPARSER_SOURCES} ${CLASSPARSER_HEADERS})
target_include_directories(MeshMC_classparser PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include")
-target_link_libraries(MeshMC_classparser PRIVATE QuaZip::QuaZip Qt6::Core)
+target_link_libraries(MeshMC_classparser PRIVATE LibArchive::LibArchive Qt6::Core)
diff --git a/libraries/classparser/src/classparser.cpp b/libraries/classparser/src/classparser.cpp
index fdfcfef9b1..4c7f0fe482 100644
--- a/libraries/classparser/src/classparser.cpp
+++ b/libraries/classparser/src/classparser.cpp
@@ -39,7 +39,8 @@
#include "classparser.h"
#include <QFile>
-#include <quazip/quazipfile.h>
+#include <archive.h>
+#include <archive_entry.h>
#include <QDebug>
namespace classparser
@@ -54,26 +55,44 @@ QString GetMinecraftJarVersion(QString jarName)
if (!jar.exists())
return version;
- // open minecraft.jar
- QuaZip zip(&jar);
- if (!zip.open(QuaZip::mdUnzip))
+ // open jar with libarchive
+ struct archive *a = archive_read_new();
+ archive_read_support_format_zip(a);
+ if (archive_read_open_filename(a, jarName.toUtf8().constData(), 10240) != ARCHIVE_OK) {
+ archive_read_free(a);
return version;
+ }
- // open Minecraft.class
- zip.setCurrentFile("net/minecraft/client/Minecraft.class", QuaZip::csSensitive);
- QuaZipFile Minecraft(&zip);
- if (!Minecraft.open(QuaZipFile::ReadOnly))
- return version;
+ // find and read Minecraft.class
+ QByteArray classData;
+ struct archive_entry *entry;
+ while (archive_read_next_header(a, &entry) == ARCHIVE_OK) {
+ QString name = QString::fromUtf8(archive_entry_pathname(entry));
+ if (name == "net/minecraft/client/Minecraft.class") {
+ la_int64_t sz = archive_entry_size(entry);
+ if (sz > 0) {
+ classData.resize(sz);
+ archive_read_data(a, classData.data(), sz);
+ } else {
+ char buf[8192];
+ la_ssize_t r;
+ while ((r = archive_read_data(a, buf, sizeof(buf))) > 0)
+ classData.append(buf, r);
+ }
+ break;
+ }
+ archive_read_data_skip(a);
+ }
+ archive_read_free(a);
- // read Minecraft.class
- qint64 size = Minecraft.size();
- char *classfile = new char[size];
- Minecraft.read(classfile, size);
+ if (classData.isEmpty())
+ return version;
// parse Minecraft.class
try
{
- char *temp = classfile;
+ char *temp = classData.data();
+ qint64 size = classData.size();
java::classfile MinecraftClass(temp, size);
java::constant_pool constants = MinecraftClass.constants;
for (java::constant_pool::container_type::const_iterator iter = constants.begin();
@@ -93,12 +112,6 @@ QString GetMinecraftJarVersion(QString jarName)
}
catch (const java::classfile_exception &) { }
- // clean up
- delete[] classfile;
- Minecraft.close();
- zip.close();
- jar.close();
-
return version;
}
}
diff --git a/nix/unwrapped.nix b/nix/unwrapped.nix
index 0a8b03898a..0ca752ea89 100644
--- a/nix/unwrapped.nix
+++ b/nix/unwrapped.nix
@@ -94,7 +94,6 @@ stdenv.mkDerivation {
kdePackages.qtbase
kdePackages.qtnetworkauth
kdePackages.qt5compat
- kdePackages.quazip
qrencode
libarchive
tomlplusplus
diff --git a/scripts/checkpatch.pl b/scripts/checkpatch.pl
index 3555120a43..43098ca2e6 100755
--- a/scripts/checkpatch.pl
+++ b/scripts/checkpatch.pl
@@ -313,10 +313,9 @@ sub main {
}
# Exit code based on results
+ # Warnings alone do not cause a non-zero exit
if ($g_error_count > 0) {
exit(1);
- } elsif ($g_warn_count > 0) {
- exit(2);
}
exit(0);
}
diff --git a/vcpkg.json b/vcpkg.json
index cb5b5e3d3d..502bd10088 100644
--- a/vcpkg.json
+++ b/vcpkg.json
@@ -29,10 +29,6 @@
"zstd"
]
},
- {
- "name": "quazip",
- "platform": "!osx"
- },
"tomlplusplus",
"zlib",
"vulkan-headers"