diff options
Diffstat (limited to 'archived/projt-launcher/launcher/minecraft/AssetsUtils.cpp')
| -rw-r--r-- | archived/projt-launcher/launcher/minecraft/AssetsUtils.cpp | 643 |
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; +} |
