summaryrefslogtreecommitdiff
path: root/archived/projt-launcher/launcher/minecraft/AssetsUtils.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'archived/projt-launcher/launcher/minecraft/AssetsUtils.cpp')
-rw-r--r--archived/projt-launcher/launcher/minecraft/AssetsUtils.cpp643
1 files changed, 643 insertions, 0 deletions
diff --git a/archived/projt-launcher/launcher/minecraft/AssetsUtils.cpp b/archived/projt-launcher/launcher/minecraft/AssetsUtils.cpp
new file mode 100644
index 0000000000..26f54ea321
--- /dev/null
+++ b/archived/projt-launcher/launcher/minecraft/AssetsUtils.cpp
@@ -0,0 +1,643 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * 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, 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * === Upstream License Block (Do Not Modify) ==============================
+ *
+ *
+ *
+ * Prism Launcher - Minecraft Launcher
+ * Copyright (c) 2022 flowln <flowlnlnln@gmail.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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * Copyright 2013-2021 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * ======================================================================== */
+
+#include <QCryptographicHash>
+#include <QDebug>
+#include <QDir>
+#include <QDirIterator>
+#include <QFileInfo>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonParseError>
+#include <QVariant>
+
+#include "AssetsUtils.h"
+#include "BuildConfig.h"
+#include "FileSystem.h"
+#include "net/ApiDownload.h"
+#include "net/ChecksumValidator.h"
+#include "net/Download.h"
+
+#include "Application.h"
+#include "net/NetRequest.h"
+
+namespace
+{
+ // Helper function to get standard asset directory structure
+ struct AssetDirs
+ {
+ QDir assets;
+ QDir indexes;
+ QDir objects;
+ QDir virtualDir;
+ };
+
+ AssetDirs getAssetDirectories()
+ {
+ AssetDirs dirs;
+ dirs.assets = QDir("assets/");
+ dirs.indexes = QDir(FS::PathCombine(dirs.assets.path(), "indexes"));
+ dirs.objects = QDir(FS::PathCombine(dirs.assets.path(), "objects"));
+ dirs.virtualDir = QDir(FS::PathCombine(dirs.assets.path(), "virtual"));
+ return dirs;
+ }
+
+ QSet<QString> collectPathsFromDir(QString dirPath)
+ {
+ QFileInfo dirInfo(dirPath);
+
+ if (!dirInfo.exists())
+ {
+ return {};
+ }
+
+ QSet<QString> out;
+
+ QDirIterator iter(dirPath, QDirIterator::Subdirectories);
+ while (iter.hasNext())
+ {
+ QString value = iter.next();
+ QFileInfo info(value);
+ if (info.isFile())
+ {
+ out.insert(value);
+ qDebug() << value;
+ }
+ }
+ return out;
+ }
+} // namespace
+
+namespace AssetsUtils
+{
+
+ /*
+ * Returns true on success, with index populated
+ * index is undefined otherwise
+ */
+ bool loadAssetsIndexJson(const QString& assetsId, const QString& path, AssetsIndex& index)
+ {
+ /*
+ {
+ "objects": {
+ "icons/icon_16x16.png": {
+ "hash": "bdf48ef6b5d0d23bbb02e17d04865216179f510a",
+ "size": 3665
+ },
+ ...
+ }
+ }
+ }
+ */
+
+ QFile file(path);
+
+ // Try to open the file and fail if we can't.
+ if (!file.open(QIODevice::ReadOnly))
+ {
+ qCritical() << "Failed to read assets index file" << path << ":" << file.errorString();
+ return false;
+ }
+ index.id = assetsId;
+
+ // Read the file and close it.
+ QByteArray jsonData = file.readAll();
+ file.close();
+
+ QJsonParseError parseError;
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData, &parseError);
+
+ // Fail if the JSON is invalid.
+ if (parseError.error != QJsonParseError::NoError)
+ {
+ qCritical() << "Failed to parse assets index file:" << parseError.errorString() << "at offset "
+ << QString::number(parseError.offset);
+ return false;
+ }
+
+ // Make sure the root is an object.
+ if (!jsonDoc.isObject())
+ {
+ qCritical() << "Invalid assets index JSON: Root should be an array.";
+ return false;
+ }
+
+ QJsonObject root = jsonDoc.object();
+
+ QJsonValue isVirtual = root.value("virtual");
+ if (!isVirtual.isUndefined())
+ {
+ index.isVirtual = isVirtual.toBool(false);
+ }
+
+ QJsonValue mapToResources = root.value("map_to_resources");
+ if (!mapToResources.isUndefined())
+ {
+ index.mapToResources = mapToResources.toBool(false);
+ }
+
+ QJsonValue objects = root.value("objects");
+ QVariantMap map = objects.toVariant().toMap();
+
+ for (QVariantMap::const_iterator iter = map.begin(); iter != map.end(); ++iter)
+ {
+ // qDebug() << iter.key();
+
+ QVariant variant = iter.value();
+ QVariantMap nested_objects = variant.toMap();
+
+ AssetObject object;
+
+ for (QVariantMap::const_iterator nested_iter = nested_objects.begin(); nested_iter != nested_objects.end();
+ ++nested_iter)
+ {
+ // qDebug() << nested_iter.key() << nested_iter.value().toString();
+ QString key = nested_iter.key();
+ QVariant value = nested_iter.value();
+
+ if (key == "hash")
+ {
+ object.hash = value.toString();
+ }
+ else if (key == "size")
+ {
+ object.size = value.toLongLong();
+ }
+ }
+
+ index.objects.insert(iter.key(), object);
+ }
+
+ return true;
+ }
+
+ QDir getAssetsDir(const QString& assetsId, const QString& resourcesFolder)
+ {
+ auto dirs = getAssetDirectories();
+ QDir indexDir = dirs.indexes;
+ QDir objectDir = dirs.objects;
+ QDir virtualDir = dirs.virtualDir;
+
+ QString indexPath = FS::PathCombine(indexDir.path(), assetsId + ".json");
+ QFile indexFile(indexPath);
+ QDir virtualRoot(FS::PathCombine(virtualDir.path(), assetsId));
+
+ if (!indexFile.exists())
+ {
+ qCritical() << "No assets index file" << indexPath << "; can't determine assets path!";
+ return virtualRoot;
+ }
+
+ AssetsIndex index;
+ if (!AssetsUtils::loadAssetsIndexJson(assetsId, indexPath, index))
+ {
+ qCritical() << "Failed to load asset index file" << indexPath << "; can't determine assets path!";
+ return virtualRoot;
+ }
+
+ QString targetPath;
+ if (index.isVirtual)
+ {
+ return virtualRoot;
+ }
+ else if (index.mapToResources)
+ {
+ return QDir(resourcesFolder);
+ }
+ return virtualRoot;
+ }
+
+ bool reconstructAssets(QString assetsId, QString resourcesFolder)
+ {
+ auto dirs = getAssetDirectories();
+ QDir indexDir = dirs.indexes;
+ QDir objectDir = dirs.objects;
+ QDir virtualDir = QDir(FS::PathCombine(dirs.assets.path(), "virtual"));
+
+ QString indexPath = FS::PathCombine(indexDir.path(), assetsId + ".json");
+ QFile indexFile(indexPath);
+ QDir virtualRoot(FS::PathCombine(virtualDir.path(), assetsId));
+
+ if (!indexFile.exists())
+ {
+ qCritical() << "No assets index file" << indexPath << "; can't reconstruct assets!";
+ return false;
+ }
+
+ qDebug() << "reconstructAssets" << dirs.assets.path() << indexDir.path() << objectDir.path()
+ << virtualDir.path() << virtualRoot.path();
+
+ AssetsIndex index;
+ if (!AssetsUtils::loadAssetsIndexJson(assetsId, indexPath, index))
+ {
+ qCritical() << "Failed to load asset index file" << indexPath << "; can't reconstruct assets!";
+ return false;
+ }
+
+ QString targetPath;
+ bool removeLeftovers = false;
+ if (index.isVirtual)
+ {
+ targetPath = virtualRoot.path();
+ removeLeftovers = true;
+ qDebug() << "Reconstructing virtual assets folder at" << targetPath;
+ }
+ else if (index.mapToResources)
+ {
+ targetPath = resourcesFolder;
+ qDebug() << "Reconstructing resources folder at" << targetPath;
+ }
+
+ if (!targetPath.isNull())
+ {
+ auto presentFiles = collectPathsFromDir(targetPath);
+ for (QString map : index.objects.keys())
+ {
+ AssetObject asset_object = index.objects.value(map);
+ QString target_path = FS::PathCombine(targetPath, map);
+ QFile target(target_path);
+
+ QString tlk = asset_object.hash.left(2);
+
+ QString original_path = FS::PathCombine(objectDir.path(), tlk, asset_object.hash);
+ QFile original(original_path);
+ if (!original.exists())
+ continue;
+
+ presentFiles.remove(target_path);
+
+ if (!target.exists())
+ {
+ QFileInfo info(target_path);
+ QDir target_dir = info.dir();
+
+ qDebug() << target_dir.path();
+ FS::ensureFolderPathExists(target_dir.path());
+
+ bool couldCopy = original.copy(target_path);
+ qDebug() << " Copying" << original_path << "to" << target_path << QString::number(couldCopy);
+ }
+ }
+
+ // Consider adding a function to update the .lastused file with the current timestamp for asset usage
+ // tracking.
+ if (removeLeftovers)
+ {
+ for (auto& file : presentFiles)
+ {
+ qDebug() << "Would remove" << file;
+ }
+ }
+ }
+ return true;
+ }
+
+} // namespace AssetsUtils
+
+// ============================================================================
+// AssetObject implementation
+// ============================================================================
+
+QString AssetObject::getRelPath() const
+{
+ return hash.left(2) + "/" + hash;
+}
+
+QString AssetObject::getLocalPath() const
+{
+ return "assets/objects/" + getRelPath();
+}
+
+QUrl AssetObject::getUrl() const
+{
+ auto resourceURL = APPLICATION->settings()->get("ResourceURL").toString();
+ if (resourceURL.isEmpty())
+ {
+ resourceURL = BuildConfig.DEFAULT_RESOURCE_BASE;
+ }
+ return resourceURL + getRelPath();
+}
+
+bool AssetObject::isValid() const
+{
+ QFileInfo objectFile(getLocalPath());
+ return objectFile.isFile() && objectFile.size() == size;
+}
+
+Net::NetRequest::Ptr AssetObject::getDownloadAction() const
+{
+ if (isValid())
+ {
+ return nullptr;
+ }
+
+ QFileInfo objectFile(getLocalPath());
+ auto objectDL = Net::ApiDownload::makeFile(getUrl(), objectFile.filePath());
+ if (!hash.isEmpty())
+ {
+ objectDL->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, hash));
+ }
+ objectDL->setProgress(objectDL->getProgress(), size);
+ return objectDL;
+}
+
+// ============================================================================
+// AssetsIndex implementation
+// ============================================================================
+
+NetJob::Ptr AssetsIndex::getDownloadJob() const
+{
+ auto job = makeShared<NetJob>(QObject::tr("Assets for %1").arg(id), APPLICATION->network());
+ for (const auto& object : objects.values())
+ {
+ auto dl = object.getDownloadAction();
+ if (dl)
+ {
+ job->addNetAction(dl);
+ }
+ }
+ return job->size() ? job : nullptr;
+}
+
+qint64 AssetsIndex::totalSize() const
+{
+ qint64 total = 0;
+ for (const auto& object : objects.values())
+ {
+ total += object.size;
+ }
+ return total;
+}
+
+int AssetsIndex::missingAssetCount() const
+{
+ int count = 0;
+ for (const auto& object : objects.values())
+ {
+ if (!object.isValid())
+ {
+ count++;
+ }
+ }
+ return count;
+}
+
+bool AssetsIndex::isComplete() const
+{
+ return missingAssetCount() == 0;
+}
+
+// ============================================================================
+// AssetsManager implementation
+// ============================================================================
+
+QDir AssetsManager::assetsBaseDir()
+{
+ return QDir("assets/");
+}
+
+QDir AssetsManager::indexesDir()
+{
+ return QDir(FS::PathCombine(assetsBaseDir().path(), "indexes"));
+}
+
+QDir AssetsManager::objectsDir()
+{
+ return QDir(FS::PathCombine(assetsBaseDir().path(), "objects"));
+}
+
+QDir AssetsManager::virtualDir()
+{
+ return QDir(FS::PathCombine(assetsBaseDir().path(), "virtual"));
+}
+
+std::optional<AssetsIndex> AssetsManager::loadIndex(const QString& indexId, const QString& filePath)
+{
+ QFile file(filePath);
+ if (!file.open(QIODevice::ReadOnly))
+ {
+ qCritical() << "Failed to read assets index file" << filePath << ":" << file.errorString();
+ return std::nullopt;
+ }
+
+ QByteArray jsonData = file.readAll();
+ file.close();
+
+ QJsonParseError parseError;
+ QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData, &parseError);
+
+ if (parseError.error != QJsonParseError::NoError)
+ {
+ qCritical() << "Failed to parse assets index file:" << parseError.errorString() << "at offset"
+ << QString::number(parseError.offset);
+ return std::nullopt;
+ }
+
+ if (!jsonDoc.isObject())
+ {
+ qCritical() << "Invalid assets index JSON: Root should be an object.";
+ return std::nullopt;
+ }
+
+ QJsonObject root = jsonDoc.object();
+ AssetsIndex index;
+ index.id = indexId;
+
+ index.isVirtual = root.value("virtual").toBool(false);
+ index.mapToResources = root.value("map_to_resources").toBool(false);
+
+ QJsonValue objectsValue = root.value("objects");
+ QVariantMap map = objectsValue.toVariant().toMap();
+
+ for (auto iter = map.constBegin(); iter != map.constEnd(); ++iter)
+ {
+ QVariantMap nested = iter.value().toMap();
+ AssetObject obj;
+ obj.hash = nested.value("hash").toString();
+ obj.size = nested.value("size").toLongLong();
+
+ index.objects.insert(iter.key(), obj);
+ }
+
+ return index;
+}
+
+QDir AssetsManager::getAssetsDir(const AssetsIndex& index, const QString& resourcesFolder)
+{
+ if (index.isVirtual)
+ {
+ return QDir(FS::PathCombine(virtualDir().path(), index.id));
+ }
+ else if (index.mapToResources)
+ {
+ return QDir(resourcesFolder);
+ }
+ return QDir(FS::PathCombine(virtualDir().path(), index.id));
+}
+
+QDir AssetsManager::getAssetsDir(const QString& assetsId, const QString& resourcesFolder)
+{
+ QString indexPath = FS::PathCombine(indexesDir().path(), assetsId + ".json");
+
+ auto index = loadIndex(assetsId, indexPath);
+ if (!index)
+ {
+ qCritical() << "Failed to load asset index" << assetsId;
+ return QDir(FS::PathCombine(virtualDir().path(), assetsId));
+ }
+
+ return getAssetsDir(*index, resourcesFolder);
+}
+
+bool AssetsManager::reconstructAssets(const AssetsIndex& index, const QString& resourcesFolder)
+{
+ QString targetPath;
+ bool removeLeftovers = false;
+
+ if (index.isVirtual)
+ {
+ targetPath = FS::PathCombine(virtualDir().path(), index.id);
+ removeLeftovers = true;
+ qDebug() << "Reconstructing virtual assets folder at" << targetPath;
+ }
+ else if (index.mapToResources)
+ {
+ targetPath = resourcesFolder;
+ qDebug() << "Reconstructing resources folder at" << targetPath;
+ }
+
+ if (targetPath.isEmpty())
+ {
+ return true; // Nothing to reconstruct
+ }
+
+ // Collect existing files
+ QSet<QString> presentFiles = collectPathsFromDir(targetPath);
+
+ // Copy missing assets
+ for (auto iter = index.objects.constBegin(); iter != index.objects.constEnd(); ++iter)
+ {
+ const QString& assetPath = iter.key();
+ const AssetObject& asset = iter.value();
+
+ QString targetFilePath = FS::PathCombine(targetPath, assetPath);
+ QString tlk = asset.hash.left(2);
+ QString originalPath = FS::PathCombine(objectsDir().path(), tlk, asset.hash);
+
+ QFile original(originalPath);
+ if (!original.exists())
+ {
+ continue;
+ }
+
+ presentFiles.remove(targetFilePath);
+
+ QFile target(targetFilePath);
+ if (!target.exists())
+ {
+ QFileInfo info(targetFilePath);
+ FS::ensureFolderPathExists(info.dir().path());
+
+ bool success = original.copy(targetFilePath);
+ qDebug() << "Copying" << originalPath << "to" << targetFilePath << (success ? "OK" : "FAILED");
+ }
+ }
+
+ // Remove leftover files (only for virtual assets)
+ if (removeLeftovers)
+ {
+ for (const auto& file : presentFiles)
+ {
+ qDebug() << "Removing leftover file:" << file;
+ QFile::remove(file);
+ }
+ }
+
+ return true;
+}
+
+bool AssetsManager::reconstructAssets(const QString& assetsId, const QString& resourcesFolder)
+{
+ QString indexPath = FS::PathCombine(indexesDir().path(), assetsId + ".json");
+
+ auto index = loadIndex(assetsId, indexPath);
+ if (!index)
+ {
+ qCritical() << "Failed to load asset index for reconstruction:" << assetsId;
+ return false;
+ }
+
+ return reconstructAssets(*index, resourcesFolder);
+}
+
+QStringList AssetsManager::validateAssets(const AssetsIndex& index)
+{
+ QStringList missing;
+
+ for (auto iter = index.objects.constBegin(); iter != index.objects.constEnd(); ++iter)
+ {
+ const AssetObject& asset = iter.value();
+ if (!asset.isValid())
+ {
+ missing.append(iter.key());
+ }
+ }
+
+ return missing;
+}