diff options
| author | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-04-02 18:51:45 +0300 |
|---|---|---|
| committer | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-04-02 18:51:45 +0300 |
| commit | d3261e64152397db2dca4d691a990c6bc2a6f4dd (patch) | |
| tree | fac2f7be638651181a72453d714f0f96675c2b8b /archived/projt-launcher/launcher/minecraft | |
| parent | 31b9a8949ed0a288143e23bf739f2eb64fdc63be (diff) | |
| download | Project-Tick-d3261e64152397db2dca4d691a990c6bc2a6f4dd.tar.gz Project-Tick-d3261e64152397db2dca4d691a990c6bc2a6f4dd.zip | |
NOISSUE add archived projects
Signed-off-by: Mehmet Samet Duman <yongdohyun@projecttick.org>
Diffstat (limited to 'archived/projt-launcher/launcher/minecraft')
169 files changed, 33099 insertions, 0 deletions
diff --git a/archived/projt-launcher/launcher/minecraft/Agent.h b/archived/projt-launcher/launcher/minecraft/Agent.h new file mode 100644 index 0000000000..72e48d81a9 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/Agent.h @@ -0,0 +1,56 @@ +// 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. + */ +#pragma once + +#include <QString> + +#include "Library.h" + +class Agent; + +using AgentPtr = std::shared_ptr<Agent>; + +class Agent +{ + public: + Agent(LibraryPtr library, const QString& argument) + { + m_library = library; + m_argument = argument; + } + + public: /* methods */ + LibraryPtr library() + { + return m_library; + } + QString argument() + { + return m_argument; + } + + protected: /* data */ + /// The library pointing to the jar this Java agent is contained within + LibraryPtr m_library; + + /// The argument to the Java agent, passed after an = if present + QString m_argument; +}; 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; +} diff --git a/archived/projt-launcher/launcher/minecraft/AssetsUtils.h b/archived/projt-launcher/launcher/minecraft/AssetsUtils.h new file mode 100644 index 0000000000..f5d2a4c440 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/AssetsUtils.h @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// 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) ============================== + * + * 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. + * ======================================================================== */ + +#pragma once + +#include <QDir> +#include <QMap> +#include <QString> +#include <optional> +#include "net/NetJob.h" +#include "net/NetRequest.h" + +/** Represents a single Minecraft asset object. + * Assets are stored by their SHA1 hash in a content-addressable storage. + */ +struct AssetObject +{ + QString hash; + qint64 size = 0; + + /** Get the relative path within the objects directory (e.g., "ab/abcdef..."). */ + QString getRelPath() const; + + /** Get the full download URL for this asset. */ + QUrl getUrl() const; + + /** Get the local filesystem path where this asset is stored. */ + QString getLocalPath() const; + + /** Create a download action for this asset if it needs to be downloaded. */ + Net::NetRequest::Ptr getDownloadAction() const; + + /** Check if the asset exists locally and has the correct size. */ + bool isValid() const; +}; + +/** Represents a Minecraft assets index. + * The index maps virtual paths to asset objects. + */ +struct AssetsIndex +{ + QString id; + QMap<QString, AssetObject> objects; + bool isVirtual = false; + bool mapToResources = false; + + /** Create a NetJob to download all missing assets. */ + NetJob::Ptr getDownloadJob() const; + + /** Get the total size of all assets in bytes. */ + qint64 totalSize() const; + + /** Get the number of assets that need to be downloaded. */ + int missingAssetCount() const; + + /** Check if all assets are present locally. */ + bool isComplete() const; +}; + +/** Asset management utilities. + * Provides functions for loading, validating, and reconstructing Minecraft assets. + */ +class AssetsManager +{ + public: + /** Load an assets index from a JSON file. + * @param id The asset index ID (e.g., "1.19", "legacy") + * @param filePath Path to the index JSON file + * @return The parsed AssetsIndex, or nullopt on failure + */ + static std::optional<AssetsIndex> loadIndex(const QString& id, const QString& filePath); + + /** Get the appropriate assets directory for an index. + * Handles virtual assets, legacy resource mapping, and standard paths. + * @param index The assets index + * @param resourcesFolder Path to the resources folder (for legacy versions) + * @return The directory where assets should be accessed + */ + static QDir getAssetsDir(const AssetsIndex& index, const QString& resourcesFolder); + + /** Get the assets directory by ID (loads index internally). + * @param assetsId The asset index ID + * @param resourcesFolder Path to the resources folder + * @return The directory where assets should be accessed + */ + static QDir getAssetsDir(const QString& assetsId, const QString& resourcesFolder); + + /** Reconstruct virtual/legacy assets from the object store. + * Copies assets from hash-based storage to named paths for older MC versions. + * @param index The assets index to reconstruct + * @param resourcesFolder Path to the resources folder + * @return true if reconstruction succeeded + */ + static bool reconstructAssets(const AssetsIndex& index, const QString& resourcesFolder); + + /** Reconstruct assets by ID (loads index internally). */ + static bool reconstructAssets(const QString& assetsId, const QString& resourcesFolder); + + /** Get the standard paths for asset storage. */ + static QDir assetsBaseDir(); + static QDir indexesDir(); + static QDir objectsDir(); + static QDir virtualDir(); + + /** Validate the integrity of all assets in an index. + * Checks that all files exist and have correct sizes. + * @param index The assets index to validate + * @return List of missing or invalid asset paths + */ + static QStringList validateAssets(const AssetsIndex& index); +}; + +// Legacy namespace - delegates to AssetsManager +namespace AssetsUtils +{ + bool loadAssetsIndexJson(const QString& id, const QString& file, AssetsIndex& index); + QDir getAssetsDir(const QString& assetsId, const QString& resourcesFolder); + bool reconstructAssets(QString assetsId, QString resourcesFolder); +} diff --git a/archived/projt-launcher/launcher/minecraft/BackupManager.cpp b/archived/projt-launcher/launcher/minecraft/BackupManager.cpp new file mode 100644 index 0000000000..9842cb7720 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/BackupManager.cpp @@ -0,0 +1,594 @@ +// 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. + * + */ + +#include "BackupManager.h" +#include <QDir> +#include <QDirIterator> +#include <QFile> +#include <QFileInfo> +#include <QJsonDocument> +#include <QJsonObject> +#include <QtConcurrent> +#include "FileSystem.h" +#include "MMCZip.h" +#include "MinecraftInstance.h" + +// BackupOptions implementation +qint64 BackupOptions::estimateSize() const +{ + // Rough estimation based on typical Minecraft instance sizes + // Note: This is a rough estimate for UI display. Actual size may vary. + qint64 size = 0; + if (includeSaves) + size += 500LL * 1024 * 1024; // 500MB average for saves + if (includeConfig) + size += 50LL * 1024 * 1024; // 50MB for config + if (includeMods) + size += 1000LL * 1024 * 1024; // 1GB for mods + if (includeResourcePacks) + size += 200LL * 1024 * 1024; // 200MB + if (includeShaderPacks) + size += 100LL * 1024 * 1024; // 100MB + if (includeScreenshots) + size += 100LL * 1024 * 1024; // 100MB + if (includeOptions) + size += 1LL * 1024 * 1024; // 1MB + // Add custom paths roughly - assume 50MB per path as placeholder + size += customPaths.size() * 50LL * 1024 * 1024; // 50MB per custom path + return size; +} + +// InstanceBackup implementation +InstanceBackup::InstanceBackup(const QString& path) : m_backupPath(path) +{ + QFileInfo info(path); + if (!info.exists()) + { + return; + } + + m_size = info.size(); + m_createdAt = info.birthTime(); + m_name = info.completeBaseName(); + + // Try to load metadata + QString metaPath = path + ".json"; + if (QFile::exists(metaPath)) + { + QFile metaFile(metaPath); + if (metaFile.open(QIODevice::ReadOnly)) + { + QJsonDocument doc = QJsonDocument::fromJson(metaFile.readAll()); + QJsonObject obj = doc.object(); + + if (obj.contains("name")) + m_name = obj["name"].toString(); + if (obj.contains("description")) + m_description = obj["description"].toString(); + if (obj.contains("created")) + m_createdAt = QDateTime::fromString(obj["created"].toString(), Qt::ISODate); + if (obj.contains("includedPaths")) + m_includedPaths = obj["includedPaths"].toVariant().toStringList(); + } + } +} + +bool InstanceBackup::isValid() const +{ + return QFile::exists(m_backupPath) && m_size > 0; +} + +QString InstanceBackup::displaySize() const +{ + double size = m_size; + QStringList units = { "B", "KB", "MB", "GB" }; + int unitIndex = 0; + + while (size >= 1024.0 && unitIndex < units.size() - 1) + { + size /= 1024.0; + unitIndex++; + } + + return QString::number(size, 'f', 2) + " " + units[unitIndex]; +} + +// BackupManager implementation +BackupManager::BackupManager(QObject* parent) : QObject(parent) +{} + +QString BackupManager::getBackupDirectory(InstancePtr instance) +{ + if (!instance) + { + return QString(); + } + + QString backupDir = FS::PathCombine(instance->instanceRoot(), "backups"); + FS::ensureFolderPathExists(backupDir); + return backupDir; +} + +bool BackupManager::createBackup(InstancePtr instance, const QString& backupName, const BackupOptions& options) +{ + if (!instance) + { + qWarning() << "BackupManager: instance is null"; + return false; + } + + QString backupDir = getBackupDirectory(instance); + qDebug() << "BackupManager: backup directory:" << backupDir; + + QString timestamp = QDateTime::currentDateTime().toString("yyyy-MM-dd_HH-mm-ss"); + QString safeName = backupName.isEmpty() ? timestamp : backupName + "_" + timestamp; + QString backupPath = FS::PathCombine(backupDir, safeName + ".zip"); + + qDebug() << "BackupManager: creating backup at:" << backupPath; + + // Create metadata + QJsonObject metadata; + metadata["name"] = backupName.isEmpty() ? timestamp : backupName; + metadata["created"] = QDateTime::currentDateTime().toString(Qt::ISODate); + metadata["instanceName"] = instance->name(); + + QStringList includedPaths; + if (options.includeSaves) + includedPaths << "saves"; + if (options.includeConfig) + includedPaths << "config"; + if (options.includeMods) + includedPaths << "mods"; + if (options.includeResourcePacks) + includedPaths << "resourcepacks"; + if (options.includeShaderPacks) + includedPaths << "shaderpacks"; + if (options.includeScreenshots) + includedPaths << "screenshots"; + if (options.includeOptions) + includedPaths << "options.txt" + << "optionsof.txt"; + + // Add custom paths to included paths list + includedPaths += options.customPaths; + + metadata["includedPaths"] = QJsonArray::fromStringList(includedPaths); + + // Save metadata + QFile metaFile(backupPath + ".json"); + if (metaFile.open(QIODevice::WriteOnly)) + { + metaFile.write(QJsonDocument(metadata).toJson()); + metaFile.close(); + } + else + { + qWarning() << "BackupManager: failed to write metadata file"; + } + + // Get the game root (where .minecraft files are) + auto minecraftInstance = std::dynamic_pointer_cast<MinecraftInstance>(instance); + QString gameRoot = minecraftInstance ? minecraftInstance->gameRoot() : instance->instanceRoot(); + + // Compress backup + qDebug() << "BackupManager: starting compression..."; + qDebug() << "BackupManager: game root is:" << gameRoot; + if (!compressBackup(gameRoot, backupPath, options)) + { + qWarning() << "BackupManager: compression failed"; + QFile::remove(backupPath); + QFile::remove(backupPath + ".json"); + return false; + } + + qDebug() << "BackupManager: backup created successfully"; + emit backupCreated(instance->id(), backupName); + return true; +} + +void BackupManager::createBackupAsync(InstancePtr instance, const QString& backupName, const BackupOptions& options) +{ + if (!instance) + { + emit backupFailed(QString(), "Instance is null"); + return; + } + + QString instanceId = instance->id(); + emit backupStarted(instanceId, backupName); + + // Run backup in background thread + auto future = QtConcurrent::run([this, instance, backupName, options]() + { return createBackup(instance, backupName, options); }); + + // Watch for completion + auto watcher = new QFutureWatcher<bool>(this); + connect(watcher, + &QFutureWatcher<bool>::finished, + this, + [this, watcher, instanceId, backupName]() + { + bool success = watcher->result(); + if (!success) + { + emit backupFailed(instanceId, "Backup creation failed"); + } + watcher->deleteLater(); + }); + watcher->setFuture(future); +} + +void BackupManager::restoreBackupAsync(InstancePtr instance, + const InstanceBackup& backup, + bool createBackupBeforeRestore) +{ + if (!instance) + { + emit restoreFailed(QString(), "Instance is null"); + return; + } + + QString instanceId = instance->id(); + QString backupName = backup.name(); + emit restoreStarted(instanceId, backupName); + + // Run restore in background thread + auto future = QtConcurrent::run([this, instance, backup, createBackupBeforeRestore]() + { return restoreBackup(instance, backup, createBackupBeforeRestore); }); + + // Watch for completion + auto watcher = new QFutureWatcher<bool>(this); + connect(watcher, + &QFutureWatcher<bool>::finished, + this, + [this, watcher, instanceId, backupName]() + { + bool success = watcher->result(); + if (!success) + { + emit restoreFailed(instanceId, "Backup restoration failed"); + } + watcher->deleteLater(); + }); + watcher->setFuture(future); +} + +bool BackupManager::compressBackup(const QString& sourcePath, const QString& backupPath, const BackupOptions& options) +{ + QFileInfoList files; + + qDebug() << "BackupManager: compressing from" << sourcePath << "to" << backupPath; + qDebug() << "BackupManager: backup options - saves:" << options.includeSaves << "config:" << options.includeConfig + << "mods:" << options.includeMods << "resourcepacks:" << options.includeResourcePacks + << "shaderpacks:" << options.includeShaderPacks << "screenshots:" << options.includeScreenshots + << "options:" << options.includeOptions << "customPaths:" << options.customPaths; + + // Helper lambda to recursively collect files + auto collectFilesRecursive = [&files](const QString& dirPath) + { + qDebug() << "BackupManager: scanning directory" << dirPath; + int count = 0; + QDirIterator it(dirPath, QDir::Files | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); + while (it.hasNext()) + { + QFileInfo info(it.next()); + files.append(info); + count++; + } + qDebug() << "BackupManager: found" << count << "files in" << dirPath; + }; + + if (options.includeSaves) + { + QString savesPath = FS::PathCombine(sourcePath, "saves"); + qDebug() << "BackupManager: checking saves path:" << savesPath; + if (QDir(savesPath).exists()) + { + collectFilesRecursive(savesPath); + } + else + { + qDebug() << "BackupManager: saves directory does not exist"; + } + } + if (options.includeConfig) + { + QString configPath = FS::PathCombine(sourcePath, "config"); + qDebug() << "BackupManager: checking config path:" << configPath; + if (QDir(configPath).exists()) + { + collectFilesRecursive(configPath); + } + else + { + qDebug() << "BackupManager: config directory does not exist"; + } + } + if (options.includeMods) + { + QString modsPath = FS::PathCombine(sourcePath, "mods"); + qDebug() << "BackupManager: checking mods path:" << modsPath; + if (QDir(modsPath).exists()) + { + collectFilesRecursive(modsPath); + } + else + { + qDebug() << "BackupManager: mods directory does not exist"; + } + } + if (options.includeResourcePacks) + { + QString rpPath = FS::PathCombine(sourcePath, "resourcepacks"); + qDebug() << "BackupManager: checking resourcepacks path:" << rpPath; + if (QDir(rpPath).exists()) + { + collectFilesRecursive(rpPath); + } + else + { + qDebug() << "BackupManager: resourcepacks directory does not exist"; + } + } + if (options.includeShaderPacks) + { + QString spPath = FS::PathCombine(sourcePath, "shaderpacks"); + qDebug() << "BackupManager: checking shaderpacks path:" << spPath; + if (QDir(spPath).exists()) + { + collectFilesRecursive(spPath); + } + else + { + qDebug() << "BackupManager: shaderpacks directory does not exist"; + } + } + if (options.includeScreenshots) + { + QString ssPath = FS::PathCombine(sourcePath, "screenshots"); + qDebug() << "BackupManager: checking screenshots path:" << ssPath; + if (QDir(ssPath).exists()) + { + collectFilesRecursive(ssPath); + } + else + { + qDebug() << "BackupManager: screenshots directory does not exist"; + } + } + if (options.includeOptions) + { + QFileInfo optionsFile(FS::PathCombine(sourcePath, "options.txt")); + qDebug() << "BackupManager: checking options.txt:" << optionsFile.absoluteFilePath(); + if (optionsFile.exists()) + { + qDebug() << "BackupManager: found options.txt"; + files.append(optionsFile); + } + else + { + qDebug() << "BackupManager: options.txt does not exist"; + } + QFileInfo optionsOfFile(FS::PathCombine(sourcePath, "optionsof.txt")); + qDebug() << "BackupManager: checking optionsof.txt:" << optionsOfFile.absoluteFilePath(); + if (optionsOfFile.exists()) + { + qDebug() << "BackupManager: found optionsof.txt"; + files.append(optionsOfFile); + } + else + { + qDebug() << "BackupManager: optionsof.txt does not exist"; + } + } + + // Include custom paths + for (const QString& customPath : options.customPaths) + { + QString fullPath = FS::PathCombine(sourcePath, customPath); + QFileInfo info(fullPath); + + if (info.isFile()) + { + files.append(info); + } + else if (info.isDir()) + { + collectFilesRecursive(fullPath); + } + } + + if (files.isEmpty()) + { + qWarning() << "BackupManager: no files to backup!"; + // For new instances with no files, consider backup successful to allow launch + return true; + } + + qDebug() << "BackupManager: collected" << files.size() << "files"; + + // Use MMCZip to compress - sourcePath is the base directory for relative paths + bool result = MMCZip::compressDirFiles(backupPath, sourcePath, files); + + if (!result) + { + qWarning() << "BackupManager: MMCZip::compressDirFiles failed"; + } + + return result; +} + +bool BackupManager::restoreBackup(InstancePtr instance, const InstanceBackup& backup, bool createBackupBeforeRestore) +{ + if (!instance || !backup.isValid()) + { + qWarning() << "BackupManager: invalid instance or backup"; + return false; + } + + // Get the game root (where .minecraft files are) + auto minecraftInstance = std::dynamic_pointer_cast<MinecraftInstance>(instance); + QString gameRoot = minecraftInstance ? minecraftInstance->gameRoot() : instance->instanceRoot(); + + qDebug() << "BackupManager: restoring backup to game root:" << gameRoot; + + // Create safety backup before restore + if (createBackupBeforeRestore) + { + qDebug() << "BackupManager: creating safety backup before restore"; + BackupOptions safetyOptions; + safetyOptions.includeSaves = true; + safetyOptions.includeConfig = true; + safetyOptions.includeMods = true; + safetyOptions.includeResourcePacks = true; + safetyOptions.includeShaderPacks = true; + safetyOptions.includeScreenshots = true; + safetyOptions.includeOptions = true; + createBackup(instance, "pre-restore_" + backup.name(), safetyOptions); + } + + // Extract backup + qDebug() << "BackupManager: extracting backup from" << backup.backupPath() << "to" << gameRoot; + if (!extractBackup(backup.backupPath(), gameRoot)) + { + qWarning() << "BackupManager: failed to extract backup"; + return false; + } + + qDebug() << "BackupManager: backup restored successfully"; + emit backupRestored(instance->id(), backup.name()); + return true; +} + +bool BackupManager::extractBackup(const QString& backupPath, const QString& targetPath) +{ + qDebug() << "BackupManager: extracting" << backupPath << "to" << targetPath; + + if (!QFile::exists(backupPath)) + { + qWarning() << "BackupManager: backup file does not exist:" << backupPath; + return false; + } + + auto result = MMCZip::extractDir(backupPath, targetPath); + + if (!result.has_value()) + { + qWarning() << "BackupManager: extraction failed"; + return false; + } + + qDebug() << "BackupManager: extraction successful"; + return true; +} + +QList<InstanceBackup> BackupManager::listBackups(InstancePtr instance) const +{ + if (!instance) + { + return {}; + } + + QString backupDir = getBackupDirectory(instance); + return scanBackupDirectory(backupDir); +} + +QList<InstanceBackup> BackupManager::scanBackupDirectory(const QString& backupDir) const +{ + QList<InstanceBackup> backups; + QDir dir(backupDir); + + if (!dir.exists()) + { + return backups; + } + + QStringList zipFiles = dir.entryList(QStringList() << "*.zip", QDir::Files, QDir::Time); + + for (const QString& zipFile : zipFiles) + { + QString fullPath = dir.absoluteFilePath(zipFile); + InstanceBackup backup(fullPath); + + if (backup.isValid()) + { + backups.append(backup); + } + } + + return backups; +} + +int BackupManager::deleteOldBackups(InstancePtr instance, int maxCount) +{ + if (!instance || maxCount < 1) + { + return 0; + } + + QList<InstanceBackup> backups = listBackups(instance); + + if (backups.size() <= maxCount) + { + return 0; + } + + // Sort by date (newest first) + std::sort(backups.begin(), + backups.end(), + [](const InstanceBackup& a, const InstanceBackup& b) { return a.createdAt() > b.createdAt(); }); + + int deletedCount = 0; + for (int i = maxCount; i < backups.size(); i++) + { + if (deleteBackup(backups[i])) + { + deletedCount++; + } + } + + return deletedCount; +} + +bool BackupManager::deleteBackup(const InstanceBackup& backup) +{ + if (!backup.isValid()) + { + return false; + } + + bool success = QFile::remove(backup.backupPath()); + QFile::remove(backup.backupPath() + ".json"); // Remove metadata too + + return success; +} + +bool BackupManager::autoBackupBeforeLaunch(InstancePtr instance) +{ + BackupOptions options; + options.includeSaves = true; + options.includeConfig = true; + options.includeOptions = true; + options.includeMods = false; // Don't backup mods by default (too large) + + return createBackup(instance, "auto-backup-pre-launch", options); +} diff --git a/archived/projt-launcher/launcher/minecraft/BackupManager.h b/archived/projt-launcher/launcher/minecraft/BackupManager.h new file mode 100644 index 0000000000..ec7b42df2f --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/BackupManager.h @@ -0,0 +1,148 @@ +// 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. + * + */ + +#pragma once + +#include <QDateTime> +#include <QFutureWatcher> +#include <QObject> +#include <QString> +#include <QStringList> +#include "BaseInstance.h" + +struct BackupOptions +{ + bool includeSaves = true; + bool includeConfig = true; + bool includeMods = false; + bool includeResourcePacks = false; + bool includeShaderPacks = false; + bool includeScreenshots = false; + bool includeOptions = true; + QStringList customPaths; // Relative paths to include (e.g., "logs", "crash-reports") + + qint64 estimateSize() const; +}; + +class InstanceBackup +{ + public: + InstanceBackup() = default; + InstanceBackup(const QString& path); + + QString name() const + { + return m_name; + } + QString backupPath() const + { + return m_backupPath; + } + QDateTime createdAt() const + { + return m_createdAt; + } + qint64 size() const + { + return m_size; + } + QString description() const + { + return m_description; + } + QStringList includedPaths() const + { + return m_includedPaths; + } + + void setName(const QString& name) + { + m_name = name; + } + void setDescription(const QString& desc) + { + m_description = desc; + } + + bool isValid() const; + QString displaySize() const; + + private: + QString m_name; + QString m_backupPath; + QString m_description; + QDateTime m_createdAt; + qint64 m_size = 0; + QStringList m_includedPaths; + + friend class BackupManager; +}; + +class BackupManager : public QObject +{ + Q_OBJECT + + public: + explicit BackupManager(QObject* parent = nullptr); + + // Create a new backup (async) + void createBackupAsync(InstancePtr instance, const QString& backupName, const BackupOptions& options); + + // Create a new backup (blocking) + bool createBackup(InstancePtr instance, const QString& backupName, const BackupOptions& options); + + // Restore from backup (async) + void restoreBackupAsync(InstancePtr instance, const InstanceBackup& backup, bool createBackupBeforeRestore = true); + + // Restore from backup (blocking) + bool restoreBackup(InstancePtr instance, const InstanceBackup& backup, bool createBackupBeforeRestore = true); + + // List all backups for an instance + QList<InstanceBackup> listBackups(InstancePtr instance) const; + + // Delete old backups (keep only maxCount newest) + int deleteOldBackups(InstancePtr instance, int maxCount); + + // Delete specific backup + bool deleteBackup(const InstanceBackup& backup); + + // Get backup directory for instance + static QString getBackupDirectory(InstancePtr instance); + + // Auto backup before launch + bool autoBackupBeforeLaunch(InstancePtr instance); + + signals: + void backupCreated(const QString& instanceId, const QString& backupName); + void backupRestored(const QString& instanceId, const QString& backupName); + void backupDeleted(const QString& instanceId, const QString& backupName); + void backupProgress(int current, int total, const QString& currentFile); + void backupStarted(const QString& instanceId, const QString& backupName); + void backupFailed(const QString& instanceId, const QString& error); + void restoreStarted(const QString& instanceId, const QString& backupName); + void restoreFailed(const QString& instanceId, const QString& error); + + private: + bool compressBackup(const QString& sourcePath, const QString& backupPath, const BackupOptions& options); + bool extractBackup(const QString& backupPath, const QString& targetPath); + QList<InstanceBackup> scanBackupDirectory(const QString& backupDir) const; +};
\ No newline at end of file diff --git a/archived/projt-launcher/launcher/minecraft/Component.cpp b/archived/projt-launcher/launcher/minecraft/Component.cpp new file mode 100644 index 0000000000..2cb5054e3f --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/Component.cpp @@ -0,0 +1,576 @@ +// 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 Sefa Eyeoglu <contact@scrumplex.net> + * + * 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 "Component.h" +#include <meta/Index.hpp> +#include <meta/VersionList.hpp> + +#include <QSaveFile> + +#include "Application.h" +#include "FileSystem.h" +#include "OneSixVersionFormat.h" +#include "VersionFile.h" +#include "meta/Version.hpp" +#include "minecraft/Component.h" +#include "minecraft/PackProfile.h" + +#include <assert.h> + +const QMap<QString, ModloaderMapEntry> Component::KNOWN_MODLOADERS = { + { "net.neoforged", + { ModPlatform::NeoForge, { "net.minecraftforge", "net.fabricmc.fabric-loader", "org.quiltmc.quilt-loader" } } }, + { "net.minecraftforge", + { ModPlatform::Forge, { "net.neoforged", "net.fabricmc.fabric-loader", "org.quiltmc.quilt-loader" } } }, + { "net.fabricmc.fabric-loader", + { ModPlatform::Fabric, { "net.minecraftforge", "net.neoforged", "org.quiltmc.quilt-loader" } } }, + { "org.quiltmc.quilt-loader", + { ModPlatform::Quilt, { "net.minecraftforge", "net.neoforged", "net.fabricmc.fabric-loader" } } }, + { "com.mumfrey.liteloader", { ModPlatform::LiteLoader, {} } } +}; + +Component::Component(PackProfile* parent, const QString& uid) +{ + assert(parent); + m_parent = parent; + + m_uid = uid; +} + +Component::Component(PackProfile* parent, const QString& uid, std::shared_ptr<VersionFile> file) +{ + assert(parent); + m_parent = parent; + + m_file = file; + m_uid = uid; + m_cachedVersion = m_file->version; + m_cachedName = m_file->name; + m_loaded = true; +} + +std::shared_ptr<projt::meta::MetaVersion> Component::getMeta() +{ + return m_metaVersion; +} + +void Component::applyTo(LaunchProfile* profile) +{ + // do not apply disabled components + if (!isEnabled()) + { + return; + } + auto vfile = getVersionFile(); + if (vfile) + { + vfile->applyTo(profile, m_parent->runtimeContext()); + } + else + { + profile->applyProblemSeverity(getProblemSeverity()); + } +} + +std::shared_ptr<class VersionFile> Component::getVersionFile() const +{ + if (m_metaVersion) + { + return m_metaVersion->detailedData(); + } + else + { + return m_file; + } +} + +std::shared_ptr<projt::meta::MetaVersionList> Component::getVersionList() const +{ + // Return nullptr if metadata index isn't loaded yet - caller should handle this case + auto index = APPLICATION->metadataIndex(); + if (!index || !index->hasComponent(m_uid)) + { + return nullptr; + } + return index->component(m_uid); +} + +int Component::getOrder() +{ + if (m_orderOverride) + return m_order; + + auto vfile = getVersionFile(); + if (vfile) + { + return vfile->order; + } + return 0; +} + +void Component::setOrder(int order) +{ + m_orderOverride = true; + m_order = order; +} + +QString Component::getID() +{ + return m_uid; +} + +QString Component::getName() +{ + if (!m_cachedName.isEmpty()) + return m_cachedName; + return m_uid; +} + +QString Component::getVersion() +{ + return m_cachedVersion; +} + +QString Component::getFilename() +{ + return m_parent->patchFilePathForUid(m_uid); +} + +QDateTime Component::getReleaseDateTime() +{ + if (m_metaVersion) + { + return m_metaVersion->releaseTime(); + } + auto vfile = getVersionFile(); + if (vfile) + { + return vfile->releaseTime; + } + // Fallback: return current time when no release time is available (e.g., custom components) + return QDateTime::currentDateTime(); +} + +bool Component::isEnabled() +{ + return !canBeDisabled() || !m_disabled; +} + +bool Component::canBeDisabled() +{ + return isRemovable() && !m_dependencyOnly; +} + +bool Component::setEnabled(bool state) +{ + bool intendedDisabled = !state; + if (!canBeDisabled()) + { + intendedDisabled = false; + } + if (intendedDisabled != m_disabled) + { + m_disabled = intendedDisabled; + emit dataChanged(); + return true; + } + return false; +} + +bool Component::isCustom() +{ + return m_file != nullptr; +} + +bool Component::isCustomizable() +{ + return m_metaVersion && getVersionFile(); +} + +bool Component::isRemovable() +{ + return !m_important; +} + +bool Component::isRevertible() +{ + if (isCustom()) + { + if (APPLICATION->metadataIndex()->hasComponent(m_uid)) + { + return true; + } + } + return false; +} + +bool Component::isMoveable() +{ + // Important components (like Minecraft) should stay at their fixed position + // Non-important components can be reordered by the user + return !m_important; +} + +bool Component::isVersionChangeable(bool wait) +{ + auto list = getVersionList(); + if (list) + { + if (wait) + list->waitUntilReady(); + return list->count() != 0; + } + return false; +} + +bool Component::isKnownModloader() +{ + auto iter = KNOWN_MODLOADERS.find(m_uid); + return iter != KNOWN_MODLOADERS.cend(); +} + +QStringList Component::knownConflictingComponents() +{ + auto iter = KNOWN_MODLOADERS.find(m_uid); + if (iter != KNOWN_MODLOADERS.cend()) + { + return (*iter).knownConflictingComponents; + } + else + { + return {}; + } +} + +void Component::setImportant(bool state) +{ + if (m_important != state) + { + m_important = state; + emit dataChanged(); + } +} + +ProblemSeverity Component::getProblemSeverity() const +{ + auto file = getVersionFile(); + if (file) + { + auto severity = file->getProblemSeverity(); + return m_componentProblemSeverity > severity ? m_componentProblemSeverity : severity; + } + return ProblemSeverity::Error; +} + +const QList<PatchProblem> Component::getProblems() const +{ + auto file = getVersionFile(); + if (file) + { + auto problems = file->getProblems(); + problems.append(m_componentProblems); + return problems; + } + return { { ProblemSeverity::Error, QObject::tr("Patch is not loaded yet.") } }; +} + +void Component::addComponentProblem(ProblemSeverity severity, const QString& description) +{ + if (severity > m_componentProblemSeverity) + { + m_componentProblemSeverity = severity; + } + m_componentProblems.append({ severity, description }); + + emit dataChanged(); +} + +void Component::resetComponentProblems() +{ + m_componentProblems.clear(); + m_componentProblemSeverity = ProblemSeverity::None; + + emit dataChanged(); +} + +void Component::setVersion(const QString& version) +{ + if (version == m_version) + { + return; + } + m_version = version; + if (m_loaded) + { + // we are loaded and potentially have state to invalidate + if (m_file) + { + // we have a file... explicit version has been changed and there is nothing else to do. + } + else + { + // we don't have a file, therefore we are loaded with metadata + m_cachedVersion = version; + // see if the meta version is loaded + auto metaVersion = APPLICATION->metadataIndex()->version(m_uid, version); + if (metaVersion->isFullyLoaded()) + { + // if yes, we can continue with that. + m_metaVersion = metaVersion; + } + else + { + // if not, we need loading + m_metaVersion.reset(); + m_loaded = false; + } + updateCachedData(); + } + } + else + { + // not loaded... assume it will be sorted out later by the update task + } + emit dataChanged(); +} + +bool Component::customize() +{ + if (isCustom()) + { + return false; + } + + auto filename = getFilename(); + if (!FS::ensureFilePathExists(filename)) + { + return false; + } + // Using try-catch to handle potential JSON serialization errors gracefully + try + { + QSaveFile jsonFile(filename); + if (!jsonFile.open(QIODevice::WriteOnly)) + { + return false; + } + auto vfile = getVersionFile(); + if (!vfile) + { + return false; + } + auto document = OneSixVersionFormat::versionFileToJson(vfile); + jsonFile.write(document.toJson()); + if (!jsonFile.commit()) + { + return false; + } + m_file = vfile; + m_metaVersion.reset(); + emit dataChanged(); + } + catch (const Exception& error) + { + qWarning() << "Version could not be loaded:" << error.cause(); + } + return true; +} + +bool Component::revert() +{ + if (!isCustom()) + { + // already not custom + return true; + } + auto filename = getFilename(); + bool result = true; + // just kill the file and reload + if (QFile::exists(filename)) + { + result = FS::deletePath(filename); + } + if (result) + { + // file gone... + m_file.reset(); + + // check local cache for metadata... + auto version = APPLICATION->metadataIndex()->version(m_uid, m_version); + if (version->isFullyLoaded()) + { + m_metaVersion = version; + } + else + { + m_metaVersion.reset(); + m_loaded = false; + } + emit dataChanged(); + } + return result; +} + +/** + * deep inspecting compare for requirement sets + * By default, only uids are compared for set operations. + * This compares all fields of the Require structs in the sets. + */ +static bool deepCompare(const projt::meta::DependencySet& a, const projt::meta::DependencySet& b) +{ + // NOTE: this needs to be rewritten if the type of DependencySet changes + if (a.size() != b.size()) + { + return false; + } + for (const auto& reqA : a) + { + const auto& iter2 = b.find(reqA); + if (iter2 == b.cend()) + { + return false; + } + const auto& reqB = *iter2; + if (!reqA.deepEquals(reqB)) + { + return false; + } + } + return true; +} + +void Component::updateCachedData() +{ + auto file = getVersionFile(); + if (file) + { + bool changed = false; + if (m_cachedName != file->name) + { + m_cachedName = file->name; + changed = true; + } + if (m_cachedVersion != file->version) + { + m_cachedVersion = file->version; + changed = true; + } + if (m_cachedVolatile != file->m_volatile) + { + m_cachedVolatile = file->m_volatile; + changed = true; + } + if (!deepCompare(m_cachedRequires, file->m_requires)) + { + m_cachedRequires = file->m_requires; + changed = true; + } + if (!deepCompare(m_cachedConflicts, file->conflicts)) + { + m_cachedConflicts = file->conflicts; + changed = true; + } + if (changed) + { + emit dataChanged(); + } + } + else + { + // in case we removed all the metadata + m_cachedRequires.clear(); + m_cachedConflicts.clear(); + emit dataChanged(); + } +} + +void Component::waitLoadMeta() +{ + if (!m_loaded) + { + if (!m_metaVersion || !m_metaVersion->isFullyLoaded()) + { + // wait for the loaded version from meta + m_metaVersion = APPLICATION->metadataIndex()->loadVersionBlocking(m_uid, m_version); + } + m_loaded = true; + updateCachedData(); + } +} + +void Component::setUpdateAction(const UpdateAction& action) +{ + m_updateAction = action; +} + +UpdateAction Component::getUpdateAction() +{ + return m_updateAction; +} + +void Component::clearUpdateAction() +{ + m_updateAction = UpdateAction{ UpdateActionNone{} }; +} + +QDebug operator<<(QDebug d, const Component& comp) +{ + d << "Component(" << comp.m_uid << " : " << comp.m_cachedVersion << ")"; + return d; +} diff --git a/archived/projt-launcher/launcher/minecraft/Component.h b/archived/projt-launcher/launcher/minecraft/Component.h new file mode 100644 index 0000000000..6e92f84f20 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/Component.h @@ -0,0 +1,187 @@ +// 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. + */ +#pragma once + +#include <QDateTime> +#include <QJsonDocument> +#include <QList> +#include <memory> +#include <optional> +#include <variant> +#include "ProblemProvider.h" +#include "QObjectPtr.h" +#include "meta/JsonFormat.hpp" +#include "modplatform/ModIndex.h" + +class PackProfile; +class LaunchProfile; +namespace projt::meta +{ + class MetaVersion; + class MetaVersionList; +} // namespace projt::meta +class VersionFile; + +struct UpdateActionChangeVersion +{ + /// version to change to + QString targetVersion; +}; +struct UpdateActionLatestRecommendedCompatible +{ + /// Parent uid + QString parentUid; + QString parentName; + /// Parent version + QString version; + /// +}; +struct UpdateActionRemove +{}; +struct UpdateActionImportantChanged +{ + QString oldVersion; +}; + +using UpdateActionNone = std::monostate; + +using UpdateAction = std::variant<UpdateActionNone, + UpdateActionChangeVersion, + UpdateActionLatestRecommendedCompatible, + UpdateActionRemove, + UpdateActionImportantChanged>; + +struct ModloaderMapEntry +{ + ModPlatform::ModLoaderType type; + QStringList knownConflictingComponents; +}; + +class Component : public QObject, public ProblemProvider +{ + Q_OBJECT + public: + Component(PackProfile* parent, const QString& uid); + + // DEPRECATED: remove these constructors? + Component(PackProfile* parent, const QString& uid, std::shared_ptr<VersionFile> file); + + virtual ~Component() + {} + + static const QMap<QString, ModloaderMapEntry> KNOWN_MODLOADERS; + + void applyTo(LaunchProfile* profile); + + bool isEnabled(); + bool setEnabled(bool state); + bool canBeDisabled(); + + bool isMoveable(); + bool isCustomizable(); + bool isRevertible(); + bool isRemovable(); + bool isCustom(); + bool isVersionChangeable(bool wait = true); + bool isKnownModloader(); + QStringList knownConflictingComponents(); + + // DEPRECATED: explicit numeric order values, used for loading old non-component config. + // NOTE: Kept for legacy migration support. + void setOrder(int order); + int getOrder(); + + QString getID(); + QString getName(); + QString getVersion(); + std::shared_ptr<projt::meta::MetaVersion> getMeta(); + QDateTime getReleaseDateTime(); + + QString getFilename(); + + std::shared_ptr<class VersionFile> getVersionFile() const; + std::shared_ptr<projt::meta::MetaVersionList> getVersionList() const; + + void setImportant(bool state); + + const QList<PatchProblem> getProblems() const override; + ProblemSeverity getProblemSeverity() const override; + void addComponentProblem(ProblemSeverity severity, const QString& description); + void resetComponentProblems(); + + void setVersion(const QString& version); + bool customize(); + bool revert(); + + void updateCachedData(); + + void waitLoadMeta(); + + void setUpdateAction(const UpdateAction& action); + void clearUpdateAction(); + UpdateAction getUpdateAction(); + + signals: + void dataChanged(); + + public: /* data */ + PackProfile* m_parent; + + // BEGIN: persistent component list properties + /// ID of the component + QString m_uid; + /// version of the component - when there's a custom json override, this is also the version the component reverts to + QString m_version; + /// if true, this has been added automatically to satisfy dependencies and may be automatically removed + bool m_dependencyOnly = false; + /// if true, the component is either the main component of the instance, or otherwise important and cannot be removed. + bool m_important = false; + /// if true, the component is disabled + bool m_disabled = false; + + /// cached name for display purposes, taken from the version file (meta or local override) + QString m_cachedName; + /// cached version for display AND other purposes, taken from the version file (meta or local override) + QString m_cachedVersion; + /// cached set of requirements, taken from the version file (meta or local override) + projt::meta::DependencySet m_cachedRequires; + projt::meta::DependencySet m_cachedConflicts; + /// if true, the component is volatile and may be automatically removed when no longer needed + bool m_cachedVolatile = false; + // END: persistent component list properties + + // DEPRECATED: explicit numeric order values, used for loading old non-component config. + // NOTE: Kept for legacy migration support. + bool m_orderOverride = false; + int m_order = 0; + + // load state + std::shared_ptr<projt::meta::MetaVersion> m_metaVersion; + std::shared_ptr<VersionFile> m_file; + bool m_loaded = false; + + private: + QList<PatchProblem> m_componentProblems; + ProblemSeverity m_componentProblemSeverity = ProblemSeverity::None; + UpdateAction m_updateAction = UpdateAction{ UpdateActionNone{} }; +}; + +using ComponentPtr = shared_qobject_ptr<Component>; diff --git a/archived/projt-launcher/launcher/minecraft/ComponentUpdateTask.cpp b/archived/projt-launcher/launcher/minecraft/ComponentUpdateTask.cpp new file mode 100644 index 0000000000..a1939f22d2 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/ComponentUpdateTask.cpp @@ -0,0 +1,1114 @@ +// 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. + */ +#include "ComponentUpdateTask.h" +#include <algorithm> + +#include "Component.h" +#include "ComponentUpdateTask_p.h" +#include "PackProfile.h" +#include "PackProfile_p.h" +#include "ProblemProvider.h" +#include "Version.h" +#include "cassert" +#include "meta/Index.hpp" +#include "meta/Version.hpp" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/OneSixVersionFormat.h" +#include "minecraft/ProfileUtils.h" +#include "net/Mode.h" + +#include "Application.h" +#include "tasks/Task.h" + +#include "minecraft/Logging.h" +#include "meta/VersionList.hpp" +#include "tasks/SequentialTask.h" + +/* + * This is responsible for loading the components of a component list AND resolving dependency issues between them + */ + +/* + * NOTE: The 'one shot async task' nature of this implementation is a known limitation. + * Ideally, this should be refactored into a reactor/state machine that receives input + * from the application and dynamically adapts to changing requirements. + * + * The reactor should be the only entry point for manipulating the PackProfile. + * See: https://en.wikipedia.org/wiki/Reactor_pattern + * + * Current implementation logic: + * - Operates on a snapshot of the PackProfile state. + * - Merges results as long as the snapshot and PackProfile haven't diverged during execution. + * - Requires a restart if the component list changes mid-operation. + */ + +/* + * Or make this operate on a snapshot of the PackProfile state, then merge results in as long as the snapshot and + * PackProfile didn't change? If the component list changes, start over. + */ + +ComponentUpdateTask::ComponentUpdateTask(Mode mode, Net::Mode netmode, PackProfile* list) : Task() +{ + d.reset(new ComponentUpdateTaskData); + d->m_profile = list; + d->mode = mode; + d->netmode = netmode; +} + +ComponentUpdateTask::~ComponentUpdateTask() +{} + +void ComponentUpdateTask::executeTask() +{ + qCDebug(instanceProfileResolveC) << "Loading components"; + loadComponents(); +} + +namespace +{ + enum class LoadResult + { + LoadedLocal, + RequiresRemote, + Failed + }; + + LoadResult composeLoadResult(LoadResult a, LoadResult b) + { + if (a < b) + { + return b; + } + return a; + } + + static LoadResult loadComponent(ComponentPtr component, Task::Ptr& loadTask, Net::Mode netmode) + { + if (component->m_loaded) + { + qCDebug(instanceProfileResolveC) << component->getName() << "is already loaded"; + return LoadResult::LoadedLocal; + } + + LoadResult result = LoadResult::Failed; + auto customPatchFilename = component->getFilename(); + if (QFile::exists(customPatchFilename)) + { + // if local file exists... + + // check for uid problems inside... + bool fileChanged = false; + auto file = ProfileUtils::parseJsonFile(QFileInfo(customPatchFilename), false); + if (file->uid != component->m_uid) + { + file->uid = component->m_uid; + fileChanged = true; + } + if (fileChanged) + { + // Ensure we don't ignore failures when writing back a fixed json file. + bool saved = + ProfileUtils::saveJsonFile(OneSixVersionFormat::versionFileToJson(file), customPatchFilename); + if (!saved) + { + qCWarning(instanceProfileResolveC) + << "Failed to save modified component file:" << customPatchFilename; + } + } + + component->m_file = file; + component->m_loaded = true; + result = LoadResult::LoadedLocal; + } + else + { + auto metaVersion = APPLICATION->metadataIndex()->version(component->m_uid, component->m_version); + component->m_metaVersion = metaVersion; + if (metaVersion->isFullyLoaded()) + { + component->m_loaded = true; + result = LoadResult::LoadedLocal; + } + else + { + loadTask = + APPLICATION->metadataIndex()->loadVersionTask(component->m_uid, component->m_version, netmode); + loadTask->start(); + if (netmode == Net::Mode::Online) + result = LoadResult::RequiresRemote; + else if (metaVersion->isFullyLoaded()) + result = LoadResult::LoadedLocal; + else + result = LoadResult::Failed; + } + } + return result; + } + + static LoadResult loadPackProfile(ComponentPtr component, Task::Ptr& loadTask, Net::Mode netmode) + { + auto index = APPLICATION->metadataIndex(); + + // If index is not yet synchronized and we're online, we need to load it first + if (index->state() != projt::meta::MetaEntity::State::Synchronized) + { + if (netmode == Net::Mode::Offline) + { + qCWarning(instanceProfileResolveC) << "Metadata index not available offline for" << component->m_uid; + return LoadResult::Failed; + } + + // Create a sequential task that first loads the index, then the version list + auto seq = makeShared<SequentialTask>( + ComponentUpdateTask::tr("Loading metadata for %1").arg(component->getName())); + seq->addTask(index->createLoadTask(netmode)); + + // After index loads, we need to load the version list + // Use a callback-based approach by connecting after index load completes + auto indexLoadTask = index->createLoadTask(netmode); + + // Create a task that will load version list after index is ready + class DeferredVersionListLoader : public Task + { + public: + DeferredVersionListLoader(ComponentPtr comp, Net::Mode mode) : m_component(comp), m_mode(mode) + {} + + void executeTask() override + { + auto versionList = m_component->getVersionList(); + if (!versionList) + { + emitFailed(tr("Component %1 not found in metadata index").arg(m_component->m_uid)); + return; + } + if (versionList->isLoaded()) + { + emitSucceeded(); + return; + } + m_innerTask = versionList->createLoadTask(m_mode); + connect(m_innerTask.get(), &Task::succeeded, this, [this]() { emitSucceeded(); }); + connect(m_innerTask.get(), + &Task::failed, + this, + [this](const QString& reason) { emitFailed(reason); }); + connect(m_innerTask.get(), &Task::progress, this, &Task::setProgress); + connect(m_innerTask.get(), &Task::status, this, &Task::setStatus); + m_innerTask->start(); + } + + bool canAbort() const override + { + return m_innerTask ? m_innerTask->canAbort() : false; + } + bool abort() override + { + return m_innerTask ? m_innerTask->abort() : Task::abort(); + } + + private: + ComponentPtr m_component; + Net::Mode m_mode; + Task::Ptr m_innerTask; + }; + + seq->addTask(makeShared<DeferredVersionListLoader>(component, netmode)); + loadTask = seq; + loadTask->start(); + return LoadResult::RequiresRemote; + } + + // Index is already synchronized, get version list directly + auto versionList = component->getVersionList(); + if (!versionList) + { + qCWarning(instanceProfileResolveC) << "No version list found for" << component->m_uid; + return LoadResult::Failed; + } + + if (versionList->isLoaded()) + { + return LoadResult::LoadedLocal; + } + + if (netmode == Net::Mode::Offline) + { + return LoadResult::Failed; + } + + loadTask = versionList->createLoadTask(netmode); + + if (loadTask) + { + loadTask->start(); + return LoadResult::RequiresRemote; + } + + return LoadResult::Failed; + } + +} // namespace + +void ComponentUpdateTask::loadComponents() +{ + LoadResult result = LoadResult::LoadedLocal; + size_t taskIndex = 0; + size_t componentIndex = 0; + d->remoteLoadSuccessful = true; + + // load all the components OR their lists... + for (auto component : d->m_profile->d->components) + { + Task::Ptr loadTask; + LoadResult singleResult = LoadResult::LoadedLocal; + RemoteLoadStatus::Type loadType; + component->resetComponentProblems(); + // Load version lists or components based on resolution mode + switch (d->mode) + { + case Mode::Launch: + { + singleResult = loadComponent(component, loadTask, d->netmode); + loadType = RemoteLoadStatus::Type::Version; + break; + } + case Mode::Resolution: + { + singleResult = loadPackProfile(component, loadTask, d->netmode); + loadType = RemoteLoadStatus::Type::List; + break; + } + } + if (singleResult == LoadResult::LoadedLocal) + { + component->updateCachedData(); + } + result = composeLoadResult(result, singleResult); + if (loadTask) + { + qCDebug(instanceProfileResolveC) << d->m_profile->d->m_instance->name() << "|" + << "Remote loading is being run for" << component->getName(); + connect(loadTask.get(), &Task::succeeded, this, [this, taskIndex]() { remoteLoadSucceeded(taskIndex); }); + connect(loadTask.get(), + &Task::failed, + this, + [this, taskIndex](const QString& error) { remoteLoadFailed(taskIndex, error); }); + connect(loadTask.get(), + &Task::aborted, + this, + [this, taskIndex]() { remoteLoadFailed(taskIndex, tr("Aborted")); }); + RemoteLoadStatus status; + status.type = loadType; + status.PackProfileIndex = componentIndex; + status.task = loadTask; + d->remoteLoadStatusList.append(status); + taskIndex++; + } + componentIndex++; + } + d->remoteTasksInProgress = taskIndex; + switch (result) + { + case LoadResult::LoadedLocal: + { + // Everything got loaded. Advance to dependency resolution. + performUpdateActions(); + // In offline mode, only check dependencies; in online mode, resolve them + resolveDependencies(d->netmode == Net::Mode::Offline); + break; + } + case LoadResult::RequiresRemote: + { + // we wait for signals. + break; + } + case LoadResult::Failed: + { + emitFailed(tr("Some component metadata load tasks failed.")); + break; + } + } +} + +namespace +{ + struct RequireEx : public projt::meta::ComponentDependency + { + size_t indexOfFirstDependee = 0; + }; + struct RequireCompositionResult + { + bool ok; + RequireEx outcome; + }; + using RequireExSet = std::set<RequireEx>; +} // namespace + +static RequireCompositionResult composeRequirement(const RequireEx& a, const RequireEx& b) +{ + assert(a.uid == b.uid); + RequireEx out; + out.uid = a.uid; + out.indexOfFirstDependee = std::min(a.indexOfFirstDependee, b.indexOfFirstDependee); + if (a.equalsVersion.isEmpty()) + { + out.equalsVersion = b.equalsVersion; + } + else if (b.equalsVersion.isEmpty()) + { + out.equalsVersion = a.equalsVersion; + } + else if (a.equalsVersion == b.equalsVersion) + { + out.equalsVersion = a.equalsVersion; + } + else + { + // Version conflict: different exact versions required for same component + qWarning() << "Version conflict for" << a.uid << ":" << a.equalsVersion << "vs" << b.equalsVersion; + return { false, out }; + } + + if (a.suggests.isEmpty()) + { + out.suggests = b.suggests; + } + else if (b.suggests.isEmpty()) + { + out.suggests = a.suggests; + } + else + { + Version aVer(a.suggests); + Version bVer(b.suggests); + out.suggests = (aVer < bVer ? b.suggests : a.suggests); + } + return { true, out }; +} + +// gather the requirements from all components, finding any obvious conflicts +static bool gatherRequirementsFromComponents(const ComponentContainer& input, RequireExSet& output) +{ + bool succeeded = true; + size_t componentNum = 0; + for (auto component : input) + { + auto& componentRequires = component->m_cachedRequires; + for (const auto& componentRequire : componentRequires) + { + auto found = std::find_if(output.cbegin(), + output.cend(), + [componentRequire](const projt::meta::ComponentDependency& req) + { return req.uid == componentRequire.uid; }); + + RequireEx componenRequireEx; + componenRequireEx.uid = componentRequire.uid; + componenRequireEx.suggests = componentRequire.suggests; + componenRequireEx.equalsVersion = componentRequire.equalsVersion; + componenRequireEx.indexOfFirstDependee = componentNum; + + if (found != output.cend()) + { + // found... process it further + auto result = composeRequirement(componenRequireEx, *found); + if (result.ok) + { + output.erase(componenRequireEx); + output.insert(result.outcome); + } + else + { + qCCritical(instanceProfileResolveC) + << "Conflicting requirements:" << componentRequire.uid + << "versions:" << componentRequire.equalsVersion << ";" << (*found).equalsVersion; + } + succeeded &= result.ok; + } + else + { + // not found, accumulate + output.insert(componenRequireEx); + } + } + componentNum++; + } + return succeeded; +} + +/// Get list of uids that can be trivially removed because nothing is depending on them anymore (and they are installed as deps) +static void getTrivialRemovals(const ComponentContainer& components, const RequireExSet& reqs, QStringList& toRemove) +{ + for (const auto& component : components) + { + if (!component->m_dependencyOnly) + continue; + if (!component->m_cachedVolatile) + continue; + RequireEx reqNeedle; + reqNeedle.uid = component->m_uid; + const auto iter = reqs.find(reqNeedle); + if (iter == reqs.cend()) + { + toRemove.append(component->m_uid); + } + } +} + +/** + * handles: + * - trivial addition (there is an unmet requirement and it can be trivially met by adding something) + * - trivial version conflict of dependencies == explicit version required and installed is different + * + * toAdd - set of requirements than mean adding a new component + * toChange - set of requirements that mean changing version of an existing component + */ +static bool getTrivialComponentChanges(const ComponentIndex& index, + const RequireExSet& input, + RequireExSet& toAdd, + RequireExSet& toChange) +{ + enum class Decision + { + Undetermined, + Met, + Missing, + VersionNotSame, + LockedVersionNotSame + } decision = Decision::Undetermined; + + QString reqStr; + bool succeeded = true; + // list the composed requirements and say if they are met or unmet + for (auto& req : input) + { + do + { + if (req.equalsVersion.isEmpty()) + { + reqStr = QString("Req: %1").arg(req.uid); + if (index.contains(req.uid)) + { + decision = Decision::Met; + } + else + { + toAdd.insert(req); + decision = Decision::Missing; + } + break; + } + else + { + reqStr = QString("Req: %1 == %2").arg(req.uid, req.equalsVersion); + const auto& compIter = index.find(req.uid); + if (compIter == index.cend()) + { + toAdd.insert(req); + decision = Decision::Missing; + break; + } + auto& comp = (*compIter); + if (comp->getVersion() != req.equalsVersion) + { + if (comp->isCustom()) + { + decision = Decision::LockedVersionNotSame; + } + else + { + if (comp->m_dependencyOnly) + { + decision = Decision::VersionNotSame; + } + else + { + decision = Decision::LockedVersionNotSame; + } + } + break; + } + decision = Decision::Met; + } + } + while (false); + switch (decision) + { + case Decision::Undetermined: + qCCritical(instanceProfileResolveC) << "No decision for" << reqStr; + succeeded = false; + break; + case Decision::Met: qCDebug(instanceProfileResolveC) << reqStr << "Is met."; break; + case Decision::Missing: + qCDebug(instanceProfileResolveC) + << reqStr << "Is missing and should be added at" << req.indexOfFirstDependee; + toAdd.insert(req); + break; + case Decision::VersionNotSame: + qCDebug(instanceProfileResolveC) << reqStr << "already has different version that can be changed."; + toChange.insert(req); + break; + case Decision::LockedVersionNotSame: + qCDebug(instanceProfileResolveC) << reqStr << "already has different version that cannot be changed."; + succeeded = false; + break; + } + } + return succeeded; +} + +ComponentContainer ComponentUpdateTask::collectTreeLinked(const QString& uid) +{ + ComponentContainer linked; + + auto& components = d->m_profile->d->components; + auto& componentIndex = d->m_profile->d->componentIndex; + auto& instance = d->m_profile->d->m_instance; + for (auto comp : components) + { + qCDebug(instanceProfileResolveC) << instance->name() << "|" + << "scanning" << comp->getID() << ":" << comp->getVersion() << "for tree link"; + auto dep = std::find_if(comp->m_cachedRequires.cbegin(), + comp->m_cachedRequires.cend(), + [uid](const projt::meta::ComponentDependency& req) -> bool { return req.uid == uid; }); + if (dep != comp->m_cachedRequires.cend()) + { + qCDebug(instanceProfileResolveC) + << instance->name() << "|" << comp->getID() << ":" << comp->getVersion() << "depends on" << uid; + linked.append(comp); + } + } + auto iter = componentIndex.find(uid); + if (iter != componentIndex.end()) + { + ComponentPtr comp = *iter; + comp->updateCachedData(); + qCDebug(instanceProfileResolveC) << instance->name() << "|" << comp->getID() << ":" << comp->getVersion() + << "has" << comp->m_cachedRequires.size() << "dependencies"; + for (auto dep : comp->m_cachedRequires) + { + qCDebug(instanceProfileC) << instance->name() << "|" << uid << "depends on" << dep.uid; + auto found = componentIndex.find(dep.uid); + if (found != componentIndex.end()) + { + qCDebug(instanceProfileC) << instance->name() << "|" << (*found)->getID() << "is present"; + linked.append(*found); + } + } + } + return linked; +} + +// Architecture Note: This method directly manipulates PackProfile internals. +// Proper abstraction would require richer data types (dependency graph, version constraints), +// but current implementation is sufficient for the launcher's use case. +// A full rewrite to use a proper dependency graph (like SAT solving) is out of scope for now. +void ComponentUpdateTask::resolveDependencies(bool checkOnly) +{ + qCDebug(instanceProfileResolveC) << "Resolving dependencies"; + /* + * this is a naive dependency resolving algorithm. all it does is check for following conditions and react in simple + * ways: + * 1. There are conflicting dependencies on the same uid with different exact version numbers + * -> hard error + * 2. A dependency has non-matching exact version number + * -> hard error + * 3. A dependency is entirely missing and needs to be injected before the dependee(s) + * -> requirements are injected + * + * NOTE: this is a placeholder and should eventually be replaced with something 'serious' + */ + auto& components = d->m_profile->d->components; + auto& componentIndex = d->m_profile->d->componentIndex; + + RequireExSet allRequires; + QStringList toRemove; + do + { + allRequires.clear(); + toRemove.clear(); + if (!gatherRequirementsFromComponents(components, allRequires)) + { + finalizeComponents(); + emitFailed(tr("Conflicting requirements detected during dependency checking!")); + return; + } + getTrivialRemovals(components, allRequires, toRemove); + if (!toRemove.isEmpty()) + { + qCDebug(instanceProfileResolveC) << "Removing obsolete components..."; + for (auto& remove : toRemove) + { + qCDebug(instanceProfileResolveC) << "Removing" << remove; + d->m_profile->remove(remove); + } + } + } + while (!toRemove.isEmpty()); + RequireExSet toAdd; + RequireExSet toChange; + bool succeeded = getTrivialComponentChanges(componentIndex, allRequires, toAdd, toChange); + if (!succeeded) + { + finalizeComponents(); + emitFailed(tr("Instance has conflicting dependencies.")); + return; + } + if (checkOnly) + { + finalizeComponents(); + if (toAdd.size() || toChange.size()) + { + emitFailed(tr("Instance has unresolved dependencies while loading/checking for launch.")); + } + else + { + emitSucceeded(); + } + return; + } + + bool recursionNeeded = false; + if (toAdd.size()) + { + // add stuff... + for (auto& add : toAdd) + { + auto component = makeShared<Component>(d->m_profile, add.uid); + if (!add.equalsVersion.isEmpty()) + { + // exact version + qCDebug(instanceProfileResolveC) << "Adding" << add.uid << "version" << add.equalsVersion + << "at position" << add.indexOfFirstDependee; + component->m_version = add.equalsVersion; + } + else + { + // version needs to be decided + qCDebug(instanceProfileResolveC) << "Adding" << add.uid << "at position" << add.indexOfFirstDependee; + if (!add.suggests.isEmpty()) + { + // Use suggested version if available + component->m_version = add.suggests; + } + else + { + // Intermediary mappings must align with the selected Minecraft version. + // Prefer a metadata entry that explicitly depends on that MC version. + if (add.uid == "net.fabricmc.intermediary" || add.uid == "org.quiltmc.hashed") + { + auto minecraft = + std::find_if(components.begin(), + components.end(), + [](ComponentPtr& cmp) { return cmp->getID() == "net.minecraft"; }); + if (minecraft != components.end()) + { + const auto minecraftVersion = (*minecraft)->getVersion(); + auto versionList = APPLICATION->metadataIndex()->component(add.uid); + if (versionList) + { + versionList->waitUntilReady(); + auto matched = versionList->stableForParent("net.minecraft", minecraftVersion); + if (!matched) + { + matched = versionList->latestForParent("net.minecraft", minecraftVersion); + } + if (matched) + { + component->m_version = matched->descriptor(); + } + } + if (component->m_version.isEmpty()) + { + component->m_version = minecraftVersion; + } + } + } + + // Try to get recommended version from metadata + if (component->m_version.isEmpty()) + { + auto versionList = APPLICATION->metadataIndex()->component(add.uid); + if (versionList) + { + versionList->waitUntilReady(); + auto recommended = versionList->getRecommended(); + if (recommended) + { + component->m_version = recommended->descriptor(); + } + } + } + + // Last resort: known defaults for LWJGL when metadata unavailable + if (component->m_version.isEmpty()) + { + if (add.uid == "org.lwjgl") + { + component->m_version = "2.9.1"; + } + else if (add.uid == "org.lwjgl3") + { + component->m_version = "3.1.2"; + } + } + } + } + component->m_dependencyOnly = true; + // Direct insertion to component list is intentional - this is part of dependency resolution + // which requires atomic updates to the profile structure. + d->m_profile->insertComponent(add.indexOfFirstDependee, component); + componentIndex[add.uid] = component; + } + recursionNeeded = true; + } + if (toChange.size()) + { + // Version changes during dependency resolution require direct component access + // as the dependency resolver may adjust versions to satisfy constraints. + for (auto& change : toChange) + { + qCDebug(instanceProfileResolveC) << "Setting version of " << change.uid << "to" << change.equalsVersion; + auto component = componentIndex[change.uid]; + component->setVersion(change.equalsVersion); + } + recursionNeeded = true; + } + + if (recursionNeeded) + { + loadComponents(); + } + else + { + finalizeComponents(); + emitSucceeded(); + } +} + +// Variant visitation via lambda +template <class... Ts> +struct overload : Ts... +{ + using Ts::operator()...; +}; +template <class... Ts> +overload(Ts...) -> overload<Ts...>; + +void ComponentUpdateTask::performUpdateActions() +{ + auto& instance = d->m_profile->d->m_instance; + bool addedActions; + QStringList toRemove; + do + { + addedActions = false; + toRemove.clear(); + auto& components = d->m_profile->d->components; + auto& componentIndex = d->m_profile->d->componentIndex; + for (auto component : components) + { + if (!component) + { + continue; + } + auto action = component->getUpdateAction(); + auto visitor = overload{ + [](const UpdateActionNone&) + { + // noop + }, + [&component, &instance](const UpdateActionChangeVersion& cv) + { + qCDebug(instanceProfileResolveC) << instance->name() << "|" + << "UpdateActionChangeVersion" << component->getID() << ":" + << component->getVersion() << "change to" << cv.targetVersion; + component->setVersion(cv.targetVersion); + component->waitLoadMeta(); + }, + [&component, &instance](const UpdateActionLatestRecommendedCompatible& lrc) + { + qCDebug(instanceProfileResolveC) + << instance->name() << "|" + << "UpdateActionLatestRecommendedCompatible" << component->getID() << ":" + << component->getVersion() << "updating to latest recommend or compatible with" << lrc.parentUid + << lrc.version; + auto versionList = APPLICATION->metadataIndex()->component(component->getID()); + if (versionList) + { + versionList->waitUntilReady(); + auto recommended = versionList->stableForParent(lrc.parentUid, lrc.version); + if (!recommended) + { + recommended = versionList->latestForParent(lrc.parentUid, lrc.version); + } + if (recommended) + { + component->setVersion(recommended->versionId()); + component->waitLoadMeta(); + return; + } + else + { + component->addComponentProblem(ProblemSeverity::Error, + QObject::tr("No compatible version of %1 found for %2 %3") + .arg(component->getName(), lrc.parentName, lrc.version)); + } + } + else + { + component->addComponentProblem( + ProblemSeverity::Error, + QObject::tr("No version list in metadata index for %1").arg(component->getID())); + } + }, + [&component, &instance, &toRemove](const UpdateActionRemove&) + { + qCDebug(instanceProfileResolveC) + << instance->name() << "|" + << "UpdateActionRemove" << component->getID() << ":" << component->getVersion() << "removing"; + toRemove.append(component->getID()); + }, + [this, &component, &instance, &addedActions, &componentIndex](const UpdateActionImportantChanged& ic) + { + qCDebug(instanceProfileResolveC) + << instance->name() << "|" + << "UpdateImportantChanged" << component->getID() << ":" << component->getVersion() + << "was changed from" << ic.oldVersion << "updating linked components"; + auto oldVersion = + APPLICATION->metadataIndex()->loadVersionBlocking(component->getID(), ic.oldVersion); + for (auto oldReq : oldVersion->dependencies()) + { + auto currentlyRequired = component->m_cachedRequires.find(oldReq); + if (currentlyRequired == component->m_cachedRequires.cend()) + { + auto oldReqComp = componentIndex.find(oldReq.uid); + if (oldReqComp != componentIndex.cend()) + { + (*oldReqComp)->setUpdateAction(UpdateAction{ UpdateActionRemove{} }); + addedActions = true; + } + } + } + auto linked = collectTreeLinked(component->getID()); + for (auto comp : linked) + { + if (comp->isCustom()) + { + continue; + } + auto compUid = comp->getID(); + auto parentReq = std::find_if(component->m_cachedRequires.begin(), + component->m_cachedRequires.end(), + [compUid](const projt::meta::ComponentDependency& req) + { return req.uid == compUid; }); + if (parentReq != component->m_cachedRequires.end()) + { + auto newVersion = + parentReq->equalsVersion.isEmpty() ? parentReq->suggests : parentReq->equalsVersion; + if (!newVersion.isEmpty()) + { + comp->setUpdateAction(UpdateAction{ UpdateActionChangeVersion{ newVersion } }); + } + else + { + comp->setUpdateAction(UpdateAction{ UpdateActionLatestRecommendedCompatible{ + component->getID(), + component->getName(), + component->getVersion(), + } }); + } + } + else + { + comp->setUpdateAction(UpdateAction{ UpdateActionLatestRecommendedCompatible{ + component->getID(), + component->getName(), + component->getVersion(), + } }); + } + addedActions = true; + } + } + }; + std::visit(visitor, action); + component->clearUpdateAction(); + for (auto uid : toRemove) + { + d->m_profile->remove(uid); + } + } + } + while (addedActions); +} + +void ComponentUpdateTask::finalizeComponents() +{ + auto& components = d->m_profile->d->components; + auto& componentIndex = d->m_profile->d->componentIndex; + for (auto component : components) + { + for (auto req : component->m_cachedRequires) + { + auto found = componentIndex.find(req.uid); + if (found == componentIndex.cend()) + { + component->addComponentProblem( + ProblemSeverity::Error, + QObject::tr("%1 is missing requirement %2 %3") + .arg(component->getName(), + req.uid, + req.equalsVersion.isEmpty() ? req.suggests : req.equalsVersion)); + } + else + { + auto reqComp = *found; + if (!reqComp->getProblems().isEmpty()) + { + component->addComponentProblem( + reqComp->getProblemSeverity(), + QObject::tr("%1, a dependency of this component, has reported issues").arg(reqComp->getName())); + } + if (!req.equalsVersion.isEmpty() && req.equalsVersion != reqComp->getVersion()) + { + component->addComponentProblem( + ProblemSeverity::Error, + QObject::tr("%1, a dependency of this component, is not the required version %2") + .arg(reqComp->getName(), req.equalsVersion)); + } + else if (!req.suggests.isEmpty() && req.suggests != reqComp->getVersion()) + { + component->addComponentProblem( + ProblemSeverity::Warning, + QObject::tr("%1, a dependency of this component, is not the suggested version %2") + .arg(reqComp->getName(), req.suggests)); + } + } + } + for (auto conflict : component->knownConflictingComponents()) + { + auto found = componentIndex.find(conflict); + if (found != componentIndex.cend()) + { + auto foundComp = *found; + if (foundComp->isCustom()) + { + continue; + } + component->addComponentProblem( + ProblemSeverity::Warning, + QObject::tr("%1 and %2 are known to not work together. It is recommended to remove one of them.") + .arg(component->getName(), foundComp->getName())); + } + } + } +} + +void ComponentUpdateTask::remoteLoadSucceeded(size_t taskIndex) +{ + if (static_cast<size_t>(d->remoteLoadStatusList.size()) < taskIndex) + { + qCWarning(instanceProfileResolveC) << "Got task index outside of results" << taskIndex; + return; + } + auto& taskSlot = d->remoteLoadStatusList[taskIndex]; + disconnect(taskSlot.task.get(), &Task::succeeded, this, nullptr); + disconnect(taskSlot.task.get(), &Task::failed, this, nullptr); + disconnect(taskSlot.task.get(), &Task::aborted, this, nullptr); + if (taskSlot.finished) + { + qCWarning(instanceProfileResolveC) << "Got multiple results from remote load task" << taskIndex; + return; + } + qCDebug(instanceProfileResolveC) << "Remote task" << taskIndex << "succeeded"; + taskSlot.succeeded = true; + taskSlot.finished = true; + d->remoteTasksInProgress--; + // update the cached data of the component from the downloaded version file. + if (taskSlot.type == RemoteLoadStatus::Type::Version) + { + auto component = d->m_profile->getComponent(taskSlot.PackProfileIndex); + component->m_loaded = true; + component->updateCachedData(); + } + checkIfAllFinished(); +} + +void ComponentUpdateTask::remoteLoadFailed(size_t taskIndex, const QString& msg) +{ + if (static_cast<size_t>(d->remoteLoadStatusList.size()) < taskIndex) + { + qCWarning(instanceProfileResolveC) << "Got task index outside of results" << taskIndex; + return; + } + auto& taskSlot = d->remoteLoadStatusList[taskIndex]; + disconnect(taskSlot.task.get(), &Task::succeeded, this, nullptr); + disconnect(taskSlot.task.get(), &Task::failed, this, nullptr); + disconnect(taskSlot.task.get(), &Task::aborted, this, nullptr); + if (taskSlot.finished) + { + qCWarning(instanceProfileResolveC) << "Got multiple results from remote load task" << taskIndex; + return; + } + qCDebug(instanceProfileResolveC) << "Remote task" << taskIndex << "failed: " << msg; + d->remoteLoadSuccessful = false; + taskSlot.succeeded = false; + taskSlot.finished = true; + d->remoteTasksInProgress--; + checkIfAllFinished(); +} + +void ComponentUpdateTask::checkIfAllFinished() +{ + if (d->remoteTasksInProgress) + { + // not yet... + return; + } + if (d->remoteLoadSuccessful) + { + // nothing bad happened... clear the temp load status and proceed with looking at dependencies + d->remoteLoadStatusList.clear(); + performUpdateActions(); + // In online mode, resolve dependencies (add missing components) + // In offline mode, only check (no network to download new components) + resolveDependencies(d->netmode == Net::Mode::Offline); + } + else + { + // remote load failed... report error and bail + QStringList allErrorsList; + for (auto& item : d->remoteLoadStatusList) + { + if (!item.succeeded) + { + const ComponentPtr component = d->m_profile->getComponent(item.PackProfileIndex); + allErrorsList.append( + tr("Could not download metadata for %1 %2. Please change the version or try again later.") + .arg(component->getName(), component->m_version)); + } + } + auto allErrors = allErrorsList.join("\n"); + emitFailed( + tr("Component metadata update task failed while downloading from remote server:\n%1").arg(allErrors)); + d->remoteLoadStatusList.clear(); + } +} diff --git a/archived/projt-launcher/launcher/minecraft/ComponentUpdateTask.h b/archived/projt-launcher/launcher/minecraft/ComponentUpdateTask.h new file mode 100644 index 0000000000..f0df31dc4d --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/ComponentUpdateTask.h @@ -0,0 +1,62 @@ +// 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. + */ +#pragma once + +#include "minecraft/Component.h" +#include "net/Mode.h" +#include "tasks/Task.h" + +#include <memory> +class PackProfile; +struct ComponentUpdateTaskData; + +class ComponentUpdateTask : public Task +{ + Q_OBJECT + public: + enum class Mode + { + Launch, + Resolution + }; + + public: + explicit ComponentUpdateTask(Mode mode, Net::Mode netmode, PackProfile* list); + virtual ~ComponentUpdateTask(); + + protected: + void executeTask() override; + + private: + void loadComponents(); + /// collects components that are dependent on or dependencies of the component + QList<ComponentPtr> collectTreeLinked(const QString& uid); + void resolveDependencies(bool checkOnly); + void performUpdateActions(); + void finalizeComponents(); + + void remoteLoadSucceeded(size_t index); + void remoteLoadFailed(size_t index, const QString& msg); + void checkIfAllFinished(); + + private: + std::unique_ptr<ComponentUpdateTaskData> d; +}; diff --git a/archived/projt-launcher/launcher/minecraft/ComponentUpdateTask_p.h b/archived/projt-launcher/launcher/minecraft/ComponentUpdateTask_p.h new file mode 100644 index 0000000000..bcb139055c --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/ComponentUpdateTask_p.h @@ -0,0 +1,55 @@ +// 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. + */ +#pragma once + +#include <QList> +#include <QString> +#include <cstddef> +#include "net/Mode.h" +#include "tasks/Task.h" + +#include "minecraft/ComponentUpdateTask.h" + +class PackProfile; + +struct RemoteLoadStatus +{ + enum class Type + { + Index, + List, + Version + } type = Type::Version; + size_t PackProfileIndex = 0; + bool finished = false; + bool succeeded = false; + Task::Ptr task; +}; + +struct ComponentUpdateTaskData +{ + PackProfile* m_profile = nullptr; + QList<RemoteLoadStatus> remoteLoadStatusList; + bool remoteLoadSuccessful = true; + size_t remoteTasksInProgress = 0; + ComponentUpdateTask::Mode mode; + Net::Mode netmode; +}; diff --git a/archived/projt-launcher/launcher/minecraft/GradleSpecifier.h b/archived/projt-launcher/launcher/minecraft/GradleSpecifier.h new file mode 100644 index 0000000000..aeb3beda88 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/GradleSpecifier.h @@ -0,0 +1,219 @@ +// 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 Sefa Eyeoglu <contact@scrumplex.net> + * + * 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. + * + * ======================================================================== */ + +#pragma once + +#include <QRegularExpression> +#include <QString> +#include <QStringList> +#include "DefaultVariable.h" + +struct GradleSpecifier +{ + GradleSpecifier() + { + m_valid = false; + } + GradleSpecifier(QString value) + { + operator=(value); + } + GradleSpecifier& operator=(const QString& value) + { + /* + org.gradle.test.classifiers : service : 1.0 : jdk15 @ jar + 0 "org.gradle.test.classifiers:service:1.0:jdk15@jar" + 1 "org.gradle.test.classifiers" + 2 "service" + 3 "1.0" + 4 "jdk15" + 5 "jar" + */ + static const QRegularExpression s_matcher(QRegularExpression::anchoredPattern("([^:@]+):([^:@]+):([^:@]+)" + "(?::([^:@]+))?" + "(?:@([^:@]+))?")); + QRegularExpressionMatch match = s_matcher.match(value); + m_valid = match.hasMatch(); + if (!m_valid) + { + m_invalidValue = value; + return *this; + } + auto elements = match.captured(); + m_groupId = match.captured(1); + m_artifactId = match.captured(2); + m_version = match.captured(3); + m_classifier = match.captured(4); + if (match.lastCapturedIndex() >= 5) + { + m_extension = match.captured(5); + } + return *this; + } + QString serialize() const + { + if (!m_valid) + { + return m_invalidValue; + } + QString retval = m_groupId + ":" + m_artifactId + ":" + m_version; + if (!m_classifier.isEmpty()) + { + retval += ":" + m_classifier; + } + if (m_extension.isExplicit()) + { + retval += "@" + m_extension; + } + return retval; + } + QString getFileName() const + { + if (!m_valid) + { + return QString(); + } + QString filename = m_artifactId + '-' + m_version; + if (!m_classifier.isEmpty()) + { + filename += "-" + m_classifier; + } + filename += "." + m_extension; + return filename; + } + QString toPath(const QString& filenameOverride = QString()) const + { + if (!m_valid) + { + return QString(); + } + QString filename; + if (filenameOverride.isEmpty()) + { + filename = getFileName(); + } + else + { + filename = filenameOverride; + } + QString path = m_groupId; + path.replace('.', '/'); + path += '/' + m_artifactId + '/' + m_version + '/' + filename; + return path; + } + inline bool valid() const + { + return m_valid; + } + inline QString version() const + { + return m_version; + } + inline QString groupId() const + { + return m_groupId; + } + inline QString artifactId() const + { + return m_artifactId; + } + inline void setClassifier(const QString& classifier) + { + m_classifier = classifier; + } + inline QString classifier() const + { + return m_classifier; + } + inline QString extension() const + { + return m_extension; + } + inline QString artifactPrefix() const + { + return m_groupId + ":" + m_artifactId; + } + bool matchName(const GradleSpecifier& other) const + { + return other.artifactId() == artifactId() && other.groupId() == groupId() && other.classifier() == classifier(); + } + bool operator==(const GradleSpecifier& other) const + { + if (m_groupId != other.m_groupId) + return false; + if (m_artifactId != other.m_artifactId) + return false; + if (m_version != other.m_version) + return false; + if (m_classifier != other.m_classifier) + return false; + if (m_extension != other.m_extension) + return false; + return true; + } + + private: + QString m_invalidValue; + QString m_groupId; + QString m_artifactId; + QString m_version; + QString m_classifier; + DefaultVariable<QString> m_extension = DefaultVariable<QString>("jar"); + bool m_valid = false; +}; diff --git a/archived/projt-launcher/launcher/minecraft/LaunchProfile.cpp b/archived/projt-launcher/launcher/minecraft/LaunchProfile.cpp new file mode 100644 index 0000000000..5ad1ef19f3 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/LaunchProfile.cpp @@ -0,0 +1,461 @@ +// 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 Sefa Eyeoglu <contact@scrumplex.net> + * + * 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 "LaunchProfile.h" +#include <Version.h> + +void LaunchProfile::clear() +{ + m_minecraftVersion.clear(); + m_minecraftVersionType.clear(); + m_minecraftAssets.reset(); + m_minecraftArguments.clear(); + m_addnJvmArguments.clear(); + m_tweakers.clear(); + m_mainClass.clear(); + m_appletClass.clear(); + m_libraries.clear(); + m_mavenFiles.clear(); + m_agents.clear(); + m_traits.clear(); + m_jarMods.clear(); + m_mainJar.reset(); + m_problemSeverity = ProblemSeverity::None; +} + +static void applyString(const QString& from, QString& to) +{ + if (from.isEmpty()) + return; + to = from; +} + +void LaunchProfile::applyMinecraftVersion(const QString& id) +{ + applyString(id, this->m_minecraftVersion); +} + +void LaunchProfile::applyAppletClass(const QString& appletClass) +{ + applyString(appletClass, this->m_appletClass); +} + +void LaunchProfile::applyMainClass(const QString& mainClass) +{ + applyString(mainClass, this->m_mainClass); +} + +void LaunchProfile::applyMinecraftArguments(const QString& minecraftArguments) +{ + applyString(minecraftArguments, this->m_minecraftArguments); +} + +void LaunchProfile::applyAddnJvmArguments(const QStringList& addnJvmArguments) +{ + this->m_addnJvmArguments.append(addnJvmArguments); +} + +void LaunchProfile::applyMinecraftVersionType(const QString& type) +{ + applyString(type, this->m_minecraftVersionType); +} + +void LaunchProfile::applyMinecraftAssets(MojangAssetIndexInfo::Ptr assets) +{ + if (assets) + { + m_minecraftAssets = assets; + } +} + +void LaunchProfile::applyTraits(const QSet<QString>& traits) +{ + this->m_traits.unite(traits); +} + +void LaunchProfile::applyTweakers(const QStringList& tweakers) +{ + // if the applied tweakers override an existing one, skip it. this effectively moves it later in the sequence + QStringList newTweakers; + for (auto& tweaker : m_tweakers) + { + if (tweakers.contains(tweaker)) + { + continue; + } + newTweakers.append(tweaker); + } + // then just append the new tweakers (or moved original ones) + newTweakers += tweakers; + m_tweakers = newTweakers; +} + +void LaunchProfile::applyJarMods(const QList<LibraryPtr>& jarMods) +{ + this->m_jarMods.append(jarMods); +} + +static int findLibraryByName(QList<LibraryPtr>* haystack, const GradleSpecifier& needle) +{ + int retval = -1; + for (int i = 0; i < haystack->size(); ++i) + { + if (haystack->at(i)->rawName().matchName(needle)) + { + // only one is allowed. + if (retval != -1) + return -1; + retval = i; + } + } + return retval; +} + +void LaunchProfile::applyMods(const QList<LibraryPtr>& mods) +{ + QList<LibraryPtr>* list = &m_mods; + for (auto& mod : mods) + { + auto modCopy = Library::limitedCopy(mod); + + // find the mod by name. + const int index = findLibraryByName(list, mod->rawName()); + // mod not found? just add it. + if (index < 0) + { + list->append(modCopy); + return; + } + + auto existingLibrary = list->at(index); + // if we are higher it means we should update + if (Version(mod->version()) > Version(existingLibrary->version())) + { + list->replace(index, modCopy); + } + } +} + +void LaunchProfile::applyCompatibleJavaMajors(QList<int>& javaMajor) +{ + m_compatibleJavaMajors.append(javaMajor); +} + +void LaunchProfile::applyCompatibleJavaName(QString javaName) +{ + if (!javaName.isEmpty()) + m_compatibleJavaName = javaName; +} + +void LaunchProfile::applyLibrary(LibraryPtr library, const RuntimeContext& runtimeContext) +{ + if (!library->isActive(runtimeContext)) + { + return; + } + + QList<LibraryPtr>* list = &m_libraries; + if (library->isNative()) + { + list = &m_nativeLibraries; + } + + auto libraryCopy = Library::limitedCopy(library); + + // find the library by name. + const int index = findLibraryByName(list, library->rawName()); + // library not found? just add it. + if (index < 0) + { + list->append(libraryCopy); + return; + } + + auto existingLibrary = list->at(index); + // if we are higher it means we should update + if (Version(library->version()) > Version(existingLibrary->version())) + { + list->replace(index, libraryCopy); + } +} + +void LaunchProfile::applyMavenFile(LibraryPtr mavenFile, const RuntimeContext& runtimeContext) +{ + if (!mavenFile->isActive(runtimeContext)) + { + return; + } + + if (mavenFile->isNative()) + { + return; + } + + // unlike libraries, we do not keep only one version or try to dedupe them + m_mavenFiles.append(Library::limitedCopy(mavenFile)); +} + +void LaunchProfile::applyAgent(AgentPtr agent, const RuntimeContext& runtimeContext) +{ + auto lib = agent->library(); + if (!lib->isActive(runtimeContext)) + { + return; + } + + if (lib->isNative()) + { + return; + } + + m_agents.append(agent); +} + +const LibraryPtr LaunchProfile::getMainJar() const +{ + return m_mainJar; +} + +void LaunchProfile::applyMainJar(LibraryPtr jar) +{ + if (jar) + { + m_mainJar = jar; + } +} + +void LaunchProfile::applyProblemSeverity(ProblemSeverity severity) +{ + if (m_problemSeverity < severity) + { + m_problemSeverity = severity; + } +} + +const QList<PatchProblem> LaunchProfile::getProblems() const +{ + QList<PatchProblem> problems; + + // Check for critical configuration issues + if (m_mainClass.isEmpty() && m_appletClass.isEmpty()) + { + problems.append({ ProblemSeverity::Error, QObject::tr("No main class or applet class specified") }); + } + + if (m_minecraftVersion.isEmpty()) + { + problems.append({ ProblemSeverity::Error, QObject::tr("Minecraft version is not specified") }); + } + + if (m_minecraftArguments.isEmpty() && m_minecraftVersionType != "snapshot" && m_minecraftVersionType != "old_alpha") + { + problems.append({ ProblemSeverity::Warning, QObject::tr("No game arguments specified (may be intentional)") }); + } + + // Check for missing main jar + if (!m_mainJar) + { + problems.append({ ProblemSeverity::Error, QObject::tr("Main jar file is missing") }); + } + + // Check if there are any libraries at all + if (m_minecraftArguments.isEmpty() && m_minecraftVersionType != "snapshot" && m_minecraftVersionType != "old_alpha") + { + problems.append( + { ProblemSeverity::Warning, QObject::tr("No libraries specified (unusual for modern Minecraft)") }); + } + + return problems; +} + +QString LaunchProfile::getMinecraftVersion() const +{ + return m_minecraftVersion; +} + +QString LaunchProfile::getAppletClass() const +{ + return m_appletClass; +} + +QString LaunchProfile::getMainClass() const +{ + return m_mainClass; +} + +const QSet<QString>& LaunchProfile::getTraits() const +{ + return m_traits; +} + +const QStringList& LaunchProfile::getTweakers() const +{ + return m_tweakers; +} + +bool LaunchProfile::hasTrait(const QString& trait) const +{ + return m_traits.contains(trait); +} + +ProblemSeverity LaunchProfile::getProblemSeverity() const +{ + return m_problemSeverity; +} + +QString LaunchProfile::getMinecraftVersionType() const +{ + return m_minecraftVersionType; +} + +std::shared_ptr<MojangAssetIndexInfo> LaunchProfile::getMinecraftAssets() const +{ + if (!m_minecraftAssets) + { + return std::make_shared<MojangAssetIndexInfo>("legacy"); + } + return m_minecraftAssets; +} + +QString LaunchProfile::getMinecraftArguments() const +{ + return m_minecraftArguments; +} + +const QStringList& LaunchProfile::getAddnJvmArguments() const +{ + return m_addnJvmArguments; +} + +const QList<LibraryPtr>& LaunchProfile::getJarMods() const +{ + return m_jarMods; +} + +const QList<LibraryPtr>& LaunchProfile::getLibraries() const +{ + return m_libraries; +} + +const QList<LibraryPtr>& LaunchProfile::getNativeLibraries() const +{ + return m_nativeLibraries; +} + +const QList<LibraryPtr>& LaunchProfile::getMavenFiles() const +{ + return m_mavenFiles; +} + +const QList<AgentPtr>& LaunchProfile::getAgents() const +{ + return m_agents; +} + +const QList<int>& LaunchProfile::getCompatibleJavaMajors() const +{ + return m_compatibleJavaMajors; +} + +const QString LaunchProfile::getCompatibleJavaName() const +{ + return m_compatibleJavaName; +} + +void LaunchProfile::getLibraryFiles(const RuntimeContext& runtimeContext, + QStringList& jars, + QStringList& nativeJars, + const QString& overridePath, + const QString& moddedJarSearchResultPath) const +{ + QStringList native32, native64; + jars.clear(); + nativeJars.clear(); + for (auto lib : getLibraries()) + { + lib->getApplicableFiles(runtimeContext, jars, nativeJars, native32, native64, overridePath); + } + // NOTE: order is important here, add main jar last to the lists + if (m_mainJar) + { + // NOTE: If we have jar mods, we use the modified jar allocated by ModMinecraftJar step + // The modified jar is expected to be named "minecraft.jar" in the search path. + if (m_jarMods.size() && !moddedJarSearchResultPath.isEmpty()) + { + QDir moddedJarDir(moddedJarSearchResultPath); + jars.append(moddedJarDir.absoluteFilePath("minecraft.jar")); + } + else + { + m_mainJar->getApplicableFiles(runtimeContext, jars, nativeJars, native32, native64, overridePath); + } + } + for (auto lib : getNativeLibraries()) + { + lib->getApplicableFiles(runtimeContext, jars, nativeJars, native32, native64, overridePath); + } + if (runtimeContext.javaArchitecture == "32") + { + nativeJars.append(native32); + } + else if (runtimeContext.javaArchitecture == "64") + { + nativeJars.append(native64); + } +} diff --git a/archived/projt-launcher/launcher/minecraft/LaunchProfile.h b/archived/projt-launcher/launcher/minecraft/LaunchProfile.h new file mode 100644 index 0000000000..f281e8797f --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/LaunchProfile.h @@ -0,0 +1,184 @@ +// 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 Sefa Eyeoglu <contact@scrumplex.net> + * + * 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. + * + * ======================================================================== */ + +#pragma once +#include <ProblemProvider.h> +#include <QString> +#include "Agent.h" +#include "Library.h" + +class LaunchProfile : public ProblemProvider +{ + public: + virtual ~LaunchProfile() + {} + + public: /* application of profile variables from patches */ + void applyMinecraftVersion(const QString& id); + void applyMainClass(const QString& mainClass); + void applyAppletClass(const QString& appletClass); + void applyMinecraftArguments(const QString& minecraftArguments); + void applyAddnJvmArguments(const QStringList& minecraftArguments); + void applyMinecraftVersionType(const QString& type); + void applyMinecraftAssets(MojangAssetIndexInfo::Ptr assets); + void applyTraits(const QSet<QString>& traits); + void applyTweakers(const QStringList& tweakers); + void applyJarMods(const QList<LibraryPtr>& jarMods); + void applyMods(const QList<LibraryPtr>& jarMods); + void applyLibrary(LibraryPtr library, const RuntimeContext& runtimeContext); + void applyMavenFile(LibraryPtr library, const RuntimeContext& runtimeContext); + void applyAgent(AgentPtr agent, const RuntimeContext& runtimeContext); + void applyCompatibleJavaMajors(QList<int>& javaMajor); + void applyCompatibleJavaName(QString javaName); + void applyMainJar(LibraryPtr jar); + void applyProblemSeverity(ProblemSeverity severity); + /// clear the profile + void clear(); + + public: /* getters for profile variables */ + QString getMinecraftVersion() const; + QString getMainClass() const; + QString getAppletClass() const; + QString getMinecraftVersionType() const; + MojangAssetIndexInfo::Ptr getMinecraftAssets() const; + QString getMinecraftArguments() const; + const QStringList& getAddnJvmArguments() const; + const QSet<QString>& getTraits() const; + const QStringList& getTweakers() const; + const QList<LibraryPtr>& getJarMods() const; + const QList<LibraryPtr>& getLibraries() const; + const QList<LibraryPtr>& getNativeLibraries() const; + const QList<LibraryPtr>& getMavenFiles() const; + const QList<AgentPtr>& getAgents() const; + const QList<int>& getCompatibleJavaMajors() const; + const QString getCompatibleJavaName() const; + const LibraryPtr getMainJar() const; + void getLibraryFiles(const RuntimeContext& runtimeContext, + QStringList& jars, + QStringList& nativeJars, + const QString& overridePath, + const QString& moddedJarSearchResultPath) const; + bool hasTrait(const QString& trait) const; + ProblemSeverity getProblemSeverity() const override; + const QList<PatchProblem> getProblems() const override; + + private: + /// the version of Minecraft - jar to use + QString m_minecraftVersion; + + /// Release type - "release" or "snapshot" + QString m_minecraftVersionType; + + /// Assets type - "legacy" or a version ID + MojangAssetIndexInfo::Ptr m_minecraftAssets; + + /** + * arguments that should be used for launching minecraft + * + * ex: "--username ${auth_player_name} --session ${auth_session} + * --version ${version_name} --gameDir ${game_directory} --assetsDir ${game_assets}" + */ + QString m_minecraftArguments; + + /** + * Additional arguments to pass to the JVM in addition to those the user has configured, + * memory settings, etc. + */ + QStringList m_addnJvmArguments; + + /// A list of all tweaker classes + QStringList m_tweakers; + + /// The main class to load first + QString m_mainClass; + + /// The applet class, for some very old minecraft releases + QString m_appletClass; + + /// the list of libraries + QList<LibraryPtr> m_libraries; + + /// the list of maven files to be placed in the libraries folder, but not acted upon + QList<LibraryPtr> m_mavenFiles; + + /// the list of java agents to add to JVM arguments + QList<AgentPtr> m_agents; + + /// the main jar + LibraryPtr m_mainJar; + + /// the list of native libraries + QList<LibraryPtr> m_nativeLibraries; + + /// traits, collected from all the version files (version files can only add) + QSet<QString> m_traits; + + /// A list of jar mods. version files can add those. + QList<LibraryPtr> m_jarMods; + + /// the list of mods + QList<LibraryPtr> m_mods; + + /// compatible java major versions + QList<int> m_compatibleJavaMajors; + + QString m_compatibleJavaName; + + ProblemSeverity m_problemSeverity = ProblemSeverity::None; +}; diff --git a/archived/projt-launcher/launcher/minecraft/Library.cpp b/archived/projt-launcher/launcher/minecraft/Library.cpp new file mode 100644 index 0000000000..551f572cc8 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/Library.cpp @@ -0,0 +1,511 @@ +// 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 Sefa Eyeoglu <contact@scrumplex.net> + * + * 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 "Library.h" +#include "MinecraftInstance.h" +#include "net/NetRequest.h" + +#include <BuildConfig.h> +#include <FileSystem.h> +#include <net/ApiDownload.h> +#include <net/ChecksumValidator.h> +#include <QUrl> + +static QString normalizeNeoForgedMavenUrl(const QString& url) +{ + const QUrl parsed(url); + if (!parsed.isValid() || parsed.host().compare("maven.neoforged.net", Qt::CaseInsensitive) != 0) + { + return url; + } + + const auto path = parsed.path(); + if (!path.startsWith("/net/neoforged/") || path.startsWith("/releases/")) + { + return url; + } + + QUrl fixed(parsed); + fixed.setPath("/releases" + path); + return fixed.toString(QUrl::FullyEncoded); +} + +/** + * @brief Collect applicable files for the library. + * + * Depending on whether the library is native or not, it adds paths to the + * appropriate lists for jar files, native libraries for 32-bit, and native + * libraries for 64-bit. + * + * @param runtimeContext The current runtime context. + * @param jar List to store paths for jar files. + * @param native List to store paths for native libraries. + * @param native32 List to store paths for 32-bit native libraries. + * @param native64 List to store paths for 64-bit native libraries. + * @param overridePath Optional path to override the default storage path. + */ +void Library::getApplicableFiles(const RuntimeContext& runtimeContext, + QStringList& jar, + QStringList& native, + QStringList& native32, + QStringList& native64, + const QString& overridePath) const +{ + bool local = isLocal(); + // Lambda function to get the absolute file path + auto actualPath = [this, local, overridePath](QString relPath) + { + relPath = FS::RemoveInvalidPathChars(relPath); + QFileInfo out(FS::PathCombine(storagePrefix(), relPath)); + if (local && !overridePath.isEmpty()) + { + QString fileName = out.fileName(); + return QFileInfo(FS::PathCombine(overridePath, fileName)).absoluteFilePath(); + } + return out.absoluteFilePath(); + }; + + QString raw_storage = storageSuffix(runtimeContext); + if (isNative()) + { + if (raw_storage.contains("${arch}")) + { + auto nat32Storage = raw_storage; + nat32Storage.replace("${arch}", "32"); + auto nat64Storage = raw_storage; + nat64Storage.replace("${arch}", "64"); + native32 += actualPath(nat32Storage); + native64 += actualPath(nat64Storage); + } + else + { + native += actualPath(raw_storage); + } + } + else + { + jar += actualPath(raw_storage); + } +} + +/** + * @brief Get download requests for the library files. + * + * Depending on whether the library is native or not, and the current runtime context, + * this function prepares download requests for the necessary files. It handles both local + * and remote files, checks for stale cache entries, and adds checksummed downloads. + * + * @param runtimeContext The current runtime context. + * @param cache Pointer to the HTTP meta cache. + * @param failedLocalFiles List to store paths for failed local files. + * @param overridePath Optional path to override the default storage path. + * @return QList<Net::NetRequest::Ptr> List of download requests. + */ +QList<Net::NetRequest::Ptr> Library::getDownloads(const RuntimeContext& runtimeContext, + class HttpMetaCache* cache, + QStringList& failedLocalFiles, + const QString& overridePath) const +{ + QList<Net::NetRequest::Ptr> out; + bool stale = isAlwaysStale(); + bool local = isLocal(); + + // Lambda function to check if a local file exists + auto check_local_file = [overridePath, &failedLocalFiles](QString storage) + { + QFileInfo fileinfo(storage); + QString fileName = fileinfo.fileName(); + auto fullPath = FS::PathCombine(overridePath, fileName); + QFileInfo localFileInfo(fullPath); + if (!localFileInfo.exists()) + { + failedLocalFiles.append(localFileInfo.filePath()); + return false; + } + return true; + }; + + // Lambda function to add a download request + auto add_download = [this, local, check_local_file, cache, stale, &out](QString storage, QString url, QString sha1) + { + if (local) + { + return check_local_file(storage); + } + url = normalizeNeoForgedMavenUrl(url); + auto entry = cache->resolveEntry("libraries", storage); + if (stale) + { + entry->setStale(true); + } + if (!entry->isStale()) + return true; + Net::Download::Options options; + if (stale) + { + options |= Net::Download::Option::AcceptLocalFiles; + } + + // Don't add a time limit for the libraries cache entry validity + options |= Net::Download::Option::MakeEternal; + + if (sha1.size()) + { + auto dl = Net::ApiDownload::makeCached(url, entry, options); + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, sha1)); + qDebug() << "Checksummed Download for:" << rawName().serialize() << "storage:" << storage << "url:" << url; + out.append(dl); + } + else + { + out.append(Net::ApiDownload::makeCached(url, entry, options)); + qDebug() << "Download for:" << rawName().serialize() << "storage:" << storage << "url:" << url; + } + return true; + }; + + QString raw_storage = storageSuffix(runtimeContext); + if (m_mojangDownloads) + { + if (isNative()) + { + auto nativeClassifier = getCompatibleNative(runtimeContext); + if (!nativeClassifier.isNull()) + { + if (nativeClassifier.contains("${arch}")) + { + auto nat32Classifier = nativeClassifier; + nat32Classifier.replace("${arch}", "32"); + auto nat64Classifier = nativeClassifier; + nat64Classifier.replace("${arch}", "64"); + auto nat32info = m_mojangDownloads->getDownloadInfo(nat32Classifier); + if (nat32info) + { + auto cooked_storage = raw_storage; + cooked_storage.replace("${arch}", "32"); + add_download(cooked_storage, nat32info->url, nat32info->sha1); + } + auto nat64info = m_mojangDownloads->getDownloadInfo(nat64Classifier); + if (nat64info) + { + auto cooked_storage = raw_storage; + cooked_storage.replace("${arch}", "64"); + add_download(cooked_storage, nat64info->url, nat64info->sha1); + } + } + else + { + auto info = m_mojangDownloads->getDownloadInfo(nativeClassifier); + if (info) + { + add_download(raw_storage, info->url, info->sha1); + } + } + } + else + { + qDebug() << "Ignoring native library" << m_name.serialize() + << "because it has no classifier for current OS"; + } + } + else + { + if (m_mojangDownloads->artifact) + { + auto artifact = m_mojangDownloads->artifact; + add_download(raw_storage, artifact->url, artifact->sha1); + } + else + { + qDebug() << "Ignoring java library" << m_name.serialize() << "because it has no artifact"; + } + } + } + else + { + auto raw_dl = [this, raw_storage]() + { + if (!m_absoluteURL.isEmpty()) + { + return m_absoluteURL; + } + + if (m_repositoryURL.isEmpty()) + { + return BuildConfig.LIBRARY_BASE + raw_storage; + } + + if (m_repositoryURL.endsWith('/')) + { + return m_repositoryURL + raw_storage; + } + else + { + return m_repositoryURL + QChar('/') + raw_storage; + } + }(); + if (raw_storage.contains("${arch}")) + { + QString cooked_storage = raw_storage; + QString cooked_dl = raw_dl; + add_download(cooked_storage.replace("${arch}", "32"), cooked_dl.replace("${arch}", "32"), QString()); + cooked_storage = raw_storage; + cooked_dl = raw_dl; + add_download(cooked_storage.replace("${arch}", "64"), cooked_dl.replace("${arch}", "64"), QString()); + } + else + { + add_download(raw_storage, raw_dl, QString()); + } + } + return out; +} + +/** + * @brief Check if the library is active in the given runtime context. + * + * This function evaluates rules to determine if the library should be active, + * considering both general rules and native compatibility. + * + * @param runtimeContext The current runtime context. + * @return bool True if the library is active, false otherwise. + */ +bool Library::isActive(const RuntimeContext& runtimeContext) const +{ + bool result = true; + if (m_rules.empty()) + { + result = true; + } + else + { + Rule::Action ruleResult = Rule::Disallow; + for (auto rule : m_rules) + { + Rule::Action temp = rule.apply(runtimeContext); + if (temp != Rule::Defer) + ruleResult = temp; + } + result = result && (ruleResult == Rule::Allow); + } + if (isNative()) + { + result = result && !getCompatibleNative(runtimeContext).isNull(); + } + return result; +} + +/** + * @brief Check if the library is considered local. + * + * @return bool True if the library is local, false otherwise. + */ +bool Library::isLocal() const +{ + return m_hint == "local"; +} + +/** + * @brief Check if the library is always considered stale. + * + * @return bool True if the library is always stale, false otherwise. + */ +bool Library::isAlwaysStale() const +{ + return m_hint == "always-stale"; +} + +/** + * @brief Get the compatible native classifier for the current runtime context. + * + * This function attempts to match the current runtime context with the appropriate + * native classifier. + * + * @param runtimeContext The current runtime context. + * @return QString The compatible native classifier, or an empty string if none is found. + */ +QString Library::getCompatibleNative(const RuntimeContext& runtimeContext) const +{ + // try to match precise classifier "[os]-[arch]" + auto entry = m_nativeClassifiers.constFind(runtimeContext.getClassifier()); + // try to match imprecise classifier on legacy architectures "[os]" + if (entry == m_nativeClassifiers.constEnd() && runtimeContext.isLegacyArch()) + entry = m_nativeClassifiers.constFind(runtimeContext.system); + + if (entry == m_nativeClassifiers.constEnd()) + return QString(); + + return entry.value(); +} + +/** + * @brief Set the storage prefix for the library. + * + * @param prefix The storage prefix to set. + */ +void Library::setStoragePrefix(QString prefix) +{ + m_storagePrefix = prefix; +} + +/** + * @brief Get the default storage prefix for libraries. + * + * @return QString The default storage prefix. + */ +QString Library::defaultStoragePrefix() +{ + return "libraries/"; +} + +/** + * @brief Get the current storage prefix for the library. + * + * @return QString The current storage prefix. + */ +QString Library::storagePrefix() const +{ + if (m_storagePrefix.isEmpty()) + { + return defaultStoragePrefix(); + } + return m_storagePrefix; +} + +/** + * @brief Get the filename for the library in the current runtime context. + * + * This function determines the appropriate filename for the library, taking into + * account native classifiers if applicable. + * + * @param runtimeContext The current runtime context. + * @return QString The filename of the library. + */ +QString Library::filename(const RuntimeContext& runtimeContext) const +{ + if (!m_filename.isEmpty()) + { + return m_filename; + } + // non-native? use only the gradle specifier + if (!isNative()) + { + return m_name.getFileName(); + } + + // otherwise native, override classifiers. Mojang HACK! + GradleSpecifier nativeSpec = m_name; + QString nativeClassifier = getCompatibleNative(runtimeContext); + if (!nativeClassifier.isNull()) + { + nativeSpec.setClassifier(nativeClassifier); + } + else + { + nativeSpec.setClassifier("INVALID"); + } + return nativeSpec.getFileName(); +} + +/** + * @brief Get the display name for the library in the current runtime context. + * + * This function returns the display name for the library, defaulting to the filename + * if no display name is set. + * + * @param runtimeContext The current runtime context. + * @return QString The display name of the library. + */ +QString Library::displayName(const RuntimeContext& runtimeContext) const +{ + if (!m_displayname.isEmpty()) + return m_displayname; + return filename(runtimeContext); +} + +/** + * @brief Get the storage suffix for the library in the current runtime context. + * + * This function determines the appropriate storage suffix for the library, taking into + * account native classifiers if applicable. + * + * @param runtimeContext The current runtime context. + * @return QString The storage suffix of the library. + */ +QString Library::storageSuffix(const RuntimeContext& runtimeContext) const +{ + // non-native? use only the gradle specifier + if (!isNative()) + { + return m_name.toPath(m_filename); + } + + // otherwise native, override classifiers. Mojang HACK! + GradleSpecifier nativeSpec = m_name; + QString nativeClassifier = getCompatibleNative(runtimeContext); + if (!nativeClassifier.isNull()) + { + nativeSpec.setClassifier(nativeClassifier); + } + else + { + nativeSpec.setClassifier("INVALID"); + } + return nativeSpec.toPath(m_filename); +} diff --git a/archived/projt-launcher/launcher/minecraft/Library.h b/archived/projt-launcher/launcher/minecraft/Library.h new file mode 100644 index 0000000000..2a42df6bff --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/Library.h @@ -0,0 +1,285 @@ +// 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 Sefa Eyeoglu <contact@scrumplex.net> + * + * 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. + * + * ======================================================================== */ + +#pragma once +#include <QDir> +#include <QList> +#include <QMap> +#include <QPair> +#include <QString> +#include <QStringList> +#include <QUrl> +#include <memory> + +#include "GradleSpecifier.h" +#include "MojangDownloadInfo.h" +#include "Rule.h" +#include "RuntimeContext.h" +#include "net/NetRequest.h" + +class Library; +class MinecraftInstance; + +using LibraryPtr = std::shared_ptr<Library>; + +class Library +{ + friend class OneSixVersionFormat; + friend class MojangVersionFormat; + friend class LibraryTest; + + public: + Library() + {} + Library(const QString& name) + { + m_name = name; + } + /// Create a copy of the library. + static LibraryPtr limitedCopy(LibraryPtr base) + { + auto newlib = std::make_shared<Library>(); + newlib->m_name = base->m_name; + newlib->m_repositoryURL = base->m_repositoryURL; + newlib->m_hint = base->m_hint; + newlib->m_absoluteURL = base->m_absoluteURL; + newlib->m_extractExcludes = base->m_extractExcludes; + newlib->m_nativeClassifiers = base->m_nativeClassifiers; + newlib->m_rules = base->m_rules; + newlib->m_storagePrefix = base->m_storagePrefix; + newlib->m_mojangDownloads = base->m_mojangDownloads; + newlib->m_filename = base->m_filename; + newlib->m_displayname = base->m_displayname; // Full copy + return newlib; + } + + public: /* methods */ + /// Returns the raw name field + const GradleSpecifier& rawName() const + { + return m_name; + } + + void setRawName(const GradleSpecifier& spec) + { + m_name = spec; + } + + void setClassifier(const QString& spec) + { + m_name.setClassifier(spec); + } + + /// returns the full group and artifact prefix + QString artifactPrefix() const + { + return m_name.artifactPrefix(); + } + + /// get the artifact ID + QString artifactId() const + { + return m_name.artifactId(); + } + + /// get the artifact version + QString version() const + { + return m_name.version(); + } + + /// Returns true if the library is native + bool isNative() const + { + return m_nativeClassifiers.size() != 0; + } + + void setStoragePrefix(QString prefix = QString()); + + /// Set the url base for downloads + void setRepositoryURL(const QString& base_url) + { + m_repositoryURL = base_url; + } + + void getApplicableFiles(const RuntimeContext& runtimeContext, + QStringList& jar, + QStringList& native, + QStringList& native32, + QStringList& native64, + const QString& overridePath) const; + + void setAbsoluteUrl(const QString& absolute_url) + { + m_absoluteURL = absolute_url; + } + + void setFilename(const QString& filename) + { + m_filename = filename; + } + + /// Get the file name of the library + QString filename(const RuntimeContext& runtimeContext) const; + + // NOTE: Legacy support for Core Mods display cutoff (approx 1.4.7 era). Used by jar mods only + void setDisplayName(const QString& displayName) + { + m_displayname = displayName; + } + + /// Get the file name of the library + QString displayName(const RuntimeContext& runtimeContext) const; + + void setMojangDownloadInfo(MojangLibraryDownloadInfo::Ptr info) + { + m_mojangDownloads = info; + } + + void setHint(const QString& hint) + { + m_hint = hint; + } + + /// Set the load rules + void setRules(QList<Rule> rules) + { + m_rules = rules; + } + + /// Returns true if the library should be loaded (or extracted, in case of natives) + bool isActive(const RuntimeContext& runtimeContext) const; + + /// Returns true if the library is contained in an instance and false if it is shared + bool isLocal() const; + + /// Returns true if the library is to always be checked for updates + bool isAlwaysStale() const; + + /// Return true if the library requires forge XZ hacks + bool isForge() const; + + // Get a list of downloads for this library + QList<Net::NetRequest::Ptr> getDownloads(const RuntimeContext& runtimeContext, + class HttpMetaCache* cache, + QStringList& failedLocalFiles, + const QString& overridePath) const; + + QString getCompatibleNative(const RuntimeContext& runtimeContext) const; + + private: /* methods */ + /// the default storage prefix used by ProjT Launcher + static QString defaultStoragePrefix(); + + /// Get the prefix - root of the storage to be used + QString storagePrefix() const; + + /// Get the relative file path where the library should be saved + QString storageSuffix(const RuntimeContext& runtimeContext) const; + + QString hint() const + { + return m_hint; + } + + protected: /* data */ + /// the basic gradle dependency specifier. + GradleSpecifier m_name; + + /// DEPRECATED URL prefix of the maven repo where the file can be downloaded + QString m_repositoryURL; + + /// DEPRECATED: ProjT Launcher-specific absolute URL. takes precedence over the implicit maven repo URL, if defined + QString m_absoluteURL; + + /// ProjT Launcher extension - filename override + QString m_filename; + + /// DEPRECATED ProjT Launcher extension - display name + QString m_displayname; + + /** + * ProjT Launcher-specific type hint - modifies how the library is treated + */ + QString m_hint; + + /** + * storage - by default the local libraries folder in ProjT Launcher, but could be elsewhere + * ProjT Launcher specific, because of FTB. + */ + QString m_storagePrefix; + + /// true if the library had an extract/excludes section (even empty) + bool m_hasExcludes = false; + + /// a list of files that shouldn't be extracted from the library + QStringList m_extractExcludes; + + /// native suffixes per OS + QMap<QString, QString> m_nativeClassifiers; + + /// true if the library had a rules section (even empty) + bool applyRules = false; + + /// rules associated with the library + QList<Rule> m_rules; + + /// MOJANG: container with Mojang style download info + MojangLibraryDownloadInfo::Ptr m_mojangDownloads; +}; diff --git a/archived/projt-launcher/launcher/minecraft/Logging.cpp b/archived/projt-launcher/launcher/minecraft/Logging.cpp new file mode 100644 index 0000000000..63862cce77 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/Logging.cpp @@ -0,0 +1,48 @@ +// 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) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#include "minecraft/Logging.h" +#include <qloggingcategory.h> + +Q_LOGGING_CATEGORY(instanceProfileC, "launcher.instance.profile") +Q_LOGGING_CATEGORY(instanceProfileResolveC, "launcher.instance.profile.resolve") +Q_LOGGING_CATEGORY(authCredentials, "launcher.auth.credentials") diff --git a/archived/projt-launcher/launcher/minecraft/Logging.h b/archived/projt-launcher/launcher/minecraft/Logging.h new file mode 100644 index 0000000000..02aa363346 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/Logging.h @@ -0,0 +1,49 @@ +// 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) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#pragma once + +#include <QLoggingCategory> + +Q_DECLARE_LOGGING_CATEGORY(instanceProfileC) +Q_DECLARE_LOGGING_CATEGORY(instanceProfileResolveC) +Q_DECLARE_LOGGING_CATEGORY(authCredentials) diff --git a/archived/projt-launcher/launcher/minecraft/MinecraftInstance.cpp b/archived/projt-launcher/launcher/minecraft/MinecraftInstance.cpp new file mode 100644 index 0000000000..29e44954ad --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/MinecraftInstance.cpp @@ -0,0 +1,1430 @@ +// 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 Sefa Eyeoglu <contact@scrumplex.net> + * Copyright (C) 2022 Jamie Mansfield <jmansfield@cadixdev.org> + * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me> + * + * 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 "MinecraftInstance.h" +#include "LaunchMode.h" +#include "Application.h" +#include "BuildConfig.h" +#include "Commandline.h" +#include "Json.h" +#include "MinecraftInstanceLaunchMenu.h" +#include "QObjectPtr.h" +#include "minecraft/launch/AutoInstallJava.hpp" +#include "minecraft/launch/CreateGameFolders.hpp" +#include "minecraft/launch/ExtractNatives.hpp" +#include "minecraft/launch/PrintInstanceInfo.hpp" +#include "minecraft/update/AssetUpdateTask.h" +#include "minecraft/update/FMLLibrariesTask.h" +#include "minecraft/update/LibrariesTask.h" +#include "settings/Setting.h" +#include "settings/SettingsObject.h" + +#include "FileSystem.h" +#include "MMCTime.h" +#include "java/core/RuntimeVersion.hpp" + +#include "launch/LaunchPipeline.hpp" +#include "launch/TaskBridgeStage.hpp" +#include "launch/steps/RuntimeProbeStep.hpp" +#include "launch/steps/ServerJoinResolveStep.hpp" +#include "launch/steps/LaunchCommandStep.hpp" +#include "launch/steps/QuitAfterGameStep.hpp" +#include "launch/steps/LogMessageStep.hpp" + +#include "minecraft/launch/ClaimAccount.hpp" +#include "minecraft/launch/LauncherPartLaunch.hpp" +#include "minecraft/launch/ModMinecraftJar.hpp" +#include "minecraft/launch/ReconstructAssets.hpp" +#include "minecraft/launch/ScanModFolders.hpp" +#include "minecraft/launch/VerifyJavaInstall.hpp" + +#include "java/services/RuntimeEnvironment.hpp" + +#include "icons/IconList.hpp" + +#include "mod/ModFolderModel.hpp" +#include "mod/ResourcePackFolderModel.hpp" +#include "mod/ShaderPackFolderModel.hpp" +#include "mod/TexturePackFolderModel.hpp" + +#include "WorldList.h" + +#include "AssetsUtils.h" +#include "MinecraftLoadAndCheck.h" +#include "PackProfile.h" +#include "minecraft/update/FoldersTask.h" + +#include "tools/BaseProfiler.h" + +#include <QActionGroup> +#include <QMainWindow> +#include <QScreen> +#include <QWindow> + +#ifdef Q_OS_LINUX +#include "MangoHud.h" +#endif + +#ifdef WITH_QTDBUS +#include <QtDBus/QtDBus> +#endif + +#define IBUS "@im=ibus" + +[[maybe_unused]] static bool switcherooSetupGPU(QProcessEnvironment& env) +{ +#ifdef WITH_QTDBUS + if (!QDBusConnection::systemBus().isConnected()) + return false; + + QDBusInterface switcheroo("net.hadess.SwitcherooControl", + "/net/hadess/SwitcherooControl", + "org.freedesktop.DBus.Properties", + QDBusConnection::systemBus()); + + if (!switcheroo.isValid()) + return false; + + QDBusReply<QDBusVariant> reply = + switcheroo.call(QStringLiteral("Get"), QStringLiteral("net.hadess.SwitcherooControl"), QStringLiteral("GPUs")); + if (!reply.isValid()) + return false; + + QDBusArgument arg = qvariant_cast<QDBusArgument>(reply.value().variant()); + QList<QVariantMap> gpus; + arg >> gpus; + + for (const auto& gpu : gpus) + { + QString name = qvariant_cast<QString>(gpu[QStringLiteral("Name")]); + bool defaultGpu = qvariant_cast<bool>(gpu[QStringLiteral("Default")]); + if (!defaultGpu) + { + QStringList envList = qvariant_cast<QStringList>(gpu[QStringLiteral("Environment")]); + for (int i = 0; i + 1 < envList.size(); i += 2) + { + env.insert(envList[i], envList[i + 1]); + } + return true; + } + } +#endif + return false; +} + +// all of this because keeping things compatible with deprecated old settings +// if either of the settings {a, b} is true, this also resolves to true +class OrSetting : public Setting +{ + Q_OBJECT + public: + OrSetting(QString id, std::shared_ptr<Setting> a, std::shared_ptr<Setting> b) + : Setting({ id }, false), + m_a(a), + m_b(b) + {} + virtual QVariant get() const + { + bool a = m_a->get().toBool(); + bool b = m_b->get().toBool(); + return a || b; + } + virtual void reset() + {} + virtual void set(QVariant value) + {} + + private: + std::shared_ptr<Setting> m_a; + std::shared_ptr<Setting> m_b; +}; + +MinecraftInstance::MinecraftInstance(SettingsObjectPtr globalSettings, + SettingsObjectPtr settings, + const QString& rootDir) + : BaseInstance(globalSettings, settings, rootDir) +{ + m_components.reset(new PackProfile(this)); +} + +void MinecraftInstance::saveNow() +{ + m_components->saveNow(); +} + +void MinecraftInstance::loadSpecificSettings() +{ + if (isSpecificSettingsLoaded()) + return; + + // Java Settings + auto locationOverride = m_settings->registerSetting("OverrideJavaLocation", false); + auto argsOverride = m_settings->registerSetting("OverrideJavaArgs", false); + m_settings->registerSetting("AutomaticJava", false); + + if (auto global_settings = globalSettings()) + { + m_settings->registerOverride(global_settings->getSetting("JavaPath"), locationOverride); + m_settings->registerOverride(global_settings->getSetting("JvmArgs"), argsOverride); + m_settings->registerOverride(global_settings->getSetting("IgnoreJavaCompatibility"), locationOverride); + + // special! + m_settings->registerPassthrough(global_settings->getSetting("JavaSignature"), locationOverride); + m_settings->registerPassthrough(global_settings->getSetting("JavaArchitecture"), locationOverride); + m_settings->registerPassthrough(global_settings->getSetting("JavaRealArchitecture"), locationOverride); + m_settings->registerPassthrough(global_settings->getSetting("JavaVersion"), locationOverride); + m_settings->registerPassthrough(global_settings->getSetting("JavaVendor"), locationOverride); + + // Window Size + auto windowSetting = m_settings->registerSetting("OverrideWindow", false); + m_settings->registerOverride(global_settings->getSetting("LaunchMaximized"), windowSetting); + m_settings->registerOverride(global_settings->getSetting("MinecraftWinWidth"), windowSetting); + m_settings->registerOverride(global_settings->getSetting("MinecraftWinHeight"), windowSetting); + + // Memory + auto memorySetting = m_settings->registerSetting("OverrideMemory", false); + m_settings->registerOverride(global_settings->getSetting("MinMemAlloc"), memorySetting); + m_settings->registerOverride(global_settings->getSetting("MaxMemAlloc"), memorySetting); + m_settings->registerOverride(global_settings->getSetting("PermGen"), memorySetting); + + // Native library workarounds + auto nativeLibraryWorkaroundsOverride = m_settings->registerSetting("OverrideNativeWorkarounds", false); + m_settings->registerOverride(global_settings->getSetting("UseNativeOpenAL"), nativeLibraryWorkaroundsOverride); + m_settings->registerOverride(global_settings->getSetting("CustomOpenALPath"), nativeLibraryWorkaroundsOverride); + m_settings->registerOverride(global_settings->getSetting("UseNativeGLFW"), nativeLibraryWorkaroundsOverride); + m_settings->registerOverride(global_settings->getSetting("CustomGLFWPath"), nativeLibraryWorkaroundsOverride); + + // Performance related options + auto performanceOverride = m_settings->registerSetting("OverridePerformance", false); + m_settings->registerOverride(global_settings->getSetting("EnableFeralGamemode"), performanceOverride); + m_settings->registerOverride(global_settings->getSetting("EnableMangoHud"), performanceOverride); + m_settings->registerOverride(global_settings->getSetting("UseDiscreteGpu"), performanceOverride); + m_settings->registerOverride(global_settings->getSetting("UseZink"), performanceOverride); + + // Miscellaneous + auto miscellaneousOverride = m_settings->registerSetting("OverrideMiscellaneous", false); + m_settings->registerOverride(global_settings->getSetting("CloseAfterLaunch"), miscellaneousOverride); + m_settings->registerOverride(global_settings->getSetting("QuitAfterGameStop"), miscellaneousOverride); + + // Legacy-related options + auto legacySettings = m_settings->registerSetting("OverrideLegacySettings", false); + m_settings->registerOverride(global_settings->getSetting("OnlineFixes"), legacySettings); + + auto envSetting = m_settings->registerSetting("OverrideEnv", false); + m_settings->registerOverride(global_settings->getSetting("Env"), envSetting); + + m_settings->set("InstanceType", "OneSix"); + } + + // Join server on launch, this does not have a global override + m_settings->registerSetting("JoinServerOnLaunch", false); + m_settings->registerSetting("JoinServerOnLaunchAddress", ""); + m_settings->registerSetting("JoinWorldOnLaunch", ""); + + // Use account for instance, this does not have a global override + m_settings->registerSetting("UseAccountForInstance", false); + m_settings->registerSetting("InstanceAccountId", ""); + + m_settings->registerSetting("ExportName", ""); + m_settings->registerSetting("ExportVersion", "1.0.0"); + m_settings->registerSetting("ExportSummary", ""); + m_settings->registerSetting("ExportAuthor", ""); + m_settings->registerSetting("ExportOptionalFiles", true); + m_settings->registerSetting("ExportRecommendedRAM"); + + auto dataPacksEnabled = m_settings->registerSetting("GlobalDataPacksEnabled", false); + auto dataPacksPath = m_settings->registerSetting("GlobalDataPacksPath", ""); + + connect(dataPacksEnabled.get(), &Setting::SettingChanged, this, [this] { m_data_pack_list.reset(); }); + connect(dataPacksPath.get(), &Setting::SettingChanged, this, [this] { m_data_pack_list.reset(); }); + + // Join server on launch, this does not have a global override + m_settings->registerSetting("OverrideModDownloadLoaders", false); + m_settings->registerSetting("ModDownloadLoaders", "[]"); + + qDebug() << "Instance-type specific settings were loaded!"; + + setSpecificSettingsLoaded(true); + + updateRuntimeContext(); +} + +void MinecraftInstance::updateRuntimeContext() +{ + m_runtimeContext.updateFromInstanceSettings(m_settings); + m_components->invalidateLaunchProfile(); +} + +QString MinecraftInstance::typeName() const +{ + return "Minecraft"; +} + +std::shared_ptr<PackProfile> MinecraftInstance::getPackProfile() const +{ + return m_components; +} + +QSet<QString> MinecraftInstance::traits() const +{ + auto components = getPackProfile(); + if (!components) + { + return { "version-incomplete" }; + } + auto profile = components->getProfile(); + if (!profile) + { + return { "version-incomplete" }; + } + return profile->getTraits(); +} + +QString MinecraftInstance::gameRoot() const +{ + QFileInfo mcDir(FS::PathCombine(instanceRoot(), "minecraft")); + QFileInfo dotMCDir(FS::PathCombine(instanceRoot(), ".minecraft")); + + if (dotMCDir.exists() && !mcDir.exists()) + return dotMCDir.filePath(); + else + return mcDir.filePath(); +} + +QString MinecraftInstance::binRoot() const +{ + return FS::PathCombine(gameRoot(), "bin"); +} + +QString MinecraftInstance::getNativePath() const +{ + QDir natives_dir(FS::PathCombine(instanceRoot(), "natives/")); + return natives_dir.absolutePath(); +} + +QString MinecraftInstance::getLocalLibraryPath() const +{ + QDir libraries_dir(FS::PathCombine(instanceRoot(), "libraries/")); + return libraries_dir.absolutePath(); +} + +bool MinecraftInstance::supportsDemo() const +{ + Version instance_ver{ getPackProfile()->getComponentVersion("net.minecraft") }; + // Demo mode was introduced in 1.3.1: https://minecraft.wiki/w/Demo_mode#History + // Note: This check may not work correctly for non-release versions due to version string formatting. Demo support + // is based on release versions. + return instance_ver >= Version("1.3.1"); +} + +QString MinecraftInstance::jarModsDir() const +{ + QDir jarmods_dir(FS::PathCombine(instanceRoot(), "jarmods/")); + return jarmods_dir.absolutePath(); +} + +QString MinecraftInstance::modsRoot() const +{ + return FS::PathCombine(gameRoot(), "mods"); +} + +QString MinecraftInstance::modsCacheLocation() const +{ + return FS::PathCombine(instanceRoot(), "mods.cache"); +} + +QString MinecraftInstance::coreModsDir() const +{ + return FS::PathCombine(gameRoot(), "coremods"); +} + +QString MinecraftInstance::nilModsDir() const +{ + return FS::PathCombine(gameRoot(), "nilmods"); +} + +QString MinecraftInstance::dataPacksDir() +{ + QString relativePath = settings()->get("GlobalDataPacksPath").toString(); + + if (relativePath.isEmpty()) + relativePath = "datapacks"; + + return QDir(gameRoot()).filePath(relativePath); +} + +QString MinecraftInstance::resourcePacksDir() const +{ + return FS::PathCombine(gameRoot(), "resourcepacks"); +} + +QString MinecraftInstance::texturePacksDir() const +{ + return FS::PathCombine(gameRoot(), "texturepacks"); +} + +QString MinecraftInstance::shaderPacksDir() const +{ + return FS::PathCombine(gameRoot(), "shaderpacks"); +} + +QString MinecraftInstance::instanceConfigFolder() const +{ + return FS::PathCombine(gameRoot(), "config"); +} + +QString MinecraftInstance::libDir() const +{ + return FS::PathCombine(gameRoot(), "lib"); +} + +QString MinecraftInstance::worldDir() const +{ + return FS::PathCombine(gameRoot(), "saves"); +} + +QString MinecraftInstance::resourcesDir() const +{ + return FS::PathCombine(gameRoot(), "resources"); +} + +QDir MinecraftInstance::librariesPath() const +{ + return QDir::current().absoluteFilePath("libraries"); +} + +QDir MinecraftInstance::jarmodsPath() const +{ + return QDir(jarModsDir()); +} + +QDir MinecraftInstance::versionsPath() const +{ + return QDir::current().absoluteFilePath("versions"); +} + +QStringList MinecraftInstance::getClassPath() +{ + QStringList jars, nativeJars; + auto profile = m_components->getProfile(); + profile->getLibraryFiles(runtimeContext(), jars, nativeJars, getLocalLibraryPath(), binRoot()); + return jars; +} + +QString MinecraftInstance::getMainClass() const +{ + auto profile = m_components->getProfile(); + return profile->getMainClass(); +} + +QStringList MinecraftInstance::getNativeJars() +{ + QStringList jars, nativeJars; + auto profile = m_components->getProfile(); + profile->getLibraryFiles(runtimeContext(), jars, nativeJars, getLocalLibraryPath(), binRoot()); + return nativeJars; +} + +QStringList MinecraftInstance::extraArguments() +{ + auto list = Commandline::splitArgs(settings()->get("JvmArgs").toString()); + auto version = getPackProfile(); + if (!version) + return list; + auto jarMods = getJarMods(); + if (!jarMods.isEmpty()) + { + list.append({ "-Dfml.ignoreInvalidMinecraftCertificates=true", "-Dfml.ignorePatchDiscrepancies=true" }); + } + auto addn = m_components->getProfile()->getAddnJvmArguments(); + if (!addn.isEmpty()) + { + list.append(addn); + } + auto agents = m_components->getProfile()->getAgents(); + for (auto agent : agents) + { + QStringList jar, temp1, temp2, temp3; + agent->library()->getApplicableFiles(runtimeContext(), jar, temp1, temp2, temp3, getLocalLibraryPath()); + list.append("-javaagent:" + jar[0] + (agent->argument().isEmpty() ? "" : "=" + agent->argument())); + } + + { + QString openALPath; + QString glfwPath; + + if (settings()->get("UseNativeOpenAL").toBool()) + { + openALPath = APPLICATION->m_detectedOpenALPath; + auto customPath = settings()->get("CustomOpenALPath").toString(); + if (!customPath.isEmpty()) + openALPath = customPath; + } + if (settings()->get("UseNativeGLFW").toBool()) + { + glfwPath = APPLICATION->m_detectedGLFWPath; + auto customPath = settings()->get("CustomGLFWPath").toString(); + if (!customPath.isEmpty()) + glfwPath = customPath; + } + + QFileInfo openALInfo(openALPath); + QFileInfo glfwInfo(glfwPath); + + if (!openALPath.isEmpty() && openALInfo.exists()) + list.append("-Dorg.lwjgl.openal.libname=" + openALInfo.absoluteFilePath()); + if (!glfwPath.isEmpty() && glfwInfo.exists()) + list.append("-Dorg.lwjgl.glfw.libname=" + glfwInfo.absoluteFilePath()); + } + + return list; +} + +QStringList MinecraftInstance::javaArguments() +{ + QStringList args; + + // custom args go first. we want to override them if we have our own here. + args.append(extraArguments()); + + // OSX dock icon and name +#ifdef Q_OS_MAC + args << "-Xdock:icon=icon.png"; + args << QString("-Xdock:name=\"%1\"").arg(windowTitle()); +#endif + auto traits_ = traits(); + // HACK: fix issues on macOS with 1.13 snapshots + // NOTE: Oracle Java option. if there are alternate jvm implementations, this would be the place to customize this + // for them +#ifdef Q_OS_MAC + if (traits_.contains("FirstThreadOnMacOS")) + { + args << QString("-XstartOnFirstThread"); + } +#endif + + // HACK: Stupid hack for Intel drivers. See: https://mojang.atlassian.net/browse/MCL-767 +#ifdef Q_OS_WIN32 + args << QString("-XX:HeapDumpPath=MojangTricksIntelDriversForPerformance_javaw.exe_" + "minecraft.exe.heapdump"); +#endif + + int min = settings()->get("MinMemAlloc").toInt(); + int max = settings()->get("MaxMemAlloc").toInt(); + if (min < max) + { + args << QString("-Xms%1m").arg(min); + args << QString("-Xmx%1m").arg(max); + } + else + { + args << QString("-Xms%1m").arg(max); + args << QString("-Xmx%1m").arg(min); + } + + // No PermGen in newer java. + projt::java::RuntimeVersion javaVersion = getRuntimeVersion(); + if (javaVersion.needsPermGen()) + { + auto permgen = settings()->get("PermGen").toInt(); + if (permgen != 64) + { + args << QString("-XX:PermSize=%1m").arg(permgen); + } + } + + args << "-Duser.language=en"; + + if (javaVersion.supportsModules() && shouldApplyOnlineFixes()) + // allow reflective access to java.net - required by the skin fix + args << "--add-opens" + << "java.base/java.net=ALL-UNNAMED"; + + return args; +} + +QString MinecraftInstance::getLauncher() +{ + // use legacy launcher if the traits are set + if (isLegacy()) + return "legacy"; + + return "standard"; +} + +bool MinecraftInstance::shouldApplyOnlineFixes() +{ + return traits().contains("legacyServices") && settings()->get("OnlineFixes").toBool(); +} + +QMap<QString, QString> MinecraftInstance::getVariables() +{ + QMap<QString, QString> out; + out.insert("INST_NAME", name()); + out.insert("INST_ID", id()); + out.insert("INST_DIR", QDir::toNativeSeparators(QDir(instanceRoot()).absolutePath())); + out.insert("INST_MC_DIR", QDir::toNativeSeparators(QDir(gameRoot()).absolutePath())); + out.insert("INST_JAVA", QDir::toNativeSeparators(QDir(settings()->get("JavaPath").toString()).absolutePath())); + out.insert("INST_JAVA_ARGS", javaArguments().join(' ')); + out.insert("NO_COLOR", "1"); +#ifdef Q_OS_MACOS + // get library for Steam overlay support + QString steamDyldInsertLibraries = qEnvironmentVariable("STEAM_DYLD_INSERT_LIBRARIES"); + if (!steamDyldInsertLibraries.isEmpty()) + { + out.insert("DYLD_INSERT_LIBRARIES", steamDyldInsertLibraries); + } +#endif + return out; +} + +QProcessEnvironment MinecraftInstance::createEnvironment() +{ + // prepare the process environment + QProcessEnvironment env = projt::java::buildCleanEnvironment(); + + // export some infos + auto variables = getVariables(); + for (auto it = variables.begin(); it != variables.end(); ++it) + { + env.insert(it.key(), it.value()); + } + // custom env + + auto insertEnv = [&env](QString value) + { + auto envMap = Json::toMap(value); + if (envMap.isEmpty()) + return; + + for (auto iter = envMap.begin(); iter != envMap.end(); iter++) + env.insert(iter.key(), iter.value().toString()); + }; + + bool overrideEnv = settings()->get("OverrideEnv").toBool(); + + if (!overrideEnv) + insertEnv(APPLICATION->settings()->get("Env").toString()); + else + insertEnv(settings()->get("Env").toString()); + return env; +} + +QProcessEnvironment MinecraftInstance::createLaunchEnvironment() +{ + // prepare the process environment + QProcessEnvironment env = createEnvironment(); + +#ifdef Q_OS_LINUX + if (settings()->get("EnableMangoHud").toBool() && APPLICATION->capabilities() & Application::SupportsMangoHud) + { + QStringList preloadList; + if (auto value = env.value("LD_PRELOAD"); !value.isEmpty()) + preloadList = value.split(QLatin1String(":")); + + auto mangoHudLibString = MangoHud::getLibraryString(); + if (!mangoHudLibString.isEmpty()) + { + QFileInfo mangoHudLib(mangoHudLibString); + QString libPath = mangoHudLib.absolutePath(); + auto appendLib = [libPath, &preloadList](QString fileName) + { + if (QFileInfo(FS::PathCombine(libPath, fileName)).exists()) + preloadList << FS::PathCombine(libPath, fileName); + }; + + // dlsym variant is only needed for OpenGL and not included in the vulkan layer + appendLib("libMangoHud_dlsym.so"); + appendLib("libMangoHud_opengl.so"); + appendLib("libMangoHud_shim.so"); + preloadList << mangoHudLibString; + } + + env.insert("LD_PRELOAD", preloadList.join(QLatin1String(":"))); + env.insert("MANGOHUD", "1"); + } + + if (settings()->get("UseDiscreteGpu").toBool()) + { + if (!switcherooSetupGPU(env)) + { + // Open Source Drivers + env.insert("DRI_PRIME", "1"); + // Proprietary Nvidia Drivers + env.insert("__NV_PRIME_RENDER_OFFLOAD", "1"); + env.insert("__VK_LAYER_NV_optimus", "NVIDIA_only"); + env.insert("__GLX_VENDOR_LIBRARY_NAME", "nvidia"); + } + } + + if (settings()->get("UseZink").toBool()) + { + // taken from https://wiki.archlinux.org/title/OpenGL#OpenGL_over_Vulkan_(Zink) + env.insert("__GLX_VENDOR_LIBRARY_NAME", "mesa"); + env.insert("MESA_LOADER_DRIVER_OVERRIDE", "zink"); + env.insert("GALLIUM_DRIVER", "zink"); + } +#endif + return env; +} + +static QString replaceTokensIn(QString text, QMap<QString, QString> with) +{ + // Replace tokens in the format ${key} with values from the map. + QString result; + static const QRegularExpression s_token_regexp("\\$\\{(.+)\\}", QRegularExpression::InvertedGreedinessOption); + QStringList list; + QRegularExpressionMatchIterator i = s_token_regexp.globalMatch(text); + int lastCapturedEnd = 0; + while (i.hasNext()) + { + QRegularExpressionMatch match = i.next(); + result.append(text.mid(lastCapturedEnd, match.capturedStart())); + QString key = match.captured(1); + auto iter = with.find(key); + if (iter != with.end()) + { + result.append(*iter); + } + lastCapturedEnd = match.capturedEnd(); + } + result.append(text.mid(lastCapturedEnd)); + return result; +} + +QStringList MinecraftInstance::processMinecraftArgs(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) const +{ + auto profile = m_components->getProfile(); + QString args_pattern = profile->getMinecraftArguments(); + for (auto tweaker : profile->getTweakers()) + { + args_pattern += " --tweakClass " + tweaker; + } + + if (targetToJoin) + { + if (!targetToJoin->address.isEmpty()) + { + if (profile->hasTrait("feature:is_quick_play_multiplayer")) + { + args_pattern += + " --quickPlayMultiplayer " + targetToJoin->address + ':' + QString::number(targetToJoin->port); + } + else + { + args_pattern += " --server " + targetToJoin->address; + args_pattern += " --port " + QString::number(targetToJoin->port); + } + } + else if (!targetToJoin->world.isEmpty() && profile->hasTrait("feature:is_quick_play_singleplayer")) + { + args_pattern += " --quickPlaySingleplayer " + targetToJoin->world; + } + } + + QMap<QString, QString> token_mapping; + // yggdrasil! + if (session) + { + // token_mapping["auth_username"] = session->username; + token_mapping["auth_session"] = session->session; + token_mapping["auth_access_token"] = session->access_token; + token_mapping["auth_player_name"] = session->player_name; + token_mapping["auth_uuid"] = session->uuid; + token_mapping["user_properties"] = session->serializeUserProperties(); + token_mapping["user_type"] = session->user_type; + if (session->launchMode == LaunchMode::Demo) + { + args_pattern += " --demo"; + } + } + + token_mapping["profile_name"] = name(); + token_mapping["version_name"] = profile->getMinecraftVersion(); + token_mapping["version_type"] = profile->getMinecraftVersionType(); + + QString absRootDir = QDir(gameRoot()).absolutePath(); + token_mapping["game_directory"] = absRootDir; + QString absAssetsDir = QDir("assets/").absolutePath(); + auto assets = profile->getMinecraftAssets(); + token_mapping["game_assets"] = AssetsUtils::getAssetsDir(assets->id, resourcesDir()).absolutePath(); + + // 1.7.3+ assets tokens + token_mapping["assets_root"] = absAssetsDir; + token_mapping["assets_index_name"] = assets->id; + + QStringList parts = args_pattern.split(' ', Qt::SkipEmptyParts); + for (int i = 0; i < parts.length(); i++) + { + parts[i] = replaceTokensIn(parts[i], token_mapping); + } + return parts; +} + +QString MinecraftInstance::createLaunchScript(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) +{ + QString launchScript; + + if (!m_components) + return QString(); + auto profile = m_components->getProfile(); + if (!profile) + return QString(); + + auto mainClass = getMainClass(); + if (!mainClass.isEmpty()) + { + launchScript += "mainClass " + mainClass + "\n"; + } + auto appletClass = profile->getAppletClass(); + if (!appletClass.isEmpty()) + { + launchScript += "appletClass " + appletClass + "\n"; + } + + if (targetToJoin) + { + if (!targetToJoin->address.isEmpty()) + { + launchScript += "serverAddress " + targetToJoin->address + "\n"; + launchScript += "serverPort " + QString::number(targetToJoin->port) + "\n"; + } + else if (!targetToJoin->world.isEmpty()) + { + launchScript += "worldName " + targetToJoin->world + "\n"; + } + } + + // generic minecraft params + for (auto param : processMinecraftArgs(session, nullptr /* When using a launch script, the server parameters are + handled by it*/ + )) + { + launchScript += "param " + param + "\n"; + } + + // window size, title and state, legacy + { + QString windowParams; + if (settings()->get("LaunchMaximized").toBool()) + { + // Note: Fullscreen/maximization support is limited. UI enhancements for fullscreen mode are needed. + if (!isLegacy()) + { + auto screen = QGuiApplication::primaryScreen(); + auto screenGeometry = screen->availableSize(); + + // small hack to get the widow decorations + for (auto w : QApplication::topLevelWidgets()) + { + auto mainWindow = qobject_cast<QMainWindow*>(w); + if (mainWindow) + { + auto m = mainWindow->windowHandle()->frameMargins(); + screenGeometry = screenGeometry.shrunkBy(m); + break; + } + } + + windowParams = QString("%1x%2").arg(screenGeometry.width()).arg(screenGeometry.height()); + } + else + { + windowParams = "maximized"; + } + } + else + { + windowParams = QString("%1x%2") + .arg(settings()->get("MinecraftWinWidth").toInt()) + .arg(settings()->get("MinecraftWinHeight").toInt()); + } + launchScript += "windowTitle " + windowTitle() + "\n"; + launchScript += "windowParams " + windowParams + "\n"; + } + + // launcher info + { + launchScript += "launcherBrand " + BuildConfig.LAUNCHER_NAME + "\n"; + launchScript += "launcherVersion " + BuildConfig.printableVersionString() + "\n"; + } + + // instance info + { + launchScript += "instanceName " + name() + "\n"; + launchScript += "instanceIconKey " + name() + "\n"; + launchScript += "instanceIconPath icon.png\n"; // we already save a copy here + } + + // legacy auth + if (session) + { + launchScript += "userName " + session->player_name + "\n"; + launchScript += "sessionId " + session->session + "\n"; + } + + for (auto trait : profile->getTraits()) + { + launchScript += "traits " + trait + "\n"; + } + + if (shouldApplyOnlineFixes()) + launchScript += "onlineFixes true\n"; + + launchScript += "launcher " + getLauncher() + "\n"; + + // qDebug() << "Generated launch script:" << launchScript; + return launchScript; +} + +QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) +{ + QStringList out; + out << "Main Class:" + << " " + getMainClass() << ""; + out << "Native path:" + << " " + getNativePath() << ""; + + auto profile = m_components->getProfile(); + + // traits + auto alltraits = traits(); + if (alltraits.size()) + { + out << "Traits:"; + for (auto trait : alltraits) + { + out << "traits " + trait; + } + out << ""; + } + + // native libraries + auto settings = this->settings(); + bool nativeOpenAL = settings->get("UseNativeOpenAL").toBool(); + bool nativeGLFW = settings->get("UseNativeGLFW").toBool(); + if (nativeOpenAL || nativeGLFW) + { + if (nativeOpenAL) + out << "Using system OpenAL."; + if (nativeGLFW) + out << "Using system GLFW."; + out << ""; + } + + // libraries and class path. + { + out << "Libraries:"; + QStringList jars, nativeJars; + profile->getLibraryFiles(runtimeContext(), jars, nativeJars, getLocalLibraryPath(), binRoot()); + auto printLibFile = [&out](const QString& path) + { + QFileInfo info(path); + if (info.exists()) + { + out << " " + path; + } + else + { + out << " " + path + " (missing)"; + } + }; + for (auto file : jars) + { + printLibFile(file); + } + out << ""; + out << "Native libraries:"; + for (auto file : nativeJars) + { + printLibFile(file); + } + out << ""; + } + + // mods and core mods + auto printModList = [&out](const QString& label, ModFolderModel& model) + { + if (model.size()) + { + out << QString("%1:").arg(label); + auto modList = model.allMods(); + std::sort(modList.begin(), + modList.end(), + [](auto a, auto b) + { + auto aName = a->fileinfo().completeBaseName(); + auto bName = b->fileinfo().completeBaseName(); + return aName.localeAwareCompare(bName) < 0; + }); + for (auto mod : modList) + { + if (mod->type() == ResourceType::FOLDER) + { + out << u8" [🖿] " + mod->fileinfo().completeBaseName() + " (folder)"; + continue; + } + + if (mod->enabled()) + { + out << u8" [✔] " + mod->fileinfo().completeBaseName(); + } + else + { + out << u8" [✘] " + mod->fileinfo().completeBaseName() + " (disabled)"; + } + } + out << ""; + } + }; + + printModList("Mods", *(loaderModList().get())); + printModList("Core Mods", *(coreModList().get())); + + // jar mods + auto& jarMods = profile->getJarMods(); + if (jarMods.size()) + { + out << "Jar Mods:"; + for (auto& jarmod : jarMods) + { + auto displayname = jarmod->displayName(runtimeContext()); + auto realname = jarmod->filename(runtimeContext()); + if (displayname != realname) + { + out << " " + displayname + " (" + realname + ")"; + } + else + { + out << " " + realname; + } + } + out << ""; + } + + // minecraft arguments + auto params = processMinecraftArgs(nullptr, targetToJoin); + out << "Params:"; + out << " " + params.join(' '); + out << ""; + + // window size + QString windowParams; + if (settings->get("LaunchMaximized").toBool()) + { + out << "Window size: max (if available)"; + } + else + { + auto width = settings->get("MinecraftWinWidth").toInt(); + auto height = settings->get("MinecraftWinHeight").toInt(); + out << "Window size: " + QString::number(width) + " x " + QString::number(height); + } + out << ""; + out << "Launcher: " + getLauncher(); + out << ""; + return out; +} + +QMap<QString, QString> MinecraftInstance::createCensorFilterFromSession(AuthSessionPtr session) +{ + if (!session) + { + return QMap<QString, QString>(); + } + auto& sessionRef = *session.get(); + QMap<QString, QString> filter; + auto addToFilter = [&filter](QString key, QString value) + { + if (key.trimmed().size()) + { + filter[key] = value; + } + }; + if (sessionRef.session != "-") + { + addToFilter(sessionRef.session, tr("<SESSION ID>")); + } + if (sessionRef.access_token != "0") + { + addToFilter(sessionRef.access_token, tr("<ACCESS TOKEN>")); + } + addToFilter(sessionRef.uuid, tr("<PROFILE ID>")); + + return filter; +} + +QStringList MinecraftInstance::getLogFileSearchPaths() +{ + return { FS::PathCombine(gameRoot(), "crash-reports"), FS::PathCombine(gameRoot(), "logs"), gameRoot() }; +} + +QString MinecraftInstance::getStatusbarDescription() +{ + QStringList traits; + if (hasVersionBroken()) + { + traits.append(tr("broken")); + } + + QString mcVersion = m_components->getComponentVersion("net.minecraft"); + if (mcVersion.isEmpty()) + { + // Load component info if needed - use Online mode to fetch metadata if not cached + m_components->reload(Net::Mode::Online); + mcVersion = m_components->getComponentVersion("net.minecraft"); + } + + QString description; + description.append(tr("Minecraft %1").arg(mcVersion)); + if (m_settings->get("ShowGameTime").toBool()) + { + if (lastTimePlayed() > 0 && lastLaunch() > 0) + { + QDateTime lastLaunchTime = QDateTime::fromMSecsSinceEpoch(lastLaunch()); + description.append( + tr(", last played on %1 for %2") + .arg(QLocale().toString(lastLaunchTime, QLocale::ShortFormat)) + .arg(Time::prettifyDuration(lastTimePlayed(), + APPLICATION->settings()->get("ShowGameTimeWithoutDays").toBool()))); + } + + if (totalTimePlayed() > 0) + { + description.append( + tr(", total played for %1") + .arg(Time::prettifyDuration(totalTimePlayed(), + APPLICATION->settings()->get("ShowGameTimeWithoutDays").toBool()))); + } + } + if (hasCrashed()) + { + description.append(tr(", has crashed.")); + } + return description; +} + +QList<Task::Ptr> MinecraftInstance::createUpdateTask() +{ + return { + // create folders + makeShared<FoldersTask>(this), + // libraries download + makeShared<LibrariesTask>(this), + // FML libraries download and copy into the instance + makeShared<FMLLibrariesTask>(this), + // assets update + makeShared<AssetUpdateTask>(this), + }; +} + +shared_qobject_ptr<projt::launch::LaunchPipeline> MinecraftInstance::createLaunchPipeline( + AuthSessionPtr session, + MinecraftTarget::Ptr targetToJoin) +{ + updateRuntimeContext(); + // Using static_pointer_cast since 'this' is guaranteed to be a MinecraftInstance + auto process = + projt::launch::LaunchPipeline::create(std::dynamic_pointer_cast<MinecraftInstance>(shared_from_this())); + auto pptr = process.get(); + + APPLICATION->icons()->saveIcon(iconKey(), FS::PathCombine(gameRoot(), "icon.png"), "PNG"); + + // print a header + { + process->appendStage( + makeShared<projt::launch::steps::LogMessageStep>(pptr, + "Minecraft folder is:\n" + gameRoot() + "\n\n", + MessageLevel::Launcher)); + } + + // create the .minecraft folder and server-resource-packs (workaround for Minecraft bug MCL-3732) + { + process->appendStage(makeShared<CreateGameFolders>(pptr)); + } + + if (!targetToJoin && settings()->get("JoinServerOnLaunch").toBool()) + { + QString fullAddress = settings()->get("JoinServerOnLaunchAddress").toString(); + if (!fullAddress.isEmpty()) + { + targetToJoin.reset(new MinecraftTarget(MinecraftTarget::parse(fullAddress, false))); + } + else + { + QString world = settings()->get("JoinWorldOnLaunch").toString(); + if (!world.isEmpty()) + { + targetToJoin.reset(new MinecraftTarget(MinecraftTarget::parse(world, true))); + } + } + } + + if (targetToJoin && targetToJoin->port == 25565) + { + // Resolve server address to join on launch + auto step = makeShared<projt::launch::steps::ServerJoinResolveStep>(pptr); + step->setLookupAddress(targetToJoin->address); + step->setOutputTarget(targetToJoin); + process->appendStage(step); + } + + // load meta + { + auto mode = session->launchMode != LaunchMode::Offline ? Net::Mode::Online : Net::Mode::Offline; + process->appendStage( + makeShared<projt::launch::TaskBridgeStage>(pptr, makeShared<MinecraftLoadAndCheck>(this, mode))); + } + + // check java + { + process->appendStage(makeShared<AutoInstallJava>(pptr)); + process->appendStage(makeShared<projt::launch::steps::RuntimeProbeStep>(pptr)); + } + + // run pre-launch command if that's needed + if (getPreLaunchCommand().size()) + { + auto step = makeShared<projt::launch::steps::LaunchCommandStep>( + pptr, + projt::launch::steps::LaunchCommandStep::Hook::PreLaunch, + getPreLaunchCommand()); + step->setWorkingDirectory(gameRoot()); + process->appendStage(step); + } + + // if we aren't in offline mode + if (session->launchMode != LaunchMode::Offline) + { + process->appendStage(makeShared<ClaimAccount>(pptr, session)); + for (auto t : createUpdateTask()) + { + process->appendStage(makeShared<projt::launch::TaskBridgeStage>(pptr, t)); + } + } + + // if there are any jar mods + { + process->appendStage(makeShared<ModMinecraftJar>(pptr)); + } + + // Scan mods folders for mods + { + process->appendStage(makeShared<ScanModFolders>(pptr)); + } + + // print some instance info here... + { + process->appendStage(makeShared<PrintInstanceInfo>(pptr, session, targetToJoin)); + } + + // extract native jars if needed + { + process->appendStage(makeShared<ExtractNatives>(pptr)); + } + + // reconstruct assets if needed + { + process->appendStage(makeShared<ReconstructAssets>(pptr)); + } + + // verify that minimum Java requirements are met + { + process->appendStage(makeShared<VerifyJavaInstall>(pptr)); + } + + { + // actually launch the game + auto step = makeShared<LauncherPartLaunch>(pptr); + step->setWorkingDirectory(gameRoot()); + step->setAuthSession(session); + step->setTargetToJoin(targetToJoin); + process->appendStage(step); + } + + // run post-exit command if that's needed + if (getPostExitCommand().size()) + { + auto step = + makeShared<projt::launch::steps::LaunchCommandStep>(pptr, + projt::launch::steps::LaunchCommandStep::Hook::PostExit, + getPostExitCommand()); + step->setWorkingDirectory(gameRoot()); + process->appendStage(step); + } + if (session) + { + process->setCensorFilter(createCensorFilterFromSession(session)); + } + if (m_settings->get("QuitAfterGameStop").toBool()) + { + process->appendStage(makeShared<projt::launch::steps::QuitAfterGameStep>(pptr)); + } + m_launchProcess = process; + emit launchPipelineChanged(m_launchProcess); + return m_launchProcess; +} + +projt::java::RuntimeVersion MinecraftInstance::getRuntimeVersion() +{ + return projt::java::RuntimeVersion(settings()->get("JavaVersion").toString()); +} + +std::shared_ptr<ModFolderModel> MinecraftInstance::loaderModList() +{ + if (!m_loader_mod_list) + { + bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); + m_loader_mod_list.reset(new ModFolderModel(modsRoot(), this, is_indexed, true)); + } + return m_loader_mod_list; +} + +std::shared_ptr<ModFolderModel> MinecraftInstance::coreModList() +{ + if (!m_core_mod_list) + { + bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); + m_core_mod_list.reset(new ModFolderModel(coreModsDir(), this, is_indexed, true)); + } + return m_core_mod_list; +} + +std::shared_ptr<ModFolderModel> MinecraftInstance::nilModList() +{ + if (!m_nil_mod_list) + { + bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); + m_nil_mod_list.reset(new ModFolderModel(nilModsDir(), this, is_indexed, false)); + } + return m_nil_mod_list; +} + +std::shared_ptr<ResourcePackFolderModel> MinecraftInstance::resourcePackList() +{ + if (!m_resource_pack_list) + { + bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); + m_resource_pack_list.reset(new ResourcePackFolderModel(resourcePacksDir(), this, is_indexed, true)); + } + return m_resource_pack_list; +} + +std::shared_ptr<TexturePackFolderModel> MinecraftInstance::texturePackList() +{ + if (!m_texture_pack_list) + { + bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); + m_texture_pack_list.reset(new TexturePackFolderModel(texturePacksDir(), this, is_indexed, true)); + } + return m_texture_pack_list; +} + +std::shared_ptr<ShaderPackFolderModel> MinecraftInstance::shaderPackList() +{ + if (!m_shader_pack_list) + { + bool is_indexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); + m_shader_pack_list.reset(new ShaderPackFolderModel(shaderPacksDir(), this, is_indexed, true)); + } + return m_shader_pack_list; +} + +std::shared_ptr<DataPackFolderModel> MinecraftInstance::dataPackList() +{ + if (!m_data_pack_list && settings()->get("GlobalDataPacksEnabled").toBool()) + { + bool isIndexed = !APPLICATION->settings()->get("ModMetadataDisabled").toBool(); + m_data_pack_list.reset(new DataPackFolderModel(dataPacksDir(), this, isIndexed, true)); + } + return m_data_pack_list; +} + +QList<std::shared_ptr<ResourceFolderModel>> MinecraftInstance::resourceLists() +{ + return { loaderModList(), coreModList(), nilModList(), resourcePackList(), + texturePackList(), shaderPackList(), dataPackList() }; +} + +std::shared_ptr<WorldList> MinecraftInstance::worldList() +{ + if (!m_world_list) + { + m_world_list.reset(new WorldList(worldDir(), this)); + } + return m_world_list; +} + +QList<Mod*> MinecraftInstance::getJarMods() const +{ + auto profile = m_components->getProfile(); + QList<Mod*> mods; + for (auto jarmod : profile->getJarMods()) + { + QStringList jar, temp1, temp2, temp3; + jarmod->getApplicableFiles(runtimeContext(), jar, temp1, temp2, temp3, jarmodsPath().absolutePath()); + // QString filePath = jarmodsPath().absoluteFilePath(jarmod->filename(currentSystem)); + mods.push_back(new Mod(QFileInfo(jar[0]))); + } + return mods; +} + +#include "MinecraftInstance.moc" diff --git a/archived/projt-launcher/launcher/minecraft/MinecraftInstance.h b/archived/projt-launcher/launcher/minecraft/MinecraftInstance.h new file mode 100644 index 0000000000..49242bee5b --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/MinecraftInstance.h @@ -0,0 +1,212 @@ +// 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 Sefa Eyeoglu <contact@scrumplex.net> + * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me> + * + * 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. + * + * ======================================================================== */ + +#pragma once +#include <java/core/RuntimeVersion.hpp> +#include <minecraft/mod/DataPackFolderModel.hpp> +#include <QDir> +#include <QProcess> +#include "BaseInstance.h" +#include "minecraft/launch/MinecraftTarget.hpp" +#include "minecraft/mod/Mod.hpp" + +class ModFolderModel; +class ResourceFolderModel; +class ResourcePackFolderModel; +class ShaderPackFolderModel; +class TexturePackFolderModel; +class WorldList; +namespace projt::launch +{ + class LaunchPipeline; +} +class PackProfile; + +class MinecraftInstance : public BaseInstance +{ + Q_OBJECT + public: + MinecraftInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString& rootDir); + virtual ~MinecraftInstance() = default; + virtual void saveNow() override; + + void loadSpecificSettings() override; + + /// @deprecated Legacy method - consider moving to PackProfile + QString typeName() const override; + /// @deprecated Legacy method - traits should come from PackProfile components + QSet<QString> traits() const override; + + bool canEdit() const override + { + return true; + } + + bool canExport() const override + { + return true; + } + + ////// Directories and files ////// + + ////// Directories and files ////// + QString jarModsDir() const; + QString resourcePacksDir() const; + QString texturePacksDir() const; + QString shaderPacksDir() const; + QString modsRoot() const override; + QString coreModsDir() const; + QString nilModsDir() const; + QString dataPacksDir(); + QString modsCacheLocation() const; + QString libDir() const; + QString worldDir() const; + QString resourcesDir() const; + QDir jarmodsPath() const; + QDir librariesPath() const; + QDir versionsPath() const; + QString instanceConfigFolder() const override; + + // Path to the instance's minecraft directory. + QString gameRoot() const override; + + // Path to the instance's minecraft bin directory. + QString binRoot() const; + + // where to put the natives during/before launch + QString getNativePath() const; + + // where the instance-local libraries should be + QString getLocalLibraryPath() const; + + /** Returns whether the instance, with its version, has support for demo mode. */ + bool supportsDemo() const; + + void updateRuntimeContext() override; + + ////// Profile management ////// + std::shared_ptr<PackProfile> getPackProfile() const; + + ////// Mod Lists ////// + std::shared_ptr<ModFolderModel> loaderModList(); + std::shared_ptr<ModFolderModel> coreModList(); + std::shared_ptr<ModFolderModel> nilModList(); + std::shared_ptr<ResourcePackFolderModel> resourcePackList(); + std::shared_ptr<TexturePackFolderModel> texturePackList(); + std::shared_ptr<ShaderPackFolderModel> shaderPackList(); + std::shared_ptr<DataPackFolderModel> dataPackList(); + QList<std::shared_ptr<ResourceFolderModel>> resourceLists(); + std::shared_ptr<WorldList> worldList(); + + ////// Launch stuff ////// + QList<Task::Ptr> createUpdateTask() override; + shared_qobject_ptr<projt::launch::LaunchPipeline> createLaunchPipeline(AuthSessionPtr account, + MinecraftTarget::Ptr targetToJoin) override; + QStringList extraArguments(); + QStringList verboseDescription(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin) override; + QList<Mod*> getJarMods() const; + QString createLaunchScript(AuthSessionPtr session, MinecraftTarget::Ptr targetToJoin); + /// get arguments passed to java + QStringList javaArguments(); + QString getLauncher(); + bool shouldApplyOnlineFixes(); + + /// get variables for launch command variable substitution/environment + QMap<QString, QString> getVariables() override; + + /// create an environment for launching processes + QProcessEnvironment createEnvironment() override; + QProcessEnvironment createLaunchEnvironment() override; + + QStringList getLogFileSearchPaths() override; + + QString getStatusbarDescription() override; + + /// @deprecated These methods expose internal launch profile data. Use LaunchProfile directly. + virtual QStringList getClassPath(); + /// @deprecated Use LaunchProfile::getNativeLibraries() instead + virtual QStringList getNativeJars(); + /// @deprecated Use LaunchProfile::getMainClass() instead + virtual QString getMainClass() const; + + /// @deprecated Argument processing should be in launch steps, not instance + virtual QStringList processMinecraftArgs(AuthSessionPtr account, MinecraftTarget::Ptr targetToJoin) const; + + virtual projt::java::RuntimeVersion getRuntimeVersion(); + + signals: + void profilerChanged(); + + protected: + QMap<QString, QString> createCensorFilterFromSession(AuthSessionPtr session); + + protected: // data + std::shared_ptr<PackProfile> m_components; + mutable std::shared_ptr<ModFolderModel> m_loader_mod_list; + mutable std::shared_ptr<ModFolderModel> m_core_mod_list; + mutable std::shared_ptr<ModFolderModel> m_nil_mod_list; + mutable std::shared_ptr<ResourcePackFolderModel> m_resource_pack_list; + mutable std::shared_ptr<ShaderPackFolderModel> m_shader_pack_list; + mutable std::shared_ptr<TexturePackFolderModel> m_texture_pack_list; + mutable std::shared_ptr<DataPackFolderModel> m_data_pack_list; + mutable std::shared_ptr<WorldList> m_world_list; +}; + +using MinecraftInstancePtr = std::shared_ptr<MinecraftInstance>; diff --git a/archived/projt-launcher/launcher/minecraft/MinecraftInstanceLaunchMenu.cpp b/archived/projt-launcher/launcher/minecraft/MinecraftInstanceLaunchMenu.cpp new file mode 100644 index 0000000000..aad5512b44 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/MinecraftInstanceLaunchMenu.cpp @@ -0,0 +1,84 @@ +// 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. + */ + +#include "MinecraftInstanceLaunchMenu.h" + +#include <QAction> +#include <QActionGroup> +#include <QKeySequence> + +#include <Application.h> +#include "tools/BaseProfiler.h" + +#include "MinecraftInstance.h" + +void MinecraftInstanceLaunchMenu::populate(MinecraftInstance* instance, QMenu* menu) +{ + QAction* normalLaunch = menu->addAction(MinecraftInstance::tr("&Launch")); + normalLaunch->setShortcut(QKeySequence::Open); + QAction* normalLaunchOffline = menu->addAction(MinecraftInstance::tr("Launch &Offline")); + normalLaunchOffline->setShortcut(QKeySequence(MinecraftInstance::tr("Ctrl+Shift+O"))); + QAction* normalLaunchDemo = menu->addAction(MinecraftInstance::tr("Launch &Demo")); + normalLaunchDemo->setShortcut(QKeySequence(MinecraftInstance::tr("Ctrl+Alt+O"))); + + normalLaunchDemo->setEnabled(instance->supportsDemo()); + + QObject::connect(normalLaunch, + &QAction::triggered, + [instance] { APPLICATION->launch(instance->shared_from_this()); }); + QObject::connect(normalLaunchOffline, + &QAction::triggered, + [instance] { APPLICATION->launch(instance->shared_from_this(), LaunchMode::Offline); }); + QObject::connect(normalLaunchDemo, + &QAction::triggered, + [instance] { APPLICATION->launch(instance->shared_from_this(), LaunchMode::Demo); }); + + QString profilersTitle = MinecraftInstance::tr("Profilers"); + menu->addSeparator()->setText(profilersTitle); + + auto profilers = new QActionGroup(menu); + profilers->setExclusive(true); + QObject::connect(profilers, + &QActionGroup::triggered, + [instance](QAction* action) + { + instance->settings()->set("Profiler", action->data()); + emit instance->profilerChanged(); + }); + + QAction* noProfilerAction = menu->addAction(MinecraftInstance::tr("&No Profiler")); + noProfilerAction->setData(""); + noProfilerAction->setCheckable(true); + noProfilerAction->setChecked(true); + profilers->addAction(noProfilerAction); + + for (auto profiler = APPLICATION->profilers().begin(); profiler != APPLICATION->profilers().end(); profiler++) + { + QAction* profilerAction = menu->addAction(profiler.value()->name()); + profilers->addAction(profilerAction); + profilerAction->setData(profiler.key()); + profilerAction->setCheckable(true); + profilerAction->setChecked(instance->settings()->get("Profiler").toString() == profiler.key()); + + QString error; + profilerAction->setEnabled(profiler.value()->check(&error)); + } +} diff --git a/archived/projt-launcher/launcher/minecraft/MinecraftInstanceLaunchMenu.h b/archived/projt-launcher/launcher/minecraft/MinecraftInstanceLaunchMenu.h new file mode 100644 index 0000000000..309c890cb6 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/MinecraftInstanceLaunchMenu.h @@ -0,0 +1,36 @@ +// 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. + */ + +#pragma once + +#include <QMenu> + +class MinecraftInstance; + +/** + * Helper class to populate launch menu for Minecraft instances. + * This separates UI code from the core MinecraftInstance class. + */ +class MinecraftInstanceLaunchMenu +{ + public: + static void populate(MinecraftInstance* instance, QMenu* menu); +}; diff --git a/archived/projt-launcher/launcher/minecraft/MinecraftLoadAndCheck.cpp b/archived/projt-launcher/launcher/minecraft/MinecraftLoadAndCheck.cpp new file mode 100644 index 0000000000..5ea700e6b7 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/MinecraftLoadAndCheck.cpp @@ -0,0 +1,71 @@ +// 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. + */ +#include "MinecraftLoadAndCheck.h" +#include "MinecraftInstance.h" +#include "PackProfile.h" + +MinecraftLoadAndCheck::MinecraftLoadAndCheck(MinecraftInstance* inst, Net::Mode netmode) + : m_inst(inst), + m_netmode(netmode) +{} + +void MinecraftLoadAndCheck::executeTask() +{ + // add offline metadata load task + auto components = m_inst->getPackProfile(); + if (auto result = components->reload(m_netmode); !result) + { + emitFailed(result.error); + return; + } + m_task = components->getCurrentTask(); + + if (!m_task) + { + emitSucceeded(); + return; + } + connect(m_task.get(), &Task::succeeded, this, &MinecraftLoadAndCheck::emitSucceeded); + connect(m_task.get(), &Task::failed, this, &MinecraftLoadAndCheck::emitFailed); + connect(m_task.get(), &Task::aborted, this, &MinecraftLoadAndCheck::emitAborted); + connect(m_task.get(), &Task::progress, this, &MinecraftLoadAndCheck::setProgress); + connect(m_task.get(), &Task::stepProgress, this, &MinecraftLoadAndCheck::propagateStepProgress); + connect(m_task.get(), &Task::status, this, &MinecraftLoadAndCheck::setStatus); + connect(m_task.get(), &Task::details, this, &MinecraftLoadAndCheck::setDetails); +} + +bool MinecraftLoadAndCheck::canAbort() const +{ + if (m_task) + { + return m_task->canAbort(); + } + return true; +} + +bool MinecraftLoadAndCheck::abort() +{ + if (m_task && m_task->canAbort()) + { + return m_task->abort(); + } + return Task::abort(); +} diff --git a/archived/projt-launcher/launcher/minecraft/MinecraftLoadAndCheck.h b/archived/projt-launcher/launcher/minecraft/MinecraftLoadAndCheck.h new file mode 100644 index 0000000000..f0f13f343d --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/MinecraftLoadAndCheck.h @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// 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) ============================== + * + * 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. + * ======================================================================== */ + +#pragma once + +#include "net/Mode.h" +#include "tasks/Task.h" + +class MinecraftInstance; + +class MinecraftLoadAndCheck : public Task +{ + Q_OBJECT + public: + explicit MinecraftLoadAndCheck(MinecraftInstance* inst, Net::Mode netmode); + virtual ~MinecraftLoadAndCheck() = default; + void executeTask() override; + + bool canAbort() const override; + public slots: + bool abort() override; + + private: + MinecraftInstance* m_inst = nullptr; + Task::Ptr m_task; + Net::Mode m_netmode; +}; diff --git a/archived/projt-launcher/launcher/minecraft/MojangDownloadInfo.h b/archived/projt-launcher/launcher/minecraft/MojangDownloadInfo.h new file mode 100644 index 0000000000..4efe23328a --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/MojangDownloadInfo.h @@ -0,0 +1,100 @@ +// 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. + */ +#pragma once +#include <QMap> +#include <QString> +#include <memory> + +struct MojangDownloadInfo +{ + // types + using Ptr = std::shared_ptr<MojangDownloadInfo>; + + // data + /// Local filesystem path. WARNING: not used, only here so we can pass through mojang files unmolested! + QString path; + /// absolute URL of this file + QString url; + /// sha-1 checksum of the file + QString sha1; + /// size of the file in bytes + int size; +}; + +struct MojangLibraryDownloadInfo +{ + MojangLibraryDownloadInfo(MojangDownloadInfo::Ptr artifact_) : artifact(artifact_) + {} + MojangLibraryDownloadInfo() + {} + + // types + using Ptr = std::shared_ptr<MojangLibraryDownloadInfo>; + + // methods + MojangDownloadInfo* getDownloadInfo(QString classifier) + { + if (classifier.isNull()) + { + return artifact.get(); + } + + return classifiers[classifier].get(); + } + + // data + MojangDownloadInfo::Ptr artifact; + QMap<QString, MojangDownloadInfo::Ptr> classifiers; +}; + +struct MojangAssetIndexInfo : public MojangDownloadInfo +{ + // types + using Ptr = std::shared_ptr<MojangAssetIndexInfo>; + + // methods + MojangAssetIndexInfo() + {} + + MojangAssetIndexInfo(QString id_) + { + this->id = id_; + // HACK: ignore assets from other version files than Minecraft + // workaround for stupid assets issue caused by amazon: + // https://www.theregister.co.uk/2017/02/28/aws_is_awol_as_s3_goes_haywire/ + if (id_ == "legacy") + { + url = + "https://piston-meta.mojang.com/mc/assets/legacy/c0fd82e8ce9fbc93119e40d96d5a4e62cfa3f729/legacy.json"; + } + // HACK + else + { + url = "https://s3.amazonaws.com/Minecraft.Download/indexes/" + id_ + ".json"; + } + known = false; + } + + // data + int totalSize; + QString id; + bool known = true; +}; diff --git a/archived/projt-launcher/launcher/minecraft/MojangVersionFormat.cpp b/archived/projt-launcher/launcher/minecraft/MojangVersionFormat.cpp new file mode 100644 index 0000000000..21b8fda1b7 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/MojangVersionFormat.cpp @@ -0,0 +1,461 @@ +// 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 Sefa Eyeoglu <contact@scrumplex.net> + * + * 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 "MojangVersionFormat.h" +#include "MojangDownloadInfo.h" +#include "OneSixVersionFormat.h" + +#include "Json.h" +using namespace Json; +#include <BuildConfig.h> +#include "ParseUtils.h" + +static const int CURRENT_MINIMUM_LAUNCHER_VERSION = 18; + +static MojangAssetIndexInfo::Ptr assetIndexFromJson(const QJsonObject& obj); +static MojangDownloadInfo::Ptr downloadInfoFromJson(const QJsonObject& obj); +static MojangLibraryDownloadInfo::Ptr libDownloadInfoFromJson(const QJsonObject& libObj); +static QJsonObject assetIndexToJson(MojangAssetIndexInfo::Ptr assetidxinfo); +static QJsonObject libDownloadInfoToJson(MojangLibraryDownloadInfo::Ptr libinfo); +static QJsonObject downloadInfoToJson(MojangDownloadInfo::Ptr info); + +namespace Bits +{ + static void readString(const QJsonObject& root, const QString& key, QString& variable) + { + if (root.contains(key)) + { + variable = requireString(root.value(key)); + } + } + + static void readDownloadInfo(MojangDownloadInfo::Ptr out, const QJsonObject& obj) + { + // optional, not used + readString(obj, "path", out->path); + // required! + out->sha1 = requireString(obj, "sha1"); + out->url = requireString(obj, "url"); + out->size = requireInteger(obj, "size"); + } + + static void readAssetIndex(MojangAssetIndexInfo::Ptr out, const QJsonObject& obj) + { + out->totalSize = requireInteger(obj, "totalSize"); + out->id = requireString(obj, "id"); + // out->known = true; + } +} // namespace Bits + +MojangDownloadInfo::Ptr downloadInfoFromJson(const QJsonObject& obj) +{ + auto out = std::make_shared<MojangDownloadInfo>(); + Bits::readDownloadInfo(out, obj); + return out; +} + +MojangAssetIndexInfo::Ptr assetIndexFromJson(const QJsonObject& obj) +{ + auto out = std::make_shared<MojangAssetIndexInfo>(); + Bits::readDownloadInfo(out, obj); + Bits::readAssetIndex(out, obj); + return out; +} + +QJsonObject downloadInfoToJson(MojangDownloadInfo::Ptr info) +{ + QJsonObject out; + if (!info->path.isNull()) + { + out.insert("path", info->path); + } + out.insert("sha1", info->sha1); + out.insert("size", info->size); + out.insert("url", info->url); + return out; +} + +MojangLibraryDownloadInfo::Ptr libDownloadInfoFromJson(const QJsonObject& libObj) +{ + auto out = std::make_shared<MojangLibraryDownloadInfo>(); + auto dlObj = requireObject(libObj.value("downloads")); + if (dlObj.contains("artifact")) + { + out->artifact = downloadInfoFromJson(requireObject(dlObj, "artifact")); + } + if (dlObj.contains("classifiers")) + { + auto classifiersObj = requireObject(dlObj, "classifiers"); + for (auto iter = classifiersObj.begin(); iter != classifiersObj.end(); iter++) + { + auto classifier = iter.key(); + auto classifierObj = requireObject(iter.value()); + out->classifiers[classifier] = downloadInfoFromJson(classifierObj); + } + } + return out; +} + +QJsonObject libDownloadInfoToJson(MojangLibraryDownloadInfo::Ptr libinfo) +{ + QJsonObject out; + if (libinfo->artifact) + { + out.insert("artifact", downloadInfoToJson(libinfo->artifact)); + } + if (!libinfo->classifiers.isEmpty()) + { + QJsonObject classifiersOut; + for (auto iter = libinfo->classifiers.begin(); iter != libinfo->classifiers.end(); iter++) + { + classifiersOut.insert(iter.key(), downloadInfoToJson(iter.value())); + } + out.insert("classifiers", classifiersOut); + } + return out; +} + +QJsonObject assetIndexToJson(MojangAssetIndexInfo::Ptr info) +{ + QJsonObject out; + if (!info->path.isNull()) + { + out.insert("path", info->path); + } + out.insert("sha1", info->sha1); + out.insert("size", info->size); + out.insert("url", info->url); + out.insert("totalSize", info->totalSize); + out.insert("id", info->id); + return out; +} + +void MojangVersionFormat::readVersionProperties(const QJsonObject& in, VersionFile* out) +{ + Bits::readString(in, "id", out->minecraftVersion); + Bits::readString(in, "mainClass", out->mainClass); + Bits::readString(in, "minecraftArguments", out->minecraftArguments); + Bits::readString(in, "type", out->type); + + Bits::readString(in, "assets", out->assets); + if (in.contains("assetIndex")) + { + out->mojangAssetIndex = assetIndexFromJson(requireObject(in, "assetIndex")); + } + else if (!out->assets.isNull()) + { + out->mojangAssetIndex = std::make_shared<MojangAssetIndexInfo>(out->assets); + } + + out->releaseTime = timeFromS3Time(in.value("releaseTime").toString("")); + out->updateTime = timeFromS3Time(in.value("time").toString("")); + + if (in.contains("minimumLauncherVersion")) + { + out->minimumLauncherVersion = requireInteger(in.value("minimumLauncherVersion")); + if (out->minimumLauncherVersion > CURRENT_MINIMUM_LAUNCHER_VERSION) + { + out->addProblem(ProblemSeverity::Warning, + QObject::tr("The 'minimumLauncherVersion' value of this version (%1) is higher than " + "supported by %3 (%2). It might not work properly!") + .arg(out->minimumLauncherVersion) + .arg(CURRENT_MINIMUM_LAUNCHER_VERSION) + .arg(BuildConfig.LAUNCHER_DISPLAYNAME)); + } + } + + if (in.contains("compatibleJavaMajors")) + { + for (auto compatible : requireArray(in.value("compatibleJavaMajors"))) + { + out->compatibleJavaMajors.append(requireInteger(compatible)); + } + } + if (in.contains("compatibleJavaName")) + { + out->compatibleJavaName = requireString(in.value("compatibleJavaName")); + } + + if (in.contains("downloads")) + { + auto downloadsObj = requireObject(in, "downloads"); + for (auto iter = downloadsObj.begin(); iter != downloadsObj.end(); iter++) + { + auto classifier = iter.key(); + auto classifierObj = requireObject(iter.value()); + out->mojangDownloads[classifier] = downloadInfoFromJson(classifierObj); + } + } +} + +VersionFilePtr MojangVersionFormat::versionFileFromJson(const QJsonDocument& doc, const QString& filename) +{ + VersionFilePtr out(new VersionFile()); + if (doc.isEmpty() || doc.isNull()) + { + throw JSONValidationError(filename + " is empty or null"); + } + if (!doc.isObject()) + { + throw JSONValidationError(filename + " is not an object"); + } + + QJsonObject root = doc.object(); + + readVersionProperties(root, out.get()); + + out->name = "Minecraft"; + out->uid = "net.minecraft"; + out->version = out->minecraftVersion; + // out->filename = filename; + + if (root.contains("libraries")) + { + for (auto libVal : requireArray(root.value("libraries"))) + { + auto libObj = requireObject(libVal); + + auto lib = MojangVersionFormat::libraryFromJson(*out, libObj, filename); + out->libraries.append(lib); + } + } + return out; +} + +void MojangVersionFormat::writeVersionProperties(const VersionFile* in, QJsonObject& out) +{ + writeString(out, "id", in->minecraftVersion); + writeString(out, "mainClass", in->mainClass); + writeString(out, "minecraftArguments", in->minecraftArguments); + writeString(out, "type", in->type); + if (!in->releaseTime.isNull()) + { + writeString(out, "releaseTime", timeToS3Time(in->releaseTime)); + } + if (!in->updateTime.isNull()) + { + writeString(out, "time", timeToS3Time(in->updateTime)); + } + if (in->minimumLauncherVersion != -1) + { + out.insert("minimumLauncherVersion", in->minimumLauncherVersion); + } + writeString(out, "assets", in->assets); + if (in->mojangAssetIndex && in->mojangAssetIndex->known) + { + out.insert("assetIndex", assetIndexToJson(in->mojangAssetIndex)); + } + if (!in->mojangDownloads.isEmpty()) + { + QJsonObject downloadsOut; + for (auto iter = in->mojangDownloads.begin(); iter != in->mojangDownloads.end(); iter++) + { + downloadsOut.insert(iter.key(), downloadInfoToJson(iter.value())); + } + out.insert("downloads", downloadsOut); + } + if (!in->compatibleJavaMajors.isEmpty()) + { + QJsonArray compatibleJavaMajorsOut; + for (auto compatibleJavaMajor : in->compatibleJavaMajors) + { + compatibleJavaMajorsOut.append(compatibleJavaMajor); + } + out.insert("compatibleJavaMajors", compatibleJavaMajorsOut); + } + if (!in->compatibleJavaName.isEmpty()) + { + writeString(out, "compatibleJavaName", in->compatibleJavaName); + } +} + +QJsonDocument MojangVersionFormat::versionFileToJson(const VersionFilePtr& patch) +{ + QJsonObject root; + writeVersionProperties(patch.get(), root); + if (!patch->libraries.isEmpty()) + { + QJsonArray array; + for (auto value : patch->libraries) + { + array.append(MojangVersionFormat::libraryToJson(value.get())); + } + root.insert("libraries", array); + } + + // write the contents to a json document. + { + QJsonDocument out; + out.setObject(root); + return out; + } +} + +LibraryPtr MojangVersionFormat::libraryFromJson(ProblemContainer& problems, + const QJsonObject& libObj, + const QString& filename) +{ + LibraryPtr out(new Library()); + if (!libObj.contains("name")) + { + throw JSONValidationError(filename + "contains a library that doesn't have a 'name' field"); + } + auto rawName = libObj.value("name").toString(); + out->m_name = rawName; + if (!out->m_name.valid()) + { + problems.addProblem(ProblemSeverity::Error, + QObject::tr("Library %1 name is broken and cannot be processed.").arg(rawName)); + } + + Bits::readString(libObj, "url", out->m_repositoryURL); + if (libObj.contains("extract")) + { + out->m_hasExcludes = true; + auto extractObj = requireObject(libObj.value("extract")); + for (auto excludeVal : requireArray(extractObj.value("exclude"))) + { + out->m_extractExcludes.append(requireString(excludeVal)); + } + } + if (libObj.contains("natives")) + { + QJsonObject nativesObj = requireObject(libObj.value("natives")); + for (auto it = nativesObj.begin(); it != nativesObj.end(); ++it) + { + if (!it.value().isString()) + { + qWarning() << filename << "contains an invalid native (skipping)"; + continue; + } + // Skip unknown platforms + QString platform = it.key(); + if (platform != "linux" && platform != "windows" && platform != "osx" && platform != "macos" + && platform != "freebsd" && platform != "openbsd" && platform != "netbsd") + { + qWarning() << filename << "contains unknown platform" << platform << "(skipping)"; + continue; + } + out->m_nativeClassifiers[it.key()] = it.value().toString(); + } + } + if (libObj.contains("rules")) + { + out->applyRules = true; + + QJsonArray rulesArray = requireArray(libObj.value("rules")); + for (auto rule : rulesArray) + { + out->m_rules.append(Rule::fromJson(requireObject(rule))); + } + } + if (libObj.contains("downloads")) + { + out->m_mojangDownloads = libDownloadInfoFromJson(libObj); + } + return out; +} + +QJsonObject MojangVersionFormat::libraryToJson(Library* library) +{ + QJsonObject libRoot; + libRoot.insert("name", library->m_name.serialize()); + if (!library->m_repositoryURL.isEmpty()) + { + libRoot.insert("url", library->m_repositoryURL); + } + if (library->isNative()) + { + QJsonObject nativeList; + auto iter = library->m_nativeClassifiers.begin(); + while (iter != library->m_nativeClassifiers.end()) + { + nativeList.insert(iter.key(), iter.value()); + iter++; + } + libRoot.insert("natives", nativeList); + if (!library->m_extractExcludes.isEmpty()) + { + QJsonArray excludes; + QJsonObject extract; + for (auto exclude : library->m_extractExcludes) + { + excludes.append(exclude); + } + extract.insert("exclude", excludes); + libRoot.insert("extract", extract); + } + } + if (!library->m_rules.isEmpty()) + { + QJsonArray allRules; + for (auto& rule : library->m_rules) + { + QJsonObject ruleObj = rule.toJson(); + allRules.append(ruleObj); + } + libRoot.insert("rules", allRules); + } + if (library->m_mojangDownloads) + { + auto downloadsObj = libDownloadInfoToJson(library->m_mojangDownloads); + libRoot.insert("downloads", downloadsObj); + } + return libRoot; +} diff --git a/archived/projt-launcher/launcher/minecraft/MojangVersionFormat.h b/archived/projt-launcher/launcher/minecraft/MojangVersionFormat.h new file mode 100644 index 0000000000..26eb64692a --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/MojangVersionFormat.h @@ -0,0 +1,46 @@ +// 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. + */ +#pragma once + +#include <ProblemProvider.h> +#include <minecraft/Library.h> +#include <minecraft/VersionFile.h> +#include <QJsonDocument> + +class MojangVersionFormat +{ + friend class OneSixVersionFormat; + + protected: + // does not include libraries + static void readVersionProperties(const QJsonObject& in, VersionFile* out); + // does not include libraries + static void writeVersionProperties(const VersionFile* in, QJsonObject& out); + + public: + // version files / profile patches + static VersionFilePtr versionFileFromJson(const QJsonDocument& doc, const QString& filename); + static QJsonDocument versionFileToJson(const VersionFilePtr& patch); + + // libraries + static LibraryPtr libraryFromJson(ProblemContainer& problems, const QJsonObject& libObj, const QString& filename); + static QJsonObject libraryToJson(Library* library); +}; diff --git a/archived/projt-launcher/launcher/minecraft/OneSixVersionFormat.cpp b/archived/projt-launcher/launcher/minecraft/OneSixVersionFormat.cpp new file mode 100644 index 0000000000..6aba9fe5df --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/OneSixVersionFormat.cpp @@ -0,0 +1,513 @@ +// 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 Sefa Eyeoglu <contact@scrumplex.net> + * + * 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 "OneSixVersionFormat.h" +#include <Json.h> +#include <minecraft/MojangVersionFormat.h> +#include <QList> +#include "java/core/RuntimePackage.hpp" +#include "minecraft/Agent.h" +#include "minecraft/ParseUtils.h" + +#include <QRegularExpression> + +using namespace Json; + +static void readString(const QJsonObject& root, const QString& key, QString& variable) +{ + if (root.contains(key)) + { + variable = requireString(root.value(key)); + } +} + +LibraryPtr OneSixVersionFormat::libraryFromJson(ProblemContainer& problems, + const QJsonObject& libObj, + const QString& filename) +{ + LibraryPtr out = MojangVersionFormat::libraryFromJson(problems, libObj, filename); + readString(libObj, "MMC-hint", out->m_hint); + readString(libObj, "MMC-absulute_url", out->m_absoluteURL); + readString(libObj, "MMC-absoluteUrl", out->m_absoluteURL); + readString(libObj, "MMC-filename", out->m_filename); + readString(libObj, "MMC-displayname", out->m_displayname); + return out; +} + +QJsonObject OneSixVersionFormat::libraryToJson(Library* library) +{ + QJsonObject libRoot = MojangVersionFormat::libraryToJson(library); + if (!library->m_absoluteURL.isEmpty()) + libRoot.insert("MMC-absoluteUrl", library->m_absoluteURL); + if (!library->m_hint.isEmpty()) + libRoot.insert("MMC-hint", library->m_hint); + if (!library->m_filename.isEmpty()) + libRoot.insert("MMC-filename", library->m_filename); + if (!library->m_displayname.isEmpty()) + libRoot.insert("MMC-displayname", library->m_displayname); + return libRoot; +} + +VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument& doc, + const QString& filename, + const bool requireOrder) +{ + VersionFilePtr out(new VersionFile()); + if (doc.isEmpty() || doc.isNull()) + { + throw JSONValidationError(filename + " is empty or null"); + } + if (!doc.isObject()) + { + throw JSONValidationError(filename + " is not an object"); + } + + QJsonObject root = doc.object(); + + projt::meta::SchemaVersion formatVersion = projt::meta::detectSchemaVersion(root, false); + switch (formatVersion) + { + case projt::meta::SchemaVersion::V1: break; + case projt::meta::SchemaVersion::Unknown: + throw JSONValidationError(filename + " does not contain a recognizable version of the metadata format."); + } + + if (requireOrder) + { + if (root.contains("order")) + { + out->order = requireInteger(root.value("order")); + } + else + { + // Order is required but missing - this is an error condition + throw JSONValidationError(filename + " requires an order field but doesn't contain one."); + } + } + + out->name = root.value("name").toString(); + + if (root.contains("uid")) + { + out->uid = root.value("uid").toString(); + } + else + { + out->uid = root.value("fileId").toString(); + } + + static const QRegularExpression s_validUidRegex{ QRegularExpression::anchoredPattern( + QStringLiteral(R"([a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]+)*)")) }; + if (!s_validUidRegex.match(out->uid).hasMatch()) + { + qCritical() << "The component's 'uid' contains illegal characters! UID:" << out->uid; + out->addProblem( + ProblemSeverity::Error, + QObject::tr("The component's 'uid' contains illegal characters! This can cause security issues.")); + } + + out->version = root.value("version").toString(); + + MojangVersionFormat::readVersionProperties(root, out.get()); + + if (root.contains("+tweakers")) + { + for (auto tweakerVal : requireArray(root.value("+tweakers"))) + { + out->addTweakers.append(requireString(tweakerVal)); + } + } + + if (root.contains("+traits")) + { + for (auto tweakerVal : requireArray(root.value("+traits"))) + { + out->traits.insert(requireString(tweakerVal)); + } + } + + if (root.contains("+jvmArgs")) + { + for (auto arg : requireArray(root.value("+jvmArgs"))) + { + out->addnJvmArguments.append(requireString(arg)); + } + } + + if (root.contains("jarMods")) + { + for (auto libVal : requireArray(root.value("jarMods"))) + { + QJsonObject libObj = requireObject(libVal); + // parse the jarmod + auto lib = OneSixVersionFormat::jarModFromJson(*out, libObj, filename); + // and add to jar mods + out->jarMods.append(lib); + } + } + else if (root.contains("+jarMods")) // DEPRECATED: old style '+jarMods' are only here for backwards compatibility + { + for (auto libVal : requireArray(root.value("+jarMods"))) + { + QJsonObject libObj = requireObject(libVal); + // parse the jarmod + auto lib = OneSixVersionFormat::plusJarModFromJson(*out, libObj, filename, out->name); + // and add to jar mods + out->jarMods.append(lib); + } + } + + if (root.contains("mods")) + { + for (auto libVal : requireArray(root.value("mods"))) + { + QJsonObject libObj = requireObject(libVal); + // parse the jarmod + auto lib = OneSixVersionFormat::modFromJson(*out, libObj, filename); + // and add to jar mods + out->mods.append(lib); + } + } + + auto readLibs = [&root, &out, &filename](const char* which, QList<LibraryPtr>& outList) + { + for (auto libVal : requireArray(root.value(which))) + { + QJsonObject libObj = requireObject(libVal); + // parse the library + auto lib = libraryFromJson(*out, libObj, filename); + outList.append(lib); + } + }; + bool hasPlusLibs = root.contains("+libraries"); + bool hasLibs = root.contains("libraries"); + if (hasPlusLibs && hasLibs) + { + out->addProblem( + ProblemSeverity::Warning, + QObject::tr("Version file has both '+libraries' and 'libraries'. This is no longer supported.")); + readLibs("libraries", out->libraries); + readLibs("+libraries", out->libraries); + } + else if (hasLibs) + { + readLibs("libraries", out->libraries); + } + else if (hasPlusLibs) + { + readLibs("+libraries", out->libraries); + } + + if (root.contains("mavenFiles")) + { + readLibs("mavenFiles", out->mavenFiles); + } + + if (root.contains("+agents")) + { + for (auto agentVal : requireArray(root.value("+agents"))) + { + QJsonObject agentObj = requireObject(agentVal); + auto lib = libraryFromJson(*out, agentObj, filename); + + QString arg = ""; + readString(agentObj, "argument", arg); + + AgentPtr agent(new Agent(lib, arg)); + out->agents.append(agent); + } + } + + // if we have mainJar, just use it + if (root.contains("mainJar")) + { + QJsonObject libObj = requireObject(root, "mainJar"); + out->mainJar = libraryFromJson(*out, libObj, filename); + } + // else reconstruct it from downloads and id ... if that's available + else if (!out->minecraftVersion.isEmpty()) + { + auto lib = std::make_shared<Library>(); + lib->setRawName(GradleSpecifier(QString("com.mojang:minecraft:%1:client").arg(out->minecraftVersion))); + // we have a reliable client download, use it. + if (out->mojangDownloads.contains("client")) + { + auto LibDLInfo = std::make_shared<MojangLibraryDownloadInfo>(); + LibDLInfo->artifact = out->mojangDownloads["client"]; + lib->setMojangDownloadInfo(LibDLInfo); + } + // we got nothing... + else + { + out->addProblem(ProblemSeverity::Error, + QObject::tr("URL for the main jar could not be determined - Mojang removed the server that " + "we used as fallback.")); + } + out->mainJar = lib; + } + + if (root.contains("requires")) + { + out->m_requires = projt::meta::parseDependencies(root, "requires"); + } + QString dependsOnMinecraftVersion = root.value("mcVersion").toString(); + if (!dependsOnMinecraftVersion.isEmpty()) + { + projt::meta::ComponentDependency mcReq; + mcReq.uid = "net.minecraft"; + mcReq.equalsVersion = dependsOnMinecraftVersion; + if (out->m_requires.count(mcReq) == 0) + { + out->m_requires.insert(mcReq); + } + } + if (root.contains("conflicts")) + { + out->conflicts = projt::meta::parseDependencies(root, "conflicts"); + } + if (root.contains("volatile")) + { + out->m_volatile = requireBoolean(root, "volatile"); + } + + if (root.contains("runtimes")) + { + out->runtimes = {}; + for (auto runtime : ensureArray(root, "runtimes")) + { + out->runtimes.append(projt::java::parseRuntimePackage(ensureObject(runtime))); + } + } + + /* removed features that shouldn't be used */ + if (root.contains("tweakers")) + { + out->addProblem(ProblemSeverity::Error, QObject::tr("Version file contains unsupported element 'tweakers'")); + } + if (root.contains("-libraries")) + { + out->addProblem(ProblemSeverity::Error, QObject::tr("Version file contains unsupported element '-libraries'")); + } + if (root.contains("-tweakers")) + { + out->addProblem(ProblemSeverity::Error, QObject::tr("Version file contains unsupported element '-tweakers'")); + } + if (root.contains("-minecraftArguments")) + { + out->addProblem(ProblemSeverity::Error, + QObject::tr("Version file contains unsupported element '-minecraftArguments'")); + } + if (root.contains("+minecraftArguments")) + { + out->addProblem(ProblemSeverity::Error, + QObject::tr("Version file contains unsupported element '+minecraftArguments'")); + } + return out; +} + +QJsonDocument OneSixVersionFormat::versionFileToJson(const VersionFilePtr& patch) +{ + QJsonObject root; + writeString(root, "name", patch->name); + + writeString(root, "uid", patch->uid); + + writeString(root, "version", patch->version); + + projt::meta::writeSchemaVersion(root, projt::meta::SchemaVersion::V1); + + MojangVersionFormat::writeVersionProperties(patch.get(), root); + + if (patch->mainJar) + { + root.insert("mainJar", libraryToJson(patch->mainJar.get())); + } + writeString(root, "appletClass", patch->appletClass); + writeStringList(root, "+tweakers", patch->addTweakers); + writeStringList(root, "+traits", patch->traits.values()); + writeStringList(root, "+jvmArgs", patch->addnJvmArguments); + if (!patch->agents.isEmpty()) + { + QJsonArray array; + for (auto value : patch->agents) + { + QJsonObject agentOut = OneSixVersionFormat::libraryToJson(value->library().get()); + if (!value->argument().isEmpty()) + agentOut.insert("argument", value->argument()); + + array.append(agentOut); + } + root.insert("+agents", array); + } + if (!patch->libraries.isEmpty()) + { + QJsonArray array; + for (auto value : patch->libraries) + { + array.append(OneSixVersionFormat::libraryToJson(value.get())); + } + root.insert("libraries", array); + } + if (!patch->mavenFiles.isEmpty()) + { + QJsonArray array; + for (auto value : patch->mavenFiles) + { + array.append(OneSixVersionFormat::libraryToJson(value.get())); + } + root.insert("mavenFiles", array); + } + if (!patch->jarMods.isEmpty()) + { + QJsonArray array; + for (auto value : patch->jarMods) + { + array.append(OneSixVersionFormat::jarModtoJson(value.get())); + } + root.insert("jarMods", array); + } + if (!patch->mods.isEmpty()) + { + QJsonArray array; + for (auto value : patch->jarMods) + { + array.append(OneSixVersionFormat::modtoJson(value.get())); + } + root.insert("mods", array); + } + if (!patch->m_requires.empty()) + { + projt::meta::writeDependencies(root, patch->m_requires, "requires"); + } + if (!patch->conflicts.empty()) + { + projt::meta::writeDependencies(root, patch->conflicts, "conflicts"); + } + if (patch->m_volatile) + { + root.insert("volatile", true); + } + // write the contents to a json document. + { + QJsonDocument out; + out.setObject(root); + return out; + } +} + +LibraryPtr OneSixVersionFormat::plusJarModFromJson([[maybe_unused]] ProblemContainer& problems, + const QJsonObject& libObj, + const QString& filename, + const QString& originalName) +{ + LibraryPtr out(new Library()); + if (!libObj.contains("name")) + { + throw JSONValidationError(filename + "contains a jarmod that doesn't have a 'name' field"); + } + + // just make up something unique on the spot for the library name. + QString id = QUuid::createUuid().toString(QUuid::WithoutBraces); + out->setRawName(GradleSpecifier("org.multimc.jarmods:" + id + ":1")); + + // filename override is the old name + out->setFilename(libObj.value("name").toString()); + + // it needs to be local, it is stored in the instance jarmods folder + out->setHint("local"); + + // read the original name if present - some versions did not set it + // it is the original jar mod filename before it got renamed at the point of addition + auto displayName = libObj.value("originalName").toString(); + if (displayName.isEmpty()) + { + auto fixed = originalName; + fixed.remove(" (jar mod)"); + out->setDisplayName(fixed); + } + else + { + out->setDisplayName(displayName); + } + return out; +} + +LibraryPtr OneSixVersionFormat::jarModFromJson(ProblemContainer& problems, + const QJsonObject& libObj, + const QString& filename) +{ + return libraryFromJson(problems, libObj, filename); +} + +QJsonObject OneSixVersionFormat::jarModtoJson(Library* jarmod) +{ + return libraryToJson(jarmod); +} + +LibraryPtr OneSixVersionFormat::modFromJson(ProblemContainer& problems, + const QJsonObject& libObj, + const QString& filename) +{ + return libraryFromJson(problems, libObj, filename); +} + +QJsonObject OneSixVersionFormat::modtoJson(Library* jarmod) +{ + return libraryToJson(jarmod); +} diff --git a/archived/projt-launcher/launcher/minecraft/OneSixVersionFormat.h b/archived/projt-launcher/launcher/minecraft/OneSixVersionFormat.h new file mode 100644 index 0000000000..669997ff0a --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/OneSixVersionFormat.h @@ -0,0 +1,53 @@ +// 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. + */ +#pragma once + +#include <ProblemProvider.h> +#include <minecraft/Library.h> +#include <minecraft/PackProfile.h> +#include <minecraft/VersionFile.h> +#include <QJsonDocument> + +class OneSixVersionFormat +{ + public: + // version files / profile patches + static VersionFilePtr versionFileFromJson(const QJsonDocument& doc, const QString& filename, bool requireOrder); + static QJsonDocument versionFileToJson(const VersionFilePtr& patch); + + // libraries + static LibraryPtr libraryFromJson(ProblemContainer& problems, const QJsonObject& libObj, const QString& filename); + static QJsonObject libraryToJson(Library* library); + + // DEPRECATED: old 'plus' jar mods generated by the application + static LibraryPtr plusJarModFromJson(ProblemContainer& problems, + const QJsonObject& libObj, + const QString& filename, + const QString& originalName); + + // new jar mods derived from libraries + static LibraryPtr jarModFromJson(ProblemContainer& problems, const QJsonObject& libObj, const QString& filename); + static QJsonObject jarModtoJson(Library* jarmod); + + // mods, also derived from libraries + static LibraryPtr modFromJson(ProblemContainer& problems, const QJsonObject& libObj, const QString& filename); + static QJsonObject modtoJson(Library* jarmod); +}; diff --git a/archived/projt-launcher/launcher/minecraft/PackProfile.cpp b/archived/projt-launcher/launcher/minecraft/PackProfile.cpp new file mode 100644 index 0000000000..0fd908e8a6 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/PackProfile.cpp @@ -0,0 +1,1311 @@ +// 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-2023 Sefa Eyeoglu <contact@scrumplex.net> + * Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me> + * + * 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 <Version.h> +#include <qlogging.h> +#include <QCryptographicHash> +#include <QDebug> +#include <QDir> +#include <QFile> +#include <QJsonArray> +#include <QJsonDocument> +#include <QSaveFile> +#include <QTimer> +#include <QUuid> +#include <algorithm> +#include <utility> + +#include "Application.h" +#include "Exception.h" +#include "FileSystem.h" +#include "Json.h" +#include "meta/Index.hpp" +#include "meta/JsonFormat.hpp" +#include "minecraft/Component.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/OneSixVersionFormat.h" +#include "minecraft/ProfileUtils.h" + +#include "ComponentUpdateTask.h" +#include "PackProfile.h" +#include "PackProfile_p.h" +#include "modplatform/ModIndex.h" + +#include "minecraft/Logging.h" + +#include "ui/dialogs/CustomMessageBox.h" + +PackProfile::PackProfile(MinecraftInstance* instance) : QAbstractListModel() +{ + d.reset(new PackProfileData); + d->m_instance = instance; + d->m_saveTimer.setSingleShot(true); + d->m_saveTimer.setInterval(5000); + d->interactionDisabled = instance->isRunning(); + connect(d->m_instance, &BaseInstance::runningStatusChanged, this, &PackProfile::disableInteraction); + connect(&d->m_saveTimer, &QTimer::timeout, this, &PackProfile::save_internal); +} + +PackProfile::~PackProfile() +{ + saveNow(); +} + +// BEGIN: component file format + +static const int currentComponentsFileVersion = 1; + +static QJsonObject componentToJsonV1(ComponentPtr component) +{ + QJsonObject obj; + // critical + obj.insert("uid", component->m_uid); + if (!component->m_version.isEmpty()) + { + obj.insert("version", component->m_version); + } + if (component->m_dependencyOnly) + { + obj.insert("dependencyOnly", true); + } + if (component->m_important) + { + obj.insert("important", true); + } + if (component->m_disabled) + { + obj.insert("disabled", true); + } + + // cached + if (!component->m_cachedVersion.isEmpty()) + { + obj.insert("cachedVersion", component->m_cachedVersion); + } + if (!component->m_cachedName.isEmpty()) + { + obj.insert("cachedName", component->m_cachedName); + } + projt::meta::writeDependencies(obj, component->m_cachedRequires, "cachedRequires"); + projt::meta::writeDependencies(obj, component->m_cachedConflicts, "cachedConflicts"); + if (component->m_cachedVolatile) + { + obj.insert("cachedVolatile", true); + } + return obj; +} + +static ComponentPtr componentFromJsonV1(PackProfile* parent, + const QString& componentJsonPattern, + const QJsonObject& obj) +{ + // critical + auto uid = Json::requireString(obj.value("uid")); + auto filePath = componentJsonPattern.arg(uid); + auto component = makeShared<Component>(parent, uid); + component->m_version = Json::ensureString(obj.value("version")); + component->m_dependencyOnly = Json::ensureBoolean(obj.value("dependencyOnly"), false); + component->m_important = Json::ensureBoolean(obj.value("important"), false); + + // Cached values - use safe parsing with fallbacks for resilience + // Invalid or missing values are silently ignored to allow loading of + // partially corrupted profiles + try + { + component->m_cachedVersion = Json::ensureString(obj.value("cachedVersion")); + } + catch (...) + { + component->m_cachedVersion = QString(); + } + + try + { + component->m_cachedName = Json::ensureString(obj.value("cachedName")); + } + catch (...) + { + component->m_cachedName = QString(); + } + + try + { + component->m_cachedRequires = projt::meta::parseDependencies(obj, "cachedRequires"); + } + catch (...) + { + component->m_cachedRequires = {}; + } + + try + { + component->m_cachedConflicts = projt::meta::parseDependencies(obj, "cachedConflicts"); + } + catch (...) + { + component->m_cachedConflicts = {}; + } + + component->m_cachedVolatile = Json::ensureBoolean(obj.value("volatile"), false); + bool disabled = Json::ensureBoolean(obj.value("disabled"), false); + component->setEnabled(!disabled); + return component; +} + +// Save the given component container data to a file +static bool savePackProfile(const QString& filename, const ComponentContainer& container) +{ + QJsonObject obj; + obj.insert("formatVersion", currentComponentsFileVersion); + QJsonArray orderArray; + for (auto component : container) + { + orderArray.append(componentToJsonV1(component)); + } + obj.insert("components", orderArray); + QSaveFile outFile(filename); + if (!outFile.open(QFile::WriteOnly)) + { + qCCritical(instanceProfileC) << "Couldn't open" << outFile.fileName() + << "for writing:" << outFile.errorString(); + return false; + } + auto data = QJsonDocument(obj).toJson(QJsonDocument::Indented); + if (outFile.write(data) != data.size()) + { + qCCritical(instanceProfileC) << "Couldn't write all the data into" << outFile.fileName() + << "because:" << outFile.errorString(); + return false; + } + if (!outFile.commit()) + { + qCCritical(instanceProfileC) << "Couldn't save" << outFile.fileName() << "because:" << outFile.errorString(); + } + return true; +} + +// Read the given file into component containers +static PackProfile::Result loadPackProfile(PackProfile* parent, + const QString& filename, + const QString& componentJsonPattern, + ComponentContainer& container) +{ + QFile componentsFile(filename); + if (!componentsFile.exists()) + { + auto message = QObject::tr("Components file %1 doesn't exist. This should never happen.").arg(filename); + qCWarning(instanceProfileC) << message; + return PackProfile::Result::Error(message); + } + if (!componentsFile.open(QFile::ReadOnly)) + { + auto message = QObject::tr("Couldn't open %1 for reading: %2") + .arg(componentsFile.fileName(), componentsFile.errorString()); + qCCritical(instanceProfileC) << message; + qCWarning(instanceProfileC) << "Ignoring overridden order"; + return PackProfile::Result::Error(message); + } + + // and it's valid JSON + QJsonParseError error; + QJsonDocument doc = QJsonDocument::fromJson(componentsFile.readAll(), &error); + if (error.error != QJsonParseError::NoError) + { + auto message = QObject::tr("Couldn't parse %1 as json: %2").arg(componentsFile.fileName(), error.errorString()); + qCCritical(instanceProfileC) << message; + qCWarning(instanceProfileC) << "Ignoring overridden order"; + return PackProfile::Result::Error(message); + } + + // and then read it and process it if all above is true. + try + { + auto obj = Json::requireObject(doc); + // check order file version. + auto version = Json::requireInteger(obj.value("formatVersion")); + if (version != currentComponentsFileVersion) + { + throw JSONValidationError( + QObject::tr("Invalid component file version, expected %1").arg(currentComponentsFileVersion)); + } + auto orderArray = Json::requireArray(obj.value("components")); + for (auto item : orderArray) + { + auto comp_obj = Json::requireObject(item, "Component must be an object."); + container.append(componentFromJsonV1(parent, componentJsonPattern, comp_obj)); + } + } + catch ([[maybe_unused]] const JSONValidationError& err) + { + auto message = QObject::tr("Couldn't parse %1 : bad file format").arg(componentsFile.fileName()); + qCCritical(instanceProfileC) << message; + qCWarning(instanceProfileC) << "error:" << err.what(); + container.clear(); + return PackProfile::Result::Error(message); + } + return PackProfile::Result::Success(); +} + +// END: component file format + +// BEGIN: save/load logic + +void PackProfile::saveNow() +{ + if (saveIsScheduled()) + { + d->m_saveTimer.stop(); + save_internal(); + } +} + +bool PackProfile::saveIsScheduled() const +{ + return d->dirty; +} + +void PackProfile::buildingFromScratch() +{ + d->loaded = true; + d->dirty = true; +} + +void PackProfile::scheduleSave() +{ + if (!d->loaded) + { + qDebug() << d->m_instance->name() << "|" + << "Component list should never save if it didn't successfully load"; + return; + } + if (!d->dirty) + { + d->dirty = true; + qDebug() << d->m_instance->name() << "|" + << "Component list save is scheduled"; + } + d->m_saveTimer.start(); +} + +RuntimeContext PackProfile::runtimeContext() +{ + return d->m_instance->runtimeContext(); +} + +QString PackProfile::componentsFilePath() const +{ + return FS::PathCombine(d->m_instance->instanceRoot(), "mmc-pack.json"); +} + +QString PackProfile::patchesPattern() const +{ + return FS::PathCombine(d->m_instance->instanceRoot(), "patches", "%1.json"); +} + +QString PackProfile::patchFilePathForUid(const QString& uid) const +{ + return patchesPattern().arg(uid); +} + +void PackProfile::save_internal() +{ + qDebug() << d->m_instance->name() << "|" + << "Component list save performed now"; + auto filename = componentsFilePath(); + savePackProfile(filename, d->components); + d->dirty = false; +} + +PackProfile::Result PackProfile::load() +{ + auto filename = componentsFilePath(); + + // load the new component list and swap it with the current one... + ComponentContainer newComponents; + if (auto result = loadPackProfile(this, filename, patchesPattern(), newComponents); !result) + { + qCritical() << d->m_instance->name() << "|" + << "Failed to load the component config"; + return result; + } + + // Optimization: check if there are any changes before resetting the model + bool changed = false; + if (d->components.size() != newComponents.size()) + { + changed = true; + } + else + { + for (int i = 0; i < d->components.size(); ++i) + { + const auto& oldC = d->components[i]; + const auto& newC = newComponents[i]; + if (oldC->getID() != newC->getID() || oldC->getVersion() != newC->getVersion() + || oldC->m_important != newC->m_important || oldC->m_disabled != newC->m_disabled + || oldC->m_dependencyOnly != newC->m_dependencyOnly) + { + changed = true; + break; + } + } + } + + if (!changed) + { + d->loaded = true; + return Result::Success(false); + } + + // NOTE: actually use fine-grained updates, not this... + beginResetModel(); + // disconnect all the old components + for (auto component : d->components) + { + disconnect(component.get(), &Component::dataChanged, this, &PackProfile::componentDataChanged); + } + d->components.clear(); + d->componentIndex.clear(); + for (auto component : newComponents) + { + if (d->componentIndex.contains(component->m_uid)) + { + qWarning() << d->m_instance->name() << "|" + << "Ignoring duplicate component entry" << component->m_uid; + continue; + } + connect(component.get(), &Component::dataChanged, this, &PackProfile::componentDataChanged); + d->components.append(component); + d->componentIndex[component->m_uid] = component; + } + endResetModel(); + d->loaded = true; + return Result::Success(); +} + +PackProfile::Result PackProfile::reload(Net::Mode netmode) +{ + // Do not reload when the update/resolve task is running. It is in control. + if (d->m_updateTask) + { + return Result::Success(); + } + + // flush any scheduled saves to not lose state + saveNow(); + + auto result = load(); + if (!result) + { + return result; + } + + if (result.changed) + { + invalidateLaunchProfile(); + } + + resolve(netmode); + return Result::Success(); +} + +Task::Ptr PackProfile::getCurrentTask() +{ + return d->m_updateTask; +} + +void PackProfile::resolve(Net::Mode netmode) +{ + // Use Mode::Launch to ensure version details are downloaded, not just version lists + auto updateTask = new ComponentUpdateTask(ComponentUpdateTask::Mode::Launch, netmode, this); + d->m_updateTask.reset(updateTask); + connect(updateTask, &ComponentUpdateTask::succeeded, this, &PackProfile::updateSucceeded); + connect(updateTask, &ComponentUpdateTask::failed, this, &PackProfile::updateFailed); + connect(updateTask, &ComponentUpdateTask::aborted, this, [this] { updateFailed(tr("Aborted")); }); + d->m_updateTask->start(); +} + +void PackProfile::updateSucceeded() +{ + qCDebug(instanceProfileC) << d->m_instance->name() << "|" + << "Component list update/resolve task succeeded"; + d->m_updateTask.reset(); + invalidateLaunchProfile(); +} + +void PackProfile::updateFailed(const QString& error) +{ + qCDebug(instanceProfileC) << d->m_instance->name() << "|" + << "Component list update/resolve task failed " + << "Reason:" << error; + d->m_updateTask.reset(); + invalidateLaunchProfile(); +} + +// END: save/load + +void PackProfile::appendComponent(ComponentPtr component) +{ + insertComponent(d->components.size(), component); +} + +void PackProfile::insertComponent(size_t index, ComponentPtr component) +{ + auto id = component->getID(); + if (id.isEmpty()) + { + qCWarning(instanceProfileC) << d->m_instance->name() << "|" + << "Attempt to add a component with empty ID!"; + return; + } + if (d->componentIndex.contains(id)) + { + qCWarning(instanceProfileC) << d->m_instance->name() << "|" + << "Attempt to add a component that is already present!"; + return; + } + beginInsertRows(QModelIndex(), static_cast<int>(index), static_cast<int>(index)); + d->components.insert(index, component); + d->componentIndex[id] = component; + endInsertRows(); + connect(component.get(), &Component::dataChanged, this, &PackProfile::componentDataChanged); + scheduleSave(); +} + +void PackProfile::componentDataChanged() +{ + auto objPtr = qobject_cast<Component*>(sender()); + if (!objPtr) + { + qCWarning(instanceProfileC) << d->m_instance->name() << "|" + << "PackProfile got dataChanged signal from a non-Component!"; + return; + } + if (objPtr->getID() == "net.minecraft") + { + emit minecraftChanged(); + } + // figure out which one is it... in a seriously dumb way. + int index = 0; + for (auto component : d->components) + { + if (component.get() == objPtr) + { + emit dataChanged(createIndex(index, 0), createIndex(index, columnCount(QModelIndex()) - 1)); + scheduleSave(); + return; + } + index++; + } + qCWarning(instanceProfileC) << d->m_instance->name() << "|" + << "PackProfile got dataChanged signal from a Component which does not belong to it!"; +} + +bool PackProfile::remove(const int index) +{ + auto patch = getComponent(index); + if (!patch->isRemovable()) + { + qCWarning(instanceProfileC) << d->m_instance->name() << "|" + << "Patch" << patch->getID() << "is non-removable"; + return false; + } + + if (!removeComponent_internal(patch)) + { + qCCritical(instanceProfileC) << d->m_instance->name() << "|" + << "Patch" << patch->getID() << "could not be removed"; + return false; + } + + beginRemoveRows(QModelIndex(), index, index); + d->components.removeAt(index); + d->componentIndex.remove(patch->getID()); + endRemoveRows(); + invalidateLaunchProfile(); + scheduleSave(); + return true; +} + +bool PackProfile::remove(const QString& id) +{ + int i = 0; + for (auto patch : d->components) + { + if (patch->getID() == id) + { + return remove(i); + } + i++; + } + return false; +} + +bool PackProfile::customize(int index) +{ + auto patch = getComponent(index); + if (!patch->isCustomizable()) + { + qCDebug(instanceProfileC) << d->m_instance->name() << "|" + << "Patch" << patch->getID() << "is not customizable"; + return false; + } + if (!patch->customize()) + { + qCCritical(instanceProfileC) << d->m_instance->name() << "|" + << "Patch" << patch->getID() << "could not be customized"; + return false; + } + invalidateLaunchProfile(); + scheduleSave(); + return true; +} + +bool PackProfile::revertToBase(int index) +{ + auto patch = getComponent(index); + if (!patch->isRevertible()) + { + qCDebug(instanceProfileC) << d->m_instance->name() << "|" + << "Patch" << patch->getID() << "is not revertible"; + return false; + } + if (!patch->revert()) + { + qCCritical(instanceProfileC) << d->m_instance->name() << "|" + << "Patch" << patch->getID() << "could not be reverted"; + return false; + } + invalidateLaunchProfile(); + scheduleSave(); + return true; +} + +ComponentPtr PackProfile::getComponent(const QString& id) +{ + auto iter = d->componentIndex.find(id); + if (iter == d->componentIndex.end()) + { + return nullptr; + } + return (*iter); +} + +ComponentPtr PackProfile::getComponent(size_t index) +{ + if (index >= static_cast<size_t>(d->components.size())) + { + return nullptr; + } + return d->components[index]; +} + +QVariant PackProfile::data(const QModelIndex& index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + int row = index.row(); + int column = index.column(); + + if (row < 0 || row >= d->components.size()) + return QVariant(); + + auto patch = d->components.at(row); + + switch (role) + { + case Qt::CheckStateRole: + { + if (column == NameColumn) + return patch->isEnabled() ? Qt::Checked : Qt::Unchecked; + return QVariant(); + } + case Qt::DisplayRole: + { + switch (column) + { + case NameColumn: return patch->getName(); + case VersionColumn: + { + if (patch->isCustom()) + { + return QString("%1 (Custom)").arg(patch->getVersion()); + } + else + { + return patch->getVersion(); + } + } + default: return QVariant(); + } + } + case Qt::DecorationRole: + { + if (column == NameColumn) + { + auto severity = patch->getProblemSeverity(); + switch (severity) + { + case ProblemSeverity::Warning: return "warning"; + case ProblemSeverity::Error: return "error"; + default: return QVariant(); + } + } + return QVariant(); + } + } + return QVariant(); +} + +bool PackProfile::setData(const QModelIndex& index, [[maybe_unused]] const QVariant& value, int role) +{ + if (!index.isValid() || index.row() < 0 || index.row() >= rowCount(index.parent())) + { + return false; + } + + if (role == Qt::CheckStateRole) + { + auto component = d->components[index.row()]; + if (component->setEnabled(!component->isEnabled())) + { + return true; + } + } + return false; +} + +QVariant PackProfile::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation == Qt::Horizontal) + { + if (role == Qt::DisplayRole) + { + switch (section) + { + case NameColumn: return tr("Name"); + case VersionColumn: return tr("Version"); + default: return QVariant(); + } + } + } + return QVariant(); +} + +// Note: This method intentionally uses no precision for row indices - +// items are indexed by position, not by any floating-point value +Qt::ItemFlags PackProfile::flags(const QModelIndex& index) const +{ + if (!index.isValid()) + { + return Qt::NoItemFlags; + } + + Qt::ItemFlags outFlags = Qt::ItemIsSelectable | Qt::ItemIsEnabled; + + int row = index.row(); + + if (row < 0 || row >= d->components.size()) + { + return Qt::NoItemFlags; + } + + auto patch = d->components.at(row); + // Components can only be toggled if they support disabling and the profile isn't locked + if (patch->canBeDisabled() && !d->interactionDisabled) + { + outFlags |= Qt::ItemIsUserCheckable; + } + return outFlags; +} + +int PackProfile::rowCount(const QModelIndex& parent) const +{ + return parent.isValid() ? 0 : d->components.size(); +} + +int PackProfile::columnCount(const QModelIndex& parent) const +{ + return parent.isValid() ? 0 : NUM_COLUMNS; +} + +void PackProfile::move(const int index, const MoveDirection direction) +{ + int theirIndex; + if (direction == MoveUp) + { + theirIndex = index - 1; + } + else + { + theirIndex = index + 1; + } + + if (index < 0 || index >= d->components.size()) + return; + if (theirIndex >= rowCount()) + theirIndex = rowCount() - 1; + if (theirIndex == -1) + theirIndex = rowCount() - 1; + if (index == theirIndex) + return; + int togap = theirIndex > index ? theirIndex + 1 : theirIndex; + + auto from = getComponent(index); + auto to = getComponent(theirIndex); + + if (!from || !to || !to->isMoveable() || !from->isMoveable()) + { + return; + } + beginMoveRows(QModelIndex(), index, index, QModelIndex(), togap); + d->components.swapItemsAt(index, theirIndex); + endMoveRows(); + invalidateLaunchProfile(); + scheduleSave(); +} + +void PackProfile::invalidateLaunchProfile() +{ + d->m_profile.reset(); +} + +void PackProfile::installJarMods(QStringList selectedFiles) +{ + installJarMods_internal(selectedFiles); +} + +void PackProfile::installCustomJar(QString selectedFile) +{ + installCustomJar_internal(selectedFile); +} + +bool PackProfile::installComponents(QStringList selectedFiles) +{ + const QString patchDir = FS::PathCombine(d->m_instance->instanceRoot(), "patches"); + if (!FS::ensureFolderPathExists(patchDir)) + return false; + + bool result = true; + for (const QString& source : selectedFiles) + { + const QFileInfo sourceInfo(source); + + auto versionFile = ProfileUtils::parseJsonFile(sourceInfo, false); + const QString target = FS::PathCombine(patchDir, versionFile->uid + ".json"); + + if (!QFile::copy(source, target)) + { + qCWarning(instanceProfileC) << d->m_instance->name() << "|" + << "Component" << source << "could not be copied to target" << target; + result = false; + continue; + } + + appendComponent(makeShared<Component>(this, versionFile->uid, versionFile)); + } + + scheduleSave(); + invalidateLaunchProfile(); + + return result; +} + +void PackProfile::installAgents(QStringList selectedFiles) +{ + installAgents_internal(selectedFiles); +} + +bool PackProfile::installEmpty(const QString& uid, const QString& name) +{ + QString patchDir = FS::PathCombine(d->m_instance->instanceRoot(), "patches"); + if (!FS::ensureFolderPathExists(patchDir)) + { + return false; + } + auto f = std::make_shared<VersionFile>(); + f->name = name; + f->uid = uid; + f->version = "1"; + QString patchFileName = FS::PathCombine(patchDir, uid + ".json"); + QFile file(patchFileName); + if (!file.open(QFile::WriteOnly)) + { + qCCritical(instanceProfileC) << d->m_instance->name() << "|" + << "Error opening" << file.fileName() << "for reading:" << file.errorString(); + return false; + } + file.write(OneSixVersionFormat::versionFileToJson(f).toJson()); + file.close(); + + appendComponent(makeShared<Component>(this, f->uid, f)); + scheduleSave(); + invalidateLaunchProfile(); + return true; +} + +bool PackProfile::removeComponent_internal(ComponentPtr patch) +{ + bool ok = true; + // first, remove the patch file. this ensures it's not used anymore + auto fileName = patch->getFilename(); + if (fileName.size()) + { + QFile patchFile(fileName); + if (patchFile.exists() && !patchFile.remove()) + { + qCCritical(instanceProfileC) << d->m_instance->name() << "|" + << "File" << fileName + << "could not be removed because:" << patchFile.errorString(); + return false; + } + } + + // Generic local resource removal + // Handles jar mods, mods, and local libraries + auto removeLocalLibrary = [this](LibraryPtr lib, const QString& overridePath = QString()) -> bool + { + if (!lib->isLocal()) + { + return true; + } + QStringList output, temp1, temp2, temp3; + lib->getApplicableFiles(d->m_instance->runtimeContext(), output, temp1, temp2, temp3, overridePath); + for (const auto& file : output) + { + QFile f(file); + if (f.exists() && !f.remove()) + { + qCCritical(instanceProfileC) << d->m_instance->name() << "|" + << "File" << file << "could not be removed because:" << f.errorString(); + return false; + } + } + return true; + }; + + auto vFile = patch->getVersionFile(); + if (vFile) + { + // Jar Mods + for (auto& lib : vFile->jarMods) + { + ok &= removeLocalLibrary(lib, d->m_instance->jarmodsPath().absolutePath()); + } + // Mods (Loader mods, generic mods) - Assuming they reside in 'mods' folder + QString modsPath = FS::PathCombine(d->m_instance->instanceRoot(), "mods"); + for (auto& lib : vFile->mods) + { + ok &= removeLocalLibrary(lib, modsPath); + } + // Local Libraries (in libraries folder) + for (auto& lib : vFile->libraries) + { + if (lib->isLocal()) + { + ok &= removeLocalLibrary(lib); + } + } + } + return ok; +} + +bool PackProfile::ensureInstallDirs(QString& patchDir, QString& libDir) +{ + patchDir = FS::PathCombine(d->m_instance->instanceRoot(), "patches"); + if (!FS::ensureFolderPathExists(patchDir)) + return false; + + libDir = d->m_instance->getLocalLibraryPath(); + if (!FS::ensureFolderPathExists(libDir)) + return false; + + return true; +} + +bool PackProfile::writeVersionFile(const QString& patchDir, const std::shared_ptr<VersionFile>& versionFile) +{ + QString patchFileName = FS::PathCombine(patchDir, versionFile->uid + ".json"); + QFile file(patchFileName); + if (!file.open(QFile::WriteOnly)) + { + qCCritical(instanceProfileC) << d->m_instance->name() << "|" + << "Error opening" << file.fileName() << "for writing:" << file.errorString(); + return false; + } + file.write(OneSixVersionFormat::versionFileToJson(versionFile).toJson()); + file.close(); + + appendComponent(makeShared<Component>(this, versionFile->uid, versionFile)); + return true; +} + +bool PackProfile::installJarMods_internal(QStringList filepaths) +{ + QString patchDir = FS::PathCombine(d->m_instance->instanceRoot(), "patches"); + if (!FS::ensureFolderPathExists(patchDir)) + { + return false; + } + + if (!FS::ensureFolderPathExists(d->m_instance->jarModsDir())) + { + return false; + } + + for (auto filepath : filepaths) + { + QFileInfo sourceInfo(filepath); + QString id = QUuid::createUuid().toString(QUuid::WithoutBraces); + QString target_filename = id + ".jar"; + QString target_id = "custom.jarmod." + id; + QString target_name = sourceInfo.completeBaseName() + " (jar mod)"; + QString finalPath = FS::PathCombine(d->m_instance->jarModsDir(), target_filename); + + QFileInfo targetInfo(finalPath); + Q_ASSERT(!targetInfo.exists()); + + if (!QFile::copy(sourceInfo.absoluteFilePath(), QFileInfo(finalPath).absoluteFilePath())) + { + return false; + } + + auto f = std::make_shared<VersionFile>(); + auto jarMod = std::make_shared<Library>(); + jarMod->setRawName(GradleSpecifier("custom.jarmods:" + id + ":1")); + jarMod->setFilename(target_filename); + jarMod->setDisplayName(sourceInfo.completeBaseName()); + jarMod->setHint("local"); + f->jarMods.append(jarMod); + f->name = target_name; + f->uid = target_id; + QString patchFileName = FS::PathCombine(patchDir, target_id + ".json"); + + QFile file(patchFileName); + if (!file.open(QFile::WriteOnly)) + { + qCCritical(instanceProfileC) << d->m_instance->name() << "|" + << "Error opening" << file.fileName() << "for reading:" << file.errorString(); + return false; + } + file.write(OneSixVersionFormat::versionFileToJson(f).toJson()); + file.close(); + + appendComponent(makeShared<Component>(this, f->uid, f)); + } + scheduleSave(); + invalidateLaunchProfile(); + return true; +} + +bool PackProfile::installCustomJar_internal(QString filepath) +{ + QString patchDir = FS::PathCombine(d->m_instance->instanceRoot(), "patches"); + if (!FS::ensureFolderPathExists(patchDir)) + { + return false; + } + + QString libDir = d->m_instance->getLocalLibraryPath(); + if (!FS::ensureFolderPathExists(libDir)) + { + return false; + } + + auto specifier = GradleSpecifier("custom:customjar:1"); + QFileInfo sourceInfo(filepath); + QString target_filename = specifier.getFileName(); + QString target_id = specifier.artifactId(); + QString target_name = sourceInfo.completeBaseName() + " (custom jar)"; + QString finalPath = FS::PathCombine(libDir, target_filename); + + QFileInfo jarInfo(finalPath); + if (jarInfo.exists()) + { + if (!FS::deletePath(finalPath)) + { + return false; + } + } + if (!QFile::copy(filepath, finalPath)) + { + return false; + } + + auto f = std::make_shared<VersionFile>(); + auto jarMod = std::make_shared<Library>(); + jarMod->setRawName(specifier); + jarMod->setDisplayName(sourceInfo.completeBaseName()); + jarMod->setHint("local"); + f->mainJar = jarMod; + f->name = target_name; + f->uid = target_id; + QString patchFileName = FS::PathCombine(patchDir, target_id + ".json"); + + QFile file(patchFileName); + if (!file.open(QFile::WriteOnly)) + { + qCCritical(instanceProfileC) << d->m_instance->name() << "|" + << "Error opening" << file.fileName() << "for reading:" << file.errorString(); + return false; + } + file.write(OneSixVersionFormat::versionFileToJson(f).toJson()); + file.close(); + + appendComponent(makeShared<Component>(this, f->uid, f)); + + scheduleSave(); + invalidateLaunchProfile(); + return true; +} + +bool PackProfile::installAgents_internal(QStringList filepaths) +{ + QString patchDir, libDir; + if (!ensureInstallDirs(patchDir, libDir)) + return false; + + for (const QString& source : filepaths) + { + const QFileInfo sourceInfo(source); + const QString id = QUuid::createUuid().toString(QUuid::WithoutBraces); + const QString targetBaseName = id + ".jar"; + const QString targetId = "custom.agent." + id; + const QString targetName = sourceInfo.completeBaseName() + " (agent)"; + const QString target = FS::PathCombine(libDir, targetBaseName); + + const QFileInfo targetInfo(target); + Q_ASSERT(!targetInfo.exists()); + + if (!QFile::copy(source, target)) + return false; + + auto versionFile = std::make_shared<VersionFile>(); + + auto agent = std::make_shared<Library>(); + + agent->setRawName("custom.agents:" + id + ":1"); + agent->setFilename(targetBaseName); + agent->setDisplayName(sourceInfo.completeBaseName()); + agent->setHint("local"); + + versionFile->agents.append(std::make_shared<Agent>(agent, QString())); + versionFile->name = targetName; + versionFile->uid = targetId; + + if (!writeVersionFile(patchDir, versionFile)) + return false; + } + + scheduleSave(); + invalidateLaunchProfile(); + + return true; +} + +std::shared_ptr<LaunchProfile> PackProfile::getProfile() const +{ + if (!d->m_profile) + { + try + { + auto profile = std::make_shared<LaunchProfile>(); + for (auto file : d->components) + { + qCDebug(instanceProfileC) << d->m_instance->name() << "|" + << "Applying" << file->getID() + << (file->getProblemSeverity() == ProblemSeverity::Error ? "ERROR" : "GOOD"); + file->applyTo(profile.get()); + } + d->m_profile = profile; + } + catch (const Exception& error) + { + qCWarning(instanceProfileC) << d->m_instance->name() << "|" + << "Couldn't apply profile patches because: " << error.cause(); + } + } + return d->m_profile; +} + +bool PackProfile::setComponentVersion(const QString& uid, const QString& version, bool important) +{ + auto iter = d->componentIndex.find(uid); + if (iter != d->componentIndex.end()) + { + ComponentPtr component = *iter; + // set existing + if (component->revert()) + { + // set new version + auto oldVersion = component->getVersion(); + component->setVersion(version); + component->setImportant(important); + + if (important) + { + component->setUpdateAction(UpdateAction{ UpdateActionImportantChanged{ oldVersion } }); + resolve(Net::Mode::Online); + } + + return true; + } + return false; + } + else + { + // add new + auto component = makeShared<Component>(this, uid); + component->m_version = version; + component->m_important = important; + appendComponent(component); + return true; + } +} + +QString PackProfile::getComponentVersion(const QString& uid) const +{ + const auto iter = d->componentIndex.find(uid); + if (iter != d->componentIndex.end()) + { + return (*iter)->getVersion(); + } + return QString(); +} + +void PackProfile::disableInteraction(bool disable) +{ + if (d->interactionDisabled != disable) + { + d->interactionDisabled = disable; + auto size = d->components.size(); + if (size) + { + emit dataChanged(index(0), index(size - 1)); + } + } +} + +std::optional<ModPlatform::ModLoaderTypes> PackProfile::getModLoaders() +{ + ModPlatform::ModLoaderTypes result; + bool has_any_loader = false; + + QMapIterator<QString, ModloaderMapEntry> i(Component::KNOWN_MODLOADERS); + + while (i.hasNext()) + { + i.next(); + if (auto c = getComponent(i.key()); c != nullptr && c->isEnabled()) + { + result |= i.value().type; + has_any_loader = true; + } + } + + if (!has_any_loader) + return {}; + return result; +} + +std::optional<ModPlatform::ModLoaderTypes> PackProfile::getSupportedModLoaders() +{ + auto loadersOpt = getModLoaders(); + if (!loadersOpt.has_value()) + return loadersOpt; + auto loaders = loadersOpt.value(); + if (loaders & ModPlatform::Quilt) + loaders |= ModPlatform::Fabric; + if (getComponentVersion("net.minecraft") == "1.20.1" && (loaders & ModPlatform::NeoForge)) + loaders |= ModPlatform::Forge; + return loaders; +} + +QList<ModPlatform::ModLoaderType> PackProfile::getModLoadersList() +{ + QList<ModPlatform::ModLoaderType> result; + for (auto c : d->components) + { + if (c->isEnabled() && Component::KNOWN_MODLOADERS.contains(c->getID())) + { + result.append(Component::KNOWN_MODLOADERS[c->getID()].type); + } + } + + // Quilt provides Fabric compatibility for Minecraft versions < 1.22 + // This may change when Quilt drops official Fabric support in future versions + if (result.contains(ModPlatform::Quilt) && !result.contains(ModPlatform::Fabric)) + { + auto mcVersion = getComponentVersion("net.minecraft"); + Version minecraftVer(mcVersion); + // Assume Quilt maintains Fabric compat for versions before 1.22 + if (minecraftVer < Version("1.22")) + { + result.append(ModPlatform::Fabric); + } + } + if (getComponentVersion("net.minecraft") == "1.20.1" && result.contains(ModPlatform::NeoForge) + && !result.contains(ModPlatform::Forge)) + { + result.append(ModPlatform::Forge); + } + return result; +} diff --git a/archived/projt-launcher/launcher/minecraft/PackProfile.h b/archived/projt-launcher/launcher/minecraft/PackProfile.h new file mode 100644 index 0000000000..f8b83ab4c7 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/PackProfile.h @@ -0,0 +1,244 @@ +// 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-2023 Sefa Eyeoglu <contact@scrumplex.net> + * Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me> + * + * 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. + * + * ======================================================================== */ + +#pragma once + +#include <QAbstractListModel> + +#include <QList> +#include <QString> +#include <memory> +#include <optional> + +#include "Component.h" +#include "LaunchProfile.h" +#include "modplatform/ModIndex.h" +#include "net/Mode.h" + +class MinecraftInstance; +struct PackProfileData; +class ComponentUpdateTask; + +class PackProfile : public QAbstractListModel +{ + Q_OBJECT + friend ComponentUpdateTask; + + public: + enum Columns + { + NameColumn = 0, + VersionColumn, + NUM_COLUMNS + }; + + struct Result + { + bool success; + QString error; + bool changed = true; + + // Implicit conversion to bool + operator bool() const + { + return success; + } + + // Factory methods for convenience + static Result Success(bool changed = true) + { + return { true, "", changed }; + } + + static Result Error(const QString& errorMessage) + { + return { false, errorMessage, false }; + } + }; + + explicit PackProfile(MinecraftInstance* instance); + virtual ~PackProfile(); + + virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + virtual bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override; + virtual QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + virtual int rowCount(const QModelIndex& parent = QModelIndex()) const override; + virtual int columnCount(const QModelIndex& parent) const override; + virtual Qt::ItemFlags flags(const QModelIndex& index) const override; + + /// call this to explicitly mark the component list as loaded - this is used to build a new component list from scratch. + void buildingFromScratch(); + + /// install more jar mods + void installJarMods(QStringList selectedFiles); + + /// install a jar/zip as a replacement for the main jar + void installCustomJar(QString selectedFile); + + /// install MMC/Prism component files + bool installComponents(QStringList selectedFiles); + + /// install Java agent files + void installAgents(QStringList selectedFiles); + + enum MoveDirection + { + MoveUp, + MoveDown + }; + /// move component file # up or down the list + void move(int index, MoveDirection direction); + + /// remove component file # - including files/records + bool remove(int index); + + /// remove component file by id - including files/records + bool remove(const QString& id); + + bool customize(int index); + + bool revertToBase(int index); + + /// reload the list, reload all components, resolve dependencies + Result reload(Net::Mode netmode); + + // reload all components, resolve dependencies + void resolve(Net::Mode netmode); + + /// get current running task... + Task::Ptr getCurrentTask(); + + std::shared_ptr<LaunchProfile> getProfile() const; + + // NOTE: used ONLY by MinecraftInstance to provide legacy version mappings from instance config + void setOldConfigVersion(const QString& uid, const QString& version); + + QString getComponentVersion(const QString& uid) const; + + bool setComponentVersion(const QString& uid, const QString& version, bool important = false); + + bool installEmpty(const QString& uid, const QString& name); + + QString patchFilePathForUid(const QString& uid) const; + + /// if there is a save scheduled, do it now. + void saveNow(); + + /// helper method, returns RuntimeContext of instance + RuntimeContext runtimeContext(); + + signals: + void minecraftChanged(); + + public: + /// get the profile component by id + ComponentPtr getComponent(const QString& id); + + /// get the profile component by index + ComponentPtr getComponent(size_t index); + + /// Add the component to the internal list of patches. + /// Components are appended (not inserted) to preserve mod loader load order. + void appendComponent(ComponentPtr component); + + std::optional<ModPlatform::ModLoaderTypes> getModLoaders(); + // this returns aditional loaders(Quilt supports fabric and NeoForge supports Forge) + std::optional<ModPlatform::ModLoaderTypes> getSupportedModLoaders(); + QList<ModPlatform::ModLoaderType> getModLoadersList(); + + /// apply the component patches. Catches all the errors and returns true/false for success/failure + void invalidateLaunchProfile(); + + private: + void scheduleSave(); + bool saveIsScheduled() const; + + /// insert component so that its index is ideally the specified one (returns real index) + void insertComponent(size_t index, ComponentPtr component); + + QString componentsFilePath() const; + QString patchesPattern() const; + + private slots: + void save_internal(); + void updateSucceeded(); + void updateFailed(const QString& error); + void componentDataChanged(); + void disableInteraction(bool disable); + + private: + Result load(); + + /// Helper function to ensure patch and library directories exist. + /// Returns true if both directories are ready, false on failure. + bool ensureInstallDirs(QString& patchDir, QString& libDir); + + /// Helper function to write a version file and register a component. + /// Returns true on success, false on failure. + bool writeVersionFile(const QString& patchDir, const std::shared_ptr<VersionFile>& versionFile); + + bool installJarMods_internal(QStringList filepaths); + bool installCustomJar_internal(QString filepath); + bool installAgents_internal(QStringList filepaths); + bool removeComponent_internal(ComponentPtr patch); + + private: /* data */ + std::unique_ptr<PackProfileData> d; +}; diff --git a/archived/projt-launcher/launcher/minecraft/PackProfile_p.h b/archived/projt-launcher/launcher/minecraft/PackProfile_p.h new file mode 100644 index 0000000000..980acba3e1 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/PackProfile_p.h @@ -0,0 +1,49 @@ +// 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. + */ +#pragma once + +#include <QList> +#include <QMap> +#include <QTimer> +#include "Component.h" +#include "tasks/Task.h" + +class MinecraftInstance; +using ComponentContainer = QList<ComponentPtr>; +using ComponentIndex = QMap<QString, ComponentPtr>; + +struct PackProfileData +{ + // the instance this belongs to + MinecraftInstance* m_instance; + + // the launch profile (volatile, temporary thing created on demand) + std::shared_ptr<LaunchProfile> m_profile; + + // persistent list of components and related machinery + ComponentContainer components; + ComponentIndex componentIndex; + bool dirty = false; + QTimer m_saveTimer; + Task::Ptr m_updateTask; + bool loaded = false; + bool interactionDisabled = true; +}; diff --git a/archived/projt-launcher/launcher/minecraft/ParseUtils.cpp b/archived/projt-launcher/launcher/minecraft/ParseUtils.cpp new file mode 100644 index 0000000000..c092e1c3eb --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/ParseUtils.cpp @@ -0,0 +1,54 @@ +// 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. + */ +#include "ParseUtils.h" +#include <QDateTime> +#include <QDebug> +#include <QString> +#include <cstdlib> + +QDateTime timeFromS3Time(QString str) +{ + return QDateTime::fromString(str, Qt::ISODate); +} + +QString timeToS3Time(QDateTime time) +{ + // this all because Qt can't format timestamps right. + int offsetRaw = time.offsetFromUtc(); + bool negative = offsetRaw < 0; + int offsetAbs = std::abs(offsetRaw); + + int offsetSeconds = offsetAbs % 60; + offsetAbs -= offsetSeconds; + + int offsetMinutes = offsetAbs % 3600; + offsetAbs -= offsetMinutes; + offsetMinutes /= 60; + + int offsetHours = offsetAbs / 3600; + + QString raw = time.toString("yyyy-MM-ddTHH:mm:ss"); + raw += (negative ? QChar('-') : QChar('+')); + raw += QString("%1").arg(offsetHours, 2, 10, QChar('0')); + raw += ":"; + raw += QString("%1").arg(offsetMinutes, 2, 10, QChar('0')); + return raw; +} diff --git a/archived/projt-launcher/launcher/minecraft/ParseUtils.h b/archived/projt-launcher/launcher/minecraft/ParseUtils.h new file mode 100644 index 0000000000..9db82fa016 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/ParseUtils.h @@ -0,0 +1,29 @@ +// 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. + */ +#pragma once +#include <QDateTime> +#include <QString> + +/// take the timestamp used by S3 and turn it into QDateTime +QDateTime timeFromS3Time(QString str); + +/// take a timestamp and convert it into an S3 timestamp +QString timeToS3Time(QDateTime); diff --git a/archived/projt-launcher/launcher/minecraft/ProfileUtils.cpp b/archived/projt-launcher/launcher/minecraft/ProfileUtils.cpp new file mode 100644 index 0000000000..69ca5a5227 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/ProfileUtils.cpp @@ -0,0 +1,222 @@ +// 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 Sefa Eyeoglu <contact@scrumplex.net> + * + * 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 "ProfileUtils.h" +#include <QDebug> +#include "Json.h" +#include "minecraft/OneSixVersionFormat.h" +#include "minecraft/VersionFilterData.h" + +#include <QJsonArray> +#include <QJsonDocument> +#include <QSaveFile> + +namespace ProfileUtils +{ + + static const int currentOrderFileVersion = 1; + + bool readOverrideOrders(QString path, PatchOrder& order) + { + QFile orderFile(path); + if (!orderFile.exists()) + { + qWarning() << "Order file doesn't exist. Ignoring."; + return false; + } + if (!orderFile.open(QFile::ReadOnly)) + { + qCritical() << "Couldn't open" << orderFile.fileName() << " for reading:" << orderFile.errorString(); + qWarning() << "Ignoring overridden order"; + return false; + } + + // and it's valid JSON + QJsonParseError error; + QJsonDocument doc = QJsonDocument::fromJson(orderFile.readAll(), &error); + if (error.error != QJsonParseError::NoError) + { + qCritical() << "Couldn't parse" << orderFile.fileName() << ":" << error.errorString(); + qWarning() << "Ignoring overridden order"; + return false; + } + + // and then read it and process it if all above is true. + try + { + auto obj = Json::requireObject(doc); + // check order file version. + auto version = Json::requireInteger(obj.value("version")); + if (version != currentOrderFileVersion) + { + throw JSONValidationError( + QObject::tr("Invalid order file version, expected %1").arg(currentOrderFileVersion)); + } + auto orderArray = Json::requireArray(obj.value("order")); + for (auto item : orderArray) + { + order.append(Json::requireString(item)); + } + } + catch ([[maybe_unused]] const JSONValidationError& err) + { + qCritical() << "Couldn't parse" << orderFile.fileName() << ": bad file format"; + qWarning() << "Ignoring overridden order"; + order.clear(); + return false; + } + return true; + } + + static VersionFilePtr createErrorVersionFile(QString fileId, QString filepath, QString error) + { + auto outError = std::make_shared<VersionFile>(); + outError->uid = outError->name = fileId; + // outError->filename = filepath; + outError->addProblem(ProblemSeverity::Error, error); + return outError; + } + + static VersionFilePtr guardedParseJson(const QJsonDocument& doc, + const QString& fileId, + const QString& filepath, + const bool& requireOrder) + { + try + { + return OneSixVersionFormat::versionFileFromJson(doc, filepath, requireOrder); + } + catch (const Exception& e) + { + return createErrorVersionFile(fileId, filepath, e.cause()); + } + } + + VersionFilePtr parseJsonFile(const QFileInfo& fileInfo, const bool requireOrder) + { + QFile file(fileInfo.absoluteFilePath()); + if (!file.open(QFile::ReadOnly)) + { + auto errorStr = + QObject::tr("Unable to open the version file %1: %2.").arg(fileInfo.fileName(), file.errorString()); + return createErrorVersionFile(fileInfo.completeBaseName(), fileInfo.absoluteFilePath(), errorStr); + } + QJsonParseError error; + auto data = file.readAll(); + QJsonDocument doc = QJsonDocument::fromJson(data, &error); + file.close(); + if (error.error != QJsonParseError::NoError) + { + int line = 1; + int column = 0; + for (int i = 0; i < error.offset; i++) + { + if (data[i] == '\n') + { + line++; + column = 0; + continue; + } + column++; + } + auto errorStr = QObject::tr("Unable to process the version file %1: %2 at line %3 column %4.") + .arg(fileInfo.fileName(), error.errorString()) + .arg(line) + .arg(column); + return createErrorVersionFile(fileInfo.completeBaseName(), fileInfo.absoluteFilePath(), errorStr); + } + return guardedParseJson(doc, fileInfo.completeBaseName(), fileInfo.absoluteFilePath(), requireOrder); + } + + bool saveJsonFile(const QJsonDocument& doc, const QString& filename) + { + auto data = doc.toJson(); + QSaveFile jsonFile(filename); + if (!jsonFile.open(QIODevice::WriteOnly)) + { + jsonFile.cancelWriting(); + qWarning() << "Couldn't open" << filename << "for writing"; + return false; + } + jsonFile.write(data); + if (!jsonFile.commit()) + { + qWarning() << "Couldn't save" << filename; + return false; + } + return true; + } + + void removeLwjglFromPatch(VersionFilePtr patch) + { + auto filter = [](QList<LibraryPtr>& libs) + { + QList<LibraryPtr> filteredLibs; + for (auto lib : libs) + { + if (!g_VersionFilterData.lwjglWhitelist.contains(lib->artifactPrefix())) + { + filteredLibs.append(lib); + } + } + libs = filteredLibs; + }; + filter(patch->libraries); + } +} // namespace ProfileUtils diff --git a/archived/projt-launcher/launcher/minecraft/ProfileUtils.h b/archived/projt-launcher/launcher/minecraft/ProfileUtils.h new file mode 100644 index 0000000000..ebfef4bf66 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/ProfileUtils.h @@ -0,0 +1,83 @@ +// 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 Sefa Eyeoglu <contact@scrumplex.net> + * + * 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. + * + * ======================================================================== */ + +#pragma once +#include "Library.h" +#include "VersionFile.h" + +namespace ProfileUtils +{ + using PatchOrder = QStringList; + + /// Read and parse a OneSix format order file + bool readOverrideOrders(QString path, PatchOrder& order); + + /// Write a OneSix format order file + bool writeOverrideOrders(QString path, const PatchOrder& order); + + /// Parse a version file in JSON format + VersionFilePtr parseJsonFile(const QFileInfo& fileInfo, bool requireOrder); + + /// Save a JSON file (in any format) + bool saveJsonFile(const QJsonDocument& doc, const QString& filename); + + /// Remove LWJGL from a patch file. This is applied to all Mojang-like profile files. + void removeLwjglFromPatch(VersionFilePtr patch); + +} // namespace ProfileUtils diff --git a/archived/projt-launcher/launcher/minecraft/Rule.cpp b/archived/projt-launcher/launcher/minecraft/Rule.cpp new file mode 100644 index 0000000000..1a21a04b9b --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/Rule.cpp @@ -0,0 +1,119 @@ +// 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 Sefa Eyeoglu <contact@scrumplex.net> + * Copyright (C) 2025 TheKodeToad <TheKodeToad@proton.me> + * + * 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 <QJsonArray> +#include <QJsonObject> + +#include "Rule.h" + +Rule Rule::fromJson(const QJsonObject& object) +{ + Rule result; + + if (object["action"] == "allow") + result.m_action = Allow; + else if (object["action"] == "disallow") + result.m_action = Disallow; + + if (auto os = object["os"]; os.isObject()) + { + if (auto name = os["name"].toString(); !name.isNull()) + { + result.m_os = OS{ + name, + os["version"].toString(), + }; + } + } + + return result; +} + +QJsonObject Rule::toJson() +{ + QJsonObject result; + + if (m_action == Allow) + result["action"] = "allow"; + else if (m_action == Disallow) + result["action"] = "disallow"; + + if (m_os.has_value()) + { + QJsonObject os; + + os["name"] = m_os->name; + + if (!m_os->version.isEmpty()) + os["version"] = m_os->version; + + result["os"] = os; + } + + return result; +} + +Rule::Action Rule::apply(const RuntimeContext& runtimeContext) +{ + if (m_os.has_value() && !runtimeContext.classifierMatches(m_os->name)) + return Defer; + + return m_action; +} diff --git a/archived/projt-launcher/launcher/minecraft/Rule.h b/archived/projt-launcher/launcher/minecraft/Rule.h new file mode 100644 index 0000000000..f3dd8fadf8 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/Rule.h @@ -0,0 +1,95 @@ +// 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 Sefa Eyeoglu <contact@scrumplex.net> + * Copyright (C) 2025 TheKodeToad <TheKodeToad@proton.me> + * + * 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. + * + * ======================================================================== */ + +#pragma once + +#include <QJsonObject> +#include <QList> +#include <QString> +#include "RuntimeContext.h" + +class Library; + +class Rule +{ + public: + enum Action + { + Allow, + Disallow, + Defer + }; + + static Rule fromJson(const QJsonObject& json); + QJsonObject toJson(); + + Action apply(const RuntimeContext& runtimeContext); + + private: + struct OS + { + QString name; + // NOTE: unsupported, but retained to avoid information loss + QString version; + }; + + Action m_action = Defer; + std::optional<OS> m_os; +}; diff --git a/archived/projt-launcher/launcher/minecraft/ShortcutUtils.cpp b/archived/projt-launcher/launcher/minecraft/ShortcutUtils.cpp new file mode 100644 index 0000000000..dc795f94b6 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/ShortcutUtils.cpp @@ -0,0 +1,324 @@ +// 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 Sefa Eyeoglu <contact@scrumplex.net> + * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me> + * Copyright (C) 2025 Yihe Li <winmikedows@hotmail.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 "ShortcutUtils.h" + +#include "FileSystem.h" + +#include <QApplication> +#include <QFileDialog> + +#include <BuildConfig.h> +#include <DesktopServices.h> +#include <icons/IconList.hpp> + +namespace ShortcutUtils +{ + + bool createInstanceShortcut(const Shortcut& shortcut, const QString& filePath) + { + if (!shortcut.instance) + return false; + + QString appPath = QApplication::applicationFilePath(); + auto icon = + APPLICATION->icons()->icon(shortcut.iconKey.isEmpty() ? shortcut.instance->iconKey() : shortcut.iconKey); + if (icon == nullptr) + { + icon = APPLICATION->icons()->icon("grass"); + } + QString iconPath; + QStringList args; +#if defined(Q_OS_MACOS) + if (appPath.startsWith("/private/var/")) + { + QMessageBox::critical( + shortcut.parent, + QObject::tr("Create Shortcut"), + QObject::tr( + "The launcher is in the folder it was extracted from, therefore it cannot create shortcuts.")); + return false; + } + + iconPath = FS::PathCombine(shortcut.instance->instanceRoot(), "Icon.icns"); + + QFile iconFile(iconPath); + if (!iconFile.open(QFile::WriteOnly)) + { + QMessageBox::critical(shortcut.parent, + QObject::tr("Create Shortcut"), + QObject::tr("Failed to create icon for application.")); + return false; + } + + QIcon iconObj = icon->icon(); + bool success = iconObj.pixmap(1024, 1024).save(iconPath, "ICNS"); + iconFile.close(); + + if (!success) + { + iconFile.remove(); + QMessageBox::critical(shortcut.parent, + QObject::tr("Create Shortcut"), + QObject::tr("Failed to create icon for application.")); + return false; + } +#elif defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) + if (appPath.startsWith("/tmp/.mount_")) + { + // AppImage! + appPath = QProcessEnvironment::systemEnvironment().value(QStringLiteral("APPIMAGE")); + if (appPath.isEmpty()) + { + QMessageBox::critical( + shortcut.parent, + QObject::tr("Create Shortcut"), + QObject::tr( + "Launcher is running as misconfigured AppImage? ($APPIMAGE environment variable is missing)")); + } + else if (appPath.endsWith("/")) + { + appPath.chop(1); + } + } + + iconPath = FS::PathCombine(shortcut.instance->instanceRoot(), "icon.png"); + + QFile iconFile(iconPath); + if (!iconFile.open(QFile::WriteOnly)) + { + QMessageBox::critical(shortcut.parent, + QObject::tr("Create Shortcut"), + QObject::tr("Failed to create icon for shortcut.")); + return false; + } + bool success = icon->icon().pixmap(64, 64).save(&iconFile, "PNG"); + iconFile.close(); + + if (!success) + { + iconFile.remove(); + QMessageBox::critical(shortcut.parent, + QObject::tr("Create Shortcut"), + QObject::tr("Failed to create icon for shortcut.")); + return false; + } + + if (DesktopServices::isFlatpak()) + { + appPath = "flatpak"; + args.append({ "run", BuildConfig.LAUNCHER_APPID }); + } + +#elif defined(Q_OS_WIN) + iconPath = FS::PathCombine(shortcut.instance->instanceRoot(), "icon.ico"); + + // part of fix for weird bug involving the window icon being replaced + // dunno why it happens, but parent 2-line fix seems to be enough, so w/e + auto appIcon = APPLICATION->logo(); + + QFile iconFile(iconPath); + if (!iconFile.open(QFile::WriteOnly)) + { + QMessageBox::critical(shortcut.parent, + QObject::tr("Create Shortcut"), + QObject::tr("Failed to create icon for shortcut.")); + return false; + } + bool success = icon->icon().pixmap(64, 64).save(&iconFile, "ICO"); + iconFile.close(); + + // restore original window icon + QGuiApplication::setWindowIcon(appIcon); + + if (!success) + { + iconFile.remove(); + QMessageBox::critical(shortcut.parent, + QObject::tr("Create Shortcut"), + QObject::tr("Failed to create icon for shortcut.")); + return false; + } + +#else + QMessageBox::critical(shortcut.parent, + QObject::tr("Create Shortcut"), + QObject::tr("Not supported on your platform!")); + return false; +#endif + args.append({ "--launch", shortcut.instance->id() }); + args.append(shortcut.extraArgs); + + QString shortcutPath = FS::createShortcut(filePath, appPath, args, shortcut.name, iconPath); + if (shortcutPath.isEmpty()) + { +#if not defined(Q_OS_MACOS) + iconFile.remove(); +#endif + QMessageBox::critical(shortcut.parent, + QObject::tr("Create Shortcut"), + QObject::tr("Failed to create %1 shortcut!").arg(shortcut.targetString)); + return false; + } + + shortcut.instance->registerShortcut({ shortcut.name, shortcutPath, shortcut.target }); + return true; + } + + bool createInstanceShortcutOnDesktop(const Shortcut& shortcut) + { + if (!shortcut.instance) + return false; + + QString desktopDir = FS::getDesktopDir(); + if (desktopDir.isEmpty()) + { + QMessageBox::critical(shortcut.parent, + QObject::tr("Create Shortcut"), + QObject::tr("Couldn't find desktop?!")); + return false; + } + + QString shortcutFilePath = FS::PathCombine(desktopDir, FS::RemoveInvalidFilenameChars(shortcut.name)); + if (!createInstanceShortcut(shortcut, shortcutFilePath)) + return false; + QMessageBox::information( + shortcut.parent, + QObject::tr("Create Shortcut"), + QObject::tr("Created a shortcut to this %1 on your desktop!").arg(shortcut.targetString)); + return true; + } + + bool createInstanceShortcutInApplications(const Shortcut& shortcut) + { + if (!shortcut.instance) + return false; + + QString applicationsDir = FS::getApplicationsDir(); + if (applicationsDir.isEmpty()) + { + QMessageBox::critical(shortcut.parent, + QObject::tr("Create Shortcut"), + QObject::tr("Couldn't find applications folder?!")); + return false; + } + +#if defined(Q_OS_MACOS) || defined(Q_OS_WIN) + applicationsDir = FS::PathCombine(applicationsDir, BuildConfig.LAUNCHER_DISPLAYNAME + " Instances"); + + QDir applicationsDirQ(applicationsDir); + if (!applicationsDirQ.mkpath(".")) + { + QMessageBox::critical(shortcut.parent, + QObject::tr("Create Shortcut"), + QObject::tr("Failed to create instances folder in applications folder!")); + return false; + } +#endif + + QString shortcutFilePath = FS::PathCombine(applicationsDir, FS::RemoveInvalidFilenameChars(shortcut.name)); + if (!createInstanceShortcut(shortcut, shortcutFilePath)) + return false; + QMessageBox::information( + shortcut.parent, + QObject::tr("Create Shortcut"), + QObject::tr("Created a shortcut to this %1 in your applications folder!").arg(shortcut.targetString)); + return true; + } + + bool createInstanceShortcutInOther(const Shortcut& shortcut) + { + if (!shortcut.instance) + return false; + + QString defaultedDir = FS::getDesktopDir(); +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) || defined(Q_OS_OPENBSD) + QString extension = ".desktop"; +#elif defined(Q_OS_WINDOWS) + QString extension = ".lnk"; +#else + QString extension = ""; +#endif + + QString shortcutFilePath = + FS::PathCombine(defaultedDir, FS::RemoveInvalidFilenameChars(shortcut.name) + extension); + QFileDialog fileDialog; + // workaround to make sure the portal file dialog opens in the desktop directory + fileDialog.setDirectoryUrl(defaultedDir); + + shortcutFilePath = fileDialog.getSaveFileName(shortcut.parent, + QObject::tr("Create Shortcut"), + shortcutFilePath, + QObject::tr("Desktop Entries") + " (*" + extension + ")"); + if (shortcutFilePath.isEmpty()) + return false; // file dialog canceled by user + + if (shortcutFilePath.endsWith(extension)) + shortcutFilePath = shortcutFilePath.mid(0, shortcutFilePath.length() - extension.length()); + if (!createInstanceShortcut(shortcut, shortcutFilePath)) + return false; + QMessageBox::information(shortcut.parent, + QObject::tr("Create Shortcut"), + QObject::tr("Created a shortcut to this %1!").arg(shortcut.targetString)); + return true; + } + +} // namespace ShortcutUtils diff --git a/archived/projt-launcher/launcher/minecraft/ShortcutUtils.h b/archived/projt-launcher/launcher/minecraft/ShortcutUtils.h new file mode 100644 index 0000000000..16b033e285 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/ShortcutUtils.h @@ -0,0 +1,94 @@ +// 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 Sefa Eyeoglu <contact@scrumplex.net> + * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me> + * Copyright (C) 2025 Yihe Li <winmikedows@hotmail.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. + * + * ======================================================================== */ + +#pragma once +#include "Application.h" + +#include <QList> +#include <QMessageBox> + +namespace ShortcutUtils +{ + /// A struct to hold parameters for creating a shortcut + struct Shortcut + { + BaseInstance* instance; + QString name; + QString targetString; + QWidget* parent = nullptr; + QStringList extraArgs = {}; + QString iconKey = ""; + ShortcutTarget target; + }; + + /// Create an instance shortcut on the specified file path + bool createInstanceShortcut(const Shortcut& shortcut, const QString& filePath); + + /// Create an instance shortcut on the desktop + bool createInstanceShortcutOnDesktop(const Shortcut& shortcut); + + /// Create an instance shortcut in the Applications directory + bool createInstanceShortcutInApplications(const Shortcut& shortcut); + + /// Create an instance shortcut in other directories + bool createInstanceShortcutInOther(const Shortcut& shortcut); + +} // namespace ShortcutUtils diff --git a/archived/projt-launcher/launcher/minecraft/VanillaInstanceCreationTask.cpp b/archived/projt-launcher/launcher/minecraft/VanillaInstanceCreationTask.cpp new file mode 100644 index 0000000000..a48c63132f --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/VanillaInstanceCreationTask.cpp @@ -0,0 +1,59 @@ +// 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. + */ +#include "VanillaInstanceCreationTask.h" + +#include <utility> + +#include "FileSystem.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" +#include "settings/INISettingsObject.h" + +VanillaCreationTask::VanillaCreationTask(BaseVersion::Ptr version, QString loader, BaseVersion::Ptr loader_version) + : InstanceCreationTask(), + m_version(std::move(version)), + m_using_loader(true), + m_loader(std::move(loader)), + m_loader_version(std::move(loader_version)) +{} + +std::unique_ptr<MinecraftInstance> VanillaCreationTask::createInstance() +{ + setStatus(tr("Creating instance from version %1").arg(m_version->name())); + + auto instance_settings = std::make_shared<INISettingsObject>(FS::PathCombine(m_stagingPath, "instance.cfg")); + instance_settings->suspendSave(); + { + auto createdInstance = std::make_unique<MinecraftInstance>(m_globalSettings, instance_settings, m_stagingPath); + auto& inst = *createdInstance; + auto components = inst.getPackProfile(); + components->buildingFromScratch(); + components->setComponentVersion("net.minecraft", m_version->descriptor(), true); + if (m_using_loader) + components->setComponentVersion(m_loader, m_loader_version->descriptor()); + + inst.setName(name()); + inst.setIconKey(m_instIcon); + + instance_settings->resumeSave(); + return createdInstance; + } +} diff --git a/archived/projt-launcher/launcher/minecraft/VanillaInstanceCreationTask.h b/archived/projt-launcher/launcher/minecraft/VanillaInstanceCreationTask.h new file mode 100644 index 0000000000..f6874fb051 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/VanillaInstanceCreationTask.h @@ -0,0 +1,44 @@ +// 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. + */ +#pragma once + +#include "InstanceCreationTask.h" + +#include <utility> + +class VanillaCreationTask final : public InstanceCreationTask +{ + Q_OBJECT + public: + VanillaCreationTask(BaseVersion::Ptr version) : InstanceCreationTask(), m_version(std::move(version)) + {} + VanillaCreationTask(BaseVersion::Ptr version, QString loader, BaseVersion::Ptr loader_version); + + std::unique_ptr<MinecraftInstance> createInstance() override; + + private: + // Version to update to / create of the instance. + BaseVersion::Ptr m_version; + + bool m_using_loader = false; + QString m_loader; + BaseVersion::Ptr m_loader_version; +}; diff --git a/archived/projt-launcher/launcher/minecraft/VersionFile.cpp b/archived/projt-launcher/launcher/minecraft/VersionFile.cpp new file mode 100644 index 0000000000..94f718dd45 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/VersionFile.cpp @@ -0,0 +1,116 @@ +// 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 Sefa Eyeoglu <contact@scrumplex.net> + * Copyright (C) 2022 Jamie Mansfield <jmansfield@cadixdev.org> + * + * 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 <QJsonArray> +#include <QJsonDocument> + +#include <QDebug> + +#include "ParseUtils.h" +#include "minecraft/Library.h" +#include "minecraft/PackProfile.h" +#include "minecraft/VersionFile.h" + +#include <Version.h> + +static bool isMinecraftVersion(const QString& uid) +{ + return uid == "net.minecraft"; +} + +void VersionFile::applyTo(LaunchProfile* profile, const RuntimeContext& runtimeContext) +{ + // Only real Minecraft can set those. Don't let anything override them. + if (isMinecraftVersion(uid)) + { + profile->applyMinecraftVersion(version); + profile->applyMinecraftVersionType(type); + // HACK: ignore assets from other version files than Minecraft + // workaround for stupid assets issue caused by amazon: + // https://www.theregister.co.uk/2017/02/28/aws_is_awol_as_s3_goes_haywire/ + profile->applyMinecraftAssets(mojangAssetIndex); + } + + profile->applyMainJar(mainJar); + profile->applyMainClass(mainClass); + profile->applyAppletClass(appletClass); + profile->applyMinecraftArguments(minecraftArguments); + profile->applyAddnJvmArguments(addnJvmArguments); + profile->applyTweakers(addTweakers); + profile->applyJarMods(jarMods); + profile->applyMods(mods); + profile->applyTraits(traits); + profile->applyCompatibleJavaMajors(compatibleJavaMajors); + profile->applyCompatibleJavaName(compatibleJavaName); + + for (auto library : libraries) + { + profile->applyLibrary(library, runtimeContext); + } + for (auto mavenFile : mavenFiles) + { + profile->applyMavenFile(mavenFile, runtimeContext); + } + for (auto agent : agents) + { + profile->applyAgent(agent, runtimeContext); + } + profile->applyProblemSeverity(getProblemSeverity()); +} diff --git a/archived/projt-launcher/launcher/minecraft/VersionFile.h b/archived/projt-launcher/launcher/minecraft/VersionFile.h new file mode 100644 index 0000000000..e51957b44a --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/VersionFile.h @@ -0,0 +1,153 @@ +// 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. + */ + +#pragma once + +#include <QDateTime> +#include <QHash> +#include <QList> +#include <QSet> +#include <QString> +#include <QStringList> + +#include <meta/JsonFormat.hpp> +#include <memory> +#include "Agent.h" +#include "Library.h" +#include "ProblemProvider.h" +#include "java/core/RuntimePackage.hpp" +#include "minecraft/Rule.h" + +class PackProfile; +class VersionFile; +class LaunchProfile; +struct MojangDownloadInfo; +struct MojangAssetIndexInfo; + +using VersionFilePtr = std::shared_ptr<VersionFile>; +class VersionFile : public ProblemContainer +{ + friend class MojangVersionFormat; + friend class OneSixVersionFormat; + + public: /* methods */ + void applyTo(LaunchProfile* profile, const RuntimeContext& runtimeContext); + + public: /* data */ + /// ProjT Launcher: order hint for this version file if no explicit order is set + int order = 0; + + /// ProjT Launcher: human readable name of this package + QString name; + + /// ProjT Launcher: package ID of this package + QString uid; + + /// ProjT Launcher: version of this package + QString version; + + /// ProjT Launcher: DEPRECATED dependency on a Minecraft version + QString dependsOnMinecraftVersion; + + /// Mojang: DEPRECATED used to version the Mojang version format + int minimumLauncherVersion = -1; + + /// Mojang: DEPRECATED version of Minecraft this is + QString minecraftVersion; + + /// Mojang: class to launch Minecraft with + QString mainClass; + + /// ProjT Launcher: class to launch legacy Minecraft with (embed in a custom window) + QString appletClass; + + /// Mojang: Minecraft launch arguments (may contain placeholders for variable substitution) + QString minecraftArguments; + + /// ProjT Launcher: Additional JVM launch arguments + QStringList addnJvmArguments; + + /// Mojang: list of compatible java majors + QList<int> compatibleJavaMajors; + + /// Mojang: the name of recommended java version + QString compatibleJavaName; + + /// Mojang: type of the Minecraft version + QString type; + + /// Mojang: the time this version was actually released by Mojang + QDateTime releaseTime; + + /// Mojang: DEPRECATED the time this version was last updated by Mojang + QDateTime updateTime; + + /// Mojang: DEPRECATED asset group to be used with Minecraft + QString assets; + + /// ProjT Launcher: list of tweaker mod arguments for launchwrapper + QStringList addTweakers; + + /// Mojang: list of libraries to add to the version + QList<LibraryPtr> libraries; + + /// ProjT Launcher: list of maven files to put in the libraries folder, but not in classpath + QList<LibraryPtr> mavenFiles; + + /// ProjT Launcher: list of agents to add to JVM arguments + QList<AgentPtr> agents; + + /// The main jar (Minecraft version library, normally) + LibraryPtr mainJar; + + /// ProjT Launcher: list of attached traits of this version file - used to enable features + QSet<QString> traits; + + /// ProjT Launcher: list of jar mods added to this version + QList<LibraryPtr> jarMods; + + /// ProjT Launcher: list of mods added to this version + QList<LibraryPtr> mods; + + /** + * ProjT Launcher: set of packages this depends on + * NOTE: this is shared with the meta format!!! + */ + projt::meta::DependencySet m_requires; + + /** + * ProjT Launcher: set of packages this conflicts with + * NOTE: this is shared with the meta format!!! + */ + projt::meta::DependencySet conflicts; + + /// is volatile -- may be removed as soon as it is no longer needed by something else + bool m_volatile = false; + + QList<projt::java::RuntimePackagePtr> runtimes; + + public: + // Mojang: DEPRECATED list of 'downloads' - client jar, server jar, windows server exe, maybe more. + QMap<QString, std::shared_ptr<MojangDownloadInfo>> mojangDownloads; + + // Mojang: extended asset index download information + std::shared_ptr<MojangAssetIndexInfo> mojangAssetIndex; +}; diff --git a/archived/projt-launcher/launcher/minecraft/VersionFilterData.cpp b/archived/projt-launcher/launcher/minecraft/VersionFilterData.cpp new file mode 100644 index 0000000000..7d9db5f7e6 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/VersionFilterData.cpp @@ -0,0 +1,89 @@ +// 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. + */ +#include "VersionFilterData.h" +#include "ParseUtils.h" + +VersionFilterData g_VersionFilterData = VersionFilterData(); + +VersionFilterData::VersionFilterData() +{ + // 1.3.* + auto libs13 = QList<FMLlib>{ { "argo-2.25.jar", "bb672829fde76cb163004752b86b0484bd0a7f4b" }, + { "guava-12.0.1.jar", "b8e78b9af7bf45900e14c6f958486b6ca682195f" }, + { "asm-all-4.0.jar", "98308890597acb64047f7e896638e0d98753ae82" } }; + + fmlLibsMapping["1.3.2"] = libs13; + + // 1.4.* + auto libs14 = QList<FMLlib>{ { "argo-2.25.jar", "bb672829fde76cb163004752b86b0484bd0a7f4b" }, + { "guava-12.0.1.jar", "b8e78b9af7bf45900e14c6f958486b6ca682195f" }, + { "asm-all-4.0.jar", "98308890597acb64047f7e896638e0d98753ae82" }, + { "bcprov-jdk15on-147.jar", "b6f5d9926b0afbde9f4dbe3db88c5247be7794bb" } }; + + fmlLibsMapping["1.4"] = libs14; + fmlLibsMapping["1.4.1"] = libs14; + fmlLibsMapping["1.4.2"] = libs14; + fmlLibsMapping["1.4.3"] = libs14; + fmlLibsMapping["1.4.4"] = libs14; + fmlLibsMapping["1.4.5"] = libs14; + fmlLibsMapping["1.4.6"] = libs14; + fmlLibsMapping["1.4.7"] = libs14; + + // 1.5 + fmlLibsMapping["1.5"] = QList<FMLlib>{ { "argo-small-3.2.jar", "58912ea2858d168c50781f956fa5b59f0f7c6b51" }, + { "guava-14.0-rc3.jar", "931ae21fa8014c3ce686aaa621eae565fefb1a6a" }, + { "asm-all-4.1.jar", "054986e962b88d8660ae4566475658469595ef58" }, + { "bcprov-jdk15on-148.jar", "960dea7c9181ba0b17e8bab0c06a43f0a5f04e65" }, + { "deobfuscation_data_1.5.zip", "5f7c142d53776f16304c0bbe10542014abad6af8" }, + { "scala-library.jar", "458d046151ad179c85429ed7420ffb1eaf6ddf85" } }; + + // 1.5.1 + fmlLibsMapping["1.5.1"] = + QList<FMLlib>{ { "argo-small-3.2.jar", "58912ea2858d168c50781f956fa5b59f0f7c6b51" }, + { "guava-14.0-rc3.jar", "931ae21fa8014c3ce686aaa621eae565fefb1a6a" }, + { "asm-all-4.1.jar", "054986e962b88d8660ae4566475658469595ef58" }, + { "bcprov-jdk15on-148.jar", "960dea7c9181ba0b17e8bab0c06a43f0a5f04e65" }, + { "deobfuscation_data_1.5.1.zip", "22e221a0d89516c1f721d6cab056a7e37471d0a6" }, + { "scala-library.jar", "458d046151ad179c85429ed7420ffb1eaf6ddf85" } }; + + // 1.5.2 + fmlLibsMapping["1.5.2"] = + QList<FMLlib>{ { "argo-small-3.2.jar", "58912ea2858d168c50781f956fa5b59f0f7c6b51" }, + { "guava-14.0-rc3.jar", "931ae21fa8014c3ce686aaa621eae565fefb1a6a" }, + { "asm-all-4.1.jar", "054986e962b88d8660ae4566475658469595ef58" }, + { "bcprov-jdk15on-148.jar", "960dea7c9181ba0b17e8bab0c06a43f0a5f04e65" }, + { "deobfuscation_data_1.5.2.zip", "446e55cd986582c70fcf12cb27bc00114c5adfd9" }, + { "scala-library.jar", "458d046151ad179c85429ed7420ffb1eaf6ddf85" } }; + + // don't use installers for those. + forgeInstallerBlacklist = QSet<QString>({ "1.5.2" }); + + // Legacy cutoff date (Minecraft 1.6.2) - used for determining core mods support + // Core mods were phased out after this version when Forge modernized + legacyCutoffDate = timeFromS3Time("2013-06-25T15:08:56+02:00"); + lwjglWhitelist = + QSet<QString>{ "net.java.jinput:jinput", "net.java.jinput:jinput-platform", "net.java.jutils:jutils", + "org.lwjgl.lwjgl:lwjgl", "org.lwjgl.lwjgl:lwjgl_util", "org.lwjgl.lwjgl:lwjgl-platform" }; + + java8BeginsDate = timeFromS3Time("2017-03-30T09:32:19+00:00"); + java16BeginsDate = timeFromS3Time("2021-05-12T11:19:15+00:00"); + java17BeginsDate = timeFromS3Time("2021-11-16T17:04:48+00:00"); +} diff --git a/archived/projt-launcher/launcher/minecraft/VersionFilterData.h b/archived/projt-launcher/launcher/minecraft/VersionFilterData.h new file mode 100644 index 0000000000..b984f5f362 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/VersionFilterData.h @@ -0,0 +1,51 @@ +// 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. + */ +#pragma once +#include <QDateTime> +#include <QMap> +#include <QSet> +#include <QString> + +struct FMLlib +{ + QString filename; + QString checksum; +}; + +struct VersionFilterData +{ + VersionFilterData(); + // mapping between minecraft versions and FML libraries required + QMap<QString, QList<FMLlib>> fmlLibsMapping; + // set of minecraft versions for which using forge installers is blacklisted + QSet<QString> forgeInstallerBlacklist; + // no new versions below this date will be accepted from Mojang servers + QDateTime legacyCutoffDate; + // Libraries that belong to LWJGL + QSet<QString> lwjglWhitelist; + // release date of first version to require Java 8 (17w13a) + QDateTime java8BeginsDate; + // release data of first version to require Java 16 (21w19a) + QDateTime java16BeginsDate; + // release data of first version to require Java 17 (1.18 Pre Release 2) + QDateTime java17BeginsDate; +}; +extern VersionFilterData g_VersionFilterData; diff --git a/archived/projt-launcher/launcher/minecraft/World.cpp b/archived/projt-launcher/launcher/minecraft/World.cpp new file mode 100644 index 0000000000..1fc040db9c --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/World.cpp @@ -0,0 +1,652 @@ +// 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 Sefa Eyeoglu <contact@scrumplex.net> + * Copyright (C) 2022 TheKodeToad <TheKodeToad@proton.me> + * + * 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 "World.h" +#include <QDebug> +#include <QDir> +#include <QDirIterator> +#include <QString> + +#include <FileSystem.h> +#include <MMCZip.h> +#include <io/stream_reader.h> +#include <quazip/quazip.h> +#include <quazip/quazipdir.h> +#include <quazip/quazipfile.h> +#include <tag_primitive.h> +#include <tag_string.h> +#include <sstream> +#include "GZip.h" + +#include <QCoreApplication> + +#include <optional> + +#include "FileSystem.h" +#include "PSaveFile.h" + +using std::nullopt; +using std::optional; + +GameType::GameType(std::optional<int> original) : original(original) +{ + if (!original) + { + return; + } + switch (*original) + { + case 0: type = GameType::Survival; break; + case 1: type = GameType::Creative; break; + case 2: type = GameType::Adventure; break; + case 3: type = GameType::Spectator; break; + default: break; + } +} + +QString GameType::toTranslatedString() const +{ + switch (type) + { + case GameType::Survival: return QCoreApplication::translate("GameType", "Survival"); + case GameType::Creative: return QCoreApplication::translate("GameType", "Creative"); + case GameType::Adventure: return QCoreApplication::translate("GameType", "Adventure"); + case GameType::Spectator: return QCoreApplication::translate("GameType", "Spectator"); + default: break; + } + if (original) + { + return QCoreApplication::translate("GameType", "Unknown (%1)").arg(*original); + } + return QCoreApplication::translate("GameType", "Undefined"); +} + +QString GameType::toLogString() const +{ + switch (type) + { + case GameType::Survival: return "Survival"; + case GameType::Creative: return "Creative"; + case GameType::Adventure: return "Adventure"; + case GameType::Spectator: return "Spectator"; + default: break; + } + if (original) + { + return QString("Unknown (%1)").arg(*original); + } + return "Undefined"; +} + +std::unique_ptr<nbt::tag_compound> parseLevelDat(QByteArray data) +{ + QByteArray output; + if (!GZip::unzip(data, output)) + { + return nullptr; + } + std::istringstream foo(std::string(output.constData(), output.size())); + try + { + auto pair = nbt::io::read_compound(foo); + + if (pair.first != "") + return nullptr; + + if (pair.second == nullptr) + return nullptr; + + return std::move(pair.second); + } + catch (const nbt::io::input_error& e) + { + qWarning() << "Unable to parse level.dat:" << e.what(); + return nullptr; + } +} + +QByteArray serializeLevelDat(nbt::tag_compound* levelInfo) +{ + std::ostringstream s; + nbt::io::write_tag("", *levelInfo, s); + QByteArray val(s.str().data(), (int)s.str().size()); + return val; +} + +QString getDatFromFS(const QFileInfo& root, QString file) +{ + QDir worldDir(root.filePath()); + if (!root.isDir() || !worldDir.exists(file)) + { + return QString(); + } + return worldDir.absoluteFilePath(file); +} + +QString getLevelDatFromFS(const QFileInfo& file) +{ + return getDatFromFS(file, "level.dat"); +} + +QByteArray getDatDataFromFS(const QFileInfo& root, QString file) +{ + auto fullFilePath = getDatFromFS(root, file); + if (fullFilePath.isNull()) + { + return QByteArray(); + } + QFile f(fullFilePath); + if (!f.open(QIODevice::ReadOnly)) + { + return QByteArray(); + } + return f.readAll(); +} + +QByteArray getLevelDatDataFromFS(const QFileInfo& file) +{ + return getDatDataFromFS(file, "level.dat"); +} + +QByteArray getWorldGenDataFromFS(const QFileInfo& file) +{ + return getDatDataFromFS(file, "data/minecraft/world_gen_settings.dat"); +} + +bool putLevelDatDataToFS(const QFileInfo& file, QByteArray& data) +{ + auto fullFilePath = getLevelDatFromFS(file); + if (fullFilePath.isNull()) + { + return false; + } + PSaveFile f(fullFilePath); + if (!f.open(QIODevice::WriteOnly)) + { + return false; + } + QByteArray compressed; + if (!GZip::zip(data, compressed)) + { + return false; + } + if (f.write(compressed) != compressed.size()) + { + f.cancelWriting(); + return false; + } + return f.commit(); +} + +World::World(const QFileInfo& file) +{ + repath(file); +} + +void World::repath(const QFileInfo& file) +{ + m_containerFile = file; + m_folderName = file.fileName(); + if (file.isFile() && file.suffix() == "zip") + { + m_iconFile = QString(); + readFromZip(file); + } + else if (file.isDir()) + { + QFileInfo assumedIconPath(file.absoluteFilePath() + "/icon.png"); + if (assumedIconPath.exists()) + { + m_iconFile = assumedIconPath.absoluteFilePath(); + } + readFromFS(file); + } +} + +bool World::resetIcon() +{ + if (m_iconFile.isNull()) + { + return false; + } + if (QFile(m_iconFile).remove()) + { + m_iconFile = QString(); + return true; + } + return false; +} + +int64_t loadSeed(QByteArray data); + +void World::readFromFS(const QFileInfo& file) +{ + auto bytes = getLevelDatDataFromFS(file); + if (bytes.isEmpty()) + { + m_isValid = false; + return; + } + loadFromLevelDat(bytes); + m_levelDatTime = file.lastModified(); + if (m_randomSeed == 0) + { + auto worldGenBytes = getWorldGenDataFromFS(file); + if (!worldGenBytes.isEmpty()) + { + m_randomSeed = loadSeed(worldGenBytes); + } + } +} + +void World::readFromZip(const QFileInfo& file) +{ + QuaZip zip(file.absoluteFilePath()); + m_isValid = zip.open(QuaZip::mdUnzip); + if (!m_isValid) + { + return; + } + auto location = MMCZip::findFolderOfFileInZip(&zip, "level.dat"); + m_isValid = !location.isEmpty(); + if (!m_isValid) + { + return; + } + m_containerOffsetPath = location; + QuaZipFile zippedFile(&zip); + // read the install profile + m_isValid = zip.setCurrentFile(location + "level.dat"); + if (!m_isValid) + { + return; + } + m_isValid = zippedFile.open(QIODevice::ReadOnly); + QuaZipFileInfo64 levelDatInfo; + zippedFile.getFileInfo(&levelDatInfo); + auto modTime = levelDatInfo.getNTFSmTime(); + if (!modTime.isValid()) + { + modTime = levelDatInfo.dateTime; + } + m_levelDatTime = modTime; + if (!m_isValid) + { + return; + } + loadFromLevelDat(zippedFile.readAll()); + zippedFile.close(); +} + +bool World::install(const QString& to, const QString& name) +{ + auto finalPath = FS::PathCombine(to, FS::DirNameFromString(m_actualName, to)); + if (!FS::ensureFolderPathExists(finalPath)) + { + return false; + } + 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); + } + else if (m_containerFile.isDir()) + { + QString from = m_containerFile.filePath(); + ok = FS::copy(from, finalPath)(); + } + + if (ok && !name.isEmpty() && m_actualName != name) + { + QFileInfo finalPathInfo(finalPath); + World newWorld(finalPathInfo); + if (newWorld.isValid()) + { + newWorld.rename(name); + } + } + return ok; +} + +bool World::rename(const QString& newName) +{ + if (m_containerFile.isFile()) + { + return false; + } + + auto data = getLevelDatDataFromFS(m_containerFile); + if (data.isEmpty()) + { + return false; + } + + auto worldData = parseLevelDat(data); + if (!worldData) + { + return false; + } + auto& val = worldData->at("Data"); + if (val.get_type() != nbt::tag_type::Compound) + { + return false; + } + auto& dataCompound = val.as<nbt::tag_compound>(); + dataCompound.put("LevelName", nbt::value_initializer(newName.toUtf8().data())); + data = serializeLevelDat(worldData.get()); + + putLevelDatDataToFS(m_containerFile, data); + + m_actualName = newName; + + QDir parentDir(m_containerFile.absoluteFilePath()); + parentDir.cdUp(); + QFile container(m_containerFile.absoluteFilePath()); + auto dirName = FS::DirNameFromString(m_actualName, parentDir.absolutePath()); + container.rename(parentDir.absoluteFilePath(dirName)); + + return true; +} + +namespace +{ + + optional<QString> read_string(nbt::value& parent, const char* name) + { + try + { + auto& namedValue = parent.at(name); + if (namedValue.get_type() != nbt::tag_type::String) + { + return nullopt; + } + auto& tag_str = namedValue.as<nbt::tag_string>(); + return QString::fromStdString(tag_str.get()); + } + catch ([[maybe_unused]] const std::out_of_range& e) + { + // fallback for old world formats + qWarning() << "String NBT tag" << name << "could not be found."; + return nullopt; + } + catch ([[maybe_unused]] const std::bad_cast& e) + { + // type mismatch + qWarning() << "NBT tag" << name << "could not be converted to string."; + return nullopt; + } + } + + optional<int64_t> read_long(nbt::value& parent, const char* name) + { + try + { + auto& namedValue = parent.at(name); + if (namedValue.get_type() != nbt::tag_type::Long) + { + return nullopt; + } + auto& tag_str = namedValue.as<nbt::tag_long>(); + return tag_str.get(); + } + catch ([[maybe_unused]] const std::out_of_range& e) + { + // fallback for old world formats + qWarning() << "Long NBT tag" << name << "could not be found."; + return nullopt; + } + catch ([[maybe_unused]] const std::bad_cast& e) + { + // type mismatch + qWarning() << "NBT tag" << name << "could not be converted to long."; + return nullopt; + } + } + + optional<int> read_int(nbt::value& parent, const char* name) + { + try + { + auto& namedValue = parent.at(name); + if (namedValue.get_type() != nbt::tag_type::Int) + { + return nullopt; + } + auto& tag_str = namedValue.as<nbt::tag_int>(); + return tag_str.get(); + } + catch ([[maybe_unused]] const std::out_of_range& e) + { + // fallback for old world formats + qWarning() << "Int NBT tag" << name << "could not be found."; + return nullopt; + } + catch ([[maybe_unused]] const std::bad_cast& e) + { + // type mismatch + qWarning() << "NBT tag" << name << "could not be converted to int."; + return nullopt; + } + } + + GameType read_gametype(nbt::value& parent, const char* name) + { + return GameType(read_int(parent, name)); + } + +} // namespace + +int64_t loadSeed(QByteArray data) +{ + auto levelData = parseLevelDat(data); + if (!levelData) + { + return 0; + } + + nbt::value* valPtr = nullptr; + try + { + valPtr = &levelData->at("data"); + } + catch (const std::out_of_range&) + { + return 0; + } + nbt::value& val = *valPtr; + + try + { + return read_long(val, "seed").value_or(0); + } + catch (const std::out_of_range&) + { + } + return 0; +} + +void World::loadFromLevelDat(QByteArray data) +{ + auto levelData = parseLevelDat(data); + if (!levelData) + { + m_isValid = false; + return; + } + + nbt::value* valPtr = nullptr; + try + { + valPtr = &levelData->at("Data"); + } + catch (const std::out_of_range& e) + { + qWarning() << "Unable to read NBT tags from " << m_folderName << ":" << e.what(); + m_isValid = false; + return; + } + nbt::value& val = *valPtr; + + m_isValid = val.get_type() == nbt::tag_type::Compound; + if (!m_isValid) + return; + + auto name = read_string(val, "LevelName"); + m_actualName = name ? *name : m_folderName; + + auto timestamp = read_long(val, "LastPlayed"); + m_lastPlayed = timestamp ? QDateTime::fromMSecsSinceEpoch(*timestamp) : m_levelDatTime; + + m_gameType = read_gametype(val, "GameType"); + + optional<int64_t> randomSeed; + try + { + auto& WorldGen_val = val.at("WorldGenSettings"); + randomSeed = read_long(WorldGen_val, "seed"); + } + catch (const std::out_of_range&) + {} + if (!randomSeed) + { + randomSeed = read_long(val, "RandomSeed"); + } + m_randomSeed = randomSeed ? *randomSeed : 0; + + qDebug() << "World Name:" << m_actualName; + qDebug() << "Last Played:" << m_lastPlayed.toString(); + if (randomSeed) + { + qDebug() << "Seed:" << *randomSeed; + } + qDebug() << "Size:" << m_size; + qDebug() << "GameType:" << m_gameType.toLogString(); +} + +bool World::replace(World& with) +{ + if (!destroy()) + return false; + bool success = FS::copy(with.m_containerFile.filePath(), m_containerFile.path())(); + if (success) + { + m_folderName = with.m_folderName; + m_containerFile.refresh(); + } + return success; +} + +bool World::destroy() +{ + if (!m_isValid) + return false; + + if (FS::trash(m_containerFile.filePath())) + return true; + + if (m_containerFile.isDir()) + { + QDir d(m_containerFile.filePath()); + return d.removeRecursively(); + } + else if (m_containerFile.isFile()) + { + QFile file(m_containerFile.absoluteFilePath()); + return file.remove(); + } + return true; +} + +bool World::operator==(const World& other) const +{ + return m_isValid == other.m_isValid && folderName() == other.folderName(); +} + +bool World::isSymLinkUnder(const QString& instPath) const +{ + if (isSymLink()) + return true; + + auto instDir = QDir(instPath); + + auto relAbsPath = instDir.relativeFilePath(m_containerFile.absoluteFilePath()); + auto relCanonPath = instDir.relativeFilePath(m_containerFile.canonicalFilePath()); + + return relAbsPath != relCanonPath; +} + +bool World::isMoreThanOneHardLink() const +{ + if (m_containerFile.isDir()) + { + return FS::hardLinkCount(QDir(m_containerFile.absoluteFilePath()).filePath("level.dat")) > 1; + } + return FS::hardLinkCount(m_containerFile.absoluteFilePath()) > 1; +} + +void World::setSize(int64_t size) +{ + m_size = size; +} diff --git a/archived/projt-launcher/launcher/minecraft/World.h b/archived/projt-launcher/launcher/minecraft/World.h new file mode 100644 index 0000000000..9466bb55e2 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/World.h @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// 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) ============================== + * + * Copyright 2015-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. + * ======================================================================== */ + +#pragma once +#include <QDateTime> +#include <QFileInfo> +#include <optional> + +struct GameType +{ + GameType() = default; + GameType(std::optional<int> original); + + QString toTranslatedString() const; + QString toLogString() const; + + enum + { + Unknown = -1, + Survival, + Creative, + Adventure, + Spectator + } type = Unknown; + std::optional<int> original; +}; + +class World +{ + public: + World(const QFileInfo& file); + QString folderName() const + { + return m_folderName; + } + QString name() const + { + return m_actualName; + } + QString iconFile() const + { + return m_iconFile; + } + int64_t bytes() const + { + return m_size; + } + QDateTime lastPlayed() const + { + return m_lastPlayed; + } + GameType gameType() const + { + return m_gameType; + } + int64_t seed() const + { + return m_randomSeed; + } + bool isValid() const + { + return m_isValid; + } + bool isOnFS() const + { + return m_containerFile.isDir(); + } + QFileInfo container() const + { + return m_containerFile; + } + // delete all the files of this world + bool destroy(); + // replace this world with a copy of the other + bool replace(World& with); + // change the world's filesystem path (used by world lists for *MAGIC* purposes) + void repath(const QFileInfo& file); + // remove the icon file, if any + bool resetIcon(); + + bool rename(const QString& to); + bool install(const QString& to, const QString& name = QString()); + + void setSize(int64_t size); + + // WEAK compare operator - used for replacing worlds + bool operator==(const World& other) const; + + auto isSymLink() const -> bool + { + return m_containerFile.isSymLink(); + } + + /** + * @brief Take a instance path, checks if the file pointed to by the resource is a symlink or under a symlink in + * that instance + * + * @param instPath path to an instance directory + * @return true + * @return false + */ + bool isSymLinkUnder(const QString& instPath) const; + + bool isMoreThanOneHardLink() const; + + QString canonicalFilePath() const + { + return m_containerFile.canonicalFilePath(); + } + + private: + void readFromZip(const QFileInfo& file); + void readFromFS(const QFileInfo& file); + void loadFromLevelDat(QByteArray data); + + protected: + QFileInfo m_containerFile; + QString m_containerOffsetPath; + QString m_folderName; + QString m_actualName; + QString m_iconFile; + QDateTime m_levelDatTime; + QDateTime m_lastPlayed; + int64_t m_size; + int64_t m_randomSeed = 0; + GameType m_gameType; + bool m_isValid = false; +}; diff --git a/archived/projt-launcher/launcher/minecraft/WorldList.cpp b/archived/projt-launcher/launcher/minecraft/WorldList.cpp new file mode 100644 index 0000000000..f4f3c8424c --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/WorldList.cpp @@ -0,0 +1,494 @@ +// 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 Sefa Eyeoglu <contact@scrumplex.net> + * + * 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 "WorldList.h" + +#include <FileSystem.h> +#include <qmimedata.h> +#include <QDebug> +#include <QDirIterator> +#include <QFileSystemWatcher> +#include <QMimeData> +#include <QString> +#include <QThreadPool> +#include <QUrl> +#include <QUuid> +#include <Qt> + +WorldList::WorldList(const QString& dir, BaseInstance* instance) + : QAbstractListModel(), + m_instance(instance), + m_dir(dir) +{ + FS::ensureFolderPathExists(m_dir.absolutePath()); + m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs); + m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); + m_watcher = new QFileSystemWatcher(this); + m_isWatching = false; + connect(m_watcher, &QFileSystemWatcher::directoryChanged, this, &WorldList::directoryChanged); +} + +void WorldList::startWatching() +{ + if (m_isWatching) + { + return; + } + update(); + m_isWatching = m_watcher->addPath(m_dir.absolutePath()); + if (m_isWatching) + { + qDebug() << "Started watching " << m_dir.absolutePath(); + } + else + { + qDebug() << "Failed to start watching " << m_dir.absolutePath(); + } +} + +void WorldList::stopWatching() +{ + if (!m_isWatching) + { + return; + } + m_isWatching = !m_watcher->removePath(m_dir.absolutePath()); + if (!m_isWatching) + { + qDebug() << "Stopped watching " << m_dir.absolutePath(); + } + else + { + qDebug() << "Failed to stop watching " << m_dir.absolutePath(); + } +} + +bool WorldList::update() +{ + if (!isValid()) + return false; + + QList<World> newWorlds; + m_dir.refresh(); + auto folderContents = m_dir.entryInfoList(); + // if there are any untracked files... + for (QFileInfo entry : folderContents) + { + if (!entry.isDir()) + continue; + + World w(entry); + if (w.isValid()) + { + newWorlds.append(w); + } + } + beginResetModel(); + m_worlds.swap(newWorlds); + endResetModel(); + loadWorldsAsync(); + return true; +} + +void WorldList::directoryChanged(QString) +{ + update(); +} + +bool WorldList::isValid() +{ + return m_dir.exists() && m_dir.isReadable(); +} + +QString WorldList::instDirPath() const +{ + return QFileInfo(m_instance->instanceRoot()).absoluteFilePath(); +} + +bool WorldList::deleteWorld(int index) +{ + if (index >= m_worlds.size() || index < 0) + return false; + World& m = m_worlds[index]; + if (m.destroy()) + { + beginRemoveRows(QModelIndex(), index, index); + m_worlds.removeAt(index); + endRemoveRows(); + emit changed(); + return true; + } + return false; +} + +bool WorldList::deleteWorlds(int first, int last) +{ + for (int i = first; i <= last; i++) + { + World& m = m_worlds[i]; + m.destroy(); + } + beginRemoveRows(QModelIndex(), first, last); + m_worlds.erase(m_worlds.begin() + first, m_worlds.begin() + last + 1); + endRemoveRows(); + emit changed(); + return true; +} + +bool WorldList::resetIcon(int row) +{ + if (row >= m_worlds.size() || row < 0) + return false; + World& m = m_worlds[row]; + if (m.resetIcon()) + { + emit dataChanged(index(row), index(row), { WorldList::IconFileRole }); + return true; + } + return false; +} + +int WorldList::columnCount(const QModelIndex& parent) const +{ + return parent.isValid() ? 0 : 5; +} + +QVariant WorldList::data(const QModelIndex& index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + int row = index.row(); + int column = index.column(); + + if (row < 0 || row >= m_worlds.size()) + return QVariant(); + + QLocale locale; + + auto& world = m_worlds[row]; + switch (role) + { + case Qt::DisplayRole: + switch (column) + { + case NameColumn: return world.name(); + + case GameModeColumn: return world.gameType().toTranslatedString(); + + case LastPlayedColumn: return world.lastPlayed(); + + case SizeColumn: return locale.formattedDataSize(world.bytes()); + + case InfoColumn: + if (world.isSymLinkUnder(instDirPath())) + { + return tr("This world is symbolically linked from elsewhere."); + } + if (world.isMoreThanOneHardLink()) + { + return tr("\nThis world is hard linked elsewhere."); + } + return ""; + default: return QVariant(); + } + + case Qt::UserRole: + if (column == SizeColumn) + return QVariant::fromValue<qlonglong>(world.bytes()); + return data(index, Qt::DisplayRole); + + case Qt::ToolTipRole: + { + if (column == InfoColumn) + { + if (world.isSymLinkUnder(instDirPath())) + { + return tr("Warning: This world is symbolically linked from elsewhere. Editing it will also change " + "the original." + "\nCanonical Path: %1") + .arg(world.canonicalFilePath()); + } + if (world.isMoreThanOneHardLink()) + { + return tr( + "Warning: This world is hard linked elsewhere. Editing it will also change the original."); + } + } + return world.folderName(); + } + case ObjectRole: + { + return QVariant::fromValue<void*>((void*)&world); + } + case FolderRole: + { + return QDir::toNativeSeparators(dir().absoluteFilePath(world.folderName())); + } + case SeedRole: + { + return QVariant::fromValue<qlonglong>(world.seed()); + } + case NameRole: + { + return world.name(); + } + case LastPlayedRole: + { + return world.lastPlayed(); + } + case SizeRole: + { + return QVariant::fromValue<qlonglong>(world.bytes()); + } + case IconFileRole: + { + return world.iconFile(); + } + default: return QVariant(); + } +} + +QVariant WorldList::headerData(int section, [[maybe_unused]] Qt::Orientation orientation, int role) const +{ + switch (role) + { + case Qt::DisplayRole: + switch (section) + { + case NameColumn: return tr("Name"); + case GameModeColumn: return tr("Game Mode"); + case LastPlayedColumn: return tr("Last Played"); + case SizeColumn: + //: World size on disk + return tr("Size"); + case InfoColumn: + //: special warnings? + return tr("Info"); + default: return QVariant(); + } + + case Qt::ToolTipRole: + switch (section) + { + case NameColumn: return tr("The name of the world."); + case GameModeColumn: return tr("Game mode of the world."); + case LastPlayedColumn: return tr("Date and time the world was last played."); + case SizeColumn: return tr("Size of the world on disk."); + case InfoColumn: return tr("Information and warnings about the world."); + default: return QVariant(); + } + default: return QVariant(); + } +} + +QStringList WorldList::mimeTypes() const +{ + QStringList types; + types << "text/uri-list"; + return types; +} + +QMimeData* WorldList::mimeData(const QModelIndexList& indexes) const +{ + QList<QUrl> urls; + + for (auto idx : indexes) + { + if (idx.column() != 0) + continue; + + int row = idx.row(); + if (row < 0 || row >= this->m_worlds.size()) + continue; + + const World& world = m_worlds[row]; + + if (!world.isValid() || !world.isOnFS()) + continue; + + QString worldPath = world.container().absoluteFilePath(); + qDebug() << worldPath; + urls.append(QUrl::fromLocalFile(worldPath)); + } + + auto result = new QMimeData(); + result->setUrls(urls); + return result; +} + +Qt::ItemFlags WorldList::flags(const QModelIndex& index) const +{ + Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index); + if (index.isValid()) + return Qt::ItemIsUserCheckable | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | defaultFlags; + else + return Qt::ItemIsDropEnabled | defaultFlags; +} + +Qt::DropActions WorldList::supportedDragActions() const +{ + // move to other mod lists or VOID + return Qt::MoveAction; +} + +Qt::DropActions WorldList::supportedDropActions() const +{ + // copy from outside, move from within and other mod lists + return Qt::CopyAction | Qt::MoveAction; +} + +void WorldList::installWorld(QFileInfo filename) +{ + qDebug() << "installing: " << filename.absoluteFilePath(); + World w(filename); + if (!w.isValid()) + { + return; + } + w.install(m_dir.absolutePath()); +} + +bool WorldList::dropMimeData(const QMimeData* data, + Qt::DropAction action, + [[maybe_unused]] int row, + [[maybe_unused]] int column, + [[maybe_unused]] const QModelIndex& parent) +{ + if (action == Qt::IgnoreAction) + return true; + // check if the action is supported + if (!data || !(action & supportedDropActions())) + return false; + // files dropped from outside? + if (data->hasUrls()) + { + bool was_watching = m_isWatching; + if (was_watching) + stopWatching(); + auto urls = data->urls(); + for (auto url : urls) + { + // only local files may be dropped... + if (!url.isLocalFile()) + continue; + QString filename = url.toLocalFile(); + + QFileInfo worldInfo(filename); + + if (!m_dir.entryInfoList().contains(worldInfo)) + { + installWorld(worldInfo); + } + } + if (was_watching) + startWatching(); + return true; + } + return false; +} + +int64_t calculateWorldSize(const QFileInfo& file) +{ + if (file.isFile() && file.suffix() == "zip") + { + return file.size(); + } + else if (file.isDir()) + { + QDirIterator it(file.absoluteFilePath(), QDir::Files, QDirIterator::Subdirectories); + int64_t total = 0; + while (it.hasNext()) + { + it.next(); + total += it.fileInfo().size(); + } + return total; + } + return -1; +} + +void WorldList::loadWorldsAsync() +{ + for (int i = 0; i < m_worlds.size(); ++i) + { + auto file = m_worlds.at(i).container(); + int row = i; + QThreadPool::globalInstance()->start( + [this, file, row]() mutable + { + auto size = calculateWorldSize(file); + + QMetaObject::invokeMethod( + this, + [this, size, row, file]() + { + if (row < m_worlds.size() && m_worlds[row].container() == file) + { + m_worlds[row].setSize(size); + + // Notify views + QModelIndex modelIndex = index(row); + emit dataChanged(modelIndex, modelIndex, { SizeRole }); + } + }, + Qt::QueuedConnection); + }); + } +} diff --git a/archived/projt-launcher/launcher/minecraft/WorldList.h b/archived/projt-launcher/launcher/minecraft/WorldList.h new file mode 100644 index 0000000000..0099fd3e5e --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/WorldList.h @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// 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) ============================== + * + * Copyright 2015-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. + * ======================================================================== */ + +#pragma once + +#include <QAbstractListModel> +#include <QDir> +#include <QList> +#include <QMimeData> +#include <QString> +#include "BaseInstance.h" +#include "minecraft/World.h" + +class QFileSystemWatcher; + +class WorldList : public QAbstractListModel +{ + Q_OBJECT + public: + enum Columns + { + NameColumn, + GameModeColumn, + LastPlayedColumn, + SizeColumn, + InfoColumn + }; + + enum Roles + { + ObjectRole = Qt::UserRole + 1, + FolderRole, + SeedRole, + NameRole, + GameModeRole, + LastPlayedRole, + SizeRole, + IconFileRole + }; + + WorldList(const QString& dir, BaseInstance* instance); + + virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const; + + virtual int rowCount(const QModelIndex& parent = QModelIndex()) const + { + return parent.isValid() ? 0 : static_cast<int>(size()); + }; + virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; + virtual int columnCount(const QModelIndex& parent) const; + + size_t size() const + { + return m_worlds.size(); + }; + bool empty() const + { + return size() == 0; + } + World& operator[](size_t index) + { + return m_worlds[index]; + } + + /// Reloads the mod list and returns true if the list changed. + virtual bool update(); + + /// Install a world from location + void installWorld(QFileInfo filename); + + /// Deletes the mod at the given index. + virtual bool deleteWorld(int index); + + /// Removes the world icon, if any + virtual bool resetIcon(int index); + + /// Deletes all the selected mods + virtual bool deleteWorlds(int first, int last); + + /// flags, mostly to support drag&drop + virtual Qt::ItemFlags flags(const QModelIndex& index) const; + /// get data for drag action + virtual QMimeData* mimeData(const QModelIndexList& indexes) const; + /// get the supported mime types + virtual QStringList mimeTypes() const; + /// process data from drop action + virtual bool dropMimeData(const QMimeData* data, + Qt::DropAction action, + int row, + int column, + const QModelIndex& parent); + /// what drag actions do we support? + virtual Qt::DropActions supportedDragActions() const; + + /// what drop actions do we support? + virtual Qt::DropActions supportedDropActions() const; + + void startWatching(); + void stopWatching(); + + virtual bool isValid(); + + QDir dir() const + { + return m_dir; + } + + QString instDirPath() const; + + const QList<World>& allWorlds() const + { + return m_worlds; + } + + private slots: + void directoryChanged(QString path); + void loadWorldsAsync(); + + signals: + void changed(); + + protected: + BaseInstance* m_instance; + QFileSystemWatcher* m_watcher; + bool m_isWatching; + QDir m_dir; + QList<World> m_worlds; +}; diff --git a/archived/projt-launcher/launcher/minecraft/auth/AccountData.cpp b/archived/projt-launcher/launcher/minecraft/auth/AccountData.cpp new file mode 100644 index 0000000000..7a6bce3ffc --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/AccountData.cpp @@ -0,0 +1,479 @@ +// 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 Sefa Eyeoglu <contact@scrumplex.net> + * + * 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 "AccountData.hpp" +#include <QDebug> +#include <QJsonArray> +#include <QJsonDocument> +#include <QJsonObject> +#include <QUuid> + +namespace +{ + void tokenToJSONV3(QJsonObject& parent, const Token& t, const char* tokenName) + { + if (!t.persistent) + { + return; + } + QJsonObject out; + if (t.issueInstant.isValid()) + { + out["iat"] = QJsonValue(t.issueInstant.toMSecsSinceEpoch() / 1000); + } + + if (t.notAfter.isValid()) + { + out["exp"] = QJsonValue(t.notAfter.toMSecsSinceEpoch() / 1000); + } + + bool save = false; + if (!t.token.isEmpty()) + { + out["token"] = QJsonValue(t.token); + save = true; + } + if (!t.refresh_token.isEmpty()) + { + out["refresh_token"] = QJsonValue(t.refresh_token); + save = true; + } + if (t.extra.size()) + { + out["extra"] = QJsonObject::fromVariantMap(t.extra); + save = true; + } + if (save) + { + parent[tokenName] = out; + } + } + + Token tokenFromJSONV3(const QJsonObject& parent, const char* tokenName) + { + Token out; + auto tokenObject = parent.value(tokenName).toObject(); + if (tokenObject.isEmpty()) + { + return out; + } + auto issueInstant = tokenObject.value("iat"); + if (issueInstant.isDouble()) + { + out.issueInstant = QDateTime::fromMSecsSinceEpoch(((int64_t)issueInstant.toDouble()) * 1000); + } + + auto notAfter = tokenObject.value("exp"); + if (notAfter.isDouble()) + { + out.notAfter = QDateTime::fromMSecsSinceEpoch(((int64_t)notAfter.toDouble()) * 1000); + } + + auto token = tokenObject.value("token"); + if (token.isString()) + { + out.token = token.toString(); + out.validity = Validity::Assumed; + } + + auto refresh_token = tokenObject.value("refresh_token"); + if (refresh_token.isString()) + { + out.refresh_token = refresh_token.toString(); + } + + auto extra = tokenObject.value("extra"); + if (extra.isObject()) + { + out.extra = extra.toObject().toVariantMap(); + } + return out; + } + + void profileToJSONV3(QJsonObject& parent, MinecraftProfile p, const char* tokenName) + { + if (p.id.isEmpty()) + { + return; + } + QJsonObject out; + out["id"] = QJsonValue(p.id); + out["name"] = QJsonValue(p.name); + if (!p.currentCape.isEmpty()) + { + out["cape"] = p.currentCape; + } + + { + QJsonObject skinObj; + skinObj["id"] = p.skin.id; + skinObj["url"] = p.skin.url; + skinObj["variant"] = p.skin.variant; + if (p.skin.data.size()) + { + skinObj["data"] = QString::fromLatin1(p.skin.data.toBase64()); + } + out["skin"] = skinObj; + } + + QJsonArray capesArray; + for (auto& cape : p.capes) + { + QJsonObject capeObj; + capeObj["id"] = cape.id; + capeObj["url"] = cape.url; + capeObj["alias"] = cape.alias; + if (cape.data.size()) + { + capeObj["data"] = QString::fromLatin1(cape.data.toBase64()); + } + capesArray.push_back(capeObj); + } + out["capes"] = capesArray; + parent[tokenName] = out; + } + + MinecraftProfile profileFromJSONV3(const QJsonObject& parent, const char* tokenName) + { + MinecraftProfile out; + auto tokenObject = parent.value(tokenName).toObject(); + if (tokenObject.isEmpty()) + { + return out; + } + { + auto idV = tokenObject.value("id"); + auto nameV = tokenObject.value("name"); + if (!idV.isString() || !nameV.isString()) + { + qWarning() << "mandatory profile attributes are missing or of unexpected type"; + return MinecraftProfile(); + } + out.name = nameV.toString(); + out.id = idV.toString(); + } + + { + auto skinV = tokenObject.value("skin"); + if (!skinV.isObject()) + { + qWarning() << "skin is missing"; + return MinecraftProfile(); + } + auto skinObj = skinV.toObject(); + auto idV = skinObj.value("id"); + auto urlV = skinObj.value("url"); + auto variantV = skinObj.value("variant"); + if (!idV.isString() || !urlV.isString() || !variantV.isString()) + { + qWarning() << "mandatory skin attributes are missing or of unexpected type"; + return MinecraftProfile(); + } + out.skin.id = idV.toString(); + out.skin.url = urlV.toString(); + out.skin.url.replace("http://textures.minecraft.net", "https://textures.minecraft.net"); + out.skin.variant = variantV.toString(); + + // data for skin is optional + auto dataV = skinObj.value("data"); + if (dataV.isString()) + { + auto base64Result = QByteArray::fromBase64Encoding(dataV.toString().toLatin1()); + if (base64Result.decodingStatus != QByteArray::Base64DecodingStatus::Ok) + { + qWarning() << "skin data is not valid base64"; + return MinecraftProfile(); + } + out.skin.data = base64Result.decoded; + } + else if (!dataV.isUndefined()) + { + qWarning() << "skin data is something unexpected"; + return MinecraftProfile(); + } + } + + { + auto capesV = tokenObject.value("capes"); + if (!capesV.isArray()) + { + qWarning() << "capes is not an array!"; + return MinecraftProfile(); + } + auto capesArray = capesV.toArray(); + for (auto capeV : capesArray) + { + if (!capeV.isObject()) + { + qWarning() << "cape is not an object!"; + return MinecraftProfile(); + } + auto capeObj = capeV.toObject(); + auto idV = capeObj.value("id"); + auto urlV = capeObj.value("url"); + auto aliasV = capeObj.value("alias"); + if (!idV.isString() || !urlV.isString() || !aliasV.isString()) + { + qWarning() << "mandatory skin attributes are missing or of unexpected type"; + return MinecraftProfile(); + } + Cape cape; + cape.id = idV.toString(); + cape.url = urlV.toString(); + cape.url.replace("http://textures.minecraft.net", "https://textures.minecraft.net"); + cape.alias = aliasV.toString(); + + // data for cape is optional. + auto dataV = capeObj.value("data"); + if (dataV.isString()) + { + auto base64Result = QByteArray::fromBase64Encoding(dataV.toString().toLatin1()); + if (base64Result.decodingStatus != QByteArray::Base64DecodingStatus::Ok) + { + qWarning() << "cape data is not valid base64"; + return MinecraftProfile(); + } + cape.data = base64Result.decoded; + } + else if (!dataV.isUndefined()) + { + qWarning() << "cape data is something unexpected"; + return MinecraftProfile(); + } + out.capes[cape.id] = cape; + } + } + // current cape + { + auto capeV = tokenObject.value("cape"); + if (capeV.isString()) + { + auto currentCape = capeV.toString(); + if (out.capes.contains(currentCape)) + { + out.currentCape = currentCape; + } + } + } + out.validity = Validity::Assumed; + return out; + } + + void entitlementToJSONV3(QJsonObject& parent, MinecraftEntitlement p) + { + if (p.validity == Validity::None) + { + return; + } + QJsonObject out; + out["ownsMinecraft"] = QJsonValue(p.ownsMinecraft); + out["canPlayMinecraft"] = QJsonValue(p.canPlayMinecraft); + parent["entitlement"] = out; + } + + bool entitlementFromJSONV3(const QJsonObject& parent, MinecraftEntitlement& out) + { + auto entitlementObject = parent.value("entitlement").toObject(); + if (entitlementObject.isEmpty()) + { + return false; + } + { + auto ownsMinecraftV = entitlementObject.value("ownsMinecraft"); + auto canPlayMinecraftV = entitlementObject.value("canPlayMinecraft"); + if (!ownsMinecraftV.isBool() || !canPlayMinecraftV.isBool()) + { + qWarning() << "mandatory attributes are missing or of unexpected type"; + return false; + } + out.canPlayMinecraft = canPlayMinecraftV.toBool(false); + out.ownsMinecraft = ownsMinecraftV.toBool(false); + out.validity = Validity::Assumed; + } + return true; + } + +} // namespace + +bool AccountData::resumeStateFromV3(QJsonObject data) +{ + auto typeV = data.value("type"); + if (!typeV.isString()) + { + qWarning() << "Failed to parse account data: type is missing."; + return false; + } + auto typeS = typeV.toString(); + if (typeS == "MSA") + { + type = AccountType::MSA; + } + else if (typeS == "Offline") + { + type = AccountType::Offline; + } + else + { + qWarning() << "Failed to parse account data: type is not recognized."; + return false; + } + + if (type == AccountType::MSA) + { + auto clientIDV = data.value("msa-client-id"); + if (clientIDV.isString()) + { + msaClientID = clientIDV.toString(); + } // leave msaClientID empty if it doesn't exist or isn't a string + msaToken = tokenFromJSONV3(data, "msa"); + userToken = tokenFromJSONV3(data, "utoken"); + xboxApiToken = tokenFromJSONV3(data, "xrp-main"); + mojangservicesToken = tokenFromJSONV3(data, "xrp-mc"); + } + + yggdrasilToken = tokenFromJSONV3(data, "ygg"); + // versions before 7.2 used "offline" as the offline token + if (yggdrasilToken.token == "offline") + yggdrasilToken.token = "0"; + + minecraftProfile = profileFromJSONV3(data, "profile"); + if (!entitlementFromJSONV3(data, minecraftEntitlement)) + { + if (minecraftProfile.validity != Validity::None) + { + minecraftEntitlement.canPlayMinecraft = true; + minecraftEntitlement.ownsMinecraft = true; + minecraftEntitlement.validity = Validity::Assumed; + } + } + + validity_ = minecraftProfile.validity; + return true; +} + +QJsonObject AccountData::saveState() const +{ + QJsonObject output; + if (type == AccountType::MSA) + { + output["type"] = "MSA"; + output["msa-client-id"] = msaClientID; + tokenToJSONV3(output, msaToken, "msa"); + tokenToJSONV3(output, userToken, "utoken"); + tokenToJSONV3(output, xboxApiToken, "xrp-main"); + tokenToJSONV3(output, mojangservicesToken, "xrp-mc"); + } + else if (type == AccountType::Offline) + { + output["type"] = "Offline"; + } + + tokenToJSONV3(output, yggdrasilToken, "ygg"); + profileToJSONV3(output, minecraftProfile, "profile"); + entitlementToJSONV3(output, minecraftEntitlement); + return output; +} + +QString AccountData::accessToken() const +{ + return yggdrasilToken.token; +} + +QString AccountData::profileId() const +{ + return minecraftProfile.id; +} + +QString AccountData::profileName() const +{ + if (minecraftProfile.name.size() == 0) + { + return QObject::tr("No profile (%1)").arg(accountDisplayString()); + } + else + { + return minecraftProfile.name; + } +} + +QString AccountData::accountDisplayString() const +{ + switch (type) + { + case AccountType::Offline: + { + return QObject::tr("<Offline>"); + } + case AccountType::MSA: + { + if (xboxApiToken.extra.contains("gtg")) + { + return xboxApiToken.extra["gtg"].toString(); + } + return "Xbox profile missing"; + } + default: + { + return "Invalid Account"; + } + } +} + +QString AccountData::lastError() const +{ + return errorString; +} diff --git a/archived/projt-launcher/launcher/minecraft/auth/AccountData.hpp b/archived/projt-launcher/launcher/minecraft/auth/AccountData.hpp new file mode 100644 index 0000000000..f30d77225f --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/AccountData.hpp @@ -0,0 +1,190 @@ +// 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 Sefa Eyeoglu <contact@scrumplex.net> + * + * 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. + * + * ======================================================================== */ + +#pragma once +#include <QByteArray> +#include <QJsonObject> +#include <QList> +#include <QString> + +#include <QDateTime> +#include <QMap> +#include <QString> +#include <QVariantMap> + +enum class Validity +{ + None, + Assumed, + Certain +}; + +struct Token +{ + QDateTime issueInstant; + QDateTime notAfter; + QString token; + QString refresh_token; + QVariantMap extra; + + Validity validity = Validity::None; + bool persistent = true; +}; + +struct Skin +{ + QString id; + QString url; + QString variant; + + QByteArray data; +}; + +struct Cape +{ + QString id; + QString url; + QString alias; + + QByteArray data; +}; + +struct MinecraftEntitlement +{ + bool ownsMinecraft = false; + bool canPlayMinecraft = false; + Validity validity = Validity::None; +}; + +struct MinecraftProfile +{ + QString id; + QString name; + Skin skin; + QString currentCape; + QMap<QString, Cape> capes; + Validity validity = Validity::None; +}; + +enum class AccountType +{ + MSA, + Offline +}; + +enum class AccountState +{ + Unchecked, + Offline, + Working, + Online, + Disabled, + Errored, + Expired, + Gone +}; + +/** + * State of an authentication task. + * Used by AuthFlow to communicate progress and results. + */ +enum class AccountTaskState +{ + STATE_CREATED, + STATE_WORKING, + STATE_SUCCEEDED, + STATE_OFFLINE, + STATE_DISABLED, + STATE_FAILED_SOFT, + STATE_FAILED_HARD, + STATE_FAILED_GONE +}; + +struct AccountData +{ + QJsonObject saveState() const; + bool resumeStateFromV3(QJsonObject data); + + //! userName for Mojang accounts, gamertag for MSA + QString accountDisplayString() const; + + //! Yggdrasil access token, as passed to the game. + QString accessToken() const; + + QString profileId() const; + QString profileName() const; + + QString lastError() const; + + AccountType type = AccountType::MSA; + + QString msaClientID; + Token msaToken; + Token userToken; + Token xboxApiToken; + Token mojangservicesToken; + + Token yggdrasilToken; + MinecraftProfile minecraftProfile; + MinecraftEntitlement minecraftEntitlement; + Validity validity_ = Validity::None; + + // runtime only information (not saved with the account) + QString internalId; + QString errorString; + AccountState accountState = AccountState::Unchecked; +}; diff --git a/archived/projt-launcher/launcher/minecraft/auth/AccountList.cpp b/archived/projt-launcher/launcher/minecraft/auth/AccountList.cpp new file mode 100644 index 0000000000..c68d214aad --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/AccountList.cpp @@ -0,0 +1,776 @@ +// 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 Sefa Eyeoglu <contact@scrumplex.net> + * + * 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 "AccountList.hpp" +#include "AccountData.hpp" +#include "tasks/Task.h" + +#include <QDir> +#include <QFile> +#include <QIODevice> +#include <QJsonArray> +#include <QJsonDocument> +#include <QJsonObject> +#include <QJsonParseError> +#include <QObject> +#include <QString> +#include <QTextStream> +#include <QTimer> + +#include <QDebug> + +#include <FileSystem.h> +#include <QSaveFile> + +enum AccountListVersion +{ + MojangMSA = 3 +}; + +AccountList::AccountList(QObject* parent) : QAbstractListModel(parent) +{ + m_refreshTimer = new QTimer(this); + m_refreshTimer->setSingleShot(true); + connect(m_refreshTimer, &QTimer::timeout, this, &AccountList::fillQueue); + m_nextTimer = new QTimer(this); + m_nextTimer->setSingleShot(true); + connect(m_nextTimer, &QTimer::timeout, this, &AccountList::tryNext); +} + +AccountList::~AccountList() noexcept +{} + +int AccountList::findAccountByProfileId(const QString& profileId) const +{ + for (int i = 0; i < count(); i++) + { + MinecraftAccountPtr account = at(i); + if (account->profileId() == profileId) + { + return i; + } + } + return -1; +} + +MinecraftAccountPtr AccountList::getAccountByProfileName(const QString& profileName) const +{ + for (int i = 0; i < count(); i++) + { + MinecraftAccountPtr account = at(i); + if (account->profileName() == profileName) + { + return account; + } + } + return nullptr; +} + +const MinecraftAccountPtr AccountList::at(int i) const +{ + return MinecraftAccountPtr(m_accounts.at(i)); +} + +QStringList AccountList::profileNames() const +{ + QStringList out; + for (auto& account : m_accounts) + { + auto profileName = account->profileName(); + if (profileName.isEmpty()) + { + continue; + } + out.append(profileName); + } + return out; +} + +void AccountList::addAccount(const MinecraftAccountPtr account) +{ + // NOTE: Do not allow adding something that's already there. We shouldn't let it continue + // because of the signal / slot connections after this. + if (m_accounts.contains(account)) + { + qDebug() << "Tried to add account that's already on the accounts list!"; + return; + } + + // hook up notifications for changes in the account + connect(account.get(), &MinecraftAccount::changed, this, &AccountList::accountChanged); + connect(account.get(), &MinecraftAccount::activityChanged, this, &AccountList::accountActivityChanged); + + // override/replace existing account with the same profileId + auto profileId = account->profileId(); + if (profileId.size()) + { + auto existingAccount = findAccountByProfileId(profileId); + if (existingAccount != -1) + { + qDebug() << "Replacing old account with a new one with the same profile ID!"; + + MinecraftAccountPtr existingAccountPtr = m_accounts[existingAccount]; + m_accounts[existingAccount] = account; + if (m_defaultAccount == existingAccountPtr) + { + m_defaultAccount = account; + } + // disconnect notifications for changes in the account being replaced + existingAccountPtr->disconnect(this); + emit dataChanged(index(existingAccount), index(existingAccount, columnCount(QModelIndex()) - 1)); + onListChanged(); + return; + } + } + + // if we don't have this profileId yet, add the account to the end + int row = m_accounts.count(); + qDebug() << "Inserting account at index" << row; + + beginInsertRows(QModelIndex(), row, row); + m_accounts.append(account); + endInsertRows(); + + onListChanged(); +} + +void AccountList::removeAccount(QModelIndex index) +{ + int row = index.row(); + if (index.isValid() && row >= 0 && row < m_accounts.size()) + { + auto& account = m_accounts[row]; + if (account == m_defaultAccount) + { + m_defaultAccount = nullptr; + onDefaultAccountChanged(); + } + account->disconnect(this); + + beginRemoveRows(QModelIndex(), row, row); + m_accounts.removeAt(index.row()); + endRemoveRows(); + onListChanged(); + } +} + +MinecraftAccountPtr AccountList::defaultAccount() const +{ + return m_defaultAccount; +} + +void AccountList::setDefaultAccount(MinecraftAccountPtr newAccount) +{ + if (!newAccount && m_defaultAccount) + { + int idx = 0; + auto previousDefaultAccount = m_defaultAccount; + m_defaultAccount = nullptr; + for (MinecraftAccountPtr account : m_accounts) + { + if (account == previousDefaultAccount) + { + emit dataChanged(index(idx), index(idx, columnCount(QModelIndex()) - 1)); + } + idx++; + } + onDefaultAccountChanged(); + } + else + { + auto currentDefaultAccount = m_defaultAccount; + int currentDefaultAccountIdx = -1; + auto newDefaultAccount = m_defaultAccount; + int newDefaultAccountIdx = -1; + int idx = 0; + for (MinecraftAccountPtr account : m_accounts) + { + if (account == newAccount) + { + newDefaultAccount = account; + newDefaultAccountIdx = idx; + } + if (currentDefaultAccount == account) + { + currentDefaultAccountIdx = idx; + } + idx++; + } + if (currentDefaultAccount != newDefaultAccount) + { + emit dataChanged(index(currentDefaultAccountIdx), + index(currentDefaultAccountIdx, columnCount(QModelIndex()) - 1)); + emit dataChanged(index(newDefaultAccountIdx), index(newDefaultAccountIdx, columnCount(QModelIndex()) - 1)); + m_defaultAccount = newDefaultAccount; + onDefaultAccountChanged(); + } + } +} + +void AccountList::accountChanged() +{ + // the list changed. there is no doubt. + onListChanged(); +} + +void AccountList::accountActivityChanged(bool active) +{ + MinecraftAccount* account = qobject_cast<MinecraftAccount*>(sender()); + bool found = false; + for (int i = 0; i < count(); i++) + { + if (at(i).get() == account) + { + emit dataChanged(index(i), index(i, columnCount(QModelIndex()) - 1)); + found = true; + break; + } + } + if (found) + { + emit listActivityChanged(); + if (active) + { + beginActivity(); + } + else + { + endActivity(); + } + } +} + +void AccountList::onListChanged() +{ + if (m_autosave) + { + if (!saveList()) + { + qWarning() << "Failed to save account list automatically"; + emit fileSaveFailed(m_listFilePath); + } + } + + emit listChanged(); +} + +void AccountList::onDefaultAccountChanged() +{ + if (m_autosave) + saveList(); + + emit defaultAccountChanged(); +} + +int AccountList::count() const +{ + return m_accounts.count(); +} + +QString getAccountStatus(AccountState status) +{ + switch (status) + { + case AccountState::Unchecked: return QObject::tr("Unchecked", "Account status"); + case AccountState::Offline: return QObject::tr("Offline", "Account status"); + case AccountState::Online: return QObject::tr("Ready", "Account status"); + case AccountState::Working: return QObject::tr("Working", "Account status"); + case AccountState::Errored: return QObject::tr("Errored", "Account status"); + case AccountState::Expired: return QObject::tr("Expired", "Account status"); + case AccountState::Disabled: return QObject::tr("Disabled", "Account status"); + case AccountState::Gone: return QObject::tr("Gone", "Account status"); + default: return QObject::tr("Unknown", "Account status"); + } +} + +QVariant AccountList::data(const QModelIndex& index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + if (index.row() > count()) + return QVariant(); + + MinecraftAccountPtr account = at(index.row()); + + switch (role) + { + case Qt::DisplayRole: + switch (index.column()) + { + case IconColumn: return QVariant(); // Icons are handled by DecorationRole + case ProfileNameColumn: return account->profileName(); + case NameColumn: return account->accountDisplayString(); + case TypeColumn: + { + switch (account->accountType()) + { + case AccountType::MSA: + { + return tr("MSA", "Account type"); + } + case AccountType::Offline: + { + return tr("Offline", "Account type"); + } + } + return tr("Unknown", "Account type"); + } + case StatusColumn: return getAccountStatus(account->accountState()); + default: return QVariant(); + } + + case Qt::DecorationRole: + if (index.column() == IconColumn) + { + return account->getFace(); + } + return QVariant(); + + case Qt::ToolTipRole: return account->accountDisplayString(); + + case PointerRole: return QVariant::fromValue(account); + + case Qt::CheckStateRole: + if (index.column() == ProfileNameColumn) + return account == m_defaultAccount ? Qt::Checked : Qt::Unchecked; + return QVariant(); + + default: return QVariant(); + } +} + +QVariant AccountList::headerData(int section, [[maybe_unused]] Qt::Orientation orientation, int role) const +{ + switch (role) + { + case Qt::DisplayRole: + switch (section) + { + case IconColumn: return QVariant(); // No header text for icon column + case ProfileNameColumn: return tr("Username"); + case NameColumn: return tr("Account"); + case TypeColumn: return tr("Type"); + case StatusColumn: return tr("Status"); + default: return QVariant(); + } + + case Qt::ToolTipRole: + switch (section) + { + case IconColumn: return tr("Account avatar"); + case ProfileNameColumn: return tr("Minecraft username associated with the account."); + case NameColumn: return tr("User name of the account."); + case TypeColumn: return tr("Type of the account (MSA or Offline)"); + case StatusColumn: return tr("Current status of the account."); + default: return QVariant(); + } + + default: return QVariant(); + } +} + +int AccountList::rowCount(const QModelIndex& parent) const +{ + // Return count + return parent.isValid() ? 0 : count(); +} + +int AccountList::columnCount(const QModelIndex& parent) const +{ + return parent.isValid() ? 0 : NUM_COLUMNS; +} + +Qt::ItemFlags AccountList::flags(const QModelIndex& index) const +{ + if (index.row() < 0 || index.row() >= rowCount(index.parent()) || !index.isValid()) + { + return Qt::NoItemFlags; + } + + return Qt::ItemIsUserCheckable | Qt::ItemIsEnabled | Qt::ItemIsSelectable; +} + +bool AccountList::setData(const QModelIndex& idx, const QVariant& value, int role) +{ + if (idx.row() < 0 || idx.row() >= rowCount(idx.parent()) || !idx.isValid()) + { + return false; + } + + if (role == Qt::CheckStateRole) + { + if (value == Qt::Checked) + { + MinecraftAccountPtr account = at(idx.row()); + setDefaultAccount(account); + } + else if (m_defaultAccount == at(idx.row())) + setDefaultAccount(nullptr); + } + + emit dataChanged(idx, index(idx.row(), columnCount(QModelIndex()) - 1)); + return true; +} + +bool AccountList::loadList() +{ + if (m_listFilePath.isEmpty()) + { + qCritical() << "Can't load Mojang account list. No file path given and no default set."; + return false; + } + + QFile file(m_listFilePath); + + // Try to open the file and fail if we can't. + if (!file.open(QIODevice::ReadOnly)) + { + qCritical() << QString("Failed to read the account list file (%1): %2") + .arg(m_listFilePath, file.errorString()) + .toUtf8(); + return false; + } + + // 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() << QString("Failed to parse account list file: %1 at offset %2") + .arg(parseError.errorString(), QString::number(parseError.offset)) + .toUtf8(); + return false; + } + + // Make sure the root is an object. + if (!jsonDoc.isObject()) + { + qCritical() << "Invalid account list JSON: Root should be an array."; + return false; + } + + QJsonObject root = jsonDoc.object(); + + // Make sure the format version matches. + auto listVersion = root.value("formatVersion").toVariant().toInt(); + if (listVersion == AccountListVersion::MojangMSA) + return loadV3(root); + + QString newName = "accounts-old.json"; + qWarning() << "Unknown format version when loading account list. Existing one will be renamed to" << newName; + // Attempt to rename the old version. + file.rename(newName); + return false; +} + +bool AccountList::loadV3(QJsonObject& root) +{ + beginResetModel(); + QJsonArray accounts = root.value("accounts").toArray(); + for (QJsonValue accountVal : accounts) + { + QJsonObject accountObj = accountVal.toObject(); + MinecraftAccountPtr account = MinecraftAccount::loadFromJsonV3(accountObj); + if (account.get() != nullptr) + { + auto profileId = account->profileId(); + if (profileId.size()) + { + if (findAccountByProfileId(profileId) != -1) + { + continue; + } + } + connect(account.get(), &MinecraftAccount::changed, this, &AccountList::accountChanged); + connect(account.get(), &MinecraftAccount::activityChanged, this, &AccountList::accountActivityChanged); + m_accounts.append(account); + if (accountObj.value("active").toBool(false)) + { + m_defaultAccount = account; + } + } + else + { + qWarning() << "Failed to load an account."; + } + } + endResetModel(); + return true; +} + +bool AccountList::saveList() +{ + if (m_listFilePath.isEmpty()) + { + qCritical() << "Can't save Mojang account list. No file path given and no default set."; + return false; + } + + // make sure the parent folder exists + if (!FS::ensureFilePathExists(m_listFilePath)) + return false; + + // make sure the file wasn't overwritten with a folder before (fixes a bug) + QFileInfo finfo(m_listFilePath); + if (finfo.isDir()) + { + QDir badDir(m_listFilePath); + badDir.removeRecursively(); + } + + qDebug() << "Writing account list to" << m_listFilePath; + + qDebug() << "Building JSON data structure."; + // Build the JSON document to write to the list file. + QJsonObject root; + + root.insert("formatVersion", AccountListVersion::MojangMSA); + + // Build a list of accounts. + qDebug() << "Building account array."; + QJsonArray accounts; + for (MinecraftAccountPtr account : m_accounts) + { + QJsonObject accountObj = account->saveToJson(); + if (m_defaultAccount == account) + { + accountObj["active"] = true; + } + accounts.append(accountObj); + } + + // Insert the account list into the root object. + root.insert("accounts", accounts); + + // Create a JSON document object to convert our JSON to bytes. + QJsonDocument doc(root); + + // Now that we're done building the JSON object, we can write it to the file. + qDebug() << "Writing account list to file."; + QSaveFile file(m_listFilePath); + + // Try to open the file and fail if we can't. + if (!file.open(QIODevice::WriteOnly)) + { + qCritical() << QString("Failed to write the account list file (%1): %2") + .arg(m_listFilePath, file.errorString()) + .toUtf8(); + return false; + } + + // Write the JSON to the file. + file.write(doc.toJson()); + file.setPermissions(QFile::ReadOwner | QFile::WriteOwner | QFile::ReadUser | QFile::WriteUser); + if (file.commit()) + { + qDebug() << "Saved account list to" << m_listFilePath; + return true; + } + else + { + qDebug() << "Failed to save accounts to" << m_listFilePath; + return false; + } +} + +void AccountList::setListFilePath(QString path, bool autosave) +{ + m_listFilePath = path; + m_autosave = autosave; +} + +bool AccountList::anyAccountIsValid() +{ + for (auto account : m_accounts) + { + if (account->ownsMinecraft()) + { + return true; + } + } + return false; +} + +void AccountList::fillQueue() +{ + if (m_defaultAccount && m_defaultAccount->shouldRefresh()) + { + auto idToRefresh = m_defaultAccount->internalId(); + m_refreshQueue.push_back(idToRefresh); + qDebug() << "AccountList: Queued default account with internal ID " << idToRefresh << " to refresh first"; + } + + for (int i = 0; i < count(); i++) + { + auto account = at(i); + if (account == m_defaultAccount) + { + continue; + } + + if (account->shouldRefresh()) + { + auto idToRefresh = account->internalId(); + queueRefresh(idToRefresh); + } + } + tryNext(); +} + +void AccountList::requestRefresh(QString accountId) +{ + auto index = m_refreshQueue.indexOf(accountId); + if (index != -1) + { + m_refreshQueue.removeAt(index); + } + m_refreshQueue.push_front(accountId); + qDebug() << "AccountList: Pushed account with internal ID " << accountId << " to the front of the queue"; + if (!isActive()) + { + tryNext(); + } +} + +void AccountList::queueRefresh(QString accountId) +{ + if (m_refreshQueue.indexOf(accountId) != -1) + { + return; + } + m_refreshQueue.push_back(accountId); + qDebug() << "AccountList: Queued account with internal ID " << accountId << " to refresh"; +} + +void AccountList::tryNext() +{ + while (m_refreshQueue.length()) + { + auto accountId = m_refreshQueue.front(); + m_refreshQueue.pop_front(); + for (int i = 0; i < count(); i++) + { + auto account = at(i); + if (account->internalId() == accountId) + { + m_currentTask = account->refresh(); + if (m_currentTask) + { + connect(m_currentTask.get(), &Task::succeeded, this, &AccountList::authSucceeded); + connect(m_currentTask.get(), &Task::failed, this, &AccountList::authFailed); + m_currentTask->start(); + qDebug() << "RefreshSchedule: Processing account " << account->accountDisplayString() + << " with internal ID " << accountId; + return; + } + } + } + qDebug() << "RefreshSchedule: Account with with internal ID " << accountId << " not found."; + } + // if we get here, no account needed refreshing. Schedule refresh in an hour. + m_refreshTimer->start(1000 * 3600); +} + +void AccountList::authSucceeded() +{ + qDebug() << "RefreshSchedule: Background account refresh succeeded"; + m_currentTask.reset(); + m_nextTimer->start(1000 * 20); +} + +void AccountList::authFailed(QString reason) +{ + qDebug() << "RefreshSchedule: Background account refresh failed: " << reason; + m_currentTask.reset(); + m_nextTimer->start(1000 * 20); +} + +bool AccountList::isActive() const +{ + return m_activityCount != 0; +} + +void AccountList::beginActivity() +{ + bool activating = m_activityCount == 0; + m_activityCount++; + if (activating) + { + emit activityChanged(true); + } +} + +void AccountList::endActivity() +{ + if (m_activityCount == 0) + { + qWarning() << m_name << " - Activity count would become below zero"; + return; + } + bool deactivating = m_activityCount == 1; + m_activityCount--; + if (deactivating) + { + emit activityChanged(false); + } +} diff --git a/archived/projt-launcher/launcher/minecraft/auth/AccountList.hpp b/archived/projt-launcher/launcher/minecraft/auth/AccountList.hpp new file mode 100644 index 0000000000..107c4bb286 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/AccountList.hpp @@ -0,0 +1,202 @@ +// 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 Sefa Eyeoglu <contact@scrumplex.net> + * + * 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. + * + * ======================================================================== */ + +#pragma once + +#include "MinecraftAccount.hpp" +#include "minecraft/auth/AuthFlow.hpp" + +#include <QAbstractListModel> +#include <QObject> +#include <QSharedPointer> +#include <QVariant> + +/*! + * List of available Mojang accounts. + * This should be loaded in the background by ProjT Launcher on startup. + */ +class AccountList : public QAbstractListModel +{ + Q_OBJECT + public: + enum ModelRoles + { + PointerRole = 0x34B1CB48 + }; + + enum VListColumns + { + IconColumn = 0, + ProfileNameColumn, + NameColumn, + TypeColumn, + StatusColumn, + + NUM_COLUMNS + }; + + explicit AccountList(QObject* parent = 0); + virtual ~AccountList() noexcept; + + const MinecraftAccountPtr at(int i) const; + int count() const; + + //////// List Model Functions //////// + QVariant data(const QModelIndex& index, int role) const override; + virtual QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + virtual int rowCount(const QModelIndex& parent) const override; + virtual int columnCount(const QModelIndex& parent) const override; + virtual Qt::ItemFlags flags(const QModelIndex& index) const override; + virtual bool setData(const QModelIndex& index, const QVariant& value, int role) override; + + void addAccount(MinecraftAccountPtr account); + void removeAccount(QModelIndex index); + int findAccountByProfileId(const QString& profileId) const; + MinecraftAccountPtr getAccountByProfileName(const QString& profileName) const; + QStringList profileNames() const; + + // requesting a refresh pushes it to the front of the queue + void requestRefresh(QString accountId); + // queuing a refresh will let it go to the back of the queue (unless it's somewhere inside the queue already) + void queueRefresh(QString accountId); + + /*! + * Sets the path to load/save the list file from/to. + * If autosave is true, this list will automatically save to the given path whenever it changes. + * THIS FUNCTION DOES NOT LOAD THE LIST. If you set autosave, be sure to call loadList() immediately + * after calling this function to ensure an autosaved change doesn't overwrite the list you intended + * to load. + */ + void setListFilePath(QString path, bool autosave = false); + + bool loadList(); + bool loadV3(QJsonObject& root); + bool saveList(); + + MinecraftAccountPtr defaultAccount() const; + void setDefaultAccount(MinecraftAccountPtr profileId); + bool anyAccountIsValid(); + + bool isActive() const; + + protected: + void beginActivity(); + void endActivity(); + + private: + const char* m_name; + uint32_t m_activityCount = 0; + signals: + void listChanged(); + void listActivityChanged(); + void defaultAccountChanged(); + void activityChanged(bool active); + void fileSaveFailed(QString path); + + public slots: + /** + * This is called when one of the accounts changes and the list needs to be updated + */ + void accountChanged(); + + /** + * This is called when a (refresh/login) task involving the account starts or ends + */ + void accountActivityChanged(bool active); + + /** + * This is initially to run background account refresh tasks, or on a hourly timer + */ + void fillQueue(); + + private slots: + void tryNext(); + + void authSucceeded(); + void authFailed(QString reason); + + protected: + QList<QString> m_refreshQueue; + QTimer* m_refreshTimer; + QTimer* m_nextTimer; + shared_qobject_ptr<AuthFlow> m_currentTask; + + /*! + * Called whenever the list changes. + * This emits the listChanged() signal and autosaves the list (if autosave is enabled). + */ + void onListChanged(); + + /*! + * Called whenever the active account changes. + * Emits the defaultAccountChanged() signal and autosaves the list if enabled. + */ + void onDefaultAccountChanged(); + + QList<MinecraftAccountPtr> m_accounts; + + MinecraftAccountPtr m_defaultAccount; + + //! Path to the account list file. Empty string if there isn't one. + QString m_listFilePath; + + /*! + * If true, the account list will automatically save to the account list path when it changes. + * Ignored if m_listFilePath is blank. + */ + bool m_autosave = false; +}; diff --git a/archived/projt-launcher/launcher/minecraft/auth/AuthFlow.cpp b/archived/projt-launcher/launcher/minecraft/auth/AuthFlow.cpp new file mode 100644 index 0000000000..7b20dc5a3f --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/AuthFlow.cpp @@ -0,0 +1,438 @@ +// 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. + */ + +#include "AuthFlow.hpp" + +#include <QDebug> + +#include "Application.h" +#include "minecraft/auth/steps/Steps.hpp" + +AuthFlow::AuthFlow(AccountData* data, Action action) : Task(), m_legacyData(data) +{ + Q_ASSERT(data != nullptr); + + // Initialize credentials from legacy data if refreshing + if (action == Action::Refresh && data) + { + m_credentials.msaClientId = data->msaClientID; + m_credentials.msaToken.accessToken = data->msaToken.token; + m_credentials.msaToken.refreshToken = data->msaToken.refresh_token; + m_credentials.msaToken.issuedAt = data->msaToken.issueInstant; + m_credentials.msaToken.expiresAt = data->msaToken.notAfter; + m_credentials.msaToken.metadata = data->msaToken.extra; + } + + m_pipelineValid = buildPipeline(action); + + if (!m_pipelineValid) + { + qWarning() << "AuthFlow: Pipeline build failed for account type" << static_cast<int>(data->type); + } + + updateState(AccountTaskState::STATE_CREATED); +} + +bool AuthFlow::buildPipeline(Action action) +{ + // Explicit handling of non-MSA accounts + if (m_legacyData->type == AccountType::Offline) + { + qDebug() << "AuthFlow: Offline account does not require authentication pipeline"; + // Offline accounts don't need auth steps - this is valid, not an error + // The caller should check account type before creating AuthFlow + return false; + } + + if (m_legacyData->type != AccountType::MSA) + { + qWarning() << "AuthFlow: Unsupported account type:" << static_cast<int>(m_legacyData->type); + return false; + } + + // Step 1: Microsoft Authentication + if (action == Action::DeviceCode) + { + auto* deviceCodeStep = new projt::minecraft::auth::DeviceCodeAuthStep(m_credentials); + connect(deviceCodeStep, + &projt::minecraft::auth::DeviceCodeAuthStep::deviceCodeReady, + this, + &AuthFlow::authorizeWithBrowserWithExtra); + connect(this, &Task::aborted, deviceCodeStep, &projt::minecraft::auth::DeviceCodeAuthStep::cancel); + m_steps.append(projt::minecraft::auth::Step::Ptr(deviceCodeStep)); + } + else + { + auto* oauthStep = new projt::minecraft::auth::MicrosoftOAuthStep(m_credentials, action == Action::Refresh); + connect(oauthStep, + &projt::minecraft::auth::MicrosoftOAuthStep::browserAuthRequired, + this, + &AuthFlow::authorizeWithBrowser); + m_steps.append(projt::minecraft::auth::Step::Ptr(oauthStep)); + } + + // Step 2: Xbox Live User Token + m_steps.append(projt::minecraft::auth::Step::Ptr(new projt::minecraft::auth::XboxLiveUserStep(m_credentials))); + + // Step 3: Xbox XSTS Token for Xbox Live services + m_steps.append(projt::minecraft::auth::Step::Ptr( + new projt::minecraft::auth::XboxSecurityTokenStep(m_credentials, + projt::minecraft::auth::XstsTarget::XboxLive))); + + // Step 4: Xbox XSTS Token for Minecraft services + m_steps.append(projt::minecraft::auth::Step::Ptr( + new projt::minecraft::auth::XboxSecurityTokenStep(m_credentials, + projt::minecraft::auth::XstsTarget::MinecraftServices))); + + // Step 5: Minecraft Services Login (get access token) + m_steps.append( + projt::minecraft::auth::Step::Ptr(new projt::minecraft::auth::MinecraftServicesLoginStep(m_credentials))); + + // Step 6: Xbox Profile (optional, for display - gamertag extraction) + m_steps.append(projt::minecraft::auth::Step::Ptr(new projt::minecraft::auth::XboxProfileFetchStep(m_credentials))); + + // Step 7: Game Entitlements + m_steps.append(projt::minecraft::auth::Step::Ptr(new projt::minecraft::auth::GameEntitlementsStep(m_credentials))); + + // Step 8: Minecraft Profile + m_steps.append( + projt::minecraft::auth::Step::Ptr(new projt::minecraft::auth::MinecraftProfileFetchStep(m_credentials))); + + // Step 9: Skin Download + m_steps.append(projt::minecraft::auth::Step::Ptr(new projt::minecraft::auth::SkinDownloadStep(m_credentials))); + + qDebug() << "AuthFlow: Built pipeline with" << m_steps.size() << "steps"; + return true; +} + +void AuthFlow::executeTask() +{ + // Handle offline accounts - they don't need authentication + if (m_legacyData->type == AccountType::Offline) + { + qDebug() << "AuthFlow: Offline account - no authentication required, succeeding immediately"; + if (m_legacyData) + { + m_legacyData->accountState = AccountState::Online; + } + updateState(AccountTaskState::STATE_SUCCEEDED, tr("Offline account ready")); + return; + } + + // Early fail for invalid pipeline (non-offline accounts) + if (!m_pipelineValid) + { + failWithState(AccountTaskState::STATE_FAILED_HARD, + tr("Failed to build authentication pipeline for this account type")); + return; + } + + // Sanity check: empty pipeline should not succeed silently + if (m_steps.isEmpty()) + { + qWarning() << "AuthFlow: Pipeline is empty after successful build - this is a bug"; + failWithState(AccountTaskState::STATE_FAILED_HARD, tr("Authentication pipeline is empty (internal error)")); + return; + } + + updateState(AccountTaskState::STATE_WORKING, tr("Initializing")); + executeNextStep(); +} + +void AuthFlow::executeNextStep() +{ + // Check abort flag before starting new step + if (m_aborted) + { + qDebug() << "AuthFlow: Skipping next step - flow was aborted"; + return; + } + + if (!Task::isRunning()) + { + return; + } + + if (m_steps.isEmpty()) + { + m_currentStep.reset(); + succeed(); + return; + } + + m_currentStep = m_steps.front(); + m_steps.pop_front(); + + qDebug() << "AuthFlow:" << m_currentStep->description(); + + connect(m_currentStep.get(), &projt::minecraft::auth::Step::completed, this, &AuthFlow::onStepCompleted); + + m_currentStep->execute(); +} + +void AuthFlow::onStepCompleted(projt::minecraft::auth::StepResult result, QString message) +{ + // Map step result to flow state + // Note: StepResult::Succeeded means "step succeeded, continue flow" + // The flow itself only succeeds when all steps complete (pipeline empty) + const auto flowState = stepResultToFlowState(result); + + if (updateState(flowState, message)) + { + executeNextStep(); + } +} + +void AuthFlow::succeed() +{ + // Sync new credentials back to legacy AccountData + if (m_legacyData) + { + m_legacyData->msaClientID = m_credentials.msaClientId; + m_legacyData->msaToken.token = m_credentials.msaToken.accessToken; + m_legacyData->msaToken.refresh_token = m_credentials.msaToken.refreshToken; + m_legacyData->msaToken.issueInstant = m_credentials.msaToken.issuedAt; + m_legacyData->msaToken.notAfter = m_credentials.msaToken.expiresAt; + m_legacyData->msaToken.extra = m_credentials.msaToken.metadata; + m_legacyData->msaToken.validity = toValidity(m_credentials.msaToken.validity); + + m_legacyData->userToken.token = m_credentials.xboxUserToken.accessToken; + m_legacyData->userToken.issueInstant = m_credentials.xboxUserToken.issuedAt; + m_legacyData->userToken.notAfter = m_credentials.xboxUserToken.expiresAt; + m_legacyData->userToken.extra = m_credentials.xboxUserToken.metadata; + m_legacyData->userToken.validity = toValidity(m_credentials.xboxUserToken.validity); + + // xboxApiToken receives gamertag from XboxProfileFetchStep via xboxServiceToken.metadata + m_legacyData->xboxApiToken.token = m_credentials.xboxServiceToken.accessToken; + m_legacyData->xboxApiToken.issueInstant = m_credentials.xboxServiceToken.issuedAt; + m_legacyData->xboxApiToken.notAfter = m_credentials.xboxServiceToken.expiresAt; + m_legacyData->xboxApiToken.extra = m_credentials.xboxServiceToken.metadata; + m_legacyData->xboxApiToken.validity = toValidity(m_credentials.xboxServiceToken.validity); + + m_legacyData->mojangservicesToken.token = m_credentials.minecraftServicesToken.accessToken; + m_legacyData->mojangservicesToken.issueInstant = m_credentials.minecraftServicesToken.issuedAt; + m_legacyData->mojangservicesToken.notAfter = m_credentials.minecraftServicesToken.expiresAt; + m_legacyData->mojangservicesToken.extra = m_credentials.minecraftServicesToken.metadata; + m_legacyData->mojangservicesToken.validity = toValidity(m_credentials.minecraftServicesToken.validity); + + m_legacyData->yggdrasilToken.token = m_credentials.minecraftAccessToken.accessToken; + m_legacyData->yggdrasilToken.issueInstant = m_credentials.minecraftAccessToken.issuedAt; + m_legacyData->yggdrasilToken.notAfter = m_credentials.minecraftAccessToken.expiresAt; + m_legacyData->yggdrasilToken.validity = toValidity(m_credentials.minecraftAccessToken.validity); + + m_legacyData->minecraftProfile.id = m_credentials.profile.id; + m_legacyData->minecraftProfile.name = m_credentials.profile.name; + m_legacyData->minecraftProfile.skin.id = m_credentials.profile.skin.id; + m_legacyData->minecraftProfile.skin.url = m_credentials.profile.skin.url; + m_legacyData->minecraftProfile.skin.variant = m_credentials.profile.skin.variant; + m_legacyData->minecraftProfile.skin.data = m_credentials.profile.skin.imageData; + m_legacyData->minecraftProfile.validity = toValidity(m_credentials.profile.validity); + m_legacyData->minecraftProfile.currentCape = m_credentials.profile.activeCapeId; + + // Sync capes + m_legacyData->minecraftProfile.capes.clear(); + for (auto it = m_credentials.profile.capes.begin(); it != m_credentials.profile.capes.end(); ++it) + { + const auto& capeIn = it.value(); + Cape capeOut; + capeOut.id = capeIn.id; + capeOut.url = capeIn.url; + capeOut.alias = capeIn.alias; + capeOut.data = capeIn.imageData; + m_legacyData->minecraftProfile.capes.insert(capeIn.id, capeOut); + } + + m_legacyData->minecraftEntitlement.ownsMinecraft = m_credentials.entitlements.ownsMinecraft; + m_legacyData->minecraftEntitlement.canPlayMinecraft = m_credentials.entitlements.canPlayMinecraft; + m_legacyData->minecraftEntitlement.validity = toValidity(m_credentials.entitlements.validity); + + m_legacyData->validity_ = Validity::Certain; + } + + updateState(AccountTaskState::STATE_SUCCEEDED, tr("Finished all authentication steps")); +} + +void AuthFlow::failWithState(AccountTaskState state, const QString& reason) +{ + if (m_legacyData) + { + m_legacyData->errorString = reason; + } + updateState(state, reason); +} + +bool AuthFlow::updateState(AccountTaskState newState, const QString& reason) +{ + m_taskState = newState; + setDetails(reason); + + switch (newState) + { + case AccountTaskState::STATE_CREATED: + setStatus(tr("Waiting...")); + if (m_legacyData) + { + m_legacyData->errorString.clear(); + } + return true; + + case AccountTaskState::STATE_WORKING: + setStatus(m_currentStep ? m_currentStep->description() : tr("Working...")); + if (m_legacyData) + { + m_legacyData->accountState = AccountState::Working; + } + return true; + + case AccountTaskState::STATE_SUCCEEDED: + setStatus(tr("Authentication task succeeded.")); + if (m_legacyData) + { + m_legacyData->accountState = AccountState::Online; + } + emitSucceeded(); + return false; + + case AccountTaskState::STATE_OFFLINE: + setStatus(tr("Failed to contact the authentication server.")); + if (m_legacyData) + { + m_legacyData->errorString = reason; + m_legacyData->accountState = AccountState::Offline; + } + emitFailed(reason); + return false; + + case AccountTaskState::STATE_DISABLED: + setStatus(tr("Client ID has changed. New session needs to be created.")); + if (m_legacyData) + { + m_legacyData->errorString = reason; + m_legacyData->accountState = AccountState::Disabled; + } + emitFailed(reason); + return false; + + case AccountTaskState::STATE_FAILED_SOFT: + setStatus(tr("Encountered an error during authentication.")); + if (m_legacyData) + { + m_legacyData->errorString = reason; + m_legacyData->accountState = AccountState::Errored; + } + emitFailed(reason); + return false; + + case AccountTaskState::STATE_FAILED_HARD: + setStatus(tr("Failed to authenticate. The session has expired.")); + if (m_legacyData) + { + m_legacyData->errorString = reason; + m_legacyData->accountState = AccountState::Expired; + } + emitFailed(reason); + return false; + + case AccountTaskState::STATE_FAILED_GONE: + setStatus(tr("Failed to authenticate. The account no longer exists.")); + if (m_legacyData) + { + m_legacyData->errorString = reason; + m_legacyData->accountState = AccountState::Gone; + } + emitFailed(reason); + return false; + + default: + setStatus(tr("...")); + const QString error = tr("Unknown account task state: %1").arg(static_cast<int>(newState)); + if (m_legacyData) + { + m_legacyData->accountState = AccountState::Errored; + } + emitFailed(error); + return false; + } +} + +AccountTaskState AuthFlow::stepResultToFlowState(projt::minecraft::auth::StepResult result) noexcept +{ + // StepResult::Continue and StepResult::Succeeded both mean "step completed successfully" + // The distinction is semantic: Continue hints more steps may follow, Succeeded suggests finality. + // At the flow level, both translate to STATE_WORKING until the pipeline is exhausted. + // + // Future: If we add optional/best-effort steps, we may want a StepResult::Skipped that + // also maps to STATE_WORKING but logs differently. + + switch (result) + { + case projt::minecraft::auth::StepResult::Continue: + case projt::minecraft::auth::StepResult::Succeeded: return AccountTaskState::STATE_WORKING; + + case projt::minecraft::auth::StepResult::Offline: return AccountTaskState::STATE_OFFLINE; + + case projt::minecraft::auth::StepResult::SoftFailure: return AccountTaskState::STATE_FAILED_SOFT; + + case projt::minecraft::auth::StepResult::HardFailure: return AccountTaskState::STATE_FAILED_HARD; + + case projt::minecraft::auth::StepResult::Disabled: return AccountTaskState::STATE_DISABLED; + + case projt::minecraft::auth::StepResult::Gone: return AccountTaskState::STATE_FAILED_GONE; + } + + return AccountTaskState::STATE_FAILED_HARD; +} + +Validity AuthFlow::toValidity(projt::minecraft::auth::TokenValidity validity) noexcept +{ + switch (validity) + { + case projt::minecraft::auth::TokenValidity::None: return Validity::None; + case projt::minecraft::auth::TokenValidity::Assumed: return Validity::Assumed; + case projt::minecraft::auth::TokenValidity::Certain: return Validity::Certain; + } + return Validity::None; +} + +bool AuthFlow::abort() +{ + // Set abort flag to prevent new steps from starting + m_aborted = true; + + qDebug() << "AuthFlow: Abort requested"; + + // Cancel current step BEFORE emitting aborted (to prevent use-after-free) + // The emitAborted() signal may cause this object to be destroyed + if (m_currentStep) + { + // Disconnect to prevent callbacks after abort + disconnect(m_currentStep.get(), nullptr, this, nullptr); + m_currentStep->cancel(); + m_currentStep.reset(); + } + + // Clear remaining steps + m_steps.clear(); + + emitAborted(); + + return true; +}
\ No newline at end of file diff --git a/archived/projt-launcher/launcher/minecraft/auth/AuthFlow.hpp b/archived/projt-launcher/launcher/minecraft/auth/AuthFlow.hpp new file mode 100644 index 0000000000..86fac44286 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/AuthFlow.hpp @@ -0,0 +1,107 @@ +// 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. + */ + +#pragma once + +#include <QList> +#include <QNetworkReply> +#include <QObject> +#include <QUrl> + +#include "minecraft/auth/AccountData.hpp" +#include "minecraft/auth/steps/Step.hpp" +#include "minecraft/auth/steps/Credentials.hpp" +#include "tasks/Task.h" + +class AuthFlow : public Task +{ + Q_OBJECT + + public: + /** + * Authentication action to perform. + */ + enum class Action + { + Refresh, ///< Silent token refresh + Login, ///< Interactive browser login + DeviceCode ///< Device code flow + }; + + explicit AuthFlow(AccountData* data, Action action = Action::Refresh); + ~AuthFlow() override = default; + + void executeTask() override; + + [[nodiscard]] AccountTaskState taskState() const noexcept + { + return m_taskState; + } + + public slots: + bool abort() override; + + signals: + /** + * Emitted when browser authorization is required. + */ + void authorizeWithBrowser(const QUrl& url); + + /** + * Emitted when device code authorization is required. + */ + void authorizeWithBrowserWithExtra(QString url, QString code, int expiresIn); + + private slots: + void onStepCompleted(projt::minecraft::auth::StepResult result, QString message); + + private: + /** + * Build the authentication pipeline based on account type and action. + * @return true if pipeline was successfully built, false on configuration error + */ + [[nodiscard]] bool buildPipeline(Action action); + + void executeNextStep(); + void succeed(); + void failWithState(AccountTaskState state, const QString& reason); + bool updateState(AccountTaskState newState, const QString& reason = QString()); + + // Convert new StepResult to intermediate flow state (not final AccountTaskState) + [[nodiscard]] static AccountTaskState stepResultToFlowState(projt::minecraft::auth::StepResult result) noexcept; + + // Convert new TokenValidity to legacy Validity + [[nodiscard]] static Validity toValidity(projt::minecraft::auth::TokenValidity validity) noexcept; + + AccountTaskState m_taskState = AccountTaskState::STATE_CREATED; + QList<projt::minecraft::auth::Step::Ptr> m_steps; + projt::minecraft::auth::Step::Ptr m_currentStep; + + // Flow control + bool m_aborted = false; + bool m_pipelineValid = false; + + // Legacy AccountData for compatibility with existing consumers + AccountData* m_legacyData = nullptr; + + // New credentials structure (populated during auth, synced to legacy at end) + projt::minecraft::auth::Credentials m_credentials; +}; diff --git a/archived/projt-launcher/launcher/minecraft/auth/AuthSession.cpp b/archived/projt-launcher/launcher/minecraft/auth/AuthSession.cpp new file mode 100644 index 0000000000..cedb2e7579 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/AuthSession.cpp @@ -0,0 +1,63 @@ +// 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. + */ +#include "AuthSession.hpp" +#include <QJsonArray> +#include <QJsonDocument> +#include <QJsonObject> +#include <QStringList> + +QString AuthSession::serializeUserProperties() +{ + QJsonObject userAttrs; + /* + for (auto key : u.properties.keys()) + { + auto array = QJsonArray::fromStringList(u.properties.values(key)); + userAttrs.insert(key, array); + } + */ + QJsonDocument value(userAttrs); + return value.toJson(QJsonDocument::Compact); +} + +bool AuthSession::MakeOffline(QString offline_playername) +{ + session = "-"; + access_token = "0"; + player_name = offline_playername; + launchMode = LaunchMode::Offline; + wants_online = false; + demo = false; + status = PlayableOffline; + return true; +} + +void AuthSession::MakeDemo(QString name, QString u) +{ + launchMode = LaunchMode::Demo; + wants_online = false; + demo = true; + uuid = u; + session = "-"; + access_token = "0"; + player_name = name; + status = PlayableOnline; // needs online to download the assets +}; diff --git a/archived/projt-launcher/launcher/minecraft/auth/AuthSession.hpp b/archived/projt-launcher/launcher/minecraft/auth/AuthSession.hpp new file mode 100644 index 0000000000..10092a0e91 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/AuthSession.hpp @@ -0,0 +1,69 @@ +// 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. + */ +#pragma once + +#include <QString> +#include <memory> + +#include "LaunchMode.h" + +class MinecraftAccount; + +struct AuthSession +{ + bool MakeOffline(QString offline_playername); + void MakeDemo(QString name, QString uuid); + + QString serializeUserProperties(); + + enum Status + { + Undetermined, + RequiresOAuth, + RequiresPassword, + RequiresProfileSetup, + PlayableOffline, + PlayableOnline, + GoneOrMigrated + } status = Undetermined; + + // combined session ID + QString session; + // volatile auth token + QString access_token; + // profile name + QString player_name; + // profile ID + QString uuid; + // 'legacy' or 'mojang', depending on account type + QString user_type; + // The resolved launch mode for this session. + LaunchMode launchMode = LaunchMode::Normal; + // Did the auth server reply? + bool auth_server_online = false; + // Did the user request online mode? + bool wants_online = true; + + // Is this a demo session? + bool demo = false; +}; + +using AuthSessionPtr = std::shared_ptr<AuthSession>; diff --git a/archived/projt-launcher/launcher/minecraft/auth/MinecraftAccount.cpp b/archived/projt-launcher/launcher/minecraft/auth/MinecraftAccount.cpp new file mode 100644 index 0000000000..5435b62809 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/MinecraftAccount.cpp @@ -0,0 +1,374 @@ +// 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 Sefa Eyeoglu <contact@scrumplex.net> + * + * 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 "MinecraftAccount.hpp" + +#include <QColor> +#include <QCryptographicHash> +#include <QJsonArray> +#include <QJsonDocument> +#include <QJsonObject> +#include <QStringList> +#include <QUuid> + +#include <QDebug> + +#include <QPainter> + +#include "minecraft/auth/AccountData.hpp" +#include "minecraft/auth/AuthFlow.hpp" + +MinecraftAccount::MinecraftAccount(QObject* parent) : QObject(parent) +{ + data.internalId = QUuid::createUuid().toString(QUuid::Id128); +} + +MinecraftAccountPtr MinecraftAccount::loadFromJsonV3(const QJsonObject& json) +{ + MinecraftAccountPtr account(new MinecraftAccount()); + if (account->data.resumeStateFromV3(json)) + { + return account; + } + return nullptr; +} + +MinecraftAccountPtr MinecraftAccount::createBlankMSA() +{ + MinecraftAccountPtr account(new MinecraftAccount()); + account->data.type = AccountType::MSA; + return account; +} + +MinecraftAccountPtr MinecraftAccount::createOffline(const QString& username) +{ + auto account = makeShared<MinecraftAccount>(); + account->data.type = AccountType::Offline; + account->data.yggdrasilToken.token = "0"; + account->data.yggdrasilToken.validity = Validity::Certain; + account->data.yggdrasilToken.issueInstant = QDateTime::currentDateTimeUtc(); + account->data.yggdrasilToken.extra["userName"] = username; + account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString(QUuid::Id128); + account->data.minecraftProfile.id = uuidFromUsername(username).toString(QUuid::Id128); + account->data.minecraftProfile.name = username; + account->data.minecraftProfile.validity = Validity::Certain; + return account; +} + +QJsonObject MinecraftAccount::saveToJson() const +{ + return data.saveState(); +} + +AccountState MinecraftAccount::accountState() const +{ + return data.accountState; +} + +QPixmap MinecraftAccount::getFace() const +{ + QPixmap skinTexture; + if (!skinTexture.loadFromData(data.minecraftProfile.skin.data, "PNG")) + { + return QPixmap(); + } + QPixmap skin = QPixmap(8, 8); + skin.fill(QColorConstants::Transparent); + QPainter painter(&skin); + painter.drawPixmap(0, 0, skinTexture.copy(8, 8, 8, 8)); + painter.drawPixmap(0, 0, skinTexture.copy(40, 8, 8, 8)); + return skin.scaled(64, 64, Qt::KeepAspectRatio); +} + +shared_qobject_ptr<AuthFlow> MinecraftAccount::login(bool useDeviceCode) +{ + Q_ASSERT(m_currentTask.get() == nullptr); + + m_currentTask.reset(new AuthFlow(&data, useDeviceCode ? AuthFlow::Action::DeviceCode : AuthFlow::Action::Login)); + connect(m_currentTask.get(), &Task::succeeded, this, &MinecraftAccount::authSucceeded); + connect(m_currentTask.get(), &Task::failed, this, &MinecraftAccount::authFailed); + connect(m_currentTask.get(), &Task::aborted, this, [this] { authFailed(tr("Aborted")); }); + emit activityChanged(true); + return m_currentTask; +} + +shared_qobject_ptr<AuthFlow> MinecraftAccount::refresh() +{ + if (m_currentTask) + { + return m_currentTask; + } + + m_currentTask.reset(new AuthFlow(&data, AuthFlow::Action::Refresh)); + + connect(m_currentTask.get(), &Task::succeeded, this, &MinecraftAccount::authSucceeded); + connect(m_currentTask.get(), &Task::failed, this, &MinecraftAccount::authFailed); + connect(m_currentTask.get(), &Task::aborted, this, [this] { authFailed(tr("Aborted")); }); + emit activityChanged(true); + return m_currentTask; +} + +shared_qobject_ptr<AuthFlow> MinecraftAccount::currentTask() +{ + return m_currentTask; +} + +void MinecraftAccount::authSucceeded() +{ + m_currentTask.reset(); + emit authenticationSucceeded(); + emit changed(); + emit activityChanged(false); +} + +void MinecraftAccount::authFailed(QString reason) +{ + auto taskState = m_currentTask->taskState(); + emit authenticationFailed(reason, taskState); + + switch (taskState) + { + case AccountTaskState::STATE_OFFLINE: + case AccountTaskState::STATE_DISABLED: + { + // NOTE: user will need to fix this themselves. + } + case AccountTaskState::STATE_FAILED_SOFT: + { + // NOTE: this doesn't do much. There was an error of some sort. + } + break; + case AccountTaskState::STATE_FAILED_HARD: + { + if (accountType() == AccountType::MSA) + { + data.msaToken.token = QString(); + data.msaToken.refresh_token = QString(); + data.msaToken.validity = Validity::None; + data.validity_ = Validity::None; + } + else + { + data.yggdrasilToken.token = QString(); + data.yggdrasilToken.validity = Validity::None; + data.validity_ = Validity::None; + } + emit validityChanged(Validity::None); + emit changed(); + } + break; + case AccountTaskState::STATE_FAILED_GONE: + { + data.validity_ = Validity::None; + emit validityChanged(Validity::None); + emit changed(); + } + break; + case AccountTaskState::STATE_WORKING: + { + data.accountState = AccountState::Unchecked; + emit accountStateChanged(AccountState::Unchecked); + } + break; + case AccountTaskState::STATE_CREATED: + case AccountTaskState::STATE_SUCCEEDED: + { + // Not reachable here, as they are not failures. + } + } + m_currentTask.reset(); + emit activityChanged(false); + emit authenticationError(reason); +} + +QString MinecraftAccount::displayName() const +{ + const QList validStates{ AccountState::Unchecked, AccountState::Working, AccountState::Offline, AccountState::Online }; + if (!validStates.contains(accountState())) + { + return QString("⚠%1").arg(profileName()); + } + return profileName(); +} + +bool MinecraftAccount::isActive() const +{ + return !m_currentTask.isNull(); +} + +bool MinecraftAccount::shouldRefresh() const +{ + /* + * Never refresh accounts that are being used by the game, it breaks the game session. + * Always refresh accounts that have not been refreshed yet during this session. + * Don't refresh broken accounts. + * Refresh accounts that would expire in the next 12 hours (fresh token validity is 24 hours). + */ + if (isInUse()) + { + return false; + } + switch (data.validity_) + { + case Validity::Certain: + { + break; + } + case Validity::None: + { + return false; + } + case Validity::Assumed: + { + return true; + } + } + auto now = QDateTime::currentDateTimeUtc(); + auto issuedTimestamp = data.yggdrasilToken.issueInstant; + auto expiresTimestamp = data.yggdrasilToken.notAfter; + + if (!expiresTimestamp.isValid()) + { + expiresTimestamp = issuedTimestamp.addSecs(24 * 3600); + } + if (now.secsTo(expiresTimestamp) < (12 * 3600)) + { + return true; + } + return false; +} + +void MinecraftAccount::fillSession(AuthSessionPtr session) +{ + session->wants_online = session->launchMode != LaunchMode::Offline; + session->demo = session->launchMode == LaunchMode::Demo; + + if (ownsMinecraft() && !hasProfile()) + { + session->status = AuthSession::RequiresProfileSetup; + } + else + { + if (session->launchMode == LaunchMode::Offline) + { + session->status = AuthSession::PlayableOffline; + } + else + { + session->status = AuthSession::PlayableOnline; + } + } + + // volatile auth token + session->access_token = data.accessToken(); + // profile name + session->player_name = data.profileName(); + // profile ID + session->uuid = data.profileId(); + if (session->uuid.isEmpty()) + session->uuid = uuidFromUsername(session->player_name).toString(QUuid::Id128); + // 'legacy' or 'mojang', depending on account type + session->user_type = typeString(); + if (!session->access_token.isEmpty()) + { + session->session = "token:" + data.accessToken() + ":" + data.profileId(); + } + else + { + session->session = "-"; + } +} + +void MinecraftAccount::decrementUses() +{ + Usable::decrementUses(); + if (!isInUse()) + { + emit changed(); + // Using internalId for account identification (profile may not be set for new accounts) + qWarning() << "Account" << data.internalId << "(" << data.profileName() << ") is no longer in use."; + } +} + +void MinecraftAccount::incrementUses() +{ + bool wasInUse = isInUse(); + Usable::incrementUses(); + if (!wasInUse) + { + emit changed(); + // Using internalId for account identification (profile may not be set for new accounts) + qWarning() << "Account" << data.internalId << "(" << data.profileName() << ") is now in use."; + } +} + +QUuid MinecraftAccount::uuidFromUsername(QString username) +{ + auto input = QString("OfflinePlayer:%1").arg(username).toUtf8(); + + // basically a reimplementation of Java's UUID#nameUUIDFromBytes + QByteArray digest = QCryptographicHash::hash(input, QCryptographicHash::Md5); + + auto bOr = [](QByteArray& array, qsizetype index, uint8_t value) { array[index] |= value; }; + auto bAnd = [](QByteArray& array, qsizetype index, uint8_t value) { array[index] &= value; }; + bAnd(digest, 6, 0x0f); // clear version + bOr(digest, 6, 0x30); // set to version 3 + bAnd(digest, 8, 0x3f); // clear variant + bOr(digest, 8, 0x80); // set to IETF variant + + return QUuid::fromRfc4122(digest); +} diff --git a/archived/projt-launcher/launcher/minecraft/auth/MinecraftAccount.hpp b/archived/projt-launcher/launcher/minecraft/auth/MinecraftAccount.hpp new file mode 100644 index 0000000000..8af81e0a26 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/MinecraftAccount.hpp @@ -0,0 +1,243 @@ +// 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 Sefa Eyeoglu <contact@scrumplex.net> + * + * 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. + * + * ======================================================================== */ + +#pragma once + +#include <QJsonObject> +#include <QList> +#include <QMap> +#include <QObject> +#include <QPair> +#include <QPixmap> +#include <QString> + +#include "AccountData.hpp" +#include "AuthSession.hpp" +#include "QObjectPtr.h" +#include "Usable.h" +#include "minecraft/auth/AuthFlow.hpp" + +class Task; +class MinecraftAccount; + +using MinecraftAccountPtr = shared_qobject_ptr<MinecraftAccount>; +Q_DECLARE_METATYPE(MinecraftAccountPtr) + +/** + * A profile within someone's Mojang account. + * + * Currently, the profile system has not been implemented by Mojang yet, + * but we might as well add some things for it in ProjT Launcher right now so + * we don't have to rip the code to pieces to add it later. + */ +struct AccountProfile +{ + QString id; + QString name; + bool legacy; +}; + +/** + * Object that stores information about a certain Mojang account. + * + * Said information may include things such as that account's username, client token, and access + * token if the user chose to stay logged in. + */ +class MinecraftAccount : public QObject, public Usable +{ + Q_OBJECT + public: /* construction */ + //! Do not copy accounts. ever. + explicit MinecraftAccount(const MinecraftAccount& other, QObject* parent) = delete; + + //! Default constructor + explicit MinecraftAccount(QObject* parent = 0); + + static MinecraftAccountPtr createBlankMSA(); + + static MinecraftAccountPtr createOffline(const QString& username); + + static MinecraftAccountPtr loadFromJsonV3(const QJsonObject& json); + + static QUuid uuidFromUsername(QString username); + + //! Saves a MinecraftAccount to a JSON object and returns it. + QJsonObject saveToJson() const; + + public: /* manipulation */ + shared_qobject_ptr<AuthFlow> login(bool useDeviceCode = false); + + shared_qobject_ptr<AuthFlow> refresh(); + + shared_qobject_ptr<AuthFlow> currentTask(); + + public: /* queries */ + QString internalId() const + { + return data.internalId; + } + + QString accountDisplayString() const + { + return data.accountDisplayString(); + } + + QString accessToken() const + { + return data.accessToken(); + } + + QString profileId() const + { + return data.profileId(); + } + + QString profileName() const + { + return data.profileName(); + } + + QString displayName() const; + + bool isActive() const; + + AccountType accountType() const noexcept + { + return data.type; + } + + bool ownsMinecraft() const + { + return data.type != AccountType::Offline && data.minecraftEntitlement.ownsMinecraft; + } + + bool hasProfile() const + { + return data.profileId().size() != 0; + } + + QString typeString() const + { + switch (data.type) + { + case AccountType::MSA: + { + return "msa"; + } + break; + case AccountType::Offline: + { + return "offline"; + } + break; + default: + { + return "unknown"; + } + } + } + + QPixmap getFace() const; + + //! Returns the current state of the account + AccountState accountState() const; + + AccountData* accountData() + { + return &data; + } + + bool shouldRefresh() const; + + void fillSession(AuthSessionPtr session); + + QString lastError() const + { + return data.lastError(); + } + + signals: + /** + * This signal is emitted when the account changes + */ + void changed(); + + void activityChanged(bool active); + + // Specific signals for different state changes + void accountStateChanged(AccountState newState); + void authenticationSucceeded(); + void authenticationFailed(QString reason, AccountTaskState taskState); + void profileUpdated(); + void validityChanged(Validity newValidity); + /// Emitted when an authentication error occurs + void authenticationError(QString errorMessage); + + protected: /* variables */ + AccountData data; + + // current task we are executing here + shared_qobject_ptr<AuthFlow> m_currentTask; + + protected: /* methods */ + void incrementUses() override; + void decrementUses() override; + + private slots: + void authSucceeded(); + void authFailed(QString reason); +}; diff --git a/archived/projt-launcher/launcher/minecraft/auth/Parsers.cpp b/archived/projt-launcher/launcher/minecraft/auth/Parsers.cpp new file mode 100644 index 0000000000..184de65898 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/Parsers.cpp @@ -0,0 +1,597 @@ +// 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. + */ +#include "Parsers.hpp" +#include "Json.h" +#include "minecraft/Logging.h" + +#include <QDebug> +#include <QJsonArray> +#include <QJsonDocument> + +namespace Parsers +{ + + bool getDateTime(QJsonValue value, QDateTime& out) + { + if (!value.isString()) + { + return false; + } + out = QDateTime::fromString(value.toString(), Qt::ISODate); + return out.isValid(); + } + + bool getString(QJsonValue value, QString& out) + { + if (!value.isString()) + { + return false; + } + out = value.toString(); + return true; + } + + bool getNumber(QJsonValue value, double& out) + { + if (!value.isDouble()) + { + return false; + } + out = value.toDouble(); + return true; + } + + bool getNumber(QJsonValue value, int64_t& out) + { + if (!value.isDouble()) + { + return false; + } + out = (int64_t)value.toDouble(); + return true; + } + + bool getBool(QJsonValue value, bool& out) + { + if (!value.isBool()) + { + return false; + } + out = value.toBool(); + return true; + } + + /* + { + "IssueInstant":"2020-12-07T19:52:08.4463796Z", + "NotAfter":"2020-12-21T19:52:08.4463796Z", + "Token":"token", + "DisplayClaims":{ + "xui":[ + { + "uhs":"userhash" + } + ] + } + } + */ + // Error responses from Xbox Live are handled in parseXTokenResponse below. + // Known error codes: + // - 2148916233: Missing Xbox account + // - 2148916238: Child account not linked to a family + /* + { + "Identity":"0", + "XErr":2148916238, + "Message":"", + "Redirect":"https://start.ui.xboxlive.com/AddChildToFamily" + } + */ + + bool parseXTokenResponse(QByteArray& data, Token& output, QString name) + { + qDebug() << "Parsing" << name << ":"; + qCDebug(authCredentials()) << data; + QJsonParseError jsonError; + QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); + if (jsonError.error) + { + qWarning() << "Failed to parse response from user.auth.xboxlive.com as JSON: " << jsonError.errorString(); + return false; + } + + auto obj = doc.object(); + if (!getDateTime(obj.value("IssueInstant"), output.issueInstant)) + { + qWarning() << "User IssueInstant is not a timestamp"; + return false; + } + if (!getDateTime(obj.value("NotAfter"), output.notAfter)) + { + qWarning() << "User NotAfter is not a timestamp"; + return false; + } + if (!getString(obj.value("Token"), output.token)) + { + qWarning() << "User Token is not a string"; + return false; + } + auto arrayVal = obj.value("DisplayClaims").toObject().value("xui"); + if (!arrayVal.isArray()) + { + qWarning() << "Missing xui claims array"; + return false; + } + bool foundUHS = false; + for (auto item : arrayVal.toArray()) + { + if (!item.isObject()) + { + continue; + } + auto obj_ = item.toObject(); + if (obj_.contains("uhs")) + { + foundUHS = true; + } + else + { + continue; + } + // consume all 'display claims' ... whatever that means + for (auto iter = obj_.begin(); iter != obj_.end(); iter++) + { + QString claim; + if (!getString(obj_.value(iter.key()), claim)) + { + qWarning() << "display claim " << iter.key() << " is not a string..."; + return false; + } + output.extra[iter.key()] = claim; + } + + break; + } + if (!foundUHS) + { + qWarning() << "Missing uhs"; + return false; + } + output.validity = Validity::Certain; + qDebug() << name << "is valid."; + return true; + } + + bool parseMinecraftProfile(QByteArray& data, MinecraftProfile& output) + { + qDebug() << "Parsing Minecraft profile..."; + qCDebug(authCredentials()) << data; + + QJsonParseError jsonError; + QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); + if (jsonError.error) + { + qWarning() << "Failed to parse response from user.auth.xboxlive.com as JSON: " << jsonError.errorString(); + return false; + } + + auto obj = doc.object(); + if (!getString(obj.value("id"), output.id)) + { + qWarning() << "Minecraft profile id is not a string"; + return false; + } + + if (!getString(obj.value("name"), output.name)) + { + qWarning() << "Minecraft profile name is not a string"; + return false; + } + + auto skinsArray = obj.value("skins").toArray(); + for (auto skin : skinsArray) + { + auto skinObj = skin.toObject(); + Skin skinOut; + if (!getString(skinObj.value("id"), skinOut.id)) + { + continue; + } + QString state; + if (!getString(skinObj.value("state"), state)) + { + continue; + } + if (state != "ACTIVE") + { + continue; + } + if (!getString(skinObj.value("url"), skinOut.url)) + { + continue; + } + skinOut.url.replace("http://textures.minecraft.net", "https://textures.minecraft.net"); + if (!getString(skinObj.value("variant"), skinOut.variant)) + { + continue; + } + // we deal with only the active skin + output.skin = skinOut; + break; + } + auto capesArray = obj.value("capes").toArray(); + + QString currentCape; + for (auto cape : capesArray) + { + auto capeObj = cape.toObject(); + Cape capeOut; + if (!getString(capeObj.value("id"), capeOut.id)) + { + continue; + } + QString state; + if (!getString(capeObj.value("state"), state)) + { + continue; + } + if (state == "ACTIVE") + { + currentCape = capeOut.id; + } + if (!getString(capeObj.value("url"), capeOut.url)) + { + continue; + } + capeOut.url.replace("http://textures.minecraft.net", "https://textures.minecraft.net"); + if (!getString(capeObj.value("alias"), capeOut.alias)) + { + continue; + } + + output.capes[capeOut.id] = capeOut; + } + output.currentCape = currentCape; + output.validity = Validity::Certain; + return true; + } + + namespace + { + // these skin URLs are for the MHF_Steve and MHF_Alex accounts (made by a Mojang employee) + // they are needed because the session server doesn't return skin urls for default skins + static const QString SKIN_URL_STEVE = + "https://textures.minecraft.net/texture/1a4af718455d4aab528e7a61f86fa25e6a369d1768dcb13f7df319a713eb810b"; + static const QString SKIN_URL_ALEX = + "https://textures.minecraft.net/texture/83cee5ca6afcdb171285aa00e8049c297b2dbeba0efb8ff970a5677a1b644032"; + + bool isDefaultModelSteve(QString uuid) + { + // need to calculate *Java* hashCode of UUID + // if number is even, skin/model is steve, otherwise it is alex + + // just in case dashes are in the id + uuid.remove('-'); + + if (uuid.size() != 32) + { + return true; + } + + // qulonglong is guaranteed to be 64 bits + // we need to use unsigned numbers to guarantee truncation below + qulonglong most = uuid.left(16).toULongLong(nullptr, 16); + qulonglong least = uuid.right(16).toULongLong(nullptr, 16); + qulonglong xored = most ^ least; + return ((static_cast<quint32>(xored >> 32)) ^ static_cast<quint32>(xored)) % 2 == 0; + } + } // namespace + + /** + Uses session server for skin/cape lookup instead of profile, + because locked Mojang accounts cannot access profile endpoint + (https://api.minecraftservices.com/minecraft/profile/) + + ref: https://wiki.vg/Mojang_API#UUID_to_Profile_and_Skin.2FCape + + { + "id": "<profile identifier>", + "name": "<player name>", + "properties": [ + { + "name": "textures", + "value": "<base64 string>" + } + ] + } + + decoded base64 "value": + { + "timestamp": <java time in ms>, + "profileId": "<profile uuid>", + "profileName": "<player name>", + "textures": { + "SKIN": { + "url": "<player skin URL>" + }, + "CAPE": { + "url": "<player cape URL>" + } + } + } + */ + + bool parseMinecraftProfileMojang(QByteArray& data, MinecraftProfile& output) + { + qDebug() << "Parsing Minecraft profile..."; + qCDebug(authCredentials()) << data; + + QJsonParseError jsonError; + QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); + if (jsonError.error) + { + qWarning() << "Failed to parse response as JSON: " << jsonError.errorString(); + return false; + } + + auto obj = Json::requireObject(doc, "mojang minecraft profile"); + if (!getString(obj.value("id"), output.id)) + { + qWarning() << "Minecraft profile id is not a string"; + return false; + } + + if (!getString(obj.value("name"), output.name)) + { + qWarning() << "Minecraft profile name is not a string"; + return false; + } + + auto propsArray = obj.value("properties").toArray(); + QByteArray texturePayload; + for (auto p : propsArray) + { + auto pObj = p.toObject(); + auto name = pObj.value("name"); + if (!name.isString() || name.toString() != "textures") + { + continue; + } + + auto value = pObj.value("value"); + if (value.isString()) + { + texturePayload = + QByteArray::fromBase64(value.toString().toUtf8(), QByteArray::AbortOnBase64DecodingErrors); + } + + if (!texturePayload.isEmpty()) + { + break; + } + } + + if (texturePayload.isNull()) + { + qWarning() << "No texture payload data"; + return false; + } + + doc = QJsonDocument::fromJson(texturePayload, &jsonError); + if (jsonError.error) + { + qWarning() << "Failed to parse response as JSON: " << jsonError.errorString(); + return false; + } + + obj = Json::requireObject(doc, "session texture payload"); + auto textures = obj.value("textures"); + if (!textures.isObject()) + { + qWarning() << "No textures array in response"; + return false; + } + + Skin skinOut; + // fill in default skin info ourselves, as this endpoint doesn't provide it + bool steve = isDefaultModelSteve(output.id); + skinOut.variant = steve ? "CLASSIC" : "SLIM"; + skinOut.url = steve ? SKIN_URL_STEVE : SKIN_URL_ALEX; + // sadly we can't figure this out, but I don't think it really matters... + skinOut.id = "00000000-0000-0000-0000-000000000000"; + Cape capeOut; + auto tObj = textures.toObject(); + for (auto idx = tObj.constBegin(); idx != tObj.constEnd(); ++idx) + { + if (idx->isObject()) + { + if (idx.key() == "SKIN") + { + auto skin = idx->toObject(); + if (!getString(skin.value("url"), skinOut.url)) + { + qWarning() << "Skin url is not a string"; + return false; + } + skinOut.url.replace("http://textures.minecraft.net", "https://textures.minecraft.net"); + + auto maybeMeta = skin.find("metadata"); + if (maybeMeta != skin.end() && maybeMeta->isObject()) + { + auto meta = maybeMeta->toObject(); + // might not be present + getString(meta.value("model"), skinOut.variant); + } + } + else if (idx.key() == "CAPE") + { + auto cape = idx->toObject(); + if (!getString(cape.value("url"), capeOut.url)) + { + qWarning() << "Cape url is not a string"; + return false; + } + capeOut.url.replace("http://textures.minecraft.net", "https://textures.minecraft.net"); + + // we don't know the cape ID as it is not returned from the session server + // so just fake it - changing capes is probably locked anyway :( + capeOut.alias = "cape"; + } + } + } + + output.skin = skinOut; + if (capeOut.alias == "cape") + { + output.capes = QMap<QString, Cape>({ { capeOut.alias, capeOut } }); + output.currentCape = capeOut.alias; + } + + output.validity = Validity::Certain; + return true; + } + + bool parseMinecraftEntitlements(QByteArray& data, MinecraftEntitlement& output) + { + qDebug() << "Parsing Minecraft entitlements..."; + qCDebug(authCredentials()) << data; + + QJsonParseError jsonError; + QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); + if (jsonError.error) + { + qWarning() << "Failed to parse response from user.auth.xboxlive.com as JSON: " << jsonError.errorString(); + return false; + } + + auto obj = doc.object(); + output.canPlayMinecraft = false; + output.ownsMinecraft = false; + + auto itemsArray = obj.value("items").toArray(); + for (auto item : itemsArray) + { + auto itemObj = item.toObject(); + QString name; + if (!getString(itemObj.value("name"), name)) + { + continue; + } + if (name == "game_minecraft") + { + output.canPlayMinecraft = true; + } + if (name == "product_minecraft") + { + output.ownsMinecraft = true; + } + } + output.validity = Validity::Certain; + return true; + } + + bool parseRolloutResponse(QByteArray& data, bool& result) + { + qDebug() << "Parsing Rollout response..."; + qCDebug(authCredentials()) << data; + + QJsonParseError jsonError; + QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); + if (jsonError.error) + { + qWarning() + << "Failed to parse response from https://api.minecraftservices.com/rollout/v1/msamigration as JSON: " + << jsonError.errorString(); + return false; + } + + auto obj = doc.object(); + QString feature; + if (!getString(obj.value("feature"), feature)) + { + qWarning() << "Rollout feature is not a string"; + return false; + } + if (feature != "msamigration") + { + qWarning() << "Rollout feature is not what we expected (msamigration), but is instead \"" << feature + << "\""; + return false; + } + if (!getBool(obj.value("rollout"), result)) + { + qWarning() << "Rollout feature is not a string"; + return false; + } + return true; + } + + bool parseMojangResponse(QByteArray& data, Token& output) + { + QJsonParseError jsonError; + qDebug() << "Parsing Mojang response..."; + qCDebug(authCredentials()) << data; + QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); + if (jsonError.error) + { + qWarning() << "Failed to parse response from api.minecraftservices.com/launcher/login as JSON: " + << jsonError.errorString(); + return false; + } + + auto obj = doc.object(); + double expires_in = 0; + if (!getNumber(obj.value("expires_in"), expires_in)) + { + qWarning() << "expires_in is not a valid number"; + return false; + } + auto currentTime = QDateTime::currentDateTimeUtc(); + output.issueInstant = currentTime; + output.notAfter = currentTime.addSecs(expires_in); + + QString username; + if (!getString(obj.value("username"), username)) + { + qWarning() << "username is not valid"; + return false; + } + + // Basic JWT structure validation: JWTs have 3 dot-separated parts (header.payload.signature) + QString accessToken; + if (!getString(obj.value("access_token"), accessToken)) + { + qWarning() << "access_token is not valid"; + return false; + } + auto parts = accessToken.split('.'); + if (parts.size() != 3) + { + qWarning() << "access_token is not a valid JWT (expected 3 parts, got" << parts.size() << ")"; + return false; + } + output.token = accessToken; + output.validity = Validity::Certain; + qDebug() << "Mojang response is valid."; + return true; + } + +} // namespace Parsers diff --git a/archived/projt-launcher/launcher/minecraft/auth/Parsers.hpp b/archived/projt-launcher/launcher/minecraft/auth/Parsers.hpp new file mode 100644 index 0000000000..577059e636 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/Parsers.hpp @@ -0,0 +1,40 @@ +// 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. + */ +#pragma once + +#include "AccountData.hpp" + +namespace Parsers +{ + bool getDateTime(QJsonValue value, QDateTime& out); + bool getString(QJsonValue value, QString& out); + bool getNumber(QJsonValue value, double& out); + bool getNumber(QJsonValue value, int64_t& out); + bool getBool(QJsonValue value, bool& out); + + bool parseXTokenResponse(QByteArray& data, Token& output, QString name); + bool parseMojangResponse(QByteArray& data, Token& output); + + bool parseMinecraftProfile(QByteArray& data, MinecraftProfile& output); + bool parseMinecraftProfileMojang(QByteArray& data, MinecraftProfile& output); + bool parseMinecraftEntitlements(QByteArray& data, MinecraftEntitlement& output); + bool parseRolloutResponse(QByteArray& data, bool& result); +} // namespace Parsers diff --git a/archived/projt-launcher/launcher/minecraft/auth/steps/Credentials.hpp b/archived/projt-launcher/launcher/minecraft/auth/steps/Credentials.hpp new file mode 100644 index 0000000000..1de4898a67 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/steps/Credentials.hpp @@ -0,0 +1,233 @@ +// 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. + */ + +#pragma once + +#include <QByteArray> +#include <QDateTime> +#include <QMap> +#include <QString> +#include <QVariantMap> + +namespace projt::minecraft::auth +{ + + /** + * Validity state for tokens and data. + */ + enum class TokenValidity + { + None, ///< Not validated or expired + Assumed, ///< Assumed valid (e.g., loaded from disk) + Certain ///< Verified valid by server + }; + + /** + * Generic OAuth/authentication token. + */ + struct AuthToken + { + QDateTime issuedAt; ///< When the token was issued + QDateTime expiresAt; ///< When the token expires + QString accessToken; ///< The access token value + QString refreshToken; ///< OAuth refresh token (if applicable) + QVariantMap metadata; ///< Additional token data (e.g., user hash) + + TokenValidity validity = TokenValidity::None; + bool persist = true; ///< Whether to save this token to disk + + [[nodiscard]] bool isExpired() const noexcept + { + return expiresAt.isValid() && QDateTime::currentDateTimeUtc() >= expiresAt; + } + + [[nodiscard]] bool hasRefreshToken() const noexcept + { + return !refreshToken.isEmpty(); + } + }; + + /** + * Minecraft player skin data. + */ + struct PlayerSkin + { + QString id; + QString url; + QString variant; ///< "CLASSIC" or "SLIM" + QByteArray imageData; + + [[nodiscard]] bool isEmpty() const noexcept + { + return id.isEmpty(); + } + }; + + /** + * Minecraft player cape data. + */ + struct PlayerCape + { + QString id; + QString url; + QString alias; + QByteArray imageData; + + [[nodiscard]] bool isEmpty() const noexcept + { + return id.isEmpty(); + } + }; + + /** + * Minecraft game entitlements (ownership info). + */ + struct GameEntitlements + { + bool ownsMinecraft = false; + bool canPlayMinecraft = false; + TokenValidity validity = TokenValidity::None; + + [[nodiscard]] bool isValid() const noexcept + { + return validity != TokenValidity::None; + } + }; + + /** + * Minecraft Java Edition profile. + */ + struct MinecraftJavaProfile + { + QString id; ///< UUID without dashes + QString name; ///< Player name (gamertag) + PlayerSkin skin; + QString activeCapeId; + QMap<QString, PlayerCape> capes; + TokenValidity validity = TokenValidity::None; + + [[nodiscard]] bool hasProfile() const noexcept + { + return !id.isEmpty(); + } + [[nodiscard]] bool hasName() const noexcept + { + return !name.isEmpty(); + } + }; + + /** + * Account type enumeration. + */ + enum class AccountKind + { + Microsoft, ///< Microsoft/Xbox Live authenticated + Offline ///< Offline mode (no authentication) + }; + + /** + * Account status enumeration. + */ + enum class AccountStatus + { + Unchecked, ///< Not yet validated + Offline, ///< Network unavailable + Working, ///< Auth in progress + Online, ///< Fully authenticated + Disabled, ///< Disabled (e.g., client ID mismatch) + Error, ///< Error state + Expired, ///< Tokens expired, needs refresh + Gone ///< Account no longer exists + }; + + /** + * Complete authentication credentials for a Minecraft account. + * + * This structure holds all tokens and profile information needed to + * authenticate and play Minecraft. It is passed by reference to Step + * implementations which populate fields as authentication progresses. + */ + struct Credentials + { + // === Account identification === + AccountKind kind = AccountKind::Microsoft; + QString internalId; ///< Internal account identifier + QString msaClientId; ///< Microsoft Application client ID used + + // === Microsoft authentication chain === + AuthToken msaToken; ///< Microsoft OAuth token + AuthToken xboxUserToken; ///< XBL user token + AuthToken xboxServiceToken; ///< XSTS token for Xbox services + AuthToken minecraftServicesToken; ///< XSTS token for Minecraft services + + // === Minecraft authentication === + AuthToken minecraftAccessToken; ///< Yggdrasil-style access token + MinecraftJavaProfile profile; ///< Player profile + GameEntitlements entitlements; ///< Game ownership + + // === Runtime state (not persisted) === + AccountStatus status = AccountStatus::Unchecked; + QString lastError; + + // === Convenience accessors === + + /** + * Display string for this account (gamertag or profile name). + */ + [[nodiscard]] QString displayName() const noexcept + { + return profile.hasName() ? profile.name : QStringLiteral("(unknown)"); + } + + /** + * Access token to pass to the game. + */ + [[nodiscard]] QString accessToken() const noexcept + { + return minecraftAccessToken.accessToken; + } + + /** + * Profile UUID for game launch. + */ + [[nodiscard]] QString profileId() const noexcept + { + return profile.id; + } + + /** + * Profile name for game launch. + */ + [[nodiscard]] QString profileName() const noexcept + { + return profile.name; + } + + /** + * Xbox user hash (uhs) from token metadata. + */ + [[nodiscard]] QString xboxUserHash() const noexcept + { + return xboxUserToken.metadata.value(QStringLiteral("uhs")).toString(); + } + }; + +} // namespace projt::minecraft::auth diff --git a/archived/projt-launcher/launcher/minecraft/auth/steps/DeviceCodeAuthStep.cpp b/archived/projt-launcher/launcher/minecraft/auth/steps/DeviceCodeAuthStep.cpp new file mode 100644 index 0000000000..48433e81f6 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/steps/DeviceCodeAuthStep.cpp @@ -0,0 +1,323 @@ +// 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. + */ + +#include "DeviceCodeAuthStep.hpp" + +#include <QDateTime> +#include <QJsonDocument> +#include <QJsonObject> +#include <QJsonParseError> +#include <QUrlQuery> + +#include "Application.h" +#include "Json.h" +#include "net/RawHeaderProxy.h" + +namespace projt::minecraft::auth +{ + + namespace + { + + // Device authorization endpoints + constexpr auto kDeviceCodeUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode"; + constexpr auto kTokenUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token"; + + /** + * Parse device code response from Microsoft. + */ + struct DeviceCodeResponse + { + QString deviceCode; + QString userCode; + QString verificationUri; + int expiresIn = 0; + int interval = 5; + QString error; + QString errorDescription; + + [[nodiscard]] bool isValid() const noexcept + { + return !deviceCode.isEmpty() && !userCode.isEmpty() && !verificationUri.isEmpty() && expiresIn > 0; + } + }; + + [[nodiscard]] DeviceCodeResponse parseDeviceCodeResponse(const QByteArray& data) + { + QJsonParseError err; + const auto doc = QJsonDocument::fromJson(data, &err); + if (err.error != QJsonParseError::NoError || !doc.isObject()) + { + qWarning() << "Failed to parse device code response:" << err.errorString(); + return {}; + } + + const auto obj = doc.object(); + return { Json::ensureString(obj, "device_code"), Json::ensureString(obj, "user_code"), + Json::ensureString(obj, "verification_uri"), Json::ensureInteger(obj, "expires_in"), + Json::ensureInteger(obj, "interval", 5), Json::ensureString(obj, "error"), + Json::ensureString(obj, "error_description") }; + } + + /** + * Parse token response from Microsoft. + */ + struct TokenResponse + { + QString accessToken; + QString tokenType; + QString refreshToken; + int expiresIn = 0; + QString error; + QString errorDescription; + QVariantMap metadata; + + [[nodiscard]] bool isSuccess() const noexcept + { + return !accessToken.isEmpty(); + } + [[nodiscard]] bool isPending() const noexcept + { + return error == QStringLiteral("authorization_pending"); + } + [[nodiscard]] bool needsSlowDown() const noexcept + { + return error == QStringLiteral("slow_down"); + } + }; + + [[nodiscard]] TokenResponse parseTokenResponse(const QByteArray& data) + { + QJsonParseError err; + const auto doc = QJsonDocument::fromJson(data, &err); + if (err.error != QJsonParseError::NoError || !doc.isObject()) + { + qWarning() << "Failed to parse token response:" << err.errorString(); + return {}; + } + + const auto obj = doc.object(); + return { Json::ensureString(obj, "access_token"), + Json::ensureString(obj, "token_type"), + Json::ensureString(obj, "refresh_token"), + Json::ensureInteger(obj, "expires_in"), + Json::ensureString(obj, "error"), + Json::ensureString(obj, "error_description"), + obj.toVariantMap() }; + } + + } // namespace + + DeviceCodeAuthStep::DeviceCodeAuthStep(Credentials& credentials) noexcept + : Step(credentials), + m_clientId(APPLICATION->getMSAClientID()) + { + m_pollTimer.setTimerType(Qt::VeryCoarseTimer); + m_pollTimer.setSingleShot(true); + m_expirationTimer.setTimerType(Qt::VeryCoarseTimer); + m_expirationTimer.setSingleShot(true); + + connect(&m_expirationTimer, &QTimer::timeout, this, &DeviceCodeAuthStep::cancel); + connect(&m_pollTimer, &QTimer::timeout, this, &DeviceCodeAuthStep::pollForCompletion); + } + + QString DeviceCodeAuthStep::description() const + { + return tr("Logging in with Microsoft account (device code)."); + } + + void DeviceCodeAuthStep::execute() + { + QUrlQuery query; + query.addQueryItem(QStringLiteral("client_id"), m_clientId); + query.addQueryItem(QStringLiteral("scope"), QStringLiteral("XboxLive.SignIn XboxLive.offline_access")); + + const auto payload = query.query(QUrl::FullyEncoded).toUtf8(); + const QUrl url(QString::fromLatin1(kDeviceCodeUrl)); + + const auto headers = QList<Net::HeaderPair>{ { "Content-Type", "application/x-www-form-urlencoded" }, + { "Accept", "application/json" } }; + + m_response = std::make_shared<QByteArray>(); + m_request = Net::Upload::makeByteArray(url, m_response, payload); + m_request->addHeaderProxy(new Net::RawHeaderProxy(headers)); + + m_task = NetJob::Ptr::create(QStringLiteral("DeviceCodeRequest"), APPLICATION->network()); + m_task->setAskRetry(false); + m_task->addNetAction(m_request); + + connect(m_task.get(), &Task::finished, this, &DeviceCodeAuthStep::onDeviceCodeReceived); + m_task->start(); + } + + void DeviceCodeAuthStep::cancel() noexcept + { + m_cancelled = true; + m_expirationTimer.stop(); + m_pollTimer.stop(); + + if (m_request) + { + m_request->abort(); + } + + emit completed(StepResult::HardFailure, tr("Authentication cancelled or timed out.")); + } + + void DeviceCodeAuthStep::onDeviceCodeReceived() + { + const auto rsp = parseDeviceCodeResponse(*m_response); + + if (!rsp.error.isEmpty()) + { + const QString msg = rsp.errorDescription.isEmpty() ? rsp.error : rsp.errorDescription; + emit completed(StepResult::HardFailure, tr("Device authorization failed: %1").arg(msg)); + return; + } + + if (!m_request->wasSuccessful() || m_request->error() != QNetworkReply::NoError) + { + emit completed(StepResult::HardFailure, tr("Failed to request device authorization.")); + return; + } + + if (!rsp.isValid()) + { + emit completed(StepResult::HardFailure, tr("Invalid device authorization response.")); + return; + } + + m_deviceCode = rsp.deviceCode; + m_pollInterval = rsp.interval > 0 ? rsp.interval : 5; + + // Notify UI to display code + emit deviceCodeReady(rsp.verificationUri, rsp.userCode, rsp.expiresIn); + + // Start polling + startPolling(m_pollInterval, rsp.expiresIn); + } + + void DeviceCodeAuthStep::startPolling(int intervalSecs, int expiresInSecs) + { + if (m_cancelled) + { + return; + } + + m_expirationTimer.setInterval(expiresInSecs * 1000); + m_expirationTimer.start(); + + m_pollTimer.setInterval(intervalSecs * 1000); + m_pollTimer.start(); + } + + void DeviceCodeAuthStep::pollForCompletion() + { + if (m_cancelled) + { + return; + } + + QUrlQuery query; + query.addQueryItem(QStringLiteral("client_id"), m_clientId); + query.addQueryItem(QStringLiteral("grant_type"), + QStringLiteral("urn:ietf:params:oauth:grant-type:device_code")); + query.addQueryItem(QStringLiteral("device_code"), m_deviceCode); + + const auto payload = query.query(QUrl::FullyEncoded).toUtf8(); + const QUrl url(QString::fromLatin1(kTokenUrl)); + + const auto headers = QList<Net::HeaderPair>{ { "Content-Type", "application/x-www-form-urlencoded" }, + { "Accept", "application/json" } }; + + m_response = std::make_shared<QByteArray>(); + m_request = Net::Upload::makeByteArray(url, m_response, payload); + m_request->addHeaderProxy(new Net::RawHeaderProxy(headers)); + m_request->setNetwork(APPLICATION->network()); + + connect(m_request.get(), &Task::finished, this, &DeviceCodeAuthStep::onPollResponse); + m_request->start(); + } + + void DeviceCodeAuthStep::onPollResponse() + { + if (m_cancelled) + { + return; + } + + // Handle timeout - exponential backoff per RFC 8628 + if (m_request->error() == QNetworkReply::TimeoutError) + { + m_pollInterval *= 2; + m_pollTimer.setInterval(m_pollInterval * 1000); + m_pollTimer.start(); + return; + } + + const auto rsp = parseTokenResponse(*m_response); + + // Handle slow_down - increase interval by 5 seconds per RFC 8628 + if (rsp.needsSlowDown()) + { + m_pollInterval += 5; + m_pollTimer.setInterval(m_pollInterval * 1000); + m_pollTimer.start(); + return; + } + + // Authorization still pending - keep polling + if (rsp.isPending()) + { + m_pollTimer.start(); + return; + } + + // Check for other errors + if (!rsp.error.isEmpty()) + { + const QString msg = rsp.errorDescription.isEmpty() ? rsp.error : rsp.errorDescription; + emit completed(StepResult::HardFailure, tr("Device authentication failed: %1").arg(msg)); + return; + } + + // Network error - retry + if (!m_request->wasSuccessful() || m_request->error() != QNetworkReply::NoError) + { + m_pollTimer.start(); + return; + } + + // Success! + m_expirationTimer.stop(); + + m_credentials.msaClientId = m_clientId; + m_credentials.msaToken.issuedAt = QDateTime::currentDateTimeUtc(); + m_credentials.msaToken.expiresAt = QDateTime::currentDateTimeUtc().addSecs(rsp.expiresIn); + m_credentials.msaToken.metadata = rsp.metadata; + m_credentials.msaToken.refreshToken = rsp.refreshToken; + m_credentials.msaToken.accessToken = rsp.accessToken; + m_credentials.msaToken.validity = TokenValidity::Certain; + + emit completed(StepResult::Continue, tr("Microsoft authentication successful.")); + } + +} // namespace projt::minecraft::auth diff --git a/archived/projt-launcher/launcher/minecraft/auth/steps/DeviceCodeAuthStep.hpp b/archived/projt-launcher/launcher/minecraft/auth/steps/DeviceCodeAuthStep.hpp new file mode 100644 index 0000000000..15d81d2e6b --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/steps/DeviceCodeAuthStep.hpp @@ -0,0 +1,83 @@ +// 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. + */ + +#pragma once + +#include <QObject> +#include <QString> +#include <QTimer> +#include <memory> + +#include "Step.hpp" +#include "net/NetJob.h" +#include "net/Upload.h" + +namespace projt::minecraft::auth +{ + + /** + * Microsoft OAuth 2.0 Device Code Flow step. + * + * Used for environments where browser-based login is impractical. + * Displays a code for the user to enter at a Microsoft URL. + * + * Flow: + * 1. Request device code from Microsoft + * 2. Emit deviceCodeReady signal with code and URL + * 3. Poll for user completion + * 4. On success, populate MSA token in Credentials + */ + class DeviceCodeAuthStep : public Step + { + Q_OBJECT + + public: + explicit DeviceCodeAuthStep(Credentials& credentials) noexcept; + ~DeviceCodeAuthStep() noexcept override = default; + + [[nodiscard]] QString description() const override; + + public slots: + void execute() override; + void cancel() noexcept override; + + private slots: + void onDeviceCodeReceived(); + void pollForCompletion(); + void onPollResponse(); + + private: + void startPolling(int intervalSecs, int expiresInSecs); + + QString m_clientId; + QString m_deviceCode; + int m_pollInterval = 5; // seconds + bool m_cancelled = false; + + QTimer m_pollTimer; + QTimer m_expirationTimer; + + std::shared_ptr<QByteArray> m_response; + Net::Upload::Ptr m_request; + NetJob::Ptr m_task; + }; + +} // namespace projt::minecraft::auth diff --git a/archived/projt-launcher/launcher/minecraft/auth/steps/GameEntitlementsStep.cpp b/archived/projt-launcher/launcher/minecraft/auth/steps/GameEntitlementsStep.cpp new file mode 100644 index 0000000000..71058bd6ac --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/steps/GameEntitlementsStep.cpp @@ -0,0 +1,145 @@ +// 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. + */ + +#include "GameEntitlementsStep.hpp" + +#include <QJsonArray> +#include <QJsonDocument> +#include <QJsonObject> +#include <QJsonParseError> +#include <QNetworkRequest> +#include <QUrl> +#include <QUuid> + +#include "Application.h" +#include "minecraft/Logging.h" +#include "net/RawHeaderProxy.h" + +namespace projt::minecraft::auth +{ + + namespace + { + + constexpr auto kEntitlementsUrl = "https://api.minecraftservices.com/entitlements/license"; + + } // namespace + + GameEntitlementsStep::GameEntitlementsStep(Credentials& credentials) noexcept : Step(credentials) + {} + + QString GameEntitlementsStep::description() const + { + return tr("Checking game ownership."); + } + + void GameEntitlementsStep::execute() + { + // Generate unique request ID for validation + m_requestId = QUuid::createUuid().toString(QUuid::WithoutBraces); + + QUrl url(QString::fromLatin1(kEntitlementsUrl)); + url.setQuery(QStringLiteral("requestId=%1").arg(m_requestId)); + + const QString authHeader = QStringLiteral("Bearer %1").arg(m_credentials.minecraftAccessToken.accessToken); + + const auto headers = QList<Net::HeaderPair>{ { "Content-Type", "application/json" }, + { "Accept", "application/json" }, + { "Authorization", authHeader.toUtf8() } }; + + m_response = std::make_shared<QByteArray>(); + m_request = Net::Download::makeByteArray(url, m_response); + m_request->addHeaderProxy(new Net::RawHeaderProxy(headers)); + + m_task = NetJob::Ptr::create(QStringLiteral("GameEntitlements"), APPLICATION->network()); + m_task->setAskRetry(false); + m_task->addNetAction(m_request); + + connect(m_task.get(), &Task::finished, this, &GameEntitlementsStep::onRequestCompleted); + m_task->start(); + + qDebug() << "Checking game entitlements..."; + } + + void GameEntitlementsStep::onRequestCompleted() + { + qCDebug(authCredentials()) << *m_response; + + // Entitlements fetch is non-critical - continue even on failure + if (!parseEntitlementsResponse(*m_response)) + { + qWarning() << "Failed to parse entitlements response; continuing without entitlements."; + } + + emit completed(StepResult::Continue, tr("Got entitlements info.")); + } + + bool GameEntitlementsStep::parseEntitlementsResponse(const QByteArray& data) + { + QJsonParseError err; + const auto doc = QJsonDocument::fromJson(data, &err); + if (err.error != QJsonParseError::NoError || !doc.isObject()) + { + qWarning() << "Failed to parse entitlements:" << err.errorString(); + return false; + } + + const auto obj = doc.object(); + + // Validate request ID matches + const QString responseRequestId = obj.value(QStringLiteral("requestId")).toString(); + if (!responseRequestId.isEmpty() && responseRequestId != m_requestId) + { + qWarning() << "Entitlements request ID mismatch! Expected:" << m_requestId << "Got:" << responseRequestId; + } + + // Parse items array for Minecraft entitlements + const auto items = obj.value(QStringLiteral("items")).toArray(); + bool hasMinecraft = false; + bool hasGamePass = false; + + for (const auto& itemVal : items) + { + const auto itemObj = itemVal.toObject(); + const QString name = itemObj.value(QStringLiteral("name")).toString(); + + if (name == QStringLiteral("game_minecraft") || name == QStringLiteral("product_minecraft")) + { + hasMinecraft = true; + } + if (name == QStringLiteral("game_minecraft_bedrock")) + { + // Bedrock edition, not Java + } + if (name.contains(QStringLiteral("gamepass"), Qt::CaseInsensitive)) + { + hasGamePass = true; + } + } + + m_credentials.entitlements.ownsMinecraft = hasMinecraft || hasGamePass; + m_credentials.entitlements.canPlayMinecraft = hasMinecraft || hasGamePass; + m_credentials.entitlements.validity = TokenValidity::Certain; + + return true; + } + +} // namespace projt::minecraft::auth diff --git a/archived/projt-launcher/launcher/minecraft/auth/steps/GameEntitlementsStep.hpp b/archived/projt-launcher/launcher/minecraft/auth/steps/GameEntitlementsStep.hpp new file mode 100644 index 0000000000..6b306107ca --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/steps/GameEntitlementsStep.hpp @@ -0,0 +1,69 @@ +// 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. + */ + +#pragma once + +#include <QObject> +#include <QString> +#include <memory> + +#include "Step.hpp" +#include "net/Download.h" +#include "net/NetJob.h" + +namespace projt::minecraft::auth +{ + + /** + * Game entitlements verification step. + * + * Fetches license/entitlement information to verify game ownership. + * This determines whether the user owns Minecraft and can play it. + * + * Endpoint: https://api.minecraftservices.com/entitlements/license + */ + class GameEntitlementsStep : public Step + { + Q_OBJECT + + public: + explicit GameEntitlementsStep(Credentials& credentials) noexcept; + ~GameEntitlementsStep() noexcept override = default; + + [[nodiscard]] QString description() const override; + + public slots: + void execute() override; + + private slots: + void onRequestCompleted(); + + private: + [[nodiscard]] bool parseEntitlementsResponse(const QByteArray& data); + + QString m_requestId; + + std::shared_ptr<QByteArray> m_response; + Net::Download::Ptr m_request; + NetJob::Ptr m_task; + }; + +} // namespace projt::minecraft::auth diff --git a/archived/projt-launcher/launcher/minecraft/auth/steps/MicrosoftOAuthStep.cpp b/archived/projt-launcher/launcher/minecraft/auth/steps/MicrosoftOAuthStep.cpp new file mode 100644 index 0000000000..a0ad612481 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/steps/MicrosoftOAuthStep.cpp @@ -0,0 +1,319 @@ +// 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. + */ + +#include "MicrosoftOAuthStep.hpp" + +#include <QAbstractOAuth2> +#include <QCoreApplication> +#include <QFileInfo> +#include <QNetworkRequest> +#include <QOAuthHttpServerReplyHandler> +#include <QOAuthOobReplyHandler> +#include <QProcess> +#include <QSettings> +#include <QStandardPaths> + +#include "Application.h" +#include "BuildConfig.h" + +namespace projt::minecraft::auth +{ + + namespace + { + + /** + * Custom OOB reply handler that forwards OAuth callbacks from the application. + */ + class CustomSchemeReplyHandler : public QOAuthOobReplyHandler + { + Q_OBJECT + + public: + explicit CustomSchemeReplyHandler(QObject* parent = nullptr) : QOAuthOobReplyHandler(parent) + { + connect(APPLICATION, &Application::oauthReplyRecieved, this, &QOAuthOobReplyHandler::callbackReceived); + } + + ~CustomSchemeReplyHandler() override + { + disconnect(APPLICATION, + &Application::oauthReplyRecieved, + this, + &QOAuthOobReplyHandler::callbackReceived); + } + + [[nodiscard]] QString callback() const override + { + return BuildConfig.LAUNCHER_APP_BINARY_NAME + QStringLiteral("://oauth/microsoft"); + } + }; + + /** + * Check if the custom URL scheme handler is registered with the OS AND + * that the registered handler points to the currently running binary. + * + * This is important when multiple builds of the launcher coexist on the same + * machine (e.g. an installed release and a locally compiled dev build). + * If the registered handler points to a *different* binary, the OAuth callback + * URL would be intercepted by that other instance instead of the current one, + * causing the login flow to fail silently. In that case we fall back to the + * HTTP loopback server handler which is always self-contained. + */ + [[nodiscard]] bool isCustomSchemeRegistered() + { +#ifdef Q_OS_LINUX + QProcess process; + process.start(QStringLiteral("xdg-mime"), + { QStringLiteral("query"), + QStringLiteral("default"), + QStringLiteral("x-scheme-handler/") + BuildConfig.LAUNCHER_APP_BINARY_NAME }); + process.waitForFinished(); + const QString output = process.readAllStandardOutput().trimmed(); + if (!output.contains(BuildConfig.LAUNCHER_APP_BINARY_NAME)) + return false; + + // Also verify the registered .desktop entry resolves to our own binary. + // xdg-mime returns something like "projtlauncher.desktop"; locate it and + // read the Exec= line to compare against our own executable path. + const QString desktopFileName = output.section(QLatin1Char('\n'), 0, 0).trimmed(); + const QStringList dataDirs = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation); + for (const QString& dataDir : dataDirs) + { + const QString desktopPath = dataDir + QStringLiteral("/applications/") + desktopFileName; + QSettings desktopFile(desktopPath, QSettings::IniFormat); + desktopFile.beginGroup(QStringLiteral("Desktop Entry")); + const QString execLine = desktopFile.value(QStringLiteral("Exec")).toString(); + desktopFile.endGroup(); + if (execLine.isEmpty()) + continue; + // Exec= may contain %U or similar; take only the binary part. + const QString registeredBin = execLine.section(QLatin1Char(' '), 0, 0); + const QFileInfo currentBin(QCoreApplication::applicationFilePath()); + const QFileInfo registeredBinInfo(registeredBin); + if (registeredBinInfo.canonicalFilePath() == currentBin.canonicalFilePath()) + return true; + // Registered handler is a different binary → do not use custom scheme. + qDebug() << "Custom URL scheme is registered for a different binary (" << registeredBin + << ") — falling back to HTTP loopback handler."; + return false; + } + return true; // Could not verify; assume it's ours. +#elif defined(Q_OS_WIN) + const QString regPath = + QStringLiteral("HKEY_CURRENT_USER\\Software\\Classes\\%1").arg(BuildConfig.LAUNCHER_APP_BINARY_NAME); + const QSettings settings(regPath, QSettings::NativeFormat); + if (!settings.contains(QStringLiteral("shell/open/command/."))) + return false; + + // Verify that the registered command actually points to this binary. + // The registry value looks like: "C:\path\to\launcher.exe" "%1" + QString registeredCmd = settings.value(QStringLiteral("shell/open/command/.")).toString(); + // Strip surrounding quotes from the executable portion. + if (registeredCmd.startsWith(QLatin1Char('"'))) + { + registeredCmd = registeredCmd.mid(1); + const int closeQuote = registeredCmd.indexOf(QLatin1Char('"')); + if (closeQuote >= 0) + registeredCmd = registeredCmd.left(closeQuote); + } + else + { + // No quotes — executable ends at the first space. + const int spaceIdx = registeredCmd.indexOf(QLatin1Char(' ')); + if (spaceIdx >= 0) + registeredCmd = registeredCmd.left(spaceIdx); + } + + const QFileInfo currentBin(QCoreApplication::applicationFilePath()); + const QFileInfo registeredBin(registeredCmd); + if (registeredBin.canonicalFilePath().compare(currentBin.canonicalFilePath(), Qt::CaseInsensitive) == 0) + return true; + + // The URL scheme is registered, but for a different launcher binary. + // Fall back to the HTTP loopback handler so our OAuth callback reaches us. + qDebug() << "Custom URL scheme is registered for a different binary (" << registeredCmd + << ") — falling back to HTTP loopback handler."; + return false; +#else + return true; +#endif + } + + } // namespace + + MicrosoftOAuthStep::MicrosoftOAuthStep(Credentials& credentials, bool silentRefresh) noexcept + : Step(credentials), + m_silentRefresh(silentRefresh), + m_clientId(APPLICATION->getMSAClientID()) + { + setupOAuthHandlers(); + } + + QString MicrosoftOAuthStep::description() const + { + return m_silentRefresh ? tr("Refreshing Microsoft account token.") : tr("Logging in with Microsoft account."); + } + + void MicrosoftOAuthStep::setupOAuthHandlers() + { + // Choose appropriate reply handler based on environment + if (shouldUseCustomScheme()) + { + m_oauth.setReplyHandler(new CustomSchemeReplyHandler(this)); + } + else + { + auto* httpHandler = new QOAuthHttpServerReplyHandler(this); + httpHandler->setCallbackText(QStringLiteral(R"XXX( + <noscript> + <meta http-equiv="Refresh" content="0; URL=%1" /> + </noscript> + Login Successful, redirecting... + <script> + window.location.replace("%1"); + </script> + )XXX") + .arg(BuildConfig.LOGIN_CALLBACK_URL)); + m_oauth.setReplyHandler(httpHandler); + } + + // Configure OAuth endpoints + m_oauth.setAuthorizationUrl( + QUrl(QStringLiteral("https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize"))); + m_oauth.setAccessTokenUrl( + QUrl(QStringLiteral("https://login.microsoftonline.com/consumers/oauth2/v2.0/token"))); + m_oauth.setScope(QStringLiteral("XboxLive.SignIn XboxLive.offline_access")); + m_oauth.setClientIdentifier(m_clientId); + m_oauth.setNetworkAccessManager(APPLICATION->network().get()); + + // Connect signals + connect(&m_oauth, &QOAuth2AuthorizationCodeFlow::granted, this, &MicrosoftOAuthStep::onGranted); + connect(&m_oauth, + &QOAuth2AuthorizationCodeFlow::authorizeWithBrowser, + this, + &MicrosoftOAuthStep::openBrowserRequested); + connect(&m_oauth, + &QOAuth2AuthorizationCodeFlow::authorizeWithBrowser, + this, + &MicrosoftOAuthStep::browserAuthRequired); + connect(&m_oauth, &QOAuth2AuthorizationCodeFlow::requestFailed, this, &MicrosoftOAuthStep::onRequestFailed); + connect(&m_oauth, &QOAuth2AuthorizationCodeFlow::error, this, &MicrosoftOAuthStep::onError); + connect(&m_oauth, + &QOAuth2AuthorizationCodeFlow::extraTokensChanged, + this, + &MicrosoftOAuthStep::onExtraTokensChanged); + connect(&m_oauth, + &QOAuth2AuthorizationCodeFlow::clientIdentifierChanged, + this, + &MicrosoftOAuthStep::onClientIdChanged); + } + + bool MicrosoftOAuthStep::shouldUseCustomScheme() const + { + // Use HTTP server handler for AppImage, portable, or unregistered scheme + const bool isAppImage = QCoreApplication::applicationFilePath().startsWith(QStringLiteral("/tmp/.mount_")); + const bool isPortable = APPLICATION->isPortable(); + return !isAppImage && !isPortable && isCustomSchemeRegistered(); + } + + void MicrosoftOAuthStep::execute() + { + if (m_silentRefresh) + { + // Validate preconditions for silent refresh + if (m_credentials.msaClientId != m_clientId) + { + emit completed(StepResult::Disabled, tr("Microsoft client ID has changed. Please log in again.")); + return; + } + + if (!m_credentials.msaToken.hasRefreshToken()) + { + emit completed(StepResult::Disabled, tr("No refresh token available. Please log in again.")); + return; + } + + m_oauth.setRefreshToken(m_credentials.msaToken.refreshToken); + m_oauth.refreshAccessToken(); + } + else + { + // Interactive login - clear existing credentials + m_credentials = Credentials{}; + m_credentials.msaClientId = m_clientId; + + // Force account selection prompt + m_oauth.setModifyParametersFunction( + [](QAbstractOAuth::Stage, QMultiMap<QString, QVariant>* params) + { params->insert(QStringLiteral("prompt"), QStringLiteral("select_account")); }); + + m_oauth.grant(); + } + } + + void MicrosoftOAuthStep::onGranted() + { + m_credentials.msaClientId = m_oauth.clientIdentifier(); + m_credentials.msaToken.issuedAt = QDateTime::currentDateTimeUtc(); + m_credentials.msaToken.expiresAt = m_oauth.expirationAt(); + m_credentials.msaToken.metadata = m_oauth.extraTokens(); + m_credentials.msaToken.refreshToken = m_oauth.refreshToken(); + m_credentials.msaToken.accessToken = m_oauth.token(); + m_credentials.msaToken.validity = TokenValidity::Certain; + + emit completed(StepResult::Continue, tr("Microsoft authentication successful.")); + } + + void MicrosoftOAuthStep::onRequestFailed(QAbstractOAuth2::Error err) + { + StepResult result = StepResult::HardFailure; + + if (m_oauth.status() == QAbstractOAuth::Status::Granted || m_silentRefresh) + { + result = (err == QAbstractOAuth2::Error::NetworkError) ? StepResult::Offline : StepResult::SoftFailure; + } + + const QString message = + m_silentRefresh ? tr("Failed to refresh Microsoft token.") : tr("Microsoft authentication failed."); + qWarning() << message; + emit completed(result, message); + } + + void MicrosoftOAuthStep::onError(const QString& error, const QString& errorDescription, const QUrl& /*uri*/) + { + qWarning() << "OAuth error:" << error << "-" << errorDescription; + emit completed(StepResult::HardFailure, errorDescription.isEmpty() ? error : errorDescription); + } + + void MicrosoftOAuthStep::onExtraTokensChanged(const QVariantMap& tokens) + { + m_credentials.msaToken.metadata = tokens; + } + + void MicrosoftOAuthStep::onClientIdChanged(const QString& clientId) + { + m_credentials.msaClientId = clientId; + } + +} // namespace projt::minecraft::auth + +#include "MicrosoftOAuthStep.moc" diff --git a/archived/projt-launcher/launcher/minecraft/auth/steps/MicrosoftOAuthStep.hpp b/archived/projt-launcher/launcher/minecraft/auth/steps/MicrosoftOAuthStep.hpp new file mode 100644 index 0000000000..16df0da21a --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/steps/MicrosoftOAuthStep.hpp @@ -0,0 +1,85 @@ +// 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. + */ + +#pragma once + +#include <QObject> +#include <QOAuth2AuthorizationCodeFlow> +#include <QString> + +#include "Step.hpp" + +namespace projt::minecraft::auth +{ + + /** + * Microsoft OAuth 2.0 Authorization Code Flow step. + * + * Handles interactive browser-based login or silent token refresh using + * the standard OAuth 2.0 authorization code flow. Upon success, populates + * the MSA token in Credentials. + * + * This step supports two modes: + * - Interactive: Opens browser for user login + * - Silent: Attempts refresh using stored refresh token + */ + class MicrosoftOAuthStep : public Step + { + Q_OBJECT + + public: + /** + * Construct a new MSA OAuth step. + * @param credentials Credentials to populate with MSA token. + * @param silentRefresh If true, attempt silent refresh; if false, interactive login. + */ + explicit MicrosoftOAuthStep(Credentials& credentials, bool silentRefresh = false) noexcept; + ~MicrosoftOAuthStep() noexcept override = default; + + [[nodiscard]] QString description() const override; + + public slots: + void execute() override; + + signals: + /** + * Emitted when browser authorization is required (interactive mode). + * @param url URL to open in user's browser. + */ + void openBrowserRequested(const QUrl& url); + + private slots: + void onGranted(); + void onRequestFailed(QAbstractOAuth2::Error error); + void onError(const QString& error, const QString& errorDescription, const QUrl& uri); + void onExtraTokensChanged(const QVariantMap& tokens); + void onClientIdChanged(const QString& clientId); + + private: + void setupOAuthHandlers(); + [[nodiscard]] bool shouldUseCustomScheme() const; + + bool m_silentRefresh = false; + QString m_clientId; + QOAuth2AuthorizationCodeFlow m_oauth; + }; + +} // namespace projt::minecraft::auth diff --git a/archived/projt-launcher/launcher/minecraft/auth/steps/MinecraftProfileFetchStep.cpp b/archived/projt-launcher/launcher/minecraft/auth/steps/MinecraftProfileFetchStep.cpp new file mode 100644 index 0000000000..5529337ef5 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/steps/MinecraftProfileFetchStep.cpp @@ -0,0 +1,170 @@ +// 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. + */ + +#include "MinecraftProfileFetchStep.hpp" + +#include <QJsonArray> +#include <QJsonDocument> +#include <QJsonObject> +#include <QJsonParseError> +#include <QNetworkRequest> + +#include "Application.h" +#include "net/NetUtils.h" +#include "net/RawHeaderProxy.h" + +namespace projt::minecraft::auth +{ + + namespace + { + + constexpr auto kMinecraftProfileUrl = "https://api.minecraftservices.com/minecraft/profile"; + + } // namespace + + MinecraftProfileFetchStep::MinecraftProfileFetchStep(Credentials& credentials) noexcept : Step(credentials) + {} + + QString MinecraftProfileFetchStep::description() const + { + return tr("Fetching Minecraft profile."); + } + + void MinecraftProfileFetchStep::execute() + { + const QUrl url(QString::fromLatin1(kMinecraftProfileUrl)); + const QString authHeader = QStringLiteral("Bearer %1").arg(m_credentials.minecraftAccessToken.accessToken); + + const auto headers = QList<Net::HeaderPair>{ { "Content-Type", "application/json" }, + { "Accept", "application/json" }, + { "Authorization", authHeader.toUtf8() } }; + + m_response = std::make_shared<QByteArray>(); + m_request = Net::Download::makeByteArray(url, m_response); + m_request->addHeaderProxy(new Net::RawHeaderProxy(headers)); + + m_task = NetJob::Ptr::create(QStringLiteral("MinecraftProfileFetch"), APPLICATION->network()); + m_task->setAskRetry(false); + m_task->addNetAction(m_request); + + connect(m_task.get(), &Task::finished, this, &MinecraftProfileFetchStep::onRequestCompleted); + m_task->start(); + } + + void MinecraftProfileFetchStep::onRequestCompleted() + { + // 404 = no profile exists (valid state for new accounts) + if (m_request->error() == QNetworkReply::ContentNotFoundError) + { + m_credentials.profile = MinecraftJavaProfile{}; + emit completed(StepResult::Continue, tr("Account has no Minecraft profile.")); + return; + } + + if (m_request->error() != QNetworkReply::NoError) + { + qWarning() << "Minecraft profile fetch error:"; + qWarning() << " HTTP Status:" << m_request->replyStatusCode(); + qWarning() << " Error:" << m_request->error() << m_request->errorString(); + qWarning() << " Response:" << QString::fromUtf8(*m_response); + + const StepResult result = + Net::isApplicationError(m_request->error()) ? StepResult::SoftFailure : StepResult::Offline; + + emit completed(result, tr("Failed to fetch Minecraft profile: %1").arg(m_request->errorString())); + return; + } + + if (!parseProfileResponse(*m_response)) + { + m_credentials.profile = MinecraftJavaProfile{}; + emit completed(StepResult::SoftFailure, tr("Could not parse Minecraft profile response.")); + return; + } + + emit completed(StepResult::Continue, tr("Got Minecraft profile.")); + } + + bool MinecraftProfileFetchStep::parseProfileResponse(const QByteArray& data) + { + QJsonParseError err; + const auto doc = QJsonDocument::fromJson(data, &err); + if (err.error != QJsonParseError::NoError || !doc.isObject()) + { + qWarning() << "Failed to parse Minecraft profile:" << err.errorString(); + return false; + } + + const auto obj = doc.object(); + + // Basic profile info + m_credentials.profile.id = obj.value(QStringLiteral("id")).toString(); + m_credentials.profile.name = obj.value(QStringLiteral("name")).toString(); + + if (m_credentials.profile.id.isEmpty()) + { + return false; + } + + // Parse skins + const auto skins = obj.value(QStringLiteral("skins")).toArray(); + for (const auto& skinVal : skins) + { + const auto skinObj = skinVal.toObject(); + const QString state = skinObj.value(QStringLiteral("state")).toString(); + + if (state == QStringLiteral("ACTIVE")) + { + m_credentials.profile.skin.id = skinObj.value(QStringLiteral("id")).toString(); + m_credentials.profile.skin.url = skinObj.value(QStringLiteral("url")).toString(); + m_credentials.profile.skin.variant = skinObj.value(QStringLiteral("variant")).toString(); + break; + } + } + + // Parse capes + const auto capes = obj.value(QStringLiteral("capes")).toArray(); + for (const auto& capeVal : capes) + { + const auto capeObj = capeVal.toObject(); + const QString capeId = capeObj.value(QStringLiteral("id")).toString(); + + PlayerCape cape; + cape.id = capeId; + cape.url = capeObj.value(QStringLiteral("url")).toString(); + cape.alias = capeObj.value(QStringLiteral("alias")).toString(); + + m_credentials.profile.capes.insert(capeId, cape); + + // Track active cape + const QString state = capeObj.value(QStringLiteral("state")).toString(); + if (state == QStringLiteral("ACTIVE")) + { + m_credentials.profile.activeCapeId = capeId; + } + } + + m_credentials.profile.validity = TokenValidity::Certain; + return true; + } + +} // namespace projt::minecraft::auth diff --git a/archived/projt-launcher/launcher/minecraft/auth/steps/MinecraftProfileFetchStep.hpp b/archived/projt-launcher/launcher/minecraft/auth/steps/MinecraftProfileFetchStep.hpp new file mode 100644 index 0000000000..f9da7960e2 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/steps/MinecraftProfileFetchStep.hpp @@ -0,0 +1,68 @@ +// 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. + */ + +#pragma once + +#include <QObject> +#include <QString> +#include <memory> + +#include "Step.hpp" +#include "net/Download.h" +#include "net/NetJob.h" + +namespace projt::minecraft::auth +{ + + /** + * Minecraft Java profile fetch step. + * + * Fetches the Minecraft Java Edition profile (UUID, username, skins, capes). + * A profile may not exist if the user hasn't bought the game or set up + * their profile name yet. + * + * Endpoint: https://api.minecraftservices.com/minecraft/profile + */ + class MinecraftProfileFetchStep : public Step + { + Q_OBJECT + + public: + explicit MinecraftProfileFetchStep(Credentials& credentials) noexcept; + ~MinecraftProfileFetchStep() noexcept override = default; + + [[nodiscard]] QString description() const override; + + public slots: + void execute() override; + + private slots: + void onRequestCompleted(); + + private: + [[nodiscard]] bool parseProfileResponse(const QByteArray& data); + + std::shared_ptr<QByteArray> m_response; + Net::Download::Ptr m_request; + NetJob::Ptr m_task; + }; + +} // namespace projt::minecraft::auth diff --git a/archived/projt-launcher/launcher/minecraft/auth/steps/MinecraftServicesLoginStep.cpp b/archived/projt-launcher/launcher/minecraft/auth/steps/MinecraftServicesLoginStep.cpp new file mode 100644 index 0000000000..eba64cc6f0 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/steps/MinecraftServicesLoginStep.cpp @@ -0,0 +1,138 @@ +// 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. + */ + +#include "MinecraftServicesLoginStep.hpp" + +#include <QJsonDocument> +#include <QJsonObject> +#include <QJsonParseError> +#include <QNetworkRequest> +#include <QUrl> + +#include "Application.h" +#include "minecraft/Logging.h" +#include "net/NetUtils.h" +#include "net/RawHeaderProxy.h" + +namespace projt::minecraft::auth +{ + + namespace + { + + constexpr auto kMinecraftLoginUrl = "https://api.minecraftservices.com/launcher/login"; + + /** + * Parse Minecraft services authentication response. + */ + [[nodiscard]] bool parseMinecraftAuthResponse(const QByteArray& data, AuthToken& token) + { + QJsonParseError err; + const auto doc = QJsonDocument::fromJson(data, &err); + if (err.error != QJsonParseError::NoError || !doc.isObject()) + { + qWarning() << "Failed to parse Minecraft login response:" << err.errorString(); + return false; + } + + const auto obj = doc.object(); + + token.accessToken = obj.value(QStringLiteral("access_token")).toString(); + token.issuedAt = QDateTime::currentDateTimeUtc(); + + const int expiresIn = obj.value(QStringLiteral("expires_in")).toInt(); + if (expiresIn > 0) + { + token.expiresAt = token.issuedAt.addSecs(expiresIn); + } + + // Store token type and other metadata + token.metadata.insert(QStringLiteral("token_type"), obj.value(QStringLiteral("token_type")).toString()); + + token.validity = TokenValidity::Certain; + return !token.accessToken.isEmpty(); + } + + } // namespace + + MinecraftServicesLoginStep::MinecraftServicesLoginStep(Credentials& credentials) noexcept : Step(credentials) + {} + + QString MinecraftServicesLoginStep::description() const + { + return tr("Logging in to Minecraft services."); + } + + void MinecraftServicesLoginStep::execute() + { + const QString uhs = m_credentials.minecraftServicesToken.metadata.value(QStringLiteral("uhs")).toString(); + const QString xToken = m_credentials.minecraftServicesToken.accessToken; + + const QString requestBody = QStringLiteral(R"({ + "xtoken": "XBL3.0 x=%1;%2", + "platform": "PC_LAUNCHER" + })") + .arg(uhs, xToken); + + const QUrl url(QString::fromLatin1(kMinecraftLoginUrl)); + const auto headers = + QList<Net::HeaderPair>{ { "Content-Type", "application/json" }, { "Accept", "application/json" } }; + + m_response = std::make_shared<QByteArray>(); + m_request = Net::Upload::makeByteArray(url, m_response, requestBody.toUtf8()); + m_request->addHeaderProxy(new Net::RawHeaderProxy(headers)); + + m_task = NetJob::Ptr::create(QStringLiteral("MinecraftServicesLogin"), APPLICATION->network()); + m_task->setAskRetry(false); + m_task->addNetAction(m_request); + + connect(m_task.get(), &Task::finished, this, &MinecraftServicesLoginStep::onRequestCompleted); + m_task->start(); + + qDebug() << "Getting Minecraft access token..."; + } + + void MinecraftServicesLoginStep::onRequestCompleted() + { + qCDebug(authCredentials()) << *m_response; + + if (m_request->error() != QNetworkReply::NoError) + { + qWarning() << "Minecraft login error:" << m_request->error(); + + const StepResult result = + Net::isApplicationError(m_request->error()) ? StepResult::SoftFailure : StepResult::Offline; + + emit completed(result, tr("Failed to get Minecraft access token: %1").arg(m_request->errorString())); + return; + } + + if (!parseMinecraftAuthResponse(*m_response, m_credentials.minecraftAccessToken)) + { + qWarning() << "Could not parse Minecraft login response"; + emit completed(StepResult::SoftFailure, tr("Failed to parse Minecraft access token response.")); + return; + } + + emit completed(StepResult::Continue, tr("Got Minecraft access token.")); + } + +} // namespace projt::minecraft::auth diff --git a/archived/projt-launcher/launcher/minecraft/auth/steps/MinecraftServicesLoginStep.hpp b/archived/projt-launcher/launcher/minecraft/auth/steps/MinecraftServicesLoginStep.hpp new file mode 100644 index 0000000000..95428b6c9a --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/steps/MinecraftServicesLoginStep.hpp @@ -0,0 +1,65 @@ +// 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. + */ + +#pragma once + +#include <QObject> +#include <QString> +#include <memory> + +#include "Step.hpp" +#include "net/NetJob.h" +#include "net/Upload.h" + +namespace projt::minecraft::auth +{ + + /** + * Minecraft Services login step. + * + * Exchanges the XSTS token for a Minecraft access token (Yggdrasil-style). + * This token is used to authenticate with Minecraft game servers. + * + * Endpoint: https://api.minecraftservices.com/launcher/login + */ + class MinecraftServicesLoginStep : public Step + { + Q_OBJECT + + public: + explicit MinecraftServicesLoginStep(Credentials& credentials) noexcept; + ~MinecraftServicesLoginStep() noexcept override = default; + + [[nodiscard]] QString description() const override; + + public slots: + void execute() override; + + private slots: + void onRequestCompleted(); + + private: + std::shared_ptr<QByteArray> m_response; + Net::Upload::Ptr m_request; + NetJob::Ptr m_task; + }; + +} // namespace projt::minecraft::auth diff --git a/archived/projt-launcher/launcher/minecraft/auth/steps/SkinDownloadStep.cpp b/archived/projt-launcher/launcher/minecraft/auth/steps/SkinDownloadStep.cpp new file mode 100644 index 0000000000..e4d7283a37 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/steps/SkinDownloadStep.cpp @@ -0,0 +1,76 @@ +// 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. + */ + +#include "SkinDownloadStep.hpp" + +#include <QNetworkRequest> + +#include "Application.h" + +namespace projt::minecraft::auth +{ + + SkinDownloadStep::SkinDownloadStep(Credentials& credentials) noexcept : Step(credentials) + {} + + QString SkinDownloadStep::description() const + { + return tr("Downloading player skin."); + } + + void SkinDownloadStep::execute() + { + // Skip if no skin URL available + if (m_credentials.profile.skin.url.isEmpty()) + { + emit completed(StepResult::Continue, tr("No skin to download.")); + return; + } + + const QUrl url(m_credentials.profile.skin.url); + + m_response = std::make_shared<QByteArray>(); + m_request = Net::Download::makeByteArray(url, m_response); + + m_task = NetJob::Ptr::create(QStringLiteral("SkinDownload"), APPLICATION->network()); + m_task->setAskRetry(false); + m_task->addNetAction(m_request); + + connect(m_task.get(), &Task::finished, this, &SkinDownloadStep::onRequestCompleted); + m_task->start(); + } + + void SkinDownloadStep::onRequestCompleted() + { + // Skin download is optional - always continue regardless of result + if (m_request->error() == QNetworkReply::NoError) + { + m_credentials.profile.skin.imageData = *m_response; + emit completed(StepResult::Continue, tr("Got player skin.")); + } + else + { + qWarning() << "Failed to download skin:" << m_request->errorString(); + emit completed(StepResult::Continue, tr("Skin download failed (continuing).")); + } + } + +} // namespace projt::minecraft::auth diff --git a/archived/projt-launcher/launcher/minecraft/auth/steps/SkinDownloadStep.hpp b/archived/projt-launcher/launcher/minecraft/auth/steps/SkinDownloadStep.hpp new file mode 100644 index 0000000000..c94fe825cd --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/steps/SkinDownloadStep.hpp @@ -0,0 +1,63 @@ +// 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. + */ + +#pragma once + +#include <QObject> +#include <QString> +#include <memory> + +#include "Step.hpp" +#include "net/Download.h" +#include "net/NetJob.h" + +namespace projt::minecraft::auth +{ + + /** + * Skin image download step. + * + * Downloads the player's skin image data from the URL in the profile. + * This is an optional step used for display in the launcher UI. + */ + class SkinDownloadStep : public Step + { + Q_OBJECT + + public: + explicit SkinDownloadStep(Credentials& credentials) noexcept; + ~SkinDownloadStep() noexcept override = default; + + [[nodiscard]] QString description() const override; + + public slots: + void execute() override; + + private slots: + void onRequestCompleted(); + + private: + std::shared_ptr<QByteArray> m_response; + Net::Download::Ptr m_request; + NetJob::Ptr m_task; + }; + +} // namespace projt::minecraft::auth diff --git a/archived/projt-launcher/launcher/minecraft/auth/steps/Step.hpp b/archived/projt-launcher/launcher/minecraft/auth/steps/Step.hpp new file mode 100644 index 0000000000..7afb7159cb --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/steps/Step.hpp @@ -0,0 +1,128 @@ +// 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. + */ + +#pragma once + +#include <QNetworkReply> +#include <QObject> +#include <QString> + +#include "QObjectPtr.h" + +namespace projt::minecraft::auth +{ + + /** + * Result state for authentication pipeline steps. + * Each step emits one of these states upon completion. + */ + enum class StepResult + { + Continue, ///< Step succeeded, proceed to next step + Succeeded, ///< Final success - authentication complete + Offline, ///< Network unavailable - soft failure, can retry + SoftFailure, ///< Recoverable error - partial auth, can retry + HardFailure, ///< Unrecoverable error - tokens invalid + Disabled, ///< Account disabled (e.g., client ID changed) + Gone ///< Account no longer exists + }; + + // Forward declaration + struct Credentials; + + /** + * Abstract base class for authentication pipeline steps. + * + * Each step performs a discrete authentication action (e.g., OAuth exchange, + * token validation, profile fetch) and emits `completed` when done. + * + * Steps are designed to be stateless between runs - all persistent data + * is stored in the Credentials object passed at construction. + */ + class Step : public QObject + { + Q_OBJECT + + public: + using Ptr = shared_qobject_ptr<Step>; + + /** + * Construct a step with a reference to the credential store. + * @param credentials Mutable reference to authentication data. + */ + explicit Step(Credentials& credentials) noexcept : QObject(nullptr), m_credentials(credentials) + {} + + ~Step() noexcept override = default; + + // Rule of Zero - no copy/move (QObject constraint) + Step(const Step&) = delete; + Step& operator=(const Step&) = delete; + Step(Step&&) = delete; + Step& operator=(Step&&) = delete; + + /** + * Human-readable description of what this step does. + * Used for progress display and logging. + */ + [[nodiscard]] virtual QString description() const = 0; + + public slots: + /** + * Execute this authentication step. + * Implementations must emit `completed` when done (success or failure). + */ + virtual void execute() = 0; + + /** + * Request cancellation of an in-progress step. + * Default implementation does nothing. Override for cancellable steps. + */ + virtual void cancel() noexcept + {} + + signals: + /** + * Emitted when the step completes (successfully or with error). + * @param result The outcome of this step. + * @param message Human-readable status message. + */ + void completed(StepResult result, QString message); + + /** + * Emitted by OAuth steps when browser authorization is required. + * @param url The URL to open in the user's browser. + */ + void browserAuthRequired(const QUrl& url); + + /** + * Emitted by device code flow steps when user action is required. + * @param verificationUrl URL to visit for authentication. + * @param userCode Code to enter at the URL. + * @param expiresInSecs Seconds until the code expires. + */ + void deviceCodeReady(QString verificationUrl, QString userCode, int expiresInSecs); + + protected: + Credentials& m_credentials; + }; + +} // namespace projt::minecraft::auth diff --git a/archived/projt-launcher/launcher/minecraft/auth/steps/Steps.hpp b/archived/projt-launcher/launcher/minecraft/auth/steps/Steps.hpp new file mode 100644 index 0000000000..4cb8b8b07b --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/steps/Steps.hpp @@ -0,0 +1,51 @@ +// 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. + */ + +/** + * @file Steps.hpp + * @brief Convenience header including all authentication pipeline steps. + * + * Include this header to get access to all step types: + * - MicrosoftOAuthStep: Browser-based MSA login + * - DeviceCodeAuthStep: Device code flow for console/headless + * - XboxLiveUserStep: XBL user token acquisition + * - XboxSecurityTokenStep: XSTS token for services + * - XboxProfileFetchStep: Xbox profile (optional) + * - MinecraftServicesLoginStep: Minecraft access token + * - MinecraftProfileFetchStep: Minecraft profile + * - GameEntitlementsStep: Game ownership check + * - SkinDownloadStep: Player skin image + */ + +#pragma once + +#include "Credentials.hpp" +#include "Step.hpp" + +#include "DeviceCodeAuthStep.hpp" +#include "GameEntitlementsStep.hpp" +#include "MicrosoftOAuthStep.hpp" +#include "MinecraftProfileFetchStep.hpp" +#include "MinecraftServicesLoginStep.hpp" +#include "SkinDownloadStep.hpp" +#include "XboxLiveUserStep.hpp" +#include "XboxProfileFetchStep.hpp" +#include "XboxSecurityTokenStep.hpp" diff --git a/archived/projt-launcher/launcher/minecraft/auth/steps/XboxLiveUserStep.cpp b/archived/projt-launcher/launcher/minecraft/auth/steps/XboxLiveUserStep.cpp new file mode 100644 index 0000000000..ef48d4ab47 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/steps/XboxLiveUserStep.cpp @@ -0,0 +1,147 @@ +// 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. + */ + +#include "XboxLiveUserStep.hpp" + +#include <QJsonDocument> +#include <QJsonObject> +#include <QNetworkRequest> + +#include "Application.h" +#include "net/NetUtils.h" +#include "net/RawHeaderProxy.h" + +namespace projt::minecraft::auth +{ + + namespace + { + + constexpr auto kXboxUserAuthUrl = "https://user.auth.xboxlive.com/user/authenticate"; + + /** + * Parse Xbox token response. + * Returns true on success, false on parse error. + */ + [[nodiscard]] bool parseXboxTokenResponse(const QByteArray& data, AuthToken& token, const QString& tokenName) + { + QJsonParseError err; + const auto doc = QJsonDocument::fromJson(data, &err); + if (err.error != QJsonParseError::NoError || !doc.isObject()) + { + qWarning() << "Failed to parse" << tokenName << "response:" << err.errorString(); + return false; + } + + const auto obj = doc.object(); + + // Parse issue and expiry times + const QString issued = obj.value(QStringLiteral("IssueInstant")).toString(); + const QString expires = obj.value(QStringLiteral("NotAfter")).toString(); + token.issuedAt = QDateTime::fromString(issued, Qt::ISODate); + token.expiresAt = QDateTime::fromString(expires, Qt::ISODate); + token.accessToken = obj.value(QStringLiteral("Token")).toString(); + + // Parse display claims for user hash (uhs) + const auto displayClaims = obj.value(QStringLiteral("DisplayClaims")).toObject(); + const auto xui = displayClaims.value(QStringLiteral("xui")).toArray(); + if (!xui.isEmpty()) + { + const auto firstClaim = xui.first().toObject(); + token.metadata.insert(QStringLiteral("uhs"), firstClaim.value(QStringLiteral("uhs")).toString()); + } + + token.validity = TokenValidity::Certain; + + if (token.accessToken.isEmpty()) + { + qWarning() << "Empty" << tokenName << "token received"; + return false; + } + + return true; + } + + } // namespace + + XboxLiveUserStep::XboxLiveUserStep(Credentials& credentials) noexcept : Step(credentials) + {} + + QString XboxLiveUserStep::description() const + { + return tr("Authenticating with Xbox Live."); + } + + void XboxLiveUserStep::execute() + { + const QString requestBody = QStringLiteral(R"({ + "Properties": { + "AuthMethod": "RPS", + "SiteName": "user.auth.xboxlive.com", + "RpsTicket": "d=%1" + }, + "RelyingParty": "http://auth.xboxlive.com", + "TokenType": "JWT" + })") + .arg(m_credentials.msaToken.accessToken); + + const QUrl url(QString::fromLatin1(kXboxUserAuthUrl)); + const auto headers = QList<Net::HeaderPair>{ { "Content-Type", "application/json" }, + { "Accept", "application/json" }, + { "x-xbl-contract-version", "1" } }; + + m_response = std::make_shared<QByteArray>(); + m_request = Net::Upload::makeByteArray(url, m_response, requestBody.toUtf8()); + m_request->addHeaderProxy(new Net::RawHeaderProxy(headers)); + + m_task = NetJob::Ptr::create(QStringLiteral("XboxLiveUserAuth"), APPLICATION->network()); + m_task->setAskRetry(false); + m_task->addNetAction(m_request); + + connect(m_task.get(), &Task::finished, this, &XboxLiveUserStep::onRequestCompleted); + m_task->start(); + + qDebug() << "Authenticating with Xbox Live..."; + } + + void XboxLiveUserStep::onRequestCompleted() + { + if (m_request->error() != QNetworkReply::NoError) + { + qWarning() << "Xbox Live user auth error:" << m_request->error(); + + const StepResult result = + Net::isApplicationError(m_request->error()) ? StepResult::SoftFailure : StepResult::Offline; + + emit completed(result, tr("Xbox Live authentication failed: %1").arg(m_request->errorString())); + return; + } + + if (!parseXboxTokenResponse(*m_response, m_credentials.xboxUserToken, QStringLiteral("User"))) + { + emit completed(StepResult::SoftFailure, tr("Could not parse Xbox Live user token response.")); + return; + } + + emit completed(StepResult::Continue, tr("Got Xbox Live user token.")); + } + +} // namespace projt::minecraft::auth diff --git a/archived/projt-launcher/launcher/minecraft/auth/steps/XboxLiveUserStep.hpp b/archived/projt-launcher/launcher/minecraft/auth/steps/XboxLiveUserStep.hpp new file mode 100644 index 0000000000..a8c0514599 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/steps/XboxLiveUserStep.hpp @@ -0,0 +1,65 @@ +// 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. + */ + +#pragma once + +#include <QObject> +#include <QString> +#include <memory> + +#include "Step.hpp" +#include "net/NetJob.h" +#include "net/Upload.h" + +namespace projt::minecraft::auth +{ + + /** + * Xbox Live User Authentication step. + * + * Exchanges the MSA token for an Xbox Live user token. + * This is the first step of Xbox authentication after MSA login. + * + * Endpoint: https://user.auth.xboxlive.com/user/authenticate + */ + class XboxLiveUserStep : public Step + { + Q_OBJECT + + public: + explicit XboxLiveUserStep(Credentials& credentials) noexcept; + ~XboxLiveUserStep() noexcept override = default; + + [[nodiscard]] QString description() const override; + + public slots: + void execute() override; + + private slots: + void onRequestCompleted(); + + private: + std::shared_ptr<QByteArray> m_response; + Net::Upload::Ptr m_request; + NetJob::Ptr m_task; + }; + +} // namespace projt::minecraft::auth diff --git a/archived/projt-launcher/launcher/minecraft/auth/steps/XboxProfileFetchStep.cpp b/archived/projt-launcher/launcher/minecraft/auth/steps/XboxProfileFetchStep.cpp new file mode 100644 index 0000000000..8660d4291f --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/steps/XboxProfileFetchStep.cpp @@ -0,0 +1,155 @@ +// 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. + */ + +#include "XboxProfileFetchStep.hpp" + +#include <QJsonArray> +#include <QJsonDocument> +#include <QJsonObject> +#include <QNetworkRequest> +#include <QUrlQuery> + +#include "Application.h" +#include "minecraft/Logging.h" +#include "net/NetUtils.h" +#include "net/RawHeaderProxy.h" + +namespace projt::minecraft::auth +{ + + namespace + { + + constexpr auto kXboxProfileUrl = "https://profile.xboxlive.com/users/me/profile/settings"; + + // Profile settings to request + constexpr auto kProfileSettings = "GameDisplayName,AppDisplayName,AppDisplayPicRaw,GameDisplayPicRaw," + "PublicGamerpic,ShowUserAsAvatar,Gamerscore,Gamertag,ModernGamertag," + "ModernGamertagSuffix,UniqueModernGamertag,AccountTier,TenureLevel," + "XboxOneRep,PreferredColor,Location,Bio,Watermarks,RealName," + "RealNameOverride,IsQuarantined"; + + } // namespace + + XboxProfileFetchStep::XboxProfileFetchStep(Credentials& credentials) noexcept : Step(credentials) + {} + + QString XboxProfileFetchStep::description() const + { + return tr("Fetching Xbox profile."); + } + + void XboxProfileFetchStep::execute() + { + QUrl url(QString::fromLatin1(kXboxProfileUrl)); + QUrlQuery query; + query.addQueryItem(QStringLiteral("settings"), QString::fromLatin1(kProfileSettings)); + url.setQuery(query); + + const QString authHeader = QStringLiteral("XBL3.0 x=%1;%2") + .arg(m_credentials.xboxUserHash(), m_credentials.xboxServiceToken.accessToken); + + const auto headers = QList<Net::HeaderPair>{ { "Content-Type", "application/json" }, + { "Accept", "application/json" }, + { "x-xbl-contract-version", "3" }, + { "Authorization", authHeader.toUtf8() } }; + + m_response = std::make_shared<QByteArray>(); + m_request = Net::Download::makeByteArray(url, m_response); + m_request->addHeaderProxy(new Net::RawHeaderProxy(headers)); + + m_task = NetJob::Ptr::create(QStringLiteral("XboxProfileFetch"), APPLICATION->network()); + m_task->setAskRetry(false); + m_task->addNetAction(m_request); + + connect(m_task.get(), &Task::finished, this, &XboxProfileFetchStep::onRequestCompleted); + m_task->start(); + + qDebug() << "Fetching Xbox profile..."; + } + + void XboxProfileFetchStep::onRequestCompleted() + { + if (m_request->error() != QNetworkReply::NoError) + { + qWarning() << "Xbox profile fetch error:" << m_request->error(); + qCDebug(authCredentials()) << *m_response; + + // Profile fetch is optional - continue even on failure + const StepResult result = + Net::isApplicationError(m_request->error()) ? StepResult::SoftFailure : StepResult::Offline; + + emit completed(result, tr("Failed to fetch Xbox profile: %1").arg(m_request->errorString())); + return; + } + + qCDebug(authCredentials()) << "Xbox profile:" << *m_response; + + // Parse the response to extract gamertag + parseProfileResponse(); + + emit completed(StepResult::Continue, tr("Got Xbox profile.")); + } + + void XboxProfileFetchStep::parseProfileResponse() + { + QJsonParseError parseError; + const auto doc = QJsonDocument::fromJson(*m_response, &parseError); + + if (parseError.error != QJsonParseError::NoError || !doc.isObject()) + { + qWarning() << "Failed to parse Xbox profile response:" << parseError.errorString(); + return; + } + + const auto root = doc.object(); + const auto profileUsers = root.value(QStringLiteral("profileUsers")).toArray(); + + if (profileUsers.isEmpty()) + { + qWarning() << "No profile users in Xbox response"; + return; + } + + const auto user = profileUsers.first().toObject(); + const auto settings = user.value(QStringLiteral("settings")).toArray(); + + for (const auto& settingValue : settings) + { + const auto setting = settingValue.toObject(); + const auto id = setting.value(QStringLiteral("id")).toString(); + const auto value = setting.value(QStringLiteral("value")).toString(); + + if (id == QStringLiteral("Gamertag")) + { + // Store gamertag in xboxServiceToken.metadata for legacy sync + // accountDisplayString() expects this in xboxApiToken.extra["gtg"] + m_credentials.xboxServiceToken.metadata[QStringLiteral("gtg")] = value; + qDebug() << "Got Xbox gamertag:" << value; + } + else if (id == QStringLiteral("GameDisplayPicRaw")) + { + m_credentials.xboxServiceToken.metadata[QStringLiteral("gamerPicUrl")] = value; + } + } + } + +} // namespace projt::minecraft::auth diff --git a/archived/projt-launcher/launcher/minecraft/auth/steps/XboxProfileFetchStep.hpp b/archived/projt-launcher/launcher/minecraft/auth/steps/XboxProfileFetchStep.hpp new file mode 100644 index 0000000000..35a7594acd --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/steps/XboxProfileFetchStep.hpp @@ -0,0 +1,67 @@ +// 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. + */ + +#pragma once + +#include <QObject> +#include <QString> +#include <memory> + +#include "Step.hpp" +#include "net/Download.h" +#include "net/NetJob.h" + +namespace projt::minecraft::auth +{ + + /** + * Xbox Live profile fetch step. + * + * Fetches the Xbox Live profile (gamertag, avatar, etc.) for logging + * and display purposes. This is an optional/informational step. + * + * Endpoint: https://profile.xboxlive.com/users/me/profile/settings + */ + class XboxProfileFetchStep : public Step + { + Q_OBJECT + + public: + explicit XboxProfileFetchStep(Credentials& credentials) noexcept; + ~XboxProfileFetchStep() noexcept override = default; + + [[nodiscard]] QString description() const override; + + public slots: + void execute() override; + + private slots: + void onRequestCompleted(); + + private: + void parseProfileResponse(); + + std::shared_ptr<QByteArray> m_response; + Net::Download::Ptr m_request; + NetJob::Ptr m_task; + }; + +} // namespace projt::minecraft::auth diff --git a/archived/projt-launcher/launcher/minecraft/auth/steps/XboxSecurityTokenStep.cpp b/archived/projt-launcher/launcher/minecraft/auth/steps/XboxSecurityTokenStep.cpp new file mode 100644 index 0000000000..6ed455c6b1 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/steps/XboxSecurityTokenStep.cpp @@ -0,0 +1,249 @@ +// 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. + */ + +#include "XboxSecurityTokenStep.hpp" + +#include <QJsonDocument> +#include <QJsonObject> +#include <QJsonParseError> +#include <QNetworkRequest> + +#include "Application.h" +#include "minecraft/Logging.h" +#include "net/NetUtils.h" +#include "net/RawHeaderProxy.h" + +namespace projt::minecraft::auth +{ + + namespace + { + + constexpr auto kXstsAuthorizeUrl = "https://xsts.auth.xboxlive.com/xsts/authorize"; + + /** + * Parse Xbox token response. + */ + [[nodiscard]] bool parseXboxTokenResponse(const QByteArray& data, AuthToken& token) + { + QJsonParseError err; + const auto doc = QJsonDocument::fromJson(data, &err); + if (err.error != QJsonParseError::NoError || !doc.isObject()) + { + qWarning() << "Failed to parse XSTS response:" << err.errorString(); + return false; + } + + const auto obj = doc.object(); + + token.issuedAt = QDateTime::fromString(obj.value(QStringLiteral("IssueInstant")).toString(), Qt::ISODate); + token.expiresAt = QDateTime::fromString(obj.value(QStringLiteral("NotAfter")).toString(), Qt::ISODate); + token.accessToken = obj.value(QStringLiteral("Token")).toString(); + + const auto displayClaims = obj.value(QStringLiteral("DisplayClaims")).toObject(); + const auto xui = displayClaims.value(QStringLiteral("xui")).toArray(); + if (!xui.isEmpty()) + { + const auto firstClaim = xui.first().toObject(); + token.metadata.insert(QStringLiteral("uhs"), firstClaim.value(QStringLiteral("uhs")).toString()); + } + + token.validity = TokenValidity::Certain; + return !token.accessToken.isEmpty(); + } + + /** + * XSTS error code messages. + * See: https://wiki.vg/Microsoft_Authentication_Scheme#Authenticate_with_XSTS + */ + struct XstsErrorInfo + { + int64_t code; + const char* message; + }; + + constexpr std::array kXstsErrors = { + XstsErrorInfo{ 2148916227, "This Microsoft account was banned by Xbox." }, + XstsErrorInfo{ 2148916229, "This account is restricted. Please check parental controls." }, + XstsErrorInfo{ 2148916233, "This account does not have an Xbox Live profile. Purchase the game first." }, + XstsErrorInfo{ 2148916234, "Please accept Xbox Terms of Service and try again." }, + XstsErrorInfo{ 2148916235, "Xbox Live is not available in your region." }, + XstsErrorInfo{ 2148916236, "This account requires age verification." }, + XstsErrorInfo{ 2148916237, "This account has reached its playtime limit." }, + XstsErrorInfo{ 2148916238, "This account is underaged and not linked to a family." } + }; + + } // namespace + + XboxSecurityTokenStep::XboxSecurityTokenStep(Credentials& credentials, XstsTarget target) noexcept + : Step(credentials), + m_target(target) + {} + + QString XboxSecurityTokenStep::description() const + { + return tr("Getting authorization for %1 services.").arg(targetName()); + } + + QString XboxSecurityTokenStep::relyingParty() const + { + switch (m_target) + { + case XstsTarget::XboxLive: return QStringLiteral("http://xboxlive.com"); + case XstsTarget::MinecraftServices: return QStringLiteral("rp://api.minecraftservices.com/"); + } + Q_UNREACHABLE(); + } + + QString XboxSecurityTokenStep::targetName() const + { + switch (m_target) + { + case XstsTarget::XboxLive: return QStringLiteral("Xbox Live"); + case XstsTarget::MinecraftServices: return QStringLiteral("Minecraft"); + } + Q_UNREACHABLE(); + } + + AuthToken& XboxSecurityTokenStep::targetToken() + { + switch (m_target) + { + case XstsTarget::XboxLive: return m_credentials.xboxServiceToken; + case XstsTarget::MinecraftServices: return m_credentials.minecraftServicesToken; + } + Q_UNREACHABLE(); + } + + void XboxSecurityTokenStep::execute() + { + const QString requestBody = QStringLiteral(R"({ + "Properties": { + "SandboxId": "RETAIL", + "UserTokens": ["%1"] + }, + "RelyingParty": "%2", + "TokenType": "JWT" + })") + .arg(m_credentials.xboxUserToken.accessToken, relyingParty()); + + const QUrl url(QString::fromLatin1(kXstsAuthorizeUrl)); + const auto headers = + QList<Net::HeaderPair>{ { "Content-Type", "application/json" }, { "Accept", "application/json" } }; + + m_response = std::make_shared<QByteArray>(); + m_request = Net::Upload::makeByteArray(url, m_response, requestBody.toUtf8()); + m_request->addHeaderProxy(new Net::RawHeaderProxy(headers)); + + m_task = NetJob::Ptr::create(QStringLiteral("XstsAuthorize"), APPLICATION->network()); + m_task->setAskRetry(false); + m_task->addNetAction(m_request); + + connect(m_task.get(), &Task::finished, this, &XboxSecurityTokenStep::onRequestCompleted); + m_task->start(); + + qDebug() << "Getting XSTS token for" << relyingParty(); + } + + void XboxSecurityTokenStep::onRequestCompleted() + { + qCDebug(authCredentials()) << *m_response; + + if (m_request->error() != QNetworkReply::NoError) + { + qWarning() << "XSTS request error:" << m_request->error(); + + if (Net::isApplicationError(m_request->error())) + { + if (handleStsError()) + { + return; + } + emit completed(StepResult::SoftFailure, + tr("Failed to get %1 authorization: %2").arg(targetName(), m_request->errorString())); + } + else + { + emit completed(StepResult::Offline, + tr("Failed to get %1 authorization: %2").arg(targetName(), m_request->errorString())); + } + return; + } + + AuthToken token; + if (!parseXboxTokenResponse(*m_response, token)) + { + emit completed(StepResult::SoftFailure, tr("Could not parse %1 authorization response.").arg(targetName())); + return; + } + + // Verify user hash matches + const QString responseUhs = token.metadata.value(QStringLiteral("uhs")).toString(); + if (responseUhs != m_credentials.xboxUserHash()) + { + emit completed(StepResult::SoftFailure, tr("User hash mismatch in %1 authorization.").arg(targetName())); + return; + } + + targetToken() = token; + emit completed(StepResult::Continue, tr("Got %1 authorization.").arg(targetName())); + } + + bool XboxSecurityTokenStep::handleStsError() + { + if (m_request->error() != QNetworkReply::AuthenticationRequiredError) + { + return false; + } + + QJsonParseError jsonError; + const auto doc = QJsonDocument::fromJson(*m_response, &jsonError); + if (jsonError.error != QJsonParseError::NoError) + { + emit completed(StepResult::SoftFailure, + tr("Cannot parse XSTS error response: %1").arg(jsonError.errorString())); + return true; + } + + const auto obj = doc.object(); + const auto errorCode = static_cast<int64_t>(obj.value(QStringLiteral("XErr")).toDouble()); + + if (errorCode == 0) + { + emit completed(StepResult::SoftFailure, tr("XSTS error response missing error code.")); + return true; + } + + // Look up error message + for (const auto& [code, message] : kXstsErrors) + { + if (code == errorCode) + { + emit completed(StepResult::SoftFailure, tr(message)); + return true; + } + } + + emit completed(StepResult::SoftFailure, tr("Unknown XSTS error: %1").arg(errorCode)); + return true; + } + +} // namespace projt::minecraft::auth diff --git a/archived/projt-launcher/launcher/minecraft/auth/steps/XboxSecurityTokenStep.hpp b/archived/projt-launcher/launcher/minecraft/auth/steps/XboxSecurityTokenStep.hpp new file mode 100644 index 0000000000..911e343728 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/auth/steps/XboxSecurityTokenStep.hpp @@ -0,0 +1,89 @@ +// 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. + */ + +#pragma once + +#include <QObject> +#include <QString> +#include <memory> + +#include "Credentials.hpp" +#include "Step.hpp" +#include "net/NetJob.h" +#include "net/Upload.h" + +namespace projt::minecraft::auth +{ + + /** + * Target services for XSTS token requests. + */ + enum class XstsTarget + { + XboxLive, ///< Xbox Live services (for profile fetch) + MinecraftServices ///< Minecraft services (for game access) + }; + + /** + * Xbox Security Token Service (XSTS) authorization step. + * + * Requests an XSTS token for a specific relying party (service). + * Two instances are typically used in the auth pipeline: + * - One for Xbox Live services (profile) + * - One for Minecraft services (game access) + * + * Endpoint: https://xsts.auth.xboxlive.com/xsts/authorize + */ + class XboxSecurityTokenStep : public Step + { + Q_OBJECT + + public: + /** + * Construct an XSTS authorization step. + * @param credentials Credentials to populate. + * @param target Which service to request authorization for. + */ + explicit XboxSecurityTokenStep(Credentials& credentials, XstsTarget target) noexcept; + ~XboxSecurityTokenStep() noexcept override = default; + + [[nodiscard]] QString description() const override; + + public slots: + void execute() override; + + private slots: + void onRequestCompleted(); + + private: + [[nodiscard]] QString relyingParty() const; + [[nodiscard]] QString targetName() const; + [[nodiscard]] AuthToken& targetToken(); + bool handleStsError(); + + XstsTarget m_target; + + std::shared_ptr<QByteArray> m_response; + Net::Upload::Ptr m_request; + NetJob::Ptr m_task; + }; + +} // namespace projt::minecraft::auth diff --git a/archived/projt-launcher/launcher/minecraft/launch/AutoInstallJava.cpp b/archived/projt-launcher/launcher/minecraft/launch/AutoInstallJava.cpp new file mode 100644 index 0000000000..518953be75 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/launch/AutoInstallJava.cpp @@ -0,0 +1,313 @@ +// 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. + */ + +#include "AutoInstallJava.hpp" +#include <QDir> +#include <QFileInfo> +#include <memory> + +#include "Application.h" +#include "FileSystem.h" +#include "MessageLevel.h" +#include "QObjectPtr.h" +#include "SysInfo.h" +#include "java/core/RuntimeInstall.hpp" +#include "java/core/RuntimeVersion.hpp" +#include "java/services/RuntimeCatalog.hpp" +#include "java/services/RuntimeScanner.hpp" +#include "java/download/RuntimeArchiveTask.hpp" +#include "java/download/RuntimeManifestTask.hpp" +#include "java/download/RuntimeLinkTask.hpp" +#include "meta/Index.hpp" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" +#include "net/Mode.h" +#include "tasks/SequentialTask.h" + +AutoInstallJava::AutoInstallJava(projt::launch::LaunchPipeline* parent) + : projt::launch::LaunchStage(parent), + m_instance(m_flow->instance()), + m_supported_arch(SysInfo::getSupportedJavaArchitecture()) {}; + +void AutoInstallJava::executeTask() +{ + auto settings = m_instance->settings(); + if (!APPLICATION->settings()->get("AutomaticJavaSwitch").toBool()) + { + emitSucceeded(); + return; + } + if (!settings->get("OverrideJavaLocation").toBool()) + { + emitSucceeded(); + return; + } + auto packProfile = m_instance->getPackProfile(); + const auto configuredJavaPath = settings->get("JavaPath").toString(); + const bool hasValidJava = !FS::ResolveExecutable(configuredJavaPath).isEmpty(); + const bool ignoreCompatibility = settings->get("IgnoreJavaCompatibility").toBool(); + const auto compatibleMajors = packProfile->getProfile()->getCompatibleJavaMajors(); + bool compatibleJava = true; + if (!ignoreCompatibility && !compatibleMajors.isEmpty()) + { + auto storedVersion = settings->get("JavaVersion").toString(); + if (storedVersion.isEmpty()) + { + compatibleJava = false; + } + else + { + projt::java::RuntimeVersion javaVersion(storedVersion); + compatibleJava = compatibleMajors.contains(javaVersion.major()); + } + } + if (hasValidJava && compatibleJava) + { + emitSucceeded(); + return; + } + if (!APPLICATION->settings()->get("AutomaticJavaDownload").toBool()) + { + auto javas = APPLICATION->runtimeCatalog(); + m_current_task = javas->getLoadTask(); + connect( + m_current_task.get(), + &Task::finished, + this, + [this, javas, packProfile] + { + for (auto i = 0; i < javas->count(); i++) + { + auto java = std::dynamic_pointer_cast<projt::java::RuntimeInstall>(javas->at(i)); + if (java && packProfile->getProfile()->getCompatibleJavaMajors().contains(java->version.major())) + { + if (!java->is_64bit) + { + emit logLine(tr("The automatic Java mechanism detected a 32-bit installation of Java."), + MessageLevel::Launcher); + } + setJavaPath(java->path); + return; + } + } + emit logLine(tr("No compatible Java version was found. Using the default one."), MessageLevel::Warning); + emitSucceeded(); + }); + connect(m_current_task.get(), &Task::progress, this, &AutoInstallJava::setProgress); + connect(m_current_task.get(), &Task::stepProgress, this, &AutoInstallJava::propagateStepProgress); + connect(m_current_task.get(), &Task::status, this, &AutoInstallJava::setStatus); + connect(m_current_task.get(), &Task::details, this, &AutoInstallJava::setDetails); + emit progressReportingRequest(); + return; + } + if (m_supported_arch.isEmpty()) + { + emit logLine( + tr("Your system (%1-%2) is not compatible with automatic Java installation. Using the default Java path.") + .arg(SysInfo::currentSystem(), SysInfo::useQTForArch()), + MessageLevel::Warning); + emitSucceeded(); + return; + } + auto wantedJavaName = packProfile->getProfile()->getCompatibleJavaName(); + if (wantedJavaName.isEmpty()) + { + emit logLine(tr("Your meta information is out of date or doesn't have the information necessary to determine " + "what installation of " + "Java should be used. " + "Using the default Java path."), + MessageLevel::Warning); + emitSucceeded(); + return; + } + QDir javaDir(APPLICATION->javaPath()); + auto wantedJavaPath = javaDir.absoluteFilePath(wantedJavaName); + if (QFileInfo::exists(wantedJavaPath)) + { + setJavaPathFromPartial(); + return; + } + auto versionList = APPLICATION->metadataIndex()->component("net.minecraft.java"); + m_current_task = versionList->getLoadTask(); + connect(m_current_task.get(), &Task::succeeded, this, &AutoInstallJava::tryNextMajorJava); + connect(m_current_task.get(), &Task::failed, this, &AutoInstallJava::emitFailed); + connect(m_current_task.get(), &Task::progress, this, &AutoInstallJava::setProgress); + connect(m_current_task.get(), &Task::stepProgress, this, &AutoInstallJava::propagateStepProgress); + connect(m_current_task.get(), &Task::status, this, &AutoInstallJava::setStatus); + connect(m_current_task.get(), &Task::details, this, &AutoInstallJava::setDetails); + if (!m_current_task->isRunning()) + { + m_current_task->start(); + } + emit progressReportingRequest(); +} + +void AutoInstallJava::setJavaPath(QString path) +{ + auto settings = m_instance->settings(); + settings->set("OverrideJavaLocation", true); + settings->set("JavaPath", path); + settings->set("AutomaticJava", true); + emit logLine(tr("Compatible Java found at: %1.").arg(path), MessageLevel::Launcher); + emitSucceeded(); +} + +void AutoInstallJava::setJavaPathFromPartial() +{ + auto packProfile = m_instance->getPackProfile(); + auto javaName = packProfile->getProfile()->getCompatibleJavaName(); + QDir javaDir(APPLICATION->javaPath()); + // just checking if the executable is there should suffice + // but if needed this can be achieved through refreshing the javalist + // and retrieving the path that contains the java name + auto relativeBinary = FS::PathCombine(javaName, "bin", projt::java::RuntimeScanner::executableName()); + auto finalPath = javaDir.absoluteFilePath(relativeBinary); + if (QFileInfo::exists(finalPath)) + { + setJavaPath(finalPath); + } + else + { + emit logLine( + tr("No compatible Java version was found (the binary file does not exist). Using the default one."), + MessageLevel::Warning); + emitSucceeded(); + } + return; +} + +void AutoInstallJava::downloadJava(projt::meta::MetaVersion::Ptr version, QString javaName) +{ + auto runtimes = version->detailedData()->runtimes; + for (auto java : runtimes) + { + if (java->runtimeOS == m_supported_arch && java->name() == javaName) + { + QDir javaDir(APPLICATION->javaPath()); + auto final_path = javaDir.absoluteFilePath(java->displayName); + switch (java->downloadKind) + { + case projt::java::PackageKind::Manifest: + m_current_task = makeShared<projt::java::RuntimeManifestTask>(java->url, + final_path, + java->checksumType, + java->checksumHash); + break; + case projt::java::PackageKind::Archive: + m_current_task = makeShared<projt::java::RuntimeArchiveTask>(java->url, + final_path, + java->checksumType, + java->checksumHash); + break; + case projt::java::PackageKind::Unknown: + emitFailed(tr("Could not determine Java download type!")); + return; + } +#if defined(Q_OS_MACOS) + auto seq = makeShared<SequentialTask>(tr("Install Java")); + seq->addTask(m_current_task); + seq->addTask(makeShared<projt::java::RuntimeLinkTask>(final_path)); + m_current_task = seq; +#endif + auto deletePath = [final_path] { FS::deletePath(final_path); }; + connect(m_current_task.get(), + &Task::failed, + this, + [this, deletePath](QString reason) + { + deletePath(); + emitFailed(reason); + }); + connect(m_current_task.get(), &Task::aborted, this, [deletePath] { deletePath(); }); + connect(m_current_task.get(), &Task::succeeded, this, &AutoInstallJava::setJavaPathFromPartial); + connect(m_current_task.get(), &Task::failed, this, &AutoInstallJava::tryNextMajorJava); + connect(m_current_task.get(), &Task::progress, this, &AutoInstallJava::setProgress); + connect(m_current_task.get(), &Task::stepProgress, this, &AutoInstallJava::propagateStepProgress); + connect(m_current_task.get(), &Task::status, this, &AutoInstallJava::setStatus); + connect(m_current_task.get(), &Task::details, this, &AutoInstallJava::setDetails); + m_current_task->start(); + return; + } + } + tryNextMajorJava(); +} + +void AutoInstallJava::tryNextMajorJava() +{ + if (!isRunning()) + return; + auto versionList = APPLICATION->metadataIndex()->component("net.minecraft.java"); + auto packProfile = m_instance->getPackProfile(); + auto wantedJavaName = packProfile->getProfile()->getCompatibleJavaName(); + auto majorJavaVersions = packProfile->getProfile()->getCompatibleJavaMajors(); + if (m_majorJavaVersionIndex >= majorJavaVersions.length()) + { + emit logLine(tr("No versions of Java were found for your operating system: %1-%2") + .arg(SysInfo::currentSystem(), SysInfo::useQTForArch()), + MessageLevel::Warning); + emit logLine(tr("No compatible version of Java was found. Using the default one."), MessageLevel::Warning); + emitSucceeded(); + return; + } + auto majorJavaVersion = majorJavaVersions[m_majorJavaVersionIndex]; + m_majorJavaVersionIndex++; + + auto javaMajor = versionList->getMetaVersion(QString("java%1").arg(majorJavaVersion)); + if (!javaMajor) + { + tryNextMajorJava(); + return; + } + + if (javaMajor->isFullyLoaded()) + { + downloadJava(javaMajor, wantedJavaName); + } + else + { + m_current_task = APPLICATION->metadataIndex()->loadVersionTask("net.minecraft.java", + javaMajor->versionId(), + Net::Mode::Online); + connect(m_current_task.get(), + &Task::succeeded, + this, + [this, javaMajor, wantedJavaName] { downloadJava(javaMajor, wantedJavaName); }); + connect(m_current_task.get(), &Task::failed, this, &AutoInstallJava::tryNextMajorJava); + connect(m_current_task.get(), &Task::progress, this, &AutoInstallJava::setProgress); + connect(m_current_task.get(), &Task::stepProgress, this, &AutoInstallJava::propagateStepProgress); + connect(m_current_task.get(), &Task::status, this, &AutoInstallJava::setStatus); + connect(m_current_task.get(), &Task::details, this, &AutoInstallJava::setDetails); + if (!m_current_task->isRunning()) + { + m_current_task->start(); + } + } +} +bool AutoInstallJava::abort() +{ + if (m_current_task && m_current_task->canAbort()) + { + auto status = m_current_task->abort(); + emitAborted(); + return status; + } + return Task::abort(); +} diff --git a/archived/projt-launcher/launcher/minecraft/launch/AutoInstallJava.hpp b/archived/projt-launcher/launcher/minecraft/launch/AutoInstallJava.hpp new file mode 100644 index 0000000000..93fa50818e --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/launch/AutoInstallJava.hpp @@ -0,0 +1,57 @@ +// 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. + */ + +#pragma once + +#include <launch/LaunchStage.hpp> +#include <launch/LaunchPipeline.hpp> +#include "meta/Version.hpp" +#include "minecraft/MinecraftInstance.h" +#include "tasks/Task.h" + +class AutoInstallJava : public projt::launch::LaunchStage +{ + Q_OBJECT + + public: + explicit AutoInstallJava(projt::launch::LaunchPipeline* parent); + ~AutoInstallJava() override = default; + + void executeTask() override; + bool canAbort() const override + { + return m_current_task ? m_current_task->canAbort() : false; + } + bool abort() override; + + protected: + void setJavaPath(QString path); + void setJavaPathFromPartial(); + void downloadJava(projt::meta::MetaVersion::Ptr version, QString javaName); + void tryNextMajorJava(); + + private: + MinecraftInstancePtr m_instance; + Task::Ptr m_current_task; + + qsizetype m_majorJavaVersionIndex = 0; + const QString m_supported_arch; +}; diff --git a/archived/projt-launcher/launcher/minecraft/launch/ClaimAccount.cpp b/archived/projt-launcher/launcher/minecraft/launch/ClaimAccount.cpp new file mode 100644 index 0000000000..2b9150214d --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/launch/ClaimAccount.cpp @@ -0,0 +1,49 @@ +// 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. + */ +#include "ClaimAccount.hpp" +#include <launch/LaunchPipeline.hpp> + +#include "Application.h" +#include "minecraft/auth/AccountList.hpp" + +ClaimAccount::ClaimAccount(projt::launch::LaunchPipeline* parent, AuthSessionPtr session) + : projt::launch::LaunchStage(parent) +{ + if (session->launchMode == LaunchMode::Normal) + { + auto accounts = APPLICATION->accounts(); + m_account = accounts->getAccountByProfileName(session->player_name); + } +} + +void ClaimAccount::executeTask() +{ + if (m_account) + { + lock.reset(new UseLock(m_account)); + } + emitSucceeded(); +} + +void ClaimAccount::finalize() +{ + lock.reset(); +} diff --git a/archived/projt-launcher/launcher/minecraft/launch/ClaimAccount.hpp b/archived/projt-launcher/launcher/minecraft/launch/ClaimAccount.hpp new file mode 100644 index 0000000000..448e77745f --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/launch/ClaimAccount.hpp @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// 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) ============================== + * + * 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. + * ======================================================================== */ + +#pragma once + +#include <launch/LaunchStage.hpp> +#include <minecraft/auth/MinecraftAccount.hpp> + +class ClaimAccount : public projt::launch::LaunchStage +{ + Q_OBJECT + public: + explicit ClaimAccount(projt::launch::LaunchPipeline* parent, AuthSessionPtr session); + virtual ~ClaimAccount() = default; + + void executeTask() override; + void finalize() override; + bool canAbort() const override + { + return false; + } + + private: + std::unique_ptr<UseLock> lock; + MinecraftAccountPtr m_account; +}; diff --git a/archived/projt-launcher/launcher/minecraft/launch/CreateGameFolders.cpp b/archived/projt-launcher/launcher/minecraft/launch/CreateGameFolders.cpp new file mode 100644 index 0000000000..039ec5fe03 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/launch/CreateGameFolders.cpp @@ -0,0 +1,46 @@ +// 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. + */ +#include "CreateGameFolders.hpp" +#include "FileSystem.h" +#include "launch/LaunchPipeline.hpp" +#include "minecraft/MinecraftInstance.h" + +CreateGameFolders::CreateGameFolders(projt::launch::LaunchPipeline* parent) : projt::launch::LaunchStage(parent) +{} + +void CreateGameFolders::executeTask() +{ + auto instance = m_flow->instance(); + + if (!FS::ensureFolderPathExists(instance->gameRoot())) + { + emit logLine("Couldn't create the main game folder", MessageLevel::Error); + emitFailed(tr("Couldn't create the main game folder")); + return; + } + + // HACK: this is a workaround for MCL-3732 - 'server-resource-packs' folder is created. + if (!FS::ensureFolderPathExists(FS::PathCombine(instance->gameRoot(), "server-resource-packs"))) + { + emit logLine("Couldn't create the 'server-resource-packs' folder", MessageLevel::Error); + } + emitSucceeded(); +} diff --git a/archived/projt-launcher/launcher/minecraft/launch/CreateGameFolders.hpp b/archived/projt-launcher/launcher/minecraft/launch/CreateGameFolders.hpp new file mode 100644 index 0000000000..68c5973165 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/launch/CreateGameFolders.hpp @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// 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) ============================== + * + * 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. + * ======================================================================== */ + +#pragma once + +#include <LoggedProcess.h> +#include <launch/LaunchStage.hpp> +#include <minecraft/auth/AuthSession.hpp> + +// Create the main .minecraft for the instance and any other necessary folders +class CreateGameFolders : public projt::launch::LaunchStage +{ + Q_OBJECT + public: + explicit CreateGameFolders(projt::launch::LaunchPipeline* parent); + virtual ~CreateGameFolders() {}; + + virtual void executeTask(); + virtual bool canAbort() const + { + return false; + } +}; diff --git a/archived/projt-launcher/launcher/minecraft/launch/ExtractNatives.cpp b/archived/projt-launcher/launcher/minecraft/launch/ExtractNatives.cpp new file mode 100644 index 0000000000..4ac7e484dc --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/launch/ExtractNatives.cpp @@ -0,0 +1,161 @@ +// 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. + */ + +#include "ExtractNatives.hpp" +#include <launch/LaunchPipeline.hpp> +#include <minecraft/MinecraftInstance.h> + +#include <quazip/quazip.h> +#include <quazip/quazipdir.h> +#include <quazip/quazipfile.h> +#include <quazip/quazipfileinfo.h> +#include <QDir> +#include "FileSystem.h" +#include "MMCZip.h" + +#ifdef major +#undef major +#endif +#ifdef minor +#undef minor +#endif + +static QString replaceSuffix(QString target, const QString& suffix, const QString& replacement) +{ + if (!target.endsWith(suffix)) + { + return target; + } + target.resize(target.length() - suffix.length()); + return target + replacement; +} + +static bool isWithinExtractionRoot(const QDir& root, const QString& path) +{ + const auto cleanRoot = QDir::cleanPath(root.absolutePath()); + const auto cleanPath = QDir::cleanPath(path); + return cleanPath == cleanRoot || cleanPath.startsWith(cleanRoot + '/'); +} + +static bool symlinkEscapesExtractionRoot(QuaZip& zip, const QString& outputPath, const QDir& root) +{ + QuaZipFileInfo64 info; + if (!zip.getCurrentFileInfo(&info) || !info.isSymbolicLink()) + { + return false; + } + + QuaZipFile linkFile(&zip); + if (!linkFile.open(QIODevice::ReadOnly)) + { + return true; + } + + const auto linkTarget = QFile::decodeName(linkFile.readAll()); + linkFile.close(); + + QString resolvedTarget; + if (QDir::isAbsolutePath(linkTarget)) + { + resolvedTarget = QDir::cleanPath(linkTarget); + } + else + { + const auto outputDir = QFileInfo(outputPath).dir(); + resolvedTarget = QDir::cleanPath(outputDir.absoluteFilePath(linkTarget)); + } + + return !isWithinExtractionRoot(root, resolvedTarget); +} + +static bool unzipNatives(QString source, QString targetFolder, bool applyJnilibHack) +{ + QuaZip zip(source); + if (!zip.open(QuaZip::mdUnzip)) + { + return false; + } + QDir directory(targetFolder); + if (!zip.goToFirstFile()) + { + return false; + } + do + { + QString name = zip.getCurrentFileName(); + auto lowercase = name.toLower(); + if (applyJnilibHack) + { + name = replaceSuffix(name, ".jnilib", ".dylib"); + } + QString absFilePath = directory.absoluteFilePath(name); + if (symlinkEscapesExtractionRoot(zip, absFilePath, directory)) + { + return false; + } + if (!JlCompress::extractFile(&zip, "", absFilePath)) + { + return false; + } + } + while (zip.goToNextFile()); + zip.close(); + if (zip.getZipError() != 0) + { + return false; + } + return true; +} + +void ExtractNatives::executeTask() +{ + auto instance = m_flow->instance(); + auto toExtract = instance->getNativeJars(); + if (toExtract.isEmpty()) + { + emitSucceeded(); + return; + } + auto settings = instance->settings(); + + auto outputPath = instance->getNativePath(); + FS::ensureFolderPathExists(outputPath); + auto javaVersion = instance->getRuntimeVersion(); + bool jniHackEnabled = javaVersion.major() >= 8; + for (const auto& source : toExtract) + { + if (!unzipNatives(source, outputPath, jniHackEnabled)) + { + const char* reason = QT_TR_NOOP("Couldn't extract native jar '%1' to destination '%2'"); + emit logLine(QString(reason).arg(source, outputPath), MessageLevel::Fatal); + emitFailed(tr(reason).arg(source, outputPath)); + } + } + emitSucceeded(); +} + +void ExtractNatives::finalize() +{ + auto instance = m_flow->instance(); + QString target_dir = FS::PathCombine(instance->instanceRoot(), "natives/"); + QDir dir(target_dir); + dir.removeRecursively(); +} diff --git a/archived/projt-launcher/launcher/minecraft/launch/ExtractNatives.hpp b/archived/projt-launcher/launcher/minecraft/launch/ExtractNatives.hpp new file mode 100644 index 0000000000..696c1ac6bf --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/launch/ExtractNatives.hpp @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// 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) ============================== + * + * 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. + * ======================================================================== */ + +#pragma once + +#include <launch/LaunchStage.hpp> + +class ExtractNatives : public projt::launch::LaunchStage +{ + Q_OBJECT + public: + explicit ExtractNatives(projt::launch::LaunchPipeline* parent) : projt::launch::LaunchStage(parent) {}; + virtual ~ExtractNatives() {}; + + void executeTask() override; + bool canAbort() const override + { + return false; + } + void finalize() override; +}; diff --git a/archived/projt-launcher/launcher/minecraft/launch/LauncherPartLaunch.cpp b/archived/projt-launcher/launcher/minecraft/launch/LauncherPartLaunch.cpp new file mode 100644 index 0000000000..642189d4ad --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/launch/LauncherPartLaunch.cpp @@ -0,0 +1,275 @@ +// 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. + */ + +#include "LauncherPartLaunch.hpp" + +#include <QRegularExpression> +#include <QStandardPaths> +#include <QStringConverter> + +#include "Application.h" +#include "Commandline.h" +#include "FileSystem.h" +#include "launch/LaunchPipeline.hpp" +#include "minecraft/MinecraftInstance.h" + +#ifdef Q_OS_LINUX +#include "gamemode_client.h" +#endif + +LauncherPartLaunch::LauncherPartLaunch(projt::launch::LaunchPipeline* parent) + : projt::launch::LaunchStage(parent), + m_process(parent->instance()->getRuntimeVersion().defaultsToUtf8() ? QStringConverter::Utf8 + : QStringConverter::System) +{ + if (parent->instance()->settings()->get("CloseAfterLaunch").toBool()) + { + static const QRegularExpression s_settingUser(".*Setting user.+", QRegularExpression::CaseInsensitiveOption); + std::shared_ptr<QMetaObject::Connection> connection{ new QMetaObject::Connection }; + *connection = connect(&m_process, + &LoggedProcess::log, + this, + [connection](const QStringList& lines, [[maybe_unused]] MessageLevel::Enum level) + { + qDebug() << lines; + if (lines.filter(s_settingUser).length() != 0) + { + APPLICATION->closeAllWindows(); + disconnect(*connection); + } + }); + } + + connect(&m_process, &LoggedProcess::log, this, &LauncherPartLaunch::logLines); + connect(&m_process, &LoggedProcess::stateChanged, this, &LauncherPartLaunch::on_state); +} + +void LauncherPartLaunch::executeTask() +{ + QString jarPath = APPLICATION->getJarPath("ProjTLaunch.jar"); + if (jarPath.isEmpty()) + { + const char* reason = QT_TR_NOOP("Launcher library could not be found. Please check your installation."); + emit logLine(tr(reason), MessageLevel::Fatal); + emitFailed(tr(reason)); + return; + } + + auto instance = m_flow->instance(); + + QString legacyJarPath; + if (instance->getLauncher() == "legacy" || instance->shouldApplyOnlineFixes()) + { + legacyJarPath = APPLICATION->getJarPath("ProjTLaunchLegacy.jar"); + if (legacyJarPath.isEmpty()) + { + const char* reason = + QT_TR_NOOP("Legacy launcher library could not be found. Please check your installation."); + emit logLine(tr(reason), MessageLevel::Fatal); + emitFailed(tr(reason)); + return; + } + } + + m_launchScript = instance->createLaunchScript(m_session, m_targetToJoin); + QStringList args = instance->javaArguments(); + QString allArgs = args.join(", "); + emit logLine("Java Arguments:\n[" + m_flow->censorPrivateInfo(allArgs) + "]\n\n", MessageLevel::Launcher); + + auto javaPath = FS::ResolveExecutable(instance->settings()->get("JavaPath").toString()); + + m_process.setProcessEnvironment(instance->createLaunchEnvironment()); + + // make detachable - this will keep the process running even if the object is destroyed + m_process.setDetachable(true); + + auto classPath = instance->getClassPath(); + classPath.prepend(jarPath); + + if (!legacyJarPath.isEmpty()) + classPath.prepend(legacyJarPath); + + auto natPath = instance->getNativePath(); +#ifdef Q_OS_WIN + natPath = FS::getPathNameInLocal8bit(natPath); +#endif + args << "-Djava.library.path=" + natPath; + + args << "-cp"; +#ifdef Q_OS_WIN + QStringList processed; + for (auto& item : classPath) + { + processed << FS::getPathNameInLocal8bit(item); + } + args << processed.join(';'); +#else + args << classPath.join(':'); +#endif + args << "org.projecttick.projtlauncher.normal.EntryPoint"; + + qDebug() << args.join(' '); + + QString wrapperCommandStr = instance->getWrapperCommand().trimmed(); + if (!wrapperCommandStr.isEmpty()) + { + wrapperCommandStr = m_flow->substituteVariables(wrapperCommandStr); + auto wrapperArgs = Commandline::splitArgs(wrapperCommandStr); + auto wrapperCommand = wrapperArgs.takeFirst(); + auto realWrapperCommand = QStandardPaths::findExecutable(wrapperCommand); + if (realWrapperCommand.isEmpty()) + { + const char* reason = QT_TR_NOOP("The wrapper command \"%1\" couldn't be found."); + emit logLine(QString(reason).arg(wrapperCommand), MessageLevel::Fatal); + emitFailed(tr(reason).arg(wrapperCommand)); + return; + } + emit logLine("Wrapper command is:\n" + wrapperCommandStr + "\n\n", MessageLevel::Launcher); + args.prepend(javaPath); + m_process.start(wrapperCommand, wrapperArgs + args); + } + else + { + m_process.start(javaPath, args); + } + +#ifdef Q_OS_LINUX + if (instance->settings()->get("EnableFeralGamemode").toBool() + && APPLICATION->capabilities() & Application::SupportsGameMode) + { + auto pid = m_process.processId(); + if (pid) + { + gamemode_request_start_for(pid); + } + } +#endif +} + +void LauncherPartLaunch::on_state(LoggedProcess::State state) +{ + switch (state) + { + case LoggedProcess::FailedToStart: + { + //: Error message displayed if instace can't start + const char* reason = QT_TR_NOOP("Could not launch Minecraft!"); + emit logLine(reason, MessageLevel::Fatal); + emitFailed(tr(reason)); + return; + } + case LoggedProcess::Aborted: + case LoggedProcess::Crashed: + { + m_flow->setPid(-1); + m_flow->instance()->setMinecraftRunning(false); + emitFailed(tr("Game crashed.")); + return; + } + case LoggedProcess::Finished: + { + auto instance = m_flow->instance(); + if (instance->settings()->get("CloseAfterLaunch").toBool()) + APPLICATION->showMainWindow(); + + m_flow->setPid(-1); + m_flow->instance()->setMinecraftRunning(false); + // if the exit code wasn't 0, report this as a crash + auto exitCode = m_process.exitCode(); + if (exitCode != 0) + { + emitFailed(tr("Game crashed.")); + return; + } + + // Set exit code in environment for post-launch hooks + auto env = m_process.processEnvironment(); + env.insert("INST_EXITCODE", QString::number(exitCode)); + env.insert("INST_NAME", instance->name()); + env.insert("INST_ID", instance->id()); + env.insert("INST_DIR", instance->instanceRoot()); + // Post-launch command can be retrieved from instance settings + QString postExitCmd = instance->settings()->get("PostExitCommand").toString(); + if (!postExitCmd.isEmpty()) + { + emit logLine(tr("Running post-exit command: %1").arg(postExitCmd), MessageLevel::Launcher); + QProcess postProcess; + postProcess.setProcessEnvironment(env); + postProcess.setWorkingDirectory(instance->instanceRoot()); + postProcess.start(postExitCmd); + postProcess.waitForFinished(30000); // 30 second timeout + if (postProcess.exitCode() != 0) + { + emit logLine(tr("Post-exit command failed with code %1").arg(postProcess.exitCode()), + MessageLevel::Warning); + } + } + + emitSucceeded(); + break; + } + case LoggedProcess::Running: + emit logLine(QString("Minecraft process ID: %1\n\n").arg(m_process.processId()), MessageLevel::Launcher); + m_flow->setPid(m_process.processId()); + // send the launch script to the launcher part + m_process.write(m_launchScript.toUtf8()); + + mayProceed = true; + emit readyForLaunch(); + break; + default: break; + } +} + +void LauncherPartLaunch::setWorkingDirectory(const QString& wd) +{ + m_process.setWorkingDirectory(wd); +} + +void LauncherPartLaunch::proceed() +{ + if (mayProceed) + { + m_flow->instance()->setMinecraftRunning(true); + QString launchString("launch\n"); + m_process.write(launchString.toUtf8()); + mayProceed = false; + } +} + +bool LauncherPartLaunch::abort() +{ + if (mayProceed) + { + mayProceed = false; + QString launchString("abort\n"); + m_process.write(launchString.toUtf8()); + } + else + { + auto state = m_process.state(); + if (state == LoggedProcess::Running || state == LoggedProcess::Starting) + { + m_process.kill(); + } + } + return true; +} diff --git a/archived/projt-launcher/launcher/minecraft/launch/LauncherPartLaunch.hpp b/archived/projt-launcher/launcher/minecraft/launch/LauncherPartLaunch.hpp new file mode 100644 index 0000000000..0160980391 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/launch/LauncherPartLaunch.hpp @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// 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) ============================== + * + * 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. + * ======================================================================== */ + +#pragma once + +#include <LoggedProcess.h> +#include <launch/LaunchStage.hpp> +#include <minecraft/auth/AuthSession.hpp> + +#include "MinecraftTarget.hpp" + +class LauncherPartLaunch : public projt::launch::LaunchStage +{ + Q_OBJECT + public: + explicit LauncherPartLaunch(projt::launch::LaunchPipeline* parent); + virtual ~LauncherPartLaunch() = default; + + virtual void executeTask(); + virtual bool abort(); + virtual void proceed(); + virtual bool canAbort() const + { + return true; + } + void setWorkingDirectory(const QString& wd); + void setAuthSession(AuthSessionPtr session) + { + m_session = session; + } + + void setTargetToJoin(MinecraftTarget::Ptr targetToJoin) + { + m_targetToJoin = std::move(targetToJoin); + } + + private slots: + void on_state(LoggedProcess::State state); + + private: + LoggedProcess m_process; + QString m_command; + AuthSessionPtr m_session; + QString m_launchScript; + MinecraftTarget::Ptr m_targetToJoin; + + bool mayProceed = false; +}; diff --git a/archived/projt-launcher/launcher/minecraft/launch/MinecraftTarget.cpp b/archived/projt-launcher/launcher/minecraft/launch/MinecraftTarget.cpp new file mode 100644 index 0000000000..5590ac05ed --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/launch/MinecraftTarget.cpp @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// 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) ============================== + * + * 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 "MinecraftTarget.hpp" + +#include <QRegularExpression> +#include <QStringList> + +// Note: This parser intentionally mirrors Minecraft's address parsing behavior exactly, +// including its tolerance for malformed input. Invalid addresses resolve to unusable +// targets, which Minecraft handles at connection time. The isValid() method can be +// used by callers requiring validation. +MinecraftTarget MinecraftTarget::parse(const QString& fullAddress, bool useWorld) +{ + // Validate input - empty or whitespace-only addresses are invalid + QString trimmed = fullAddress.trimmed(); + if (trimmed.isEmpty()) + { + return MinecraftTarget{}; // Return empty target for invalid input + } + + if (useWorld) + { + MinecraftTarget target; + target.world = trimmed; + return target; + } + + QStringList split = trimmed.split(":"); + + // The logic below replicates the exact logic minecraft uses for parsing server addresses. + // While the conversion is not lossless and eats errors, it ensures the same behavior + // within Minecraft and ProjT Launcher when entering server addresses. + if (trimmed.startsWith("[")) + { + int bracket = trimmed.indexOf("]"); + if (bracket > 0) + { + QString ipv6 = trimmed.mid(1, bracket - 1); + QString port = trimmed.mid(bracket + 1).trimmed(); + + if (port.startsWith(":") && !ipv6.isEmpty()) + { + port = port.mid(1); + split = QStringList({ ipv6, port }); + } + else + { + split = QStringList({ ipv6 }); + } + } + } + + if (split.size() > 2) + { + split = QStringList({ trimmed }); + } + + QString realAddress = split[0]; + + // Validate address is not empty after parsing + if (realAddress.isEmpty()) + { + return MinecraftTarget{}; // Invalid address + } + + quint16 realPort = 25565; + if (split.size() > 1) + { + bool ok; + uint portValue = split[1].toUInt(&ok); + + // Validate port is in valid range (1-65535) + if (ok && portValue > 0 && portValue <= 65535) + { + realPort = static_cast<quint16>(portValue); + } + else + { + // Invalid port, use default + realPort = 25565; + } + } + + return MinecraftTarget{ realAddress, realPort }; +} diff --git a/archived/projt-launcher/launcher/minecraft/launch/MinecraftTarget.hpp b/archived/projt-launcher/launcher/minecraft/launch/MinecraftTarget.hpp new file mode 100644 index 0000000000..11ff4b9bab --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/launch/MinecraftTarget.hpp @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// 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) ============================== + * + * 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. + * ======================================================================== */ + +#pragma once + +#include <memory> + +#include <QString> + +struct MinecraftTarget +{ + QString address; + quint16 port; + + QString world; + static MinecraftTarget parse(const QString& fullAddress, bool useWorld); + + bool isValid() const + { + return !address.isEmpty() || !world.isEmpty(); + } + + using Ptr = std::shared_ptr<MinecraftTarget>; +}; diff --git a/archived/projt-launcher/launcher/minecraft/launch/ModMinecraftJar.cpp b/archived/projt-launcher/launcher/minecraft/launch/ModMinecraftJar.cpp new file mode 100644 index 0000000000..f635420498 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/launch/ModMinecraftJar.cpp @@ -0,0 +1,123 @@ +// 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 Sefa Eyeoglu <contact@scrumplex.net> + * + * 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 "ModMinecraftJar.hpp" +#include "FileSystem.h" +#include "MMCZip.h" +#include "launch/LaunchPipeline.hpp" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" + +void ModMinecraftJar::executeTask() +{ + auto m_inst = m_flow->instance(); + + if (!m_inst->getJarMods().size()) + { + emitSucceeded(); + return; + } + // nuke obsolete stripped jar(s) if needed + if (!FS::ensureFolderPathExists(m_inst->binRoot())) + { + emitFailed(tr("Couldn't create the bin folder for Minecraft.jar")); + } + + auto finalJarPath = QDir(m_inst->binRoot()).absoluteFilePath("minecraft.jar"); + if (!removeJar()) + { + emitFailed(tr("Couldn't remove stale jar file: %1").arg(finalJarPath)); + } + + // create temporary modded jar, if needed + auto components = m_inst->getPackProfile(); + auto profile = components->getProfile(); + auto jarMods = m_inst->getJarMods(); + if (jarMods.size()) + { + auto mainJar = profile->getMainJar(); + QStringList jars, temp1, temp2, temp3, temp4; + mainJar->getApplicableFiles(m_inst->runtimeContext(), jars, temp1, temp2, temp3, m_inst->getLocalLibraryPath()); + auto sourceJarPath = jars[0]; + if (!MMCZip::createModdedJar(sourceJarPath, finalJarPath, jarMods)) + { + emitFailed(tr("Failed to create the custom Minecraft jar file.")); + return; + } + } + emitSucceeded(); +} + +void ModMinecraftJar::finalize() +{ + removeJar(); +} + +bool ModMinecraftJar::removeJar() +{ + auto m_inst = m_flow->instance(); + auto finalJarPath = QDir(m_inst->binRoot()).absoluteFilePath("minecraft.jar"); + QFile finalJar(finalJarPath); + if (finalJar.exists()) + { + if (!finalJar.remove()) + { + return false; + } + } + return true; +} diff --git a/archived/projt-launcher/launcher/minecraft/launch/ModMinecraftJar.hpp b/archived/projt-launcher/launcher/minecraft/launch/ModMinecraftJar.hpp new file mode 100644 index 0000000000..0dee222941 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/launch/ModMinecraftJar.hpp @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// 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) ============================== + * + * 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. + * ======================================================================== */ + +#pragma once + +#include <launch/LaunchStage.hpp> +#include <memory> + +class ModMinecraftJar : public projt::launch::LaunchStage +{ + Q_OBJECT + public: + explicit ModMinecraftJar(projt::launch::LaunchPipeline* parent) : projt::launch::LaunchStage(parent) {}; + virtual ~ModMinecraftJar() {}; + + virtual void executeTask() override; + virtual bool canAbort() const override + { + return false; + } + void finalize() override; + + private: + bool removeJar(); +}; diff --git a/archived/projt-launcher/launcher/minecraft/launch/PrintInstanceInfo.cpp b/archived/projt-launcher/launcher/minecraft/launch/PrintInstanceInfo.cpp new file mode 100644 index 0000000000..186254353c --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/launch/PrintInstanceInfo.cpp @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// 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) ============================== + * + * 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 <cstdio> +#include <cstring> +#include <string> + +#include <QSysInfo> +#include <launch/LaunchPipeline.hpp> +#include "PrintInstanceInfo.hpp" + +#include "HardwareInfo.h" + +#if defined(Q_OS_FREEBSD) +namespace +{ + void runSysctlHwModel(QStringList& log) + { + char buff[512]; + FILE* hwmodel = popen("sysctl hw.model", "r"); + while (fgets(buff, 512, hwmodel) != nullptr) + { + log << QString::fromUtf8(buff); + break; + } + pclose(hwmodel); + } + + void runPciconf(QStringList& log) + { + char buff[512]; + std::string strcard; + FILE* pciconf = popen("pciconf -lv -a vgapci0", "r"); + while (fgets(buff, 512, pciconf) != nullptr) + { + if (strncmp(buff, " vendor", 10) == 0) + { + std::string str(buff); + strcard.append( + str.substr(str.find_first_of("'") + 1, str.find_last_not_of("'") - (str.find_first_of("'") + 2))); + strcard.append(" "); + } + else if (strncmp(buff, " device", 10) == 0) + { + std::string str2(buff); + strcard.append( + str2.substr(str2.find_first_of("'") + 1, str2.find_last_not_of("'") - (str2.find_first_of("'") + 2))); + } + log << QString::fromStdString(strcard); + break; + } + pclose(pciconf); + } +} // namespace +#endif + +void PrintInstanceInfo::executeTask() +{ + auto instance = m_flow->instance(); + QStringList log; + + log << "OS: " + QString("%1 | %2 | %3").arg(QSysInfo::prettyProductName(), QSysInfo::kernelType(), QSysInfo::kernelVersion()); +#ifdef Q_OS_FREEBSD + ::runSysctlHwModel(log); + ::runPciconf(log); +#else + log << "CPU: " + HardwareInfo::cpuInfo(); + log << QString("RAM: %1 MiB (available: %2 MiB)").arg(HardwareInfo::totalRamMiB()).arg(HardwareInfo::availableRamMiB()); +#endif + log.append(HardwareInfo::gpuInfo()); + log << ""; + + logLines(log, MessageLevel::Launcher); + logLines(instance->verboseDescription(m_session, m_targetToJoin), MessageLevel::Launcher); + emitSucceeded(); +} diff --git a/archived/projt-launcher/launcher/minecraft/launch/PrintInstanceInfo.hpp b/archived/projt-launcher/launcher/minecraft/launch/PrintInstanceInfo.hpp new file mode 100644 index 0000000000..10bf98a66d --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/launch/PrintInstanceInfo.hpp @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// 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) ============================== + * + * 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. + * ======================================================================== */ + +#pragma once + +#include <launch/LaunchStage.hpp> +#include "minecraft/auth/AuthSession.hpp" +#include "minecraft/launch/MinecraftTarget.hpp" + +class PrintInstanceInfo : public projt::launch::LaunchStage +{ + Q_OBJECT + public: + explicit PrintInstanceInfo(projt::launch::LaunchPipeline* parent, + AuthSessionPtr session, + MinecraftTarget::Ptr targetToJoin) + : projt::launch::LaunchStage(parent), + m_session(session), + m_targetToJoin(targetToJoin) {}; + virtual ~PrintInstanceInfo() = default; + + virtual void executeTask(); + virtual bool canAbort() const + { + return false; + } + + private: + AuthSessionPtr m_session; + MinecraftTarget::Ptr m_targetToJoin; +}; diff --git a/archived/projt-launcher/launcher/minecraft/launch/ReconstructAssets.cpp b/archived/projt-launcher/launcher/minecraft/launch/ReconstructAssets.cpp new file mode 100644 index 0000000000..9f719f3539 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/launch/ReconstructAssets.cpp @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// 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) ============================== + * + * 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 "ReconstructAssets.hpp" +#include "launch/LaunchPipeline.hpp" +#include "minecraft/AssetsUtils.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" + +void ReconstructAssets::executeTask() +{ + auto instance = m_flow->instance(); + auto components = instance->getPackProfile(); + auto profile = components->getProfile(); + auto assets = profile->getMinecraftAssets(); + + if (!AssetsUtils::reconstructAssets(assets->id, instance->resourcesDir())) + { + emit logLine("Failed to reconstruct Minecraft assets.", MessageLevel::Error); + } + + emitSucceeded(); +} diff --git a/archived/projt-launcher/launcher/minecraft/launch/ReconstructAssets.hpp b/archived/projt-launcher/launcher/minecraft/launch/ReconstructAssets.hpp new file mode 100644 index 0000000000..715d78cbd4 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/launch/ReconstructAssets.hpp @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// 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) ============================== + * + * 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. + * ======================================================================== */ + +#pragma once + +#include <launch/LaunchStage.hpp> +#include <memory> + +class ReconstructAssets : public projt::launch::LaunchStage +{ + Q_OBJECT + public: + explicit ReconstructAssets(projt::launch::LaunchPipeline* parent) : projt::launch::LaunchStage(parent) {}; + virtual ~ReconstructAssets() {}; + + void executeTask() override; + bool canAbort() const override + { + return false; + } +}; diff --git a/archived/projt-launcher/launcher/minecraft/launch/ScanModFolders.cpp b/archived/projt-launcher/launcher/minecraft/launch/ScanModFolders.cpp new file mode 100644 index 0000000000..bad7feca25 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/launch/ScanModFolders.cpp @@ -0,0 +1,116 @@ +// 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 Sefa Eyeoglu <contact@scrumplex.net> + * + * 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 "ScanModFolders.hpp" +#include "FileSystem.h" +#include "MMCZip.h" +#include "launch/LaunchPipeline.hpp" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/mod/ModFolderModel.hpp" + +void ScanModFolders::executeTask() +{ + auto m_inst = m_flow->instance(); + + auto loaders = m_inst->loaderModList(); + connect(loaders.get(), &ModFolderModel::updateFinished, this, &ScanModFolders::modsDone); + if (!loaders->update()) + { + m_modsDone = true; + } + + auto cores = m_inst->coreModList(); + connect(cores.get(), &ModFolderModel::updateFinished, this, &ScanModFolders::coreModsDone); + if (!cores->update()) + { + m_coreModsDone = true; + } + + auto nils = m_inst->nilModList(); + connect(nils.get(), &ModFolderModel::updateFinished, this, &ScanModFolders::nilModsDone); + if (!nils->update()) + { + m_nilModsDone = true; + } + checkDone(); +} + +void ScanModFolders::modsDone() +{ + m_modsDone = true; + checkDone(); +} + +void ScanModFolders::coreModsDone() +{ + m_coreModsDone = true; + checkDone(); +} + +void ScanModFolders::nilModsDone() +{ + m_nilModsDone = true; + checkDone(); +} + +void ScanModFolders::checkDone() +{ + if (m_modsDone && m_coreModsDone && m_nilModsDone) + { + emitSucceeded(); + } +} diff --git a/archived/projt-launcher/launcher/minecraft/launch/ScanModFolders.hpp b/archived/projt-launcher/launcher/minecraft/launch/ScanModFolders.hpp new file mode 100644 index 0000000000..13648fc9d6 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/launch/ScanModFolders.hpp @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// 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) ============================== + * + * 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. + * ======================================================================== */ + +#pragma once + +#include <launch/LaunchStage.hpp> +#include <memory> + +class ScanModFolders : public projt::launch::LaunchStage +{ + Q_OBJECT + public: + explicit ScanModFolders(projt::launch::LaunchPipeline* parent) : projt::launch::LaunchStage(parent) {}; + virtual ~ScanModFolders() {}; + + virtual void executeTask() override; + virtual bool canAbort() const override + { + return false; + } + private slots: + void coreModsDone(); + void modsDone(); + void nilModsDone(); + + private: + void checkDone(); + + private: // DATA + bool m_modsDone = false; + bool m_nilModsDone = false; + bool m_coreModsDone = false; +}; diff --git a/archived/projt-launcher/launcher/minecraft/launch/VerifyJavaInstall.cpp b/archived/projt-launcher/launcher/minecraft/launch/VerifyJavaInstall.cpp new file mode 100644 index 0000000000..18c6d0efca --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/launch/VerifyJavaInstall.cpp @@ -0,0 +1,80 @@ +// 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. + */ + +#include "VerifyJavaInstall.hpp" +#include <memory> + +#include "Application.h" +#include "MessageLevel.h" +#include "java/core/RuntimeVersion.hpp" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" + +void VerifyJavaInstall::executeTask() +{ + auto instance = m_flow->instance(); + auto packProfile = instance->getPackProfile(); + auto settings = instance->settings(); + auto storedVersion = settings->get("JavaVersion").toString(); + auto ignoreCompatibility = settings->get("IgnoreJavaCompatibility").toBool(); + auto javaArchitecture = settings->get("JavaArchitecture").toString(); + auto maxMemAlloc = settings->get("MaxMemAlloc").toInt(); + + if (javaArchitecture == "32" && maxMemAlloc > 2048) + { + emit logLine(tr("Max memory allocation exceeds the supported value.\n" + "The selected installation of Java is 32-bit and doesn't support more than 2048MiB of RAM.\n" + "The instance may not start due to this."), + MessageLevel::Error); + } + + auto compatibleMajors = packProfile->getProfile()->getCompatibleJavaMajors(); + + projt::java::RuntimeVersion javaVersion(storedVersion); + + if (compatibleMajors.isEmpty() || compatibleMajors.contains(javaVersion.major())) + { + emitSucceeded(); + return; + } + + if (ignoreCompatibility) + { + emit logLine(tr("Java major version is incompatible. Things might break."), MessageLevel::Warning); + emitSucceeded(); + return; + } + + emit logLine(tr("This instance is not compatible with Java version %1.\n" + "Please switch to one of the following Java versions for this instance:") + .arg(javaVersion.major()), + MessageLevel::Error); + for (auto major : compatibleMajors) + { + emit logLine(tr("Java version %1").arg(major), MessageLevel::Error); + } + emit logLine(tr("Go to instance Java settings to change your Java version or disable the Java compatibility check " + "if you know what " + "you're doing."), + MessageLevel::Error); + + emitFailed(QString("Incompatible Java major version")); +} diff --git a/archived/projt-launcher/launcher/minecraft/launch/VerifyJavaInstall.hpp b/archived/projt-launcher/launcher/minecraft/launch/VerifyJavaInstall.hpp new file mode 100644 index 0000000000..ec5961e0a7 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/launch/VerifyJavaInstall.hpp @@ -0,0 +1,40 @@ +// 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. + */ + +#pragma once + +#include <launch/LaunchStage.hpp> +#include <launch/LaunchPipeline.hpp> + +class VerifyJavaInstall : public projt::launch::LaunchStage +{ + Q_OBJECT + + public: + explicit VerifyJavaInstall(projt::launch::LaunchPipeline* parent) : projt::launch::LaunchStage(parent) {}; + ~VerifyJavaInstall() override = default; + + void executeTask() override; + bool canAbort() const override + { + return false; + } +}; diff --git a/archived/projt-launcher/launcher/minecraft/mod/DataPack.cpp b/archived/projt-launcher/launcher/minecraft/mod/DataPack.cpp new file mode 100644 index 0000000000..5834a0e52a --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/DataPack.cpp @@ -0,0 +1,267 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// 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 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#include "DataPack.hpp" + +#include <QDebug> +#include <QMap> +#include <QRegularExpression> + +#include "MTPixmapCache.h" +#include "Version.h" +#include "minecraft/mod/tasks/LocalDataPackParseTask.hpp" + +// Values taken from: +// https://minecraft.wiki/w/Pack_format#List_of_data_pack_formats +static const QMap<int, std::pair<Version, Version>> s_pack_format_versions = { + { 4, { Version("1.13"), Version("1.14.4") } }, + { 5, { Version("1.15"), Version("1.16.1") } }, + { 6, { Version("1.16.2"), Version("1.16.5") } }, + { 7, { Version("1.17"), Version("1.17.1") } }, + { 8, { Version("1.18"), Version("1.18.1") } }, + { 9, { Version("1.18.2"), Version("1.18.2") } }, + { 10, { Version("1.19"), Version("1.19.3") } }, + { 11, { Version("23w03a"), Version("23w05a") } }, + { 12, { Version("1.19.4"), Version("1.19.4") } }, + { 13, { Version("23w12a"), Version("23w14a") } }, + { 14, { Version("23w16a"), Version("23w17a") } }, + { 15, { Version("1.20"), Version("1.20.1") } }, + { 16, { Version("23w31a"), Version("23w31a") } }, + { 17, { Version("23w32a"), Version("23w35a") } }, + { 18, { Version("1.20.2"), Version("1.20.2") } }, + { 19, { Version("23w40a"), Version("23w40a") } }, + { 20, { Version("23w41a"), Version("23w41a") } }, + { 21, { Version("23w42a"), Version("23w42a") } }, + { 22, { Version("23w43a"), Version("23w43b") } }, + { 23, { Version("23w44a"), Version("23w44a") } }, + { 24, { Version("23w45a"), Version("23w45a") } }, + { 25, { Version("23w46a"), Version("23w46a") } }, + { 26, { Version("1.20.3"), Version("1.20.4") } }, + { 27, { Version("23w51a"), Version("23w51b") } }, + { 28, { Version("24w05a"), Version("24w05b") } }, + { 29, { Version("24w04a"), Version("24w04a") } }, + { 30, { Version("24w05a"), Version("24w05b") } }, + { 31, { Version("24w06a"), Version("24w06a") } }, + { 32, { Version("24w07a"), Version("24w07a") } }, + { 33, { Version("24w09a"), Version("24w09a") } }, + { 34, { Version("24w10a"), Version("24w10a") } }, + { 35, { Version("24w11a"), Version("24w11a") } }, + { 36, { Version("24w12a"), Version("24w12a") } }, + { 37, { Version("24w13a"), Version("24w13a") } }, + { 38, { Version("24w14a"), Version("24w14a") } }, + { 39, { Version("1.20.5-pre1"), Version("1.20.5-pre1") } }, + { 40, { Version("1.20.5-pre2"), Version("1.20.5-pre2") } }, + { 41, { Version("1.20.5"), Version("1.20.6") } }, + { 42, { Version("24w18a"), Version("24w18a") } }, + { 43, { Version("24w19a"), Version("24w19b") } }, + { 44, { Version("24w20a"), Version("24w20a") } }, + { 45, { Version("21w21a"), Version("21w21b") } }, + { 46, { Version("1.21-pre1"), Version("1.21-pre1") } }, + { 47, { Version("1.21-pre2"), Version("1.21-pre2") } }, + { 48, { Version("1.21-pre3"), Version("1.21.1") } }, + { 49, { Version("24w33a"), Version("24w33a") } }, + { 50, { Version("24w34a"), Version("24w34a") } }, + { 51, { Version("24w35a"), Version("24w35a") } }, + { 52, { Version("24w36a"), Version("24w36a") } }, + { 53, { Version("24w37a"), Version("24w37a") } }, + { 54, { Version("24w38a"), Version("24w38a") } }, + { 55, { Version("24w39a"), Version("24w39a") } }, + { 56, { Version("24w40a"), Version("24w40a") } }, + { 57, { Version("1.21.2-pre1"), Version("1.21.3") } }, + { 58, { Version("24w44a"), Version("24w39a") } }, + { 59, { Version("24w45a"), Version("24w39a") } }, + { 60, { Version("24w46a"), Version("1.21.4-pre1") } }, + { 61, { Version("1.21.4-pre2"), Version("1.21.4") } }, + { 62, { Version("25w02a"), Version("25w02a") } }, + { 63, { Version("25w03a"), Version("25w03a") } }, + { 64, { Version("25w04a"), Version("25w04a") } }, + { 65, { Version("25w05a"), Version("25w05a") } }, + { 66, { Version("25w06a"), Version("25w06a") } }, + { 67, { Version("25w07a"), Version("25w07a") } }, + { 68, { Version("25w08a"), Version("25w08a") } }, + { 69, { Version("25w09a"), Version("25w09b") } }, + { 70, { Version("25w10a"), Version("1.21.5-pre1") } } +}; + +void DataPack::setPackFormat(int new_format_id) +{ + QMutexLocker locker(&m_data_lock); + + if (!s_pack_format_versions.contains(new_format_id)) + { + qWarning() << "Pack format '" << new_format_id << "' is not a recognized data pack id!"; + } + + m_pack_format = new_format_id; +} + +void DataPack::setDescription(QString new_description) +{ + QMutexLocker locker(&m_data_lock); + + m_description = new_description; +} + +void DataPack::setLocalizedDescription(const QString& langCode, const QString& description) +{ + QMutexLocker locker(&m_data_lock); + + if (langCode.isEmpty()) + { + m_description = description; + } + else + { + m_localized_descriptions[langCode] = description; + } +} + +void DataPack::setLocalizedDescriptions(const QHash<QString, QString>& descriptions) +{ + QMutexLocker locker(&m_data_lock); + + m_localized_descriptions = descriptions; +} + +void DataPack::setImage(QImage new_image) const +{ + QMutexLocker locker(&m_data_lock); + + Q_ASSERT(!new_image.isNull()); + + if (m_pack_image_cache_key.key.isValid()) + PixmapCache::instance().remove(m_pack_image_cache_key.key); + + // scale the image to avoid flooding the pixmapcache + auto pixmap = QPixmap::fromImage( + new_image.scaled({ 64, 64 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding, Qt::SmoothTransformation)); + + m_pack_image_cache_key.key = PixmapCache::instance().insert(pixmap); + m_pack_image_cache_key.was_ever_used = true; + + // This can happen if the pixmap is too big to fit in the cache :c + if (!m_pack_image_cache_key.key.isValid()) + { + qWarning() << "Could not insert a image cache entry! Ignoring it."; + m_pack_image_cache_key.was_ever_used = false; + } +} + +QPixmap DataPack::image(QSize size, Qt::AspectRatioMode mode) const +{ + QPixmap cached_image; + if (PixmapCache::instance().find(m_pack_image_cache_key.key, &cached_image)) + { + if (size.isNull()) + return cached_image; + return cached_image.scaled(size, mode, Qt::SmoothTransformation); + } + + // No valid image we can get + if (!m_pack_image_cache_key.was_ever_used) + { + return {}; + } + else + { + qDebug() << "Data Pack" << name() << "Had it's image evicted from the cache. reloading..."; + PixmapCache::markCacheMissByEviciton(); + } + + // Imaged got evicted from the cache. Re-process it and retry. + DataPackUtils::processPackPNG(this); + return image(size); +} + +std::pair<Version, Version> DataPack::compatibleVersions() const +{ + if (!s_pack_format_versions.contains(m_pack_format)) + { + return { {}, {} }; + } + + return s_pack_format_versions.constFind(m_pack_format).value(); +} + +int DataPack::compare(const Resource& other, SortType type) const +{ + auto const& cast_other = static_cast<DataPack const&>(other); + if (type == SortType::PACK_FORMAT) + { + auto this_ver = packFormat(); + auto other_ver = cast_other.packFormat(); + + if (this_ver > other_ver) + return 1; + if (this_ver < other_ver) + return -1; + } + else + { + return Resource::compare(other, type); + } + return 0; +} + +bool DataPack::applyFilter(QRegularExpression filter) const +{ + if (filter.match(description()).hasMatch()) + return true; + + if (filter.match(QString::number(packFormat())).hasMatch()) + return true; + + if (filter.match(compatibleVersions().first.toString()).hasMatch()) + return true; + if (filter.match(compatibleVersions().second.toString()).hasMatch()) + return true; + + return Resource::applyFilter(filter); +} + +bool DataPack::valid() const +{ + return m_pack_format != 0; +} diff --git a/archived/projt-launcher/launcher/minecraft/mod/DataPack.hpp b/archived/projt-launcher/launcher/minecraft/mod/DataPack.hpp new file mode 100644 index 0000000000..84bef9c0b7 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/DataPack.hpp @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// 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 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#pragma once + +#include "Resource.hpp" + +#include <QHash> +#include <QLocale> +#include <QMutex> +#include <QPixmapCache> + +class Version; + +class DataPack : public Resource +{ + Q_OBJECT + public: + DataPack(QObject* parent = nullptr) : Resource(parent) + {} + DataPack(QFileInfo file_info) : Resource(file_info) + {} + + /** Gets the numerical ID of the pack format. */ + int packFormat() const + { + return m_pack_format; + } + /** Gets, respectively, the lower and upper versions supported by the set pack format. */ + virtual std::pair<Version, Version> compatibleVersions() const; + + /** Gets the description of the data pack. + * If a localized description exists for the current locale, returns that. + * Otherwise returns the default description. + */ + QString description() const + { + QString lang = QLocale::system().name().section('_', 0, 0); + if (m_localized_descriptions.contains(lang)) + { + return m_localized_descriptions.value(lang); + } + return m_description; + } + + /** Gets the raw (non-localized) description. */ + QString rawDescription() const + { + return m_description; + } + + /** Gets description for a specific language code. */ + QString localizedDescription(const QString& langCode) const + { + return m_localized_descriptions.value(langCode, m_description); + } + + /** Gets all available localized descriptions. */ + QHash<QString, QString> allLocalizedDescriptions() const + { + return m_localized_descriptions; + } + + /** Gets the image of the data pack, converted to a QPixmap for drawing, and scaled to size. */ + QPixmap image(QSize size, Qt::AspectRatioMode mode = Qt::AspectRatioMode::IgnoreAspectRatio) const; + + /** Thread-safe. */ + void setPackFormat(int new_format_id); + + /** Thread-safe. */ + void setDescription(QString new_description); + + /** Thread-safe. Sets a localized description for a specific language. */ + void setLocalizedDescription(const QString& langCode, const QString& description); + + /** Thread-safe. Sets all localized descriptions at once. */ + void setLocalizedDescriptions(const QHash<QString, QString>& descriptions); + + /** Thread-safe. */ + void setImage(QImage new_image) const; + + bool valid() const override; + + [[nodiscard]] int compare(Resource const& other, SortType type) const override; + [[nodiscard]] bool applyFilter(QRegularExpression filter) const override; + + protected: + mutable QMutex m_data_lock; + + /* The 'version' of a data pack, as defined in the pack.mcmeta file. + * See https://minecraft.wiki/w/Data_pack#pack.mcmeta + */ + int m_pack_format = 0; + + /** The data pack's description, as defined in the pack.mcmeta file. + */ + QString m_description; + + /** Localized descriptions keyed by language code (e.g., "en", "de", "ja"). + * Parsed from pack.mcmeta language files if available. + */ + QHash<QString, QString> m_localized_descriptions; + + /** The data pack's image file cache key, for access in the QPixmapCache global instance. + * + * The 'was_ever_used' state simply identifies whether the key was never inserted on the cache (true), + * so as to tell whether a cache entry is inexistent or if it was just evicted from the cache. + */ + struct + { + QPixmapCache::Key key; + bool was_ever_used = false; + } mutable m_pack_image_cache_key; +}; diff --git a/archived/projt-launcher/launcher/minecraft/mod/DataPackFolderModel.cpp b/archived/projt-launcher/launcher/minecraft/mod/DataPackFolderModel.cpp new file mode 100644 index 0000000000..1248e62af4 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/DataPackFolderModel.cpp @@ -0,0 +1,205 @@ +// 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> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me> + * + * 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 "DataPackFolderModel.hpp" +#include <qnamespace.h> +#include <qsize.h> + +#include <QIcon> +#include <QStyle> + +#include "Version.h" + +#include "minecraft/mod/tasks/LocalDataPackParseTask.hpp" + +DataPackFolderModel::DataPackFolderModel(const QString& dir, + BaseInstance* instance, + bool is_indexed, + bool create_dir, + QObject* parent) + : ResourceFolderModel(QDir(dir), instance, is_indexed, create_dir, parent) +{ + m_column_names = QStringList({ "Enable", "Image", "Name", "Pack Format", "Last Modified" }); + m_column_names_translated = + QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Pack Format"), tr("Last Modified") }); + m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::PACK_FORMAT, SortType::DATE }; + m_column_resize_modes = { QHeaderView::Interactive, + QHeaderView::Interactive, + QHeaderView::Stretch, + QHeaderView::Interactive, + QHeaderView::Interactive }; + m_columnsHideable = { false, true, false, true, true }; +} + +QVariant DataPackFolderModel::data(const QModelIndex& index, int role) const +{ + if (!validateIndex(index)) + return {}; + + int row = index.row(); + int column = index.column(); + + switch (role) + { + case Qt::BackgroundRole: return rowBackground(row); + case Qt::DisplayRole: + if (column == PackFormatColumn) + { + auto& resource = at(row); + auto pack_format = resource.packFormat(); + if (pack_format == 0) + return tr("Unrecognized"); + + auto version_bounds = resource.compatibleVersions(); + if (version_bounds.first.toString().isEmpty()) + return QString::number(pack_format); + + return QString("%1 (%2 - %3)") + .arg(QString::number(pack_format), version_bounds.first.toString(), version_bounds.second.toString()); + } + break; + case Qt::DecorationRole: + if (column == ImageColumn) + { + return at(row).image({ 32, 32 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding); + } + break; + case Qt::ToolTipRole: + if (column == PackFormatColumn) + { + return tr("The data pack format ID, as well as the Minecraft versions it was designed for."); + } + break; + case Qt::SizeHintRole: + if (column == ImageColumn) + { + return QSize(32, 32); + } + break; + default: break; + } + + QModelIndex mappedIndex; + switch (column) + { + case ActiveColumn: mappedIndex = index.siblingAtColumn(ResourceFolderModel::ActiveColumn); break; + case NameColumn: mappedIndex = index.siblingAtColumn(ResourceFolderModel::NameColumn); break; + case DateColumn: mappedIndex = index.siblingAtColumn(ResourceFolderModel::DateColumn); break; + case ProviderColumn: mappedIndex = index.siblingAtColumn(ResourceFolderModel::ProviderColumn); break; + default: break; + } + + if (mappedIndex.isValid()) + return ResourceFolderModel::data(mappedIndex, role); + + return {}; +} + +QVariant DataPackFolderModel::headerData(int section, [[maybe_unused]] Qt::Orientation orientation, int role) const +{ + switch (role) + { + case Qt::DisplayRole: + switch (section) + { + case ActiveColumn: + case NameColumn: + case PackFormatColumn: + case DateColumn: + case ImageColumn: return columnNames().at(section); + default: return {}; + } + + case Qt::ToolTipRole: + switch (section) + { + case ActiveColumn: return tr("Is the data pack enabled? (Only valid for ZIPs)"); + case NameColumn: return tr("The name of the data pack."); + case PackFormatColumn: + //: The string being explained by this is in the format: ID (Lower version - Upper version) + return tr("The data pack format ID, as well as the Minecraft versions it was designed for."); + case DateColumn: return tr("The date and time this data pack was last changed (or added)."); + default: return {}; + } + case Qt::SizeHintRole: + if (section == ImageColumn) + { + return QSize(64, 0); + } + return {}; + default: return {}; + } +} + +int DataPackFolderModel::columnCount(const QModelIndex& parent) const +{ + return parent.isValid() ? 0 : NUM_COLUMNS; +} + +Resource* DataPackFolderModel::createResource(const QFileInfo& file) +{ + return new DataPack(file); +} + +Task* DataPackFolderModel::createParseTask(Resource& resource) +{ + return new LocalDataPackParseTask(m_next_resolution_ticket, static_cast<DataPack*>(&resource)); +} diff --git a/archived/projt-launcher/launcher/minecraft/mod/DataPackFolderModel.hpp b/archived/projt-launcher/launcher/minecraft/mod/DataPackFolderModel.hpp new file mode 100644 index 0000000000..28b113bcb1 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/DataPackFolderModel.hpp @@ -0,0 +1,102 @@ +// 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> + * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me> + * + * 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. + * + * ======================================================================== */ + +#pragma once + +#include "ResourceFolderModel.hpp" + +#include "DataPack.hpp" +#include "ResourcePack.hpp" + +class DataPackFolderModel : public ResourceFolderModel +{ + Q_OBJECT + public: + enum Columns + { + ActiveColumn = 0, + ImageColumn, + NameColumn, + PackFormatColumn, + DateColumn, + NUM_COLUMNS + }; + + explicit DataPackFolderModel(const QString& dir, + BaseInstance* instance, + bool is_indexed, + bool create_dir, + QObject* parent = nullptr); + + virtual QString id() const override + { + return "datapacks"; + } + + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + int columnCount(const QModelIndex& parent) const override; + + [[nodiscard]] Resource* createResource(const QFileInfo& file) override; + [[nodiscard]] Task* createParseTask(Resource&) override; + + RESOURCE_HELPERS(DataPack) +}; diff --git a/archived/projt-launcher/launcher/minecraft/mod/MetadataHandler.hpp b/archived/projt-launcher/launcher/minecraft/mod/MetadataHandler.hpp new file mode 100644 index 0000000000..43c611bb66 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/MetadataHandler.hpp @@ -0,0 +1,79 @@ +// 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> + * Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me> + * + * 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. + * + * ======================================================================== */ + +#pragma once + +#include "modplatform/packwiz/Packwiz.h" + +namespace Metadata +{ + using ModStruct = Packwiz::V1::Mod; + + inline ModStruct create(const QDir& index_dir, + ModPlatform::IndexedPack& mod_pack, + ModPlatform::IndexedVersion& mod_version) + { + return Packwiz::V1::createModFormat(index_dir, mod_pack, mod_version); + } + + inline void update(const QDir& index_dir, ModStruct& mod) + { + Packwiz::V1::updateModIndex(index_dir, mod); + } + + inline void remove(const QDir& index_dir, QString mod_slug) + { + Packwiz::V1::deleteModIndex(index_dir, mod_slug); + } + + inline ModStruct get(const QDir& index_dir, QString mod_slug) + { + return Packwiz::V1::getIndexForMod(index_dir, std::move(mod_slug)); + } + + inline ModStruct get(const QDir& index_dir, QVariant& mod_id) + { + return Packwiz::V1::getIndexForMod(index_dir, mod_id); + } + +}; // namespace Metadata diff --git a/archived/projt-launcher/launcher/minecraft/mod/Mod.cpp b/archived/projt-launcher/launcher/minecraft/mod/Mod.cpp new file mode 100644 index 0000000000..97aabac583 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/Mod.cpp @@ -0,0 +1,324 @@ +// 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> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * Copyright (c) 2023 Trial97 <alexandru.tripon97@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 "Mod.hpp" +#include <qpixmap.h> + +#include <QDir> +#include <QRegularExpression> +#include <QString> + +#include "MTPixmapCache.h" +#include "MetadataHandler.hpp" +#include "Resource.hpp" +#include "Version.h" +#include "minecraft/mod/ModDetails.hpp" +#include "minecraft/mod/tasks/LocalModParseTask.hpp" +#include "modplatform/ModIndex.h" + +Mod::Mod(const QFileInfo& file) : Resource(file), m_local_details() +{ + m_enabled = (file.suffix() != "disabled"); +} + +void Mod::setDetails(const ModDetails& details) +{ + m_local_details = details; +} + +int Mod::compare(const Resource& other, SortType type) const +{ + auto cast_other = dynamic_cast<Mod const*>(&other); + if (!cast_other) + return Resource::compare(other, type); + + switch (type) + { + default: + case SortType::ENABLED: + case SortType::NAME: + case SortType::DATE: + case SortType::SIZE: return Resource::compare(other, type); + case SortType::VERSION: + { + auto this_ver = Version(version()); + auto other_ver = Version(cast_other->version()); + if (this_ver > other_ver) + return 1; + if (this_ver < other_ver) + return -1; + break; + } + case SortType::SIDE: + { + auto compare_result = QString::compare(side(), cast_other->side(), Qt::CaseInsensitive); + if (compare_result != 0) + return compare_result; + break; + } + case SortType::MC_VERSIONS: + { + auto compare_result = QString::compare(mcVersions(), cast_other->mcVersions(), Qt::CaseInsensitive); + if (compare_result != 0) + return compare_result; + break; + } + case SortType::LOADERS: + { + auto compare_result = QString::compare(loaders(), cast_other->loaders(), Qt::CaseInsensitive); + if (compare_result != 0) + return compare_result; + break; + } + case SortType::RELEASE_TYPE: + { + auto compare_result = QString::compare(releaseType(), cast_other->releaseType(), Qt::CaseInsensitive); + if (compare_result != 0) + return compare_result; + break; + } + } + return 0; +} + +bool Mod::applyFilter(QRegularExpression filter) const +{ + if (filter.match(description()).hasMatch()) + return true; + + for (auto& author : authors()) + { + if (filter.match(author).hasMatch()) + { + return true; + } + } + + return Resource::applyFilter(filter); +} + +auto Mod::details() const -> const ModDetails& +{ + return m_local_details; +} + +auto Mod::name() const -> QString +{ + auto d_name = details().name; + if (!d_name.isEmpty()) + return d_name; + + return Resource::name(); +} + +auto Mod::mod_id() const -> QString +{ + auto d_mod_id = details().mod_id; + if (!d_mod_id.isEmpty()) + return d_mod_id; + + return Resource::name(); +} + +auto Mod::version() const -> QString +{ + return details().version; +} + +auto Mod::homepage() const -> QString +{ + QString metaUrl = Resource::homepage(); + + if (metaUrl.isEmpty()) + return details().homeurl; + else + return metaUrl; +} + +auto Mod::loaders() const -> QString +{ + if (metadata()) + { + QStringList loaders; + auto modLoaders = metadata()->loaders; + for (auto loader : ModPlatform::modLoaderTypesToList(modLoaders)) + { + loaders << getModLoaderAsString(loader); + } + return loaders.join(", "); + } + + return {}; +} + +auto Mod::side() const -> QString +{ + if (metadata()) + return ModPlatform::SideUtils::toString(metadata()->side); + + return ModPlatform::SideUtils::toString(ModPlatform::Side::UniversalSide); +} + +auto Mod::mcVersions() const -> QString +{ + if (metadata()) + return metadata()->mcVersions.join(", "); + + return {}; +} + +auto Mod::releaseType() const -> QString +{ + if (metadata()) + return metadata()->releaseType.toString(); + + return ModPlatform::IndexedVersionType().toString(); +} + +auto Mod::description() const -> QString +{ + return details().description; +} + +auto Mod::authors() const -> QStringList +{ + return details().authors; +} + +void Mod::finishResolvingWithDetails(ModDetails&& details) +{ + m_is_resolving = false; + m_is_resolved = true; + + m_local_details = std::move(details); + if (!iconPath().isEmpty()) + { + m_packImageCacheKey.wasReadAttempt = false; + } +} + +auto Mod::licenses() const -> const QList<ModLicense>& +{ + return details().licenses; +} + +auto Mod::issueTracker() const -> QString +{ + return details().issue_tracker; +} + +QPixmap Mod::setIcon(QImage new_image) const +{ + QMutexLocker locker(&m_data_lock); + + Q_ASSERT(!new_image.isNull()); + + if (m_packImageCacheKey.key.isValid()) + PixmapCache::remove(m_packImageCacheKey.key); + + // scale the image to avoid flooding the pixmapcache + auto pixmap = QPixmap::fromImage( + new_image.scaled({ 64, 64 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding, Qt::SmoothTransformation)); + + m_packImageCacheKey.key = PixmapCache::insert(pixmap); + m_packImageCacheKey.wasEverUsed = true; + m_packImageCacheKey.wasReadAttempt = true; + return pixmap; +} + +QPixmap Mod::icon(QSize size, Qt::AspectRatioMode mode) const +{ + auto pixmap_transform = [&size, &mode](QPixmap pixmap) + { + if (size.isNull()) + return pixmap; + return pixmap.scaled(size, mode, Qt::SmoothTransformation); + }; + + QPixmap cached_image; + if (PixmapCache::find(m_packImageCacheKey.key, &cached_image)) + { + return pixmap_transform(cached_image); + } + + // No valid image we can get + if ((!m_packImageCacheKey.wasEverUsed && m_packImageCacheKey.wasReadAttempt) || iconPath().isEmpty()) + return {}; + + if (m_packImageCacheKey.wasEverUsed) + { + qDebug() << "Mod" << name() << "Had it's icon evicted from the cache. reloading..."; + PixmapCache::markCacheMissByEviciton(); + } + // Image got evicted from the cache or an attempt to load it has not been made. load it and retry. + m_packImageCacheKey.wasReadAttempt = true; + if (ModUtils::loadIconFile(*this, &cached_image)) + { + return pixmap_transform(cached_image); + } + // Image failed to load + return {}; +} + +bool Mod::valid() const +{ + return !m_local_details.mod_id.isEmpty(); +} diff --git a/archived/projt-launcher/launcher/minecraft/mod/Mod.hpp b/archived/projt-launcher/launcher/minecraft/mod/Mod.hpp new file mode 100644 index 0000000000..f2756cac6a --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/Mod.hpp @@ -0,0 +1,137 @@ +// 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> + * Copyright (c) 2023 Trial97 <alexandru.tripon97@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. + * + * ======================================================================== */ + +#pragma once + +#include <QDateTime> +#include <QFileInfo> +#include <QImage> +#include <QList> +#include <QMutex> +#include <QPixmap> +#include <QPixmapCache> + +#include <optional> + +#include "ModDetails.hpp" +#include "Resource.hpp" + +class Mod : public Resource +{ + Q_OBJECT + public: + using Ptr = shared_qobject_ptr<Mod>; + using WeakPtr = QPointer<Mod>; + + Mod() = default; + Mod(const QFileInfo& file); + Mod(QString file_path) : Mod(QFileInfo(file_path)) + {} + + auto details() const -> const ModDetails&; + auto name() const -> QString override; + auto mod_id() const -> QString; + auto version() const -> QString; + auto homepage() const -> QString override; + auto description() const -> QString; + auto authors() const -> QStringList; + auto licenses() const -> const QList<ModLicense>&; + auto issueTracker() const -> QString; + auto side() const -> QString; + auto loaders() const -> QString; + auto mcVersions() const -> QString; + auto releaseType() const -> QString; + + /** Get the intneral path to the mod's icon file*/ + QString iconPath() const + { + return m_local_details.icon_file; + } + /** Gets the icon of the mod, converted to a QPixmap for drawing, and scaled to size. */ + QPixmap icon(QSize size, Qt::AspectRatioMode mode = Qt::AspectRatioMode::IgnoreAspectRatio) const; + /** Thread-safe. */ + QPixmap setIcon(QImage new_image) const; + + void setDetails(const ModDetails& details); + + bool valid() const override; + + [[nodiscard]] int compare(const Resource& other, SortType type) const override; + [[nodiscard]] bool applyFilter(QRegularExpression filter) const override; + + // Delete all the files of this mod + auto destroy(QDir& index_dir, bool preserve_metadata = false, bool attempt_trash = true) -> bool; + // Delete the metadata only + void destroyMetadata(QDir& index_dir); + + void finishResolvingWithDetails(ModDetails&& details); + + protected: + ModDetails m_local_details; + + mutable QMutex m_data_lock; + + struct + { + QPixmapCache::Key key; + bool wasEverUsed = false; + bool wasReadAttempt = false; + } mutable m_packImageCacheKey; +}; diff --git a/archived/projt-launcher/launcher/minecraft/mod/ModDetails.hpp b/archived/projt-launcher/launcher/minecraft/mod/ModDetails.hpp new file mode 100644 index 0000000000..a9900d6ee2 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/ModDetails.hpp @@ -0,0 +1,297 @@ +// 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. + * + * ======================================================================== */ + +#pragma once + +#include <memory> + +#include <QString> +#include <QStringList> +#include <QUrl> + +#include "minecraft/mod/MetadataHandler.hpp" + +struct ModLicense +{ + QString name = {}; + QString id = {}; + QString url = {}; + QString description = {}; + + ModLicense() + {} + + /** Parse a license string which may contain: + * - An SPDX identifier (e.g., "MIT", "GPL-3.0-only", "Apache-2.0") + * - A URL in parentheses or standalone + * - A plain text license name + * + * Supports SPDX license expressions: https://spdx.org/licenses/ + */ + ModLicense(const QString license) + { + if (license.isEmpty()) + { + return; + } + + QString remaining = license.trimmed(); + + // Common SPDX identifiers for quick matching + static const QStringList spdxIdentifiers = { "MIT", + "GPL-2.0", + "GPL-2.0-only", + "GPL-2.0-or-later", + "GPL-3.0", + "GPL-3.0-only", + "GPL-3.0-or-later", + "LGPL-2.0", + "LGPL-2.1", + "LGPL-3.0", + "Apache-2.0", + "BSD-2-Clause", + "BSD-3-Clause", + "MPL-2.0", + "ISC", + "Unlicense", + "WTFPL", + "Zlib", + "CC0-1.0", + "CC-BY-4.0", + "CC-BY-SA-4.0", + "AGPL-3.0", + "AGPL-3.0-only", + "AGPL-3.0-or-later", + "EPL-2.0", + "OSL-3.0", + "Artistic-2.0", + "BSL-1.0", + "All Rights Reserved", + "ARR", + "Custom", + "Proprietary" }; + + // SPDX to URL mapping for common licenses + static const QHash<QString, QString> spdxUrls = { + { "MIT", "https://opensource.org/licenses/MIT" }, + { "GPL-2.0", "https://www.gnu.org/licenses/old-licenses/gpl-2.0.html" }, + { "GPL-2.0-only", "https://www.gnu.org/licenses/old-licenses/gpl-2.0.html" }, + { "GPL-2.0-or-later", "https://www.gnu.org/licenses/old-licenses/gpl-2.0.html" }, + { "GPL-3.0", "https://www.gnu.org/licenses/gpl-3.0.html" }, + { "GPL-3.0-only", "https://www.gnu.org/licenses/gpl-3.0.html" }, + { "GPL-3.0-or-later", "https://www.gnu.org/licenses/gpl-3.0.html" }, + { "LGPL-2.1", "https://www.gnu.org/licenses/lgpl-2.1.html" }, + { "LGPL-3.0", "https://www.gnu.org/licenses/lgpl-3.0.html" }, + { "Apache-2.0", "https://www.apache.org/licenses/LICENSE-2.0" }, + { "BSD-2-Clause", "https://opensource.org/licenses/BSD-2-Clause" }, + { "BSD-3-Clause", "https://opensource.org/licenses/BSD-3-Clause" }, + { "MPL-2.0", "https://www.mozilla.org/en-US/MPL/2.0/" }, + { "ISC", "https://opensource.org/licenses/ISC" }, + { "Unlicense", "https://unlicense.org/" }, + { "CC0-1.0", "https://creativecommons.org/publicdomain/zero/1.0/" }, + { "CC-BY-4.0", "https://creativecommons.org/licenses/by/4.0/" }, + { "CC-BY-SA-4.0", "https://creativecommons.org/licenses/by-sa/4.0/" }, + { "AGPL-3.0", "https://www.gnu.org/licenses/agpl-3.0.html" }, + { "AGPL-3.0-only", "https://www.gnu.org/licenses/agpl-3.0.html" }, + }; + + // Extract URL from parentheses or standalone + auto parts = remaining.split(' '); + QStringList urlParts; + + for (const auto& part : parts) + { + QString urlCandidate = part; + if (part.startsWith("(") && part.endsWith(")")) + { + urlCandidate = part.mid(1, part.size() - 2); + } + + QUrl parsedUrl(urlCandidate); + if (parsedUrl.isValid() && !parsedUrl.scheme().isEmpty() && !parsedUrl.host().isEmpty()) + { + this->url = parsedUrl.toString(); + urlParts.append(part); + } + } + + // Remove URL parts from remaining + for (const auto& urlPart : urlParts) + { + parts.removeOne(urlPart); + } + + QString licensePart = parts.join(' ').trimmed(); + + // Check if it's a known SPDX identifier + for (const QString& spdx : spdxIdentifiers) + { + if (licensePart.compare(spdx, Qt::CaseInsensitive) == 0) + { + this->id = spdx; + this->name = spdx; + this->description = spdx; + + // Set URL from SPDX mapping if not already set + if (this->url.isEmpty() && spdxUrls.contains(spdx)) + { + this->url = spdxUrls[spdx]; + } + return; + } + } + + // Not a known SPDX - treat as custom license + this->name = licensePart; + this->description = licensePart; + + if (parts.size() == 1) + { + this->id = parts.first(); + } + } + + ModLicense(const QString& name_, const QString& id_, const QString& url_, const QString& description_) + : name(name_), + id(id_), + url(url_), + description(description_) + {} + + ModLicense(const ModLicense& other) : name(other.name), id(other.id), url(other.url), description(other.description) + {} + + ModLicense& operator=(const ModLicense& other) + { + this->name = other.name; + this->id = other.id; + this->url = other.url; + this->description = other.description; + + return *this; + } + + ModLicense& operator=(const ModLicense&& other) + { + this->name = other.name; + this->id = other.id; + this->url = other.url; + this->description = other.description; + + return *this; + } + + bool isEmpty() + { + return this->name.isEmpty() && this->id.isEmpty() && this->url.isEmpty() && this->description.isEmpty(); + } +}; + +struct ModDetails +{ + /* Mod ID as defined in the ModLoader-specific metadata */ + QString mod_id = {}; + + /* Human-readable name */ + QString name = {}; + + /* Human-readable mod version */ + QString version = {}; + + /* Human-readable minecraft version */ + QString mcversion = {}; + + /* URL for mod's home page */ + QString homeurl = {}; + + /* Human-readable description */ + QString description = {}; + + /* List of the author's names */ + QStringList authors = {}; + + /* Issue Tracker URL */ + QString issue_tracker = {}; + + /* License */ + QList<ModLicense> licenses = {}; + + /* Path of mod logo */ + QString icon_file = {}; + + ModDetails() = default; + + /** Metadata should be handled manually to properly set the mod status. */ + ModDetails(const ModDetails& other) + : mod_id(other.mod_id), + name(other.name), + version(other.version), + mcversion(other.mcversion), + homeurl(other.homeurl), + description(other.description), + authors(other.authors), + issue_tracker(other.issue_tracker), + licenses(other.licenses), + icon_file(other.icon_file) + {} + + ModDetails& operator=(const ModDetails& other) = default; + + ModDetails& operator=(ModDetails&& other) = default; +}; diff --git a/archived/projt-launcher/launcher/minecraft/mod/ModFolderModel.cpp b/archived/projt-launcher/launcher/minecraft/mod/ModFolderModel.cpp new file mode 100644 index 0000000000..e248059c40 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/ModFolderModel.cpp @@ -0,0 +1,255 @@ +// 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> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * Copyright (c) 2023 Trial97 <alexandru.tripon97@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 "ModFolderModel.hpp" + +#include <FileSystem.h> +#include <QDebug> +#include <QFileSystemWatcher> +#include <QHeaderView> +#include <QIcon> +#include <QMimeData> +#include <QString> +#include <QStyle> +#include <QThreadPool> +#include <QUrl> +#include <QUuid> + +#include "minecraft/mod/tasks/LocalModParseTask.hpp" + +ModFolderModel::ModFolderModel(const QDir& dir, + BaseInstance* instance, + bool is_indexed, + bool create_dir, + QObject* parent) + : ResourceFolderModel(QDir(dir), instance, is_indexed, create_dir, parent) +{ + m_column_names = QStringList({ "Enable", + "Image", + "Name", + "Version", + "Last Modified", + "Provider", + "Size", + "Side", + "Loaders", + "Minecraft Versions", + "Release Type" }); + m_column_names_translated = QStringList({ tr("Enable"), + tr("Image"), + tr("Name"), + tr("Version"), + tr("Last Modified"), + tr("Provider"), + tr("Size"), + tr("Side"), + tr("Loaders"), + tr("Minecraft Versions"), + tr("Release Type") }); + m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::VERSION, + SortType::DATE, SortType::PROVIDER, SortType::SIZE, SortType::SIDE, + SortType::LOADERS, SortType::MC_VERSIONS, SortType::RELEASE_TYPE }; + m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, + QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, + QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, + QHeaderView::Interactive, QHeaderView::Interactive }; + m_columnsHideable = { false, true, false, true, true, true, true, true, true, true, true }; +} + +QVariant ModFolderModel::data(const QModelIndex& index, int role) const +{ + if (!validateIndex(index)) + return {}; + + int row = index.row(); + int column = index.column(); + + switch (role) + { + case Qt::BackgroundRole: return rowBackground(row); + case Qt::DisplayRole: + switch (column) + { + case VersionColumn: + { + switch (at(row).type()) + { + case ResourceType::FOLDER: return tr("Folder"); + case ResourceType::SINGLEFILE: return tr("File"); + default: return at(row).version(); + } + } + case SideColumn: return at(row).side(); + case LoadersColumn: return at(row).loaders(); + case McVersionsColumn: return at(row).mcVersions(); + case ReleaseTypeColumn: return at(row).releaseType(); + default: break; + } + break; + case Qt::DecorationRole: + if (column == ImageColumn) + { + return at(row).icon({ 32, 32 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding); + } + break; + case Qt::SizeHintRole: + if (column == ImageColumn) + { + return QSize(32, 32); + } + break; + default: break; + } + + QModelIndex mappedIndex; + switch (column) + { + case ActiveColumn: mappedIndex = index.siblingAtColumn(ResourceFolderModel::ActiveColumn); break; + case NameColumn: mappedIndex = index.siblingAtColumn(ResourceFolderModel::NameColumn); break; + case DateColumn: mappedIndex = index.siblingAtColumn(ResourceFolderModel::DateColumn); break; + case ProviderColumn: mappedIndex = index.siblingAtColumn(ResourceFolderModel::ProviderColumn); break; + case SizeColumn: mappedIndex = index.siblingAtColumn(ResourceFolderModel::SizeColumn); break; + default: break; + } + + if (mappedIndex.isValid()) + return ResourceFolderModel::data(mappedIndex, role); + + return {}; +} + +QVariant ModFolderModel::headerData(int section, [[maybe_unused]] Qt::Orientation orientation, int role) const +{ + switch (role) + { + case Qt::DisplayRole: + switch (section) + { + case ActiveColumn: + case NameColumn: + case VersionColumn: + case DateColumn: + case ProviderColumn: + case ImageColumn: + case SideColumn: + case LoadersColumn: + case McVersionsColumn: + case ReleaseTypeColumn: + case SizeColumn: return columnNames().at(section); + default: return QVariant(); + } + + case Qt::ToolTipRole: + switch (section) + { + case ActiveColumn: return tr("Is the mod enabled?"); + case NameColumn: return tr("The name of the mod."); + case VersionColumn: return tr("The version of the mod."); + case DateColumn: return tr("The date and time this mod was last changed (or added)."); + case ProviderColumn: return tr("The source provider of the mod."); + case SideColumn: return tr("On what environment the mod is running."); + case LoadersColumn: return tr("The mod loader."); + case McVersionsColumn: return tr("The supported minecraft versions."); + case ReleaseTypeColumn: return tr("The release type."); + case SizeColumn: return tr("The size of the mod."); + default: return QVariant(); + } + default: return QVariant(); + } + return QVariant(); +} + +int ModFolderModel::columnCount(const QModelIndex& parent) const +{ + return parent.isValid() ? 0 : NUM_COLUMNS; +} + +Task* ModFolderModel::createParseTask(Resource& resource) +{ + return new LocalModParseTask(m_next_resolution_ticket, resource.type(), resource.fileinfo()); +} + +bool ModFolderModel::isValid() +{ + return m_dir.exists() && m_dir.isReadable(); +} + +void ModFolderModel::onParseSucceeded(int ticket, QString mod_id) +{ + auto iter = m_active_parse_tasks.constFind(ticket); + if (iter == m_active_parse_tasks.constEnd()) + return; + + int row = m_resources_index[mod_id]; + + auto parse_task = *iter; + auto cast_task = static_cast<LocalModParseTask*>(parse_task.get()); + + Q_ASSERT(cast_task->token() == ticket); + + auto resource = find(mod_id); + + auto result = cast_task->result(); + if (result && resource) + static_cast<Mod*>(resource.get())->finishResolvingWithDetails(std::move(result->details)); + + emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1)); +} diff --git a/archived/projt-launcher/launcher/minecraft/mod/ModFolderModel.hpp b/archived/projt-launcher/launcher/minecraft/mod/ModFolderModel.hpp new file mode 100644 index 0000000000..88f127704f --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/ModFolderModel.hpp @@ -0,0 +1,128 @@ +// 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> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * Copyright (c) 2023 Trial97 <alexandru.tripon97@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. + * + * ======================================================================== */ + +#pragma once + +#include <QAbstractListModel> +#include <QDir> +#include <QList> +#include <QMap> +#include <QSet> +#include <QString> + +#include "Mod.hpp" +#include "ResourceFolderModel.hpp" + +class BaseInstance; +class QFileSystemWatcher; + +/** + * A legacy mod list. + * Backed by a folder. + */ +class ModFolderModel : public ResourceFolderModel +{ + Q_OBJECT + public: + enum Columns + { + ActiveColumn = 0, + ImageColumn, + NameColumn, + VersionColumn, + DateColumn, + ProviderColumn, + SizeColumn, + SideColumn, + LoadersColumn, + McVersionsColumn, + ReleaseTypeColumn, + NUM_COLUMNS + }; + ModFolderModel(const QDir& dir, + BaseInstance* instance, + bool is_indexed, + bool create_dir, + QObject* parent = nullptr); + + virtual QString id() const override + { + return "mods"; + } + + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + int columnCount(const QModelIndex& parent) const override; + + [[nodiscard]] Resource* createResource(const QFileInfo& file) override + { + return new Mod(file); + } + [[nodiscard]] Task* createParseTask(Resource&) override; + + bool isValid(); + + RESOURCE_HELPERS(Mod) + + private slots: + void onParseSucceeded(int ticket, QString resource_id) override; +}; diff --git a/archived/projt-launcher/launcher/minecraft/mod/Resource.cpp b/archived/projt-launcher/launcher/minecraft/mod/Resource.cpp new file mode 100644 index 0000000000..3f70a24f41 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/Resource.cpp @@ -0,0 +1,343 @@ +// 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. + */ +#include "Resource.hpp" + +#include <QDirIterator> +#include <QFileInfo> +#include <QRegularExpression> +#include <tuple> + +#include "FileSystem.h" +#include "StringUtils.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" + +Resource::Resource(QObject* parent) : QObject(parent) +{} + +Resource::Resource(QFileInfo file_info) : QObject() +{ + setFile(file_info); +} + +void Resource::setFile(QFileInfo file_info) +{ + m_file_info = file_info; + parseFile(); +} + +static std::tuple<QString, qint64> calculateFileSize(const QFileInfo& file) +{ + if (file.isDir()) + { + auto dir = QDir(file.absoluteFilePath()); + dir.setFilter(QDir::AllEntries | QDir::NoDotAndDotDot); + auto count = dir.count(); + auto str = QObject::tr("item"); + if (count != 1) + str = QObject::tr("items"); + return { QString("%1 %2").arg(QString::number(count), str), count }; + } + return { StringUtils::humanReadableFileSize(file.size(), true), file.size() }; +} + +void Resource::parseFile() +{ + QString file_name{ m_file_info.fileName() }; + + m_type = ResourceType::UNKNOWN; + + m_internal_id = file_name; + + std::tie(m_size_str, m_size_info) = calculateFileSize(m_file_info); + if (m_file_info.isDir()) + { + m_type = ResourceType::FOLDER; + m_name = file_name; + } + else if (m_file_info.isFile()) + { + if (file_name.endsWith(".disabled")) + { + file_name.chop(9); + m_enabled = false; + } + + if (file_name.endsWith(".zip") || file_name.endsWith(".jar")) + { + m_type = ResourceType::ZIPFILE; + file_name.chop(4); + } + else if (file_name.endsWith(".nilmod")) + { + m_type = ResourceType::ZIPFILE; + file_name.chop(7); + } + else if (file_name.endsWith(".litemod")) + { + m_type = ResourceType::LITEMOD; + file_name.chop(8); + } + else + { + m_type = ResourceType::SINGLEFILE; + } + + m_name = file_name; + } + + m_changed_date_time = m_file_info.lastModified(); +} + +auto Resource::name() const -> QString +{ + if (metadata()) + return metadata()->name; + + return m_name; +} + +static void removeThePrefix(QString& string) +{ + static const QRegularExpression s_regex(QStringLiteral("^(?:the|teh) +"), + QRegularExpression::CaseInsensitiveOption); + string.remove(s_regex); + string = string.trimmed(); +} + +auto Resource::provider() const -> QString +{ + if (metadata()) + return ModPlatform::ProviderCapabilities::readableName(metadata()->provider); + + return tr("Unknown"); +} + +auto Resource::homepage() const -> QString +{ + if (metadata()) + return ModPlatform::getMetaURL(metadata()->provider, metadata()->project_id); + + return {}; +} + +void Resource::setMetadata(std::shared_ptr<Metadata::ModStruct>&& metadata) +{ + if (status() == ResourceStatus::NO_METADATA) + setStatus(ResourceStatus::INSTALLED); + + m_metadata = metadata; +} + +QStringList Resource::issues() const +{ + QStringList result; + result.reserve(m_issues.length()); + + for (const char* issue : m_issues) { + result.append(tr(issue)); + } + + return result; +} + +void Resource::updateIssues(const BaseInstance* inst) +{ + m_issues.clear(); + + if (m_metadata == nullptr) { + return; + } + + auto mcInst = dynamic_cast<const MinecraftInstance*>(inst); + if (mcInst == nullptr) { + return; + } + + auto profile = mcInst->getPackProfile(); + QString mcVersion = profile->getComponentVersion("net.minecraft"); + + if (!m_metadata->mcVersions.empty() && !m_metadata->mcVersions.contains(mcVersion)) { + // delay translation until issues() is called + m_issues.append(QT_TR_NOOP("Not marked as compatible with the instance's game version.")); + } +} + +int Resource::compare(const Resource& other, SortType type) const +{ + switch (type) + { + default: + case SortType::ENABLED: + if (enabled() && !other.enabled()) + return 1; + if (!enabled() && other.enabled()) + return -1; + break; + case SortType::NAME: + { + QString this_name{ name() }; + QString other_name{ other.name() }; + + // Remove common prefixes like "The" for better alphabetical sorting + removeThePrefix(this_name); + removeThePrefix(other_name); + + return QString::compare(this_name, other_name, Qt::CaseInsensitive); + } + case SortType::DATE: + if (dateTimeChanged() > other.dateTimeChanged()) + return 1; + if (dateTimeChanged() < other.dateTimeChanged()) + return -1; + break; + case SortType::SIZE: + { + if (this->type() != other.type()) + { + if (this->type() == ResourceType::FOLDER) + return -1; + if (other.type() == ResourceType::FOLDER) + return 1; + } + + if (sizeInfo() > other.sizeInfo()) + return 1; + if (sizeInfo() < other.sizeInfo()) + return -1; + break; + } + case SortType::PROVIDER: + { + auto compare_result = QString::compare(provider(), other.provider(), Qt::CaseInsensitive); + if (compare_result != 0) + return compare_result; + break; + } + } + + return 0; +} + +bool Resource::applyFilter(QRegularExpression filter) const +{ + return filter.match(name()).hasMatch(); +} + +bool Resource::enable(EnableAction action) +{ + if (m_type == ResourceType::UNKNOWN || m_type == ResourceType::FOLDER) + return false; + + QString path = m_file_info.absoluteFilePath(); + QFile file(path); + + bool enable = true; + switch (action) + { + case EnableAction::ENABLE: enable = true; break; + case EnableAction::DISABLE: enable = false; break; + case EnableAction::TOGGLE: + default: enable = !enabled(); break; + } + + if (m_enabled == enable) + return false; + + if (enable) + { + // m_enabled is false, but there's no '.disabled' suffix. + if (!path.endsWith(".disabled")) + { + qWarning() << "Cannot enable resource" << name() << ": file does not have .disabled suffix"; + return false; + } + path.chop(9); + } + else + { + path += ".disabled"; + if (QFile::exists(path)) + { + path = FS::getUniqueResourceName(path); + } + } + if (!file.rename(path)) + return false; + + setFile(QFileInfo(path)); + + m_enabled = enable; + return true; +} + +auto Resource::destroy(const QDir& index_dir, bool preserve_metadata, bool attempt_trash) -> bool +{ + m_type = ResourceType::UNKNOWN; + + if (!preserve_metadata) + { + qDebug() << QString("Destroying metadata for '%1' on purpose").arg(name()); + destroyMetadata(index_dir); + } + + return (attempt_trash && FS::trash(m_file_info.filePath())) || FS::deletePath(m_file_info.filePath()); +} + +auto Resource::destroyMetadata(const QDir& index_dir) -> void +{ + if (metadata()) + { + Metadata::remove(index_dir, metadata()->slug); + } + else + { + auto n = name(); + Metadata::remove(index_dir, n); + } + m_metadata = nullptr; +} + +bool Resource::isSymLinkUnder(const QString& instPath) const +{ + if (isSymLink()) + return true; + + auto instDir = QDir(instPath); + + auto relAbsPath = instDir.relativeFilePath(m_file_info.absoluteFilePath()); + auto relCanonPath = instDir.relativeFilePath(m_file_info.canonicalFilePath()); + + return relAbsPath != relCanonPath; +} + +bool Resource::isMoreThanOneHardLink() const +{ + return FS::hardLinkCount(m_file_info.absoluteFilePath()) > 1; +} + +auto Resource::getOriginalFileName() const -> QString +{ + auto fileName = m_file_info.fileName(); + if (!m_enabled) + fileName.chop(9); + return fileName; +}
\ No newline at end of file diff --git a/archived/projt-launcher/launcher/minecraft/mod/Resource.hpp b/archived/projt-launcher/launcher/minecraft/mod/Resource.hpp new file mode 100644 index 0000000000..a95ce79dd6 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/Resource.hpp @@ -0,0 +1,304 @@ +// 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) 2023 Trial97 <alexandru.tripon97@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. + * + * ======================================================================== */ + +#pragma once + +#include <QDateTime> +#include <QFileInfo> +#include <QObject> +#include <QPointer> + +#include "MetadataHandler.hpp" +#include "QObjectPtr.h" + +class BaseInstance; + +enum class ResourceType +{ + UNKNOWN, //!< Indicates an unspecified resource type. + ZIPFILE, //!< The resource is a zip file containing the resource's class files. + SINGLEFILE, //!< The resource is a single file (not a zip file). + FOLDER, //!< The resource is in a folder on the filesystem. + LITEMOD, //!< The resource is a litemod +}; + +enum class ResourceStatus +{ + INSTALLED, // Both JAR and Metadata are present + NOT_INSTALLED, // Only the Metadata is present + NO_METADATA, // Only the JAR is present + UNKNOWN, // Default status +}; + +enum class SortType +{ + NAME, + DATE, + VERSION, + ENABLED, + PACK_FORMAT, + PROVIDER, + SIZE, + SIDE, + MC_VERSIONS, + LOADERS, + RELEASE_TYPE +}; + +enum class EnableAction +{ + ENABLE, + DISABLE, + TOGGLE +}; + +/** General class for managed resources. It mirrors a file in disk, with some more info + * for display and house-keeping purposes. + * + * Subclass it to add additional data / behavior, such as Mods or Resource packs. + */ +class Resource : public QObject +{ + Q_OBJECT + Q_DISABLE_COPY(Resource) + public: + using Ptr = shared_qobject_ptr<Resource>; + using WeakPtr = QPointer<Resource>; + + Resource(QObject* parent = nullptr); + Resource(QFileInfo file_info); + Resource(QString file_path) : Resource(QFileInfo(file_path)) + {} + + ~Resource() override = default; + + void setFile(QFileInfo file_info); + void parseFile(); + + auto fileinfo() const -> QFileInfo + { + return m_file_info; + } + auto dateTimeChanged() const -> QDateTime + { + return m_changed_date_time; + } + auto internal_id() const -> QString + { + return m_internal_id; + } + auto type() const -> ResourceType + { + return m_type; + } + bool enabled() const + { + return m_enabled; + } + auto getOriginalFileName() const -> QString; + QString sizeStr() const + { + return m_size_str; + } + qint64 sizeInfo() const + { + return m_size_info; + } + + virtual auto name() const -> QString; + virtual bool valid() const + { + return m_type != ResourceType::UNKNOWN; + } + + auto status() const -> ResourceStatus + { + return m_status; + }; + auto metadata() -> std::shared_ptr<Metadata::ModStruct> + { + return m_metadata; + } + auto metadata() const -> std::shared_ptr<const Metadata::ModStruct> + { + return m_metadata; + } + auto provider() const -> QString; + virtual auto homepage() const -> QString; + + void setStatus(ResourceStatus status) + { + m_status = status; + } + void setMetadata(std::shared_ptr<Metadata::ModStruct>&& metadata); + void setMetadata(const Metadata::ModStruct& metadata) + { + setMetadata(std::make_shared<Metadata::ModStruct>(metadata)); + } + + QStringList issues() const; + void updateIssues(const BaseInstance* inst); + bool hasIssues() const + { + return !m_issues.empty(); + } + + /** Compares two Resources, for sorting purposes, considering a ascending order, returning: + * > 0: 'this' comes after 'other' + * = 0: 'this' is equal to 'other' + * < 0: 'this' comes before 'other' + */ + virtual int compare(Resource const& other, SortType type = SortType::NAME) const; + + /** Returns whether the given filter should filter out 'this' (false), + * or if such filter includes the Resource (true). + */ + virtual bool applyFilter(QRegularExpression filter) const; + + /** Changes the enabled property, according to 'action'. + * + * Returns whether a change was applied to the Resource's properties. + */ + bool enable(EnableAction action); + + auto shouldResolve() const -> bool + { + return !m_is_resolving && !m_is_resolved; + } + auto isResolving() const -> bool + { + return m_is_resolving; + } + auto isResolved() const -> bool + { + return m_is_resolved; + } + auto resolutionTicket() const -> int + { + return m_resolution_ticket; + } + + void setResolving(bool resolving, int resolutionTicket) + { + m_is_resolving = resolving; + m_resolution_ticket = resolutionTicket; + } + + // Delete all files of this resource. + auto destroy(const QDir& index_dir, bool preserve_metadata = false, bool attempt_trash = true) -> bool; + // Delete the metadata only. + auto destroyMetadata(const QDir& index_dir) -> void; + + auto isSymLink() const -> bool + { + return m_file_info.isSymLink(); + } + + /** + * @brief Take a instance path, checks if the file pointed to by the resource is a symlink or under a symlink in + * that instance + * + * @param instPath path to an instance directory + * @return true + * @return false + */ + bool isSymLinkUnder(const QString& instPath) const; + + bool isMoreThanOneHardLink() const; + + auto mod_id() const -> QString + { + return m_mod_id; + } + void setModId(const QString& modId) + { + m_mod_id = modId; + } + + protected: + /* The file corresponding to this resource. */ + QFileInfo m_file_info; + /* The cached date when this file was last changed. */ + QDateTime m_changed_date_time; + + /* Internal ID for internal purposes. Properties such as human-readability should not be assumed. */ + QString m_internal_id; + /* Name as reported via the file name. In the absence of a better name, this is shown to the user. */ + QString m_name; + QString m_mod_id; + + /* The type of file we're dealing with. */ + ResourceType m_type = ResourceType::UNKNOWN; + + /* Installation status of the resource. */ + ResourceStatus m_status = ResourceStatus::UNKNOWN; + + std::shared_ptr<Metadata::ModStruct> m_metadata = nullptr; + + /* Whether the resource is enabled (e.g. shows up in the game) or not. */ + bool m_enabled = true; + QList<const char*> m_issues; + + /* Used to keep trach of pending / concluded actions on the resource. */ + bool m_is_resolving = false; + bool m_is_resolved = false; + int m_resolution_ticket = 0; + QString m_size_str; + qint64 m_size_info; +}; diff --git a/archived/projt-launcher/launcher/minecraft/mod/ResourceFolderModel.cpp b/archived/projt-launcher/launcher/minecraft/mod/ResourceFolderModel.cpp new file mode 100644 index 0000000000..93cffb6da2 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/ResourceFolderModel.cpp @@ -0,0 +1,1094 @@ +// 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. + */ +#include "ResourceFolderModel.hpp" +#include <QMessageBox> + +#include <QCoreApplication> +#include <QDebug> +#include <QFileInfo> +#include <QHeaderView> +#include <QIcon> +#include <QMenu> +#include <QMimeData> +#include <QStyle> +#include <QThreadPool> +#include <QUrl> +#include <utility> + +#include "Application.h" +#include "FileSystem.h" + +#include "minecraft/mod/tasks/ResourceFolderLoadTask.hpp" + +#include "Json.h" +#include "minecraft/mod/tasks/LocalResourceUpdateTask.hpp" +#include "modplatform/flame/FlameAPI.h" +#include "modplatform/flame/FlameModIndex.h" +#include "settings/Setting.h" +#include "tasks/Task.h" +#include "ui/dialogs/CustomMessageBox.h" + +ResourceFolderModel::ResourceFolderModel(const QDir& dir, + BaseInstance* instance, + bool is_indexed, + bool create_dir, + QObject* parent) + : QAbstractListModel(parent), + m_dir(dir), + m_instance(instance), + m_watcher(this), + m_is_indexed(is_indexed) +{ + if (create_dir) + { + FS::ensureFolderPathExists(m_dir.absolutePath()); + } + + m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs); + m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); + + connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &ResourceFolderModel::directoryChanged); + connect(&m_helper_thread_task, &ConcurrentTask::finished, this, [this] { m_helper_thread_task.clear(); }); + if (APPLICATION_DYN) + { // in tests the application macro doesn't work + m_helper_thread_task.setMaxConcurrent(APPLICATION->settings()->get("NumberOfConcurrentTasks").toInt()); + } +} + +ResourceFolderModel::~ResourceFolderModel() +{ + while (!QThreadPool::globalInstance()->waitForDone(100)) + QCoreApplication::processEvents(); +} + +bool ResourceFolderModel::startWatching(const QStringList& paths) +{ + // Remove orphaned metadata next time + m_first_folder_load = true; + + if (m_is_watching) + return false; + + auto couldnt_be_watched = m_watcher.addPaths(paths); + for (auto path : paths) + { + if (couldnt_be_watched.contains(path)) + qDebug() << "Failed to start watching " << path; + else + qDebug() << "Started watching " << path; + } + + update(); + + m_is_watching = !m_is_watching; + return m_is_watching; +} + +bool ResourceFolderModel::stopWatching(const QStringList& paths) +{ + if (!m_is_watching) + return false; + + auto couldnt_be_stopped = m_watcher.removePaths(paths); + for (auto path : paths) + { + if (couldnt_be_stopped.contains(path)) + qDebug() << "Failed to stop watching " << path; + else + qDebug() << "Stopped watching " << path; + } + + m_is_watching = !m_is_watching; + return !m_is_watching; +} + +bool ResourceFolderModel::installResource(QString original_path) +{ + // NOTE: fix for GH-1178: remove trailing slash to avoid issues with using the empty result of QFileInfo::fileName + original_path = FS::NormalizePath(original_path); + QFileInfo file_info(original_path); + + if (!file_info.exists() || !file_info.isReadable()) + { + qWarning() << "Caught attempt to install non-existing file or file-like object:" << original_path; + return false; + } + qDebug() << "Installing: " << file_info.absoluteFilePath(); + + Resource resource(file_info); + if (!resource.valid()) + { + qWarning() << original_path << "is not a valid resource. Ignoring it."; + return false; + } + + auto new_path = FS::NormalizePath(m_dir.filePath(file_info.fileName())); + if (original_path == new_path) + { + qWarning() << "Overwriting the mod (" << original_path << ") with itself makes no sense..."; + return false; + } + + auto triggerUpdate = [this]() + { + // Always schedule an update so callers relying on updateFinished (like tests) don't hang when watching is on. + auto started = update(); + return m_is_watching ? true : started; + }; + + switch (resource.type()) + { + case ResourceType::SINGLEFILE: + case ResourceType::ZIPFILE: + case ResourceType::LITEMOD: + { + if (QFile::exists(new_path) || QFile::exists(new_path + QString(".disabled"))) + { + if (!FS::deletePath(new_path)) + { + qCritical() << "Cleaning up new location (" << new_path << ") was unsuccessful!"; + return false; + } + qDebug() << new_path << "has been deleted."; + } + + if (!QFile::copy(original_path, new_path)) + { + qCritical() << "Copy from" << original_path << "to" << new_path << "has failed."; + return false; + } + + FS::updateTimestamp(new_path); + + QFileInfo new_path_file_info(new_path); + resource.setFile(new_path_file_info); + + return triggerUpdate(); + } + case ResourceType::FOLDER: + { + if (QFile::exists(new_path)) + { + qDebug() << "Ignoring folder '" << original_path << "', it would merge with" << new_path; + return false; + } + + if (!FS::copy(original_path, new_path)()) + { + qWarning() << "Copy of folder from" << original_path << "to" << new_path + << "has (potentially partially) failed."; + return false; + } + + QFileInfo newpathInfo(new_path); + resource.setFile(newpathInfo); + + return triggerUpdate(); + } + default: break; + } + return false; +} + +void ResourceFolderModel::installResourceWithFlameMetadata(QString path, ModPlatform::IndexedVersion& vers) +{ + auto install = [this, path] { installResource(std::move(path)); }; + if (vers.addonId.isValid()) + { + ModPlatform::IndexedPack pack{ + vers.addonId, + ModPlatform::ResourceProvider::FLAME, + }; + + auto response = std::make_shared<QByteArray>(); + auto job = FlameAPI().getProject(vers.addonId.toString(), response); + connect(job.get(), &Task::failed, this, install); + connect(job.get(), &Task::aborted, this, install); + connect(job.get(), + &Task::succeeded, + [response, this, &vers, install, &pack] + { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) + { + qWarning() << "Error while parsing JSON response for mod info at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qDebug() << *response; + return; + } + try + { + auto obj = Json::requireObject(Json::requireObject(doc), "data"); + FlameMod::loadIndexedPack(pack, obj); + } + catch (const JSONValidationError& e) + { + qDebug() << doc; + qWarning() << "Error while reading mod info: " << e.cause(); + } + LocalResourceUpdateTask update_metadata(indexDir(), pack, vers); + connect(&update_metadata, &Task::finished, this, install); + update_metadata.start(); + }); + + job->start(); + } + else + { + install(); + } +} + +bool ResourceFolderModel::uninstallResource(QString file_name, bool preserve_metadata) +{ + for (auto& resource : m_resources) + { + if (resource->fileinfo().fileName() == file_name) + { + auto res = resource->destroy(indexDir(), preserve_metadata, false); + + update(); + + return res; + } + } + return false; +} + +bool ResourceFolderModel::deleteResources(const QModelIndexList& indexes) +{ + if (indexes.isEmpty()) + return true; + + for (auto i : indexes) + { + if (i.column() != 0) + continue; + + auto& resource = m_resources.at(i.row()); + resource->destroy(indexDir()); + } + + update(); + + return true; +} + +void ResourceFolderModel::deleteMetadata(const QModelIndexList& indexes) +{ + if (indexes.isEmpty()) + return; + + for (auto i : indexes) + { + if (i.column() != 0) + continue; + + auto& resource = m_resources.at(i.row()); + resource->destroyMetadata(indexDir()); + } + + update(); +} + +bool ResourceFolderModel::setResourceEnabled(const QModelIndexList& indexes, EnableAction action) +{ + if (m_instance != nullptr && m_instance->isRunning()) + { + auto response = CustomMessageBox::selectable( + nullptr, + tr("Confirm toggle"), + tr("If you enable/disable this resource while the game is running it may crash your game.\n" + "Are you sure you want to do this?"), + QMessageBox::Warning, + QMessageBox::Yes | QMessageBox::No, + QMessageBox::No) + ->exec(); + + if (response != QMessageBox::Yes) + return false; + } + + if (indexes.isEmpty()) + return true; + + bool succeeded = true; + for (auto const& idx : indexes) + { + if (!validateIndex(idx) || idx.column() != 0) + continue; + + int row = idx.row(); + + auto& resource = m_resources[row]; + + // Preserve the row, but change its ID + auto old_id = resource->internal_id(); + if (!resource->enable(action)) + { + succeeded = false; + continue; + } + + auto new_id = resource->internal_id(); + + m_resources_index.remove(old_id); + m_resources_index[new_id] = row; + + emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1)); + } + + return succeeded; +} + +static QMutex s_update_task_mutex; +bool ResourceFolderModel::update() +{ + // We hold a lock here to prevent race conditions on the m_current_update_task reset. + QMutexLocker lock(&s_update_task_mutex); + + // Already updating, so we schedule a future update and return. + if (m_current_update_task) + { + m_scheduled_update = true; + return false; + } + + m_current_update_task.reset(createUpdateTask()); + if (!m_current_update_task) + return false; + + connect(m_current_update_task.get(), + &Task::succeeded, + this, + &ResourceFolderModel::onUpdateSucceeded, + Qt::ConnectionType::QueuedConnection); + connect(m_current_update_task.get(), + &Task::failed, + this, + &ResourceFolderModel::onUpdateFailed, + Qt::ConnectionType::QueuedConnection); + connect( + m_current_update_task.get(), + &Task::finished, + this, + [this] + { + m_current_update_task.reset(); + if (m_scheduled_update) + { + m_scheduled_update = false; + update(); + } + else + { + emit updateFinished(); + } + }, + Qt::ConnectionType::QueuedConnection); + + QThreadPool::globalInstance()->start(m_current_update_task.get()); + + return true; +} + +void ResourceFolderModel::resolveResource(Resource::Ptr res) +{ + if (!res->shouldResolve()) + { + return; + } + + Task::Ptr task{ createParseTask(*res) }; + if (!task) + return; + + int ticket = m_next_resolution_ticket.fetch_add(1); + + res->setResolving(true, ticket); + m_active_parse_tasks.insert(ticket, task); + + connect( + task.get(), + &Task::succeeded, + this, + [this, ticket, res] { onParseSucceeded(ticket, res->internal_id()); }, + Qt::ConnectionType::QueuedConnection); + connect( + task.get(), + &Task::failed, + this, + [this, ticket, res] { onParseFailed(ticket, res->internal_id()); }, + Qt::ConnectionType::QueuedConnection); + connect( + task.get(), + &Task::finished, + this, + [this, ticket] + { + m_active_parse_tasks.remove(ticket); + emit parseFinished(); + }, + Qt::ConnectionType::QueuedConnection); + + m_helper_thread_task.addTask(task); + + if (!m_helper_thread_task.isRunning()) + { + QThreadPool::globalInstance()->start(&m_helper_thread_task); + } +} + +void ResourceFolderModel::onUpdateSucceeded() +{ + auto update_results = static_cast<ResourceFolderLoadTask*>(m_current_update_task.get())->result(); + + auto& new_resources = update_results->resources; + + auto current_list = m_resources_index.keys(); + QSet<QString> current_set(current_list.begin(), current_list.end()); + + auto new_list = new_resources.keys(); + QSet<QString> new_set(new_list.begin(), new_list.end()); + + applyUpdates(current_set, new_set, new_resources); +} + +void ResourceFolderModel::onParseSucceeded(int ticket, QString resource_id) +{ + auto iter = m_active_parse_tasks.constFind(ticket); + if (iter == m_active_parse_tasks.constEnd() || !m_resources_index.contains(resource_id)) + return; + + int row = m_resources_index[resource_id]; + emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1)); +} + +Task* ResourceFolderModel::createUpdateTask() +{ + auto index_dir = indexDir(); + auto task = new ResourceFolderLoadTask(dir(), + index_dir, + m_is_indexed, + m_first_folder_load, + [this](const QFileInfo& file) { return createResource(file); }); + m_first_folder_load = false; + return task; +} + +bool ResourceFolderModel::hasPendingParseTasks() const +{ + return !m_active_parse_tasks.isEmpty(); +} + +void ResourceFolderModel::directoryChanged(QString path) +{ + update(); +} + +Qt::DropActions ResourceFolderModel::supportedDropActions() const +{ + // copy from outside, move from within and other resource lists + return Qt::CopyAction | Qt::MoveAction; +} + +Qt::ItemFlags ResourceFolderModel::flags(const QModelIndex& index) const +{ + Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index); + auto flags = defaultFlags | Qt::ItemIsDropEnabled; + if (index.isValid()) + flags |= Qt::ItemIsUserCheckable; + return flags; +} + +QStringList ResourceFolderModel::mimeTypes() const +{ + QStringList types; + types << "text/uri-list"; + return types; +} + +bool ResourceFolderModel::dropMimeData(const QMimeData* data, Qt::DropAction action, int, int, const QModelIndex&) +{ + if (action == Qt::IgnoreAction) + { + return true; + } + + // check if the action is supported + if (!data || !(action & supportedDropActions())) + { + return false; + } + + // files dropped from outside? + if (data->hasUrls()) + { + auto urls = data->urls(); + for (auto url : urls) + { + // only local files may be dropped... + if (!url.isLocalFile()) + { + continue; + } + + QString sourcePath = url.toLocalFile(); + + // Handle move vs copy action + if (action == Qt::MoveAction) + { + // Move: install then delete source + if (installResource(sourcePath)) + { + QFile::remove(sourcePath); + } + else + { + qWarning() << "Failed to move resource from" << sourcePath; + } + } + else + { + // Copy: just install (copies the file) + if (!installResource(sourcePath)) + { + qWarning() << "Failed to install resource from" << sourcePath; + } + } + } + return true; + } + return false; +} + +bool ResourceFolderModel::validateIndex(const QModelIndex& index) const +{ + if (!index.isValid()) + return false; + + int row = index.row(); + if (row < 0 || row >= m_resources.size()) + return false; + + return true; +} + +QBrush ResourceFolderModel::rowBackground(int row) const +{ + if (APPLICATION->settings()->get("ShowModIncompat").toBool() && m_resources[row]->hasIssues()) + { + return { QColor(255, 0, 0, 40) }; + } + return {}; +} + +QVariant ResourceFolderModel::data(const QModelIndex& index, int role) const +{ + if (!validateIndex(index)) + return {}; + + int row = index.row(); + int column = index.column(); + + switch (role) + { + case Qt::BackgroundRole: return rowBackground(row); + case Qt::DisplayRole: + switch (column) + { + case NameColumn: return m_resources[row]->name(); + case DateColumn: return m_resources[row]->dateTimeChanged(); + case ProviderColumn: return m_resources[row]->provider(); + case SizeColumn: return m_resources[row]->sizeStr(); + default: return {}; + } + case Qt::ToolTipRole: + { + QString tooltip = m_resources[row]->internal_id(); + if (column == NameColumn) + { + if (APPLICATION->settings()->get("ShowModIncompat").toBool()) + { + for (const QString& issue : at(row).issues()) + { + tooltip += "\n" + issue; + } + } + if (at(row).isSymLinkUnder(instDirPath())) + { + tooltip += tr("\nWarning: This resource is symbolically linked from elsewhere. Editing it will also " + "change the original." + "\nCanonical Path: %1") + .arg(at(row).fileinfo().canonicalFilePath()); + } + if (at(row).isMoreThanOneHardLink()) + { + tooltip += tr( + "\nWarning: This resource is hard linked elsewhere. Editing it will also change the original."); + } + } + return tooltip; + } + case Qt::DecorationRole: + { + if (column == NameColumn) + { + if (APPLICATION->settings()->get("ShowModIncompat").toBool() && at(row).hasIssues()) + return QIcon::fromTheme("status-bad"); + if (at(row).isSymLinkUnder(instDirPath()) || at(row).isMoreThanOneHardLink()) + return QIcon::fromTheme("status-yellow"); + } + + return {}; + } + case Qt::CheckStateRole: + if (column == ActiveColumn) + return m_resources[row]->enabled() ? Qt::Checked : Qt::Unchecked; + return {}; + default: return {}; + } +} + +bool ResourceFolderModel::setData(const QModelIndex& index, [[maybe_unused]] const QVariant& value, int role) +{ + int row = index.row(); + if (row < 0 || row >= rowCount(index.parent()) || !index.isValid()) + return false; + + if (role == Qt::CheckStateRole) + { + return setResourceEnabled({ index }, EnableAction::TOGGLE); + } + + return false; +} + +QVariant ResourceFolderModel::headerData(int section, [[maybe_unused]] Qt::Orientation orientation, int role) const +{ + switch (role) + { + case Qt::DisplayRole: + switch (section) + { + case ActiveColumn: + case NameColumn: + case DateColumn: + case ProviderColumn: + case SizeColumn: return columnNames().at(section); + default: return {}; + } + case Qt::ToolTipRole: + { + //: Here, resource is a generic term for external resources, like Mods, Resource Packs, Shader Packs, etc. + switch (section) + { + case ActiveColumn: return tr("Is the resource enabled?"); + case NameColumn: return tr("The name of the resource."); + case DateColumn: return tr("The date and time this resource was last changed (or added)."); + case ProviderColumn: return tr("The source provider of the resource."); + case SizeColumn: return tr("The size of the resource."); + default: return {}; + } + } + default: break; + } + + return {}; +} + +void ResourceFolderModel::setupHeaderAction(QAction* act, int column) +{ + Q_ASSERT(act); + + act->setText(columnNames().at(column)); +} + +void ResourceFolderModel::saveColumns(QTreeView* tree) +{ + auto const stateSettingName = QString("UI/%1_Page/Columns").arg(id()); + auto const overrideSettingName = QString("UI/%1_Page/ColumnsOverride").arg(id()); + auto const visibilitySettingName = QString("UI/%1_Page/ColumnsVisibility").arg(id()); + + auto stateSetting = m_instance->settings()->getSetting(stateSettingName); + stateSetting->set(QString::fromUtf8(tree->header()->saveState().toBase64())); + + // neither passthrough nor override settings works for this usecase as I need to only set the global when the gate + // is false + auto settings = m_instance->settings(); + if (!settings->get(overrideSettingName).toBool()) + { + settings = APPLICATION->settings(); + } + auto visibility = Json::toMap(settings->get(visibilitySettingName).toString()); + for (auto i = 0; i < m_column_names.size(); ++i) + { + if (m_columnsHideable[i]) + { + auto name = m_column_names[i]; + visibility[name] = !tree->isColumnHidden(i); + } + } + settings->set(visibilitySettingName, Json::fromMap(visibility)); +} + +void ResourceFolderModel::loadColumns(QTreeView* tree) +{ + auto const stateSettingName = QString("UI/%1_Page/Columns").arg(id()); + auto const overrideSettingName = QString("UI/%1_Page/ColumnsOverride").arg(id()); + auto const visibilitySettingName = QString("UI/%1_Page/ColumnsVisibility").arg(id()); + + auto stateSetting = m_instance->settings()->getOrRegisterSetting(stateSettingName, ""); + tree->header()->restoreState(QByteArray::fromBase64(stateSetting->get().toString().toUtf8())); + + auto setVisible = [this, tree](QVariant value) + { + auto visibility = Json::toMap(value.toString()); + for (auto i = 0; i < m_column_names.size(); ++i) + { + if (m_columnsHideable[i]) + { + auto name = m_column_names[i]; + tree->setColumnHidden(i, !visibility.value(name, false).toBool()); + } + } + }; + + auto const defaultValue = Json::fromMap({ + { "Image", true }, + { "Version", true }, + { "Last Modified", true }, + { "Provider", true }, + { "Pack Format", true }, + }); + // neither passthrough nor override settings works for this usecase as I need to only set the global when the gate + // is false + auto settings = m_instance->settings(); + if (!settings->getOrRegisterSetting(overrideSettingName, false)->get().toBool()) + { + settings = APPLICATION->settings(); + } + auto visibility = settings->getOrRegisterSetting(visibilitySettingName, defaultValue); + setVisible(visibility->get()); + + // allways connect the signal in case the setting is toggled on and off + auto gSetting = APPLICATION->settings()->getOrRegisterSetting(visibilitySettingName, defaultValue); + connect(gSetting.get(), + &Setting::SettingChanged, + tree, + [this, setVisible, overrideSettingName](const Setting&, QVariant value) + { + if (!m_instance->settings()->get(overrideSettingName).toBool()) + { + setVisible(value); + } + }); +} + +QMenu* ResourceFolderModel::createHeaderContextMenu(QTreeView* tree) +{ + auto menu = new QMenu(tree); + + { // action to decide if the visibility is per instance or not + auto act = new QAction(tr("Override Columns Visibility"), menu); + auto const overrideSettingName = QString("UI/%1_Page/ColumnsOverride").arg(id()); + + act->setCheckable(true); + act->setChecked(m_instance->settings()->getOrRegisterSetting(overrideSettingName, false)->get().toBool()); + + connect(act, + &QAction::toggled, + tree, + [this, tree, overrideSettingName](bool toggled) + { + m_instance->settings()->set(overrideSettingName, toggled); + saveColumns(tree); + }); + + menu->addAction(act); + } + menu->addSeparator()->setText(tr("Show / Hide Columns")); + + for (int col = 0; col < columnCount(); ++col) + { + // Skip creating actions for columns that should not be hidden + if (!m_columnsHideable.at(col)) + continue; + auto act = new QAction(menu); + setupHeaderAction(act, col); + + act->setCheckable(true); + act->setChecked(!tree->isColumnHidden(col)); + + connect(act, + &QAction::toggled, + tree, + [this, col, tree](bool toggled) + { + tree->setColumnHidden(col, !toggled); + for (int c = 0; c < columnCount(); ++c) + { + if (m_column_resize_modes.at(c) == QHeaderView::ResizeToContents) + tree->resizeColumnToContents(c); + } + saveColumns(tree); + }); + + menu->addAction(act); + } + + return menu; +} + +QSortFilterProxyModel* ResourceFolderModel::createFilterProxyModel(QObject* parent) +{ + return new ProxyModel(parent); +} + +SortType ResourceFolderModel::columnToSortKey(size_t column) const +{ + Q_ASSERT(m_column_sort_keys.size() == columnCount()); + return m_column_sort_keys.at(column); +} + +/* Standard Proxy Model for createFilterProxyModel */ +bool ResourceFolderModel::ProxyModel::filterAcceptsRow(int source_row, + [[maybe_unused]] const QModelIndex& source_parent) const +{ + auto* model = qobject_cast<ResourceFolderModel*>(sourceModel()); + if (!model) + return true; + + const auto& resource = model->at(source_row); + + return resource.applyFilter(filterRegularExpression()); +} + +bool ResourceFolderModel::ProxyModel::lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const +{ + auto* model = qobject_cast<ResourceFolderModel*>(sourceModel()); + if (!model || !source_left.isValid() || !source_right.isValid() || source_left.column() != source_right.column()) + { + return QSortFilterProxyModel::lessThan(source_left, source_right); + } + + // we are now guaranteed to have two valid indexes in the same column... we love the provided invariants + // unconditionally and proceed. + + auto column_sort_key = model->columnToSortKey(source_left.column()); + auto const& resource_left = model->at(source_left.row()); + auto const& resource_right = model->at(source_right.row()); + + auto compare_result = resource_left.compare(resource_right, column_sort_key); + if (compare_result == 0) + return QSortFilterProxyModel::lessThan(source_left, source_right); + + return compare_result < 0; +} + +QString ResourceFolderModel::instDirPath() const +{ + return QFileInfo(m_instance->instanceRoot()).absoluteFilePath(); +} + +void ResourceFolderModel::onParseFailed(int ticket, QString resource_id) +{ + auto iter = m_active_parse_tasks.constFind(ticket); + if (iter == m_active_parse_tasks.constEnd() || !m_resources_index.contains(resource_id)) + return; + + auto removed_index = m_resources_index[resource_id]; + auto removed_it = m_resources.begin() + removed_index; + Q_ASSERT(removed_it != m_resources.end()); + + beginRemoveRows(QModelIndex(), removed_index, removed_index); + m_resources.erase(removed_it); + + // update index + m_resources_index.clear(); + int idx = 0; + for (auto const& mod : qAsConst(m_resources)) + { + m_resources_index[mod->internal_id()] = idx; + idx++; + } + endRemoveRows(); +} + +void ResourceFolderModel::applyUpdates(QSet<QString>& current_set, + QSet<QString>& new_set, + QMap<QString, Resource::Ptr>& new_resources) +{ + // see if the kept resources changed in some way + { + QSet<QString> kept_set = current_set; + kept_set.intersect(new_set); + + for (auto const& kept : kept_set) + { + auto row_it = m_resources_index.constFind(kept); + Q_ASSERT(row_it != m_resources_index.constEnd()); + auto row = row_it.value(); + + auto& new_resource = new_resources[kept]; + auto const& current_resource = m_resources.at(row); + + if (new_resource->dateTimeChanged() == current_resource->dateTimeChanged()) + { + bool hadIssues = current_resource->hasIssues(); + current_resource->updateIssues(m_instance); + if (hadIssues != current_resource->hasIssues()) + { + emit dataChanged(index(row, 0), index(row, columnCount({}) - 1)); + } + continue; + } + + // If the resource is resolving, but something about it changed, we don't want to + // continue the resolving. + if (current_resource->isResolving()) + { + auto ticket = current_resource->resolutionTicket(); + if (m_active_parse_tasks.contains(ticket)) + { + auto task = (*m_active_parse_tasks.find(ticket)).get(); + task->abort(); + } + } + + m_resources[row].reset(new_resource); + new_resource->updateIssues(m_instance); + resolveResource(m_resources.at(row)); + emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1)); + } + } + + // remove resources no longer present + { + QSet<QString> removed_set = current_set; + removed_set.subtract(new_set); + + QList<int> removed_rows; + for (auto& removed : removed_set) + removed_rows.append(m_resources_index[removed]); + + std::sort(removed_rows.begin(), removed_rows.end(), std::greater<int>()); + + for (auto& removed_index : removed_rows) + { + auto removed_it = m_resources.begin() + removed_index; + + Q_ASSERT(removed_it != m_resources.end()); + + if ((*removed_it)->isResolving()) + { + auto ticket = (*removed_it)->resolutionTicket(); + if (m_active_parse_tasks.contains(ticket)) + { + auto task = (*m_active_parse_tasks.find(ticket)).get(); + task->abort(); + } + } + + beginRemoveRows(QModelIndex(), removed_index, removed_index); + m_resources.erase(removed_it); + endRemoveRows(); + } + } + + // add new resources to the end + { + QSet<QString> added_set = new_set; + added_set.subtract(current_set); + + // When you have a Qt build with assertions turned on, proceeding here will abort the application + if (added_set.size() > 0) + { + beginInsertRows(QModelIndex(), + static_cast<int>(m_resources.size()), + static_cast<int>(m_resources.size() + added_set.size() - 1)); + + for (auto& added : added_set) + { + auto res = new_resources[added]; + res->updateIssues(m_instance); + m_resources.append(res); + resolveResource(m_resources.last()); + } + + endInsertRows(); + } + } + + // update index + { + m_resources_index.clear(); + int idx = 0; + for (auto const& mod : qAsConst(m_resources)) + { + m_resources_index[mod->internal_id()] = idx; + idx++; + } + } +} +Resource::Ptr ResourceFolderModel::find(QString id) +{ + auto iter = std::find_if(m_resources.constBegin(), + m_resources.constEnd(), + [&](Resource::Ptr const& r) { return r->internal_id() == id; }); + if (iter == m_resources.constEnd()) + return nullptr; + return *iter; +} + +Resource::WeakPtr ResourceFolderModel::findWeak(const QString& id) +{ + auto it = m_resources_index.constFind(id); + if (it == m_resources_index.constEnd()) + return Resource::WeakPtr(); + + int idx = it.value(); + if (idx < 0 || idx >= m_resources.size()) + return Resource::WeakPtr(); + + return Resource::WeakPtr(m_resources[idx].get()); +} + +QList<Resource*> ResourceFolderModel::allResources() +{ + QList<Resource*> result; + result.reserve(m_resources.size()); + for (const Resource ::Ptr& resource : m_resources) + result.append((resource.get())); + return result; +} +QList<Resource*> ResourceFolderModel::selectedResources(const QModelIndexList& indexes) +{ + QList<Resource*> result; + for (const QModelIndex& index : indexes) + { + if (index.column() != 0) + continue; + result.append(&at(index.row())); + } + return result; +} diff --git a/archived/projt-launcher/launcher/minecraft/mod/ResourceFolderModel.hpp b/archived/projt-launcher/launcher/minecraft/mod/ResourceFolderModel.hpp new file mode 100644 index 0000000000..e909f0d26b --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/ResourceFolderModel.hpp @@ -0,0 +1,394 @@ +// 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. + */ +#pragma once + +#include <QAbstractListModel> +#include <QAction> +#include <QDir> +#include <QFileSystemWatcher> +#include <QHeaderView> +#include <QMutex> +#include <QSet> +#include <QSortFilterProxyModel> +#include <QTreeView> + +#include "Resource.hpp" + +#include "BaseInstance.h" + +#include "tasks/ConcurrentTask.h" +#include "tasks/Task.h" + +class QSortFilterProxyModel; + +/* A macro to define useful functions to handle Resource* -> T* more easily on derived classes */ +#define RESOURCE_HELPERS(T) \ + T& at(int index) \ + { \ + return *static_cast<T*>(m_resources[index].get()); \ + } \ + const T& at(int index) const \ + { \ + return *static_cast<const T*>(m_resources.at(index).get()); \ + } \ + QList<T*> selected##T##s(const QModelIndexList& indexes) \ + { \ + QList<T*> result; \ + for (const QModelIndex& index : indexes) \ + { \ + if (index.column() != 0) \ + continue; \ + \ + result.append(&at(index.row())); \ + } \ + return result; \ + } \ + QList<T*> all##T##s() \ + { \ + QList<T*> result; \ + result.reserve(m_resources.size()); \ + \ + for (const Resource::Ptr& resource : m_resources) \ + result.append(static_cast<T*>(resource.get())); \ + \ + return result; \ + } + +/** A basic model for external resources. + * + * This model manages a list of resources. As such, external users of such resources do not own them, + * and the resource's lifetime is contingent on the model's lifetime. + * + * Weak pointer access is provided via weakAt(), findWeak(), and isResourceValid() methods. + * External code should use these to avoid extending resource lifetime. + */ +class ResourceFolderModel : public QAbstractListModel +{ + Q_OBJECT + public: + ResourceFolderModel(const QDir& dir, + BaseInstance* instance, + bool is_indexed, + bool create_dir, + QObject* parent = nullptr); + ~ResourceFolderModel() override; + + virtual QString id() const + { + return "resource"; + } + + /** Starts watching the paths for changes. + * + * Returns whether starting to watch all the paths was successful. + * If one or more fails, it returns false. + */ + bool startWatching(const QStringList& paths); + + /** Stops watching the paths for changes. + * + * Returns whether stopping to watch all the paths was successful. + * If one or more fails, it returns false. + */ + bool stopWatching(const QStringList& paths); + + /* Helper methods for subclasses, using a predetermined list of paths. */ + virtual bool startWatching() + { + return startWatching({ indexDir().absolutePath(), m_dir.absolutePath() }); + } + virtual bool stopWatching() + { + return stopWatching({ indexDir().absolutePath(), m_dir.absolutePath() }); + } + + QDir indexDir() + { + return { QString("%1/.index").arg(dir().absolutePath()) }; + } + + /** Given a path in the system, install that resource, moving it to its place in the + * instance file hierarchy. + * + * Returns whether the installation was succcessful. + */ + virtual bool installResource(QString path); + + virtual void installResourceWithFlameMetadata(QString path, ModPlatform::IndexedVersion& vers); + + /** Uninstall (i.e. remove all data about it) a resource, given its file name. + * + * Returns whether the removal was successful. + */ + virtual bool uninstallResource(QString file_name, bool preserve_metadata = false); + virtual bool deleteResources(const QModelIndexList&); + virtual void deleteMetadata(const QModelIndexList&); + + /** Applies the given 'action' to the resources in 'indexes'. + * + * Returns whether the action was successfully applied to all resources. + */ + virtual bool setResourceEnabled(const QModelIndexList& indexes, EnableAction action); + + /** Creates a new update task and start it. Returns false if no update was done, like when an update is already underway. */ + virtual bool update(); + + /** Creates a new parse task, if needed, for 'res' and start it.*/ + virtual void resolveResource(Resource::Ptr res); + + qsizetype size() const + { + return m_resources.size(); + } + [[nodiscard]] bool empty() const + { + return size() == 0; + } + + Resource& at(int index) + { + return *m_resources[index].get(); + } + const Resource& at(int index) const + { + return *m_resources.at(index).get(); + } + + /** Get a weak pointer to a resource by index. + * This is the preferred way to access resources from external code, + * as it doesn't extend the resource's lifetime. + */ + Resource::WeakPtr weakAt(int index) + { + if (index < 0 || index >= m_resources.size()) + return Resource::WeakPtr(); + return Resource::WeakPtr(m_resources[index].get()); + } + + /** Get a weak pointer to a resource by internal ID. + * Returns null WeakPtr if not found. + */ + Resource::WeakPtr findWeak(const QString& id); + + /** Check if a weak pointer is still valid (resource still exists). + * This is useful for external code that holds weak references. + */ + bool isResourceValid(Resource::WeakPtr ptr) const + { + return !ptr.isNull() && m_resources_index.contains(ptr->internal_id()); + } + + QList<Resource*> selectedResources(const QModelIndexList& indexes); + QList<Resource*> allResources(); + + Resource::Ptr find(QString id); + + QDir const& dir() const + { + return m_dir; + } + + /** Checks whether there's any parse tasks being done. + * + * Since they can be quite expensive, and are usually done in a separate thread, if we were to destroy the model + * while having such tasks would introduce an undefined behavior, most likely resulting in a crash. + */ + bool hasPendingParseTasks() const; + + /* Qt behavior */ + + /* Basic columns */ + enum Columns + { + ActiveColumn = 0, + NameColumn, + DateColumn, + ProviderColumn, + SizeColumn, + NUM_COLUMNS + }; + + QStringList columnNames(bool translated = true) const + { + return translated ? m_column_names_translated : m_column_names; + } + + int rowCount(const QModelIndex& parent = {}) const override + { + return parent.isValid() ? 0 : static_cast<int>(size()); + } + int columnCount(const QModelIndex& parent = {}) const override + { + return parent.isValid() ? 0 : NUM_COLUMNS; + } + + Qt::DropActions supportedDropActions() const override; + + /// flags, mostly to support drag&drop + Qt::ItemFlags flags(const QModelIndex& index) const override; + QStringList mimeTypes() const override; + [[nodiscard]] bool dropMimeData(const QMimeData* data, + Qt::DropAction action, + int row, + int column, + const QModelIndex& parent) override; + + [[nodiscard]] bool validateIndex(const QModelIndex& index) const; + + QBrush rowBackground(int row) const; + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override; + + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + + void setupHeaderAction(QAction* act, int column); + void saveColumns(QTreeView* tree); + void loadColumns(QTreeView* tree); + QMenu* createHeaderContextMenu(QTreeView* tree); + + /** This creates a proxy model to filter / sort the model for a UI. + * + * The actual comparisons and filtering are done directly by the Resource, so to modify behavior go there instead! + */ + QSortFilterProxyModel* createFilterProxyModel(QObject* parent = nullptr); + + SortType columnToSortKey(size_t column) const; + QList<QHeaderView::ResizeMode> columnResizeModes() const + { + return m_column_resize_modes; + } + + class ProxyModel : public QSortFilterProxyModel + { + public: + explicit ProxyModel(QObject* parent = nullptr) : QSortFilterProxyModel(parent) + {} + + protected: + bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override; + bool lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const override; + }; + + QString instDirPath() const; + + signals: + void updateFinished(); + void parseFinished(); + + protected: + /** This creates a new update task to be executed by update(). + * + * The task should load and parse all resources necessary, and provide a way of accessing such results. + * + * This Task is normally executed when opening a page, so it shouldn't contain much heavy work. + * If such work is needed, try using it in the Task create by createParseTask() instead! + */ + [[nodiscard]] Task* createUpdateTask(); + + [[nodiscard]] virtual Resource* createResource(const QFileInfo& info) + { + return new Resource(info); + } + + /** This creates a new parse task to be executed by onUpdateSucceeded(). + * + * This task should load and parse all heavy info needed by a resource, such as parsing a manifest. It gets + * executed in the background, so it slowly updates the UI as tasks get done. + */ + [[nodiscard]] virtual Task* createParseTask(Resource&) + { + return nullptr; + } + + /** Standard implementation of the model update logic. + * + * It uses set operations to find differences between the current state and the updated state, + * to act only on those disparities. + * + */ + void applyUpdates(QSet<QString>& current_set, QSet<QString>& new_set, QMap<QString, Resource::Ptr>& new_resources); + + protected slots: + void directoryChanged(QString); + + /** Called when the update task is successful. + * + * Override in subclasses to handle specific task result types. + * The implementation typically uses static_cast to convert the Task to + * the specific type returned by createUpdateTask(). + * + * Note: Qt's Q_OBJECT macro doesn't support template classes, so we use + * runtime polymorphism with virtual methods and static_cast instead. + * The type relationship is documented in createUpdateTask() for each subclass. + */ + virtual void onUpdateSucceeded(); + virtual void onUpdateFailed() + {} + + /** Called when the parse task with the given ticket is successful. + * + * This is just a simple reference implementation. You probably want to override it with your own logic in a + * subclass if the resource is complex and has more stuff to parse. + */ + virtual void onParseSucceeded(int ticket, QString resource_id); + virtual void onParseFailed(int ticket, QString resource_id); + + protected: + // Represents the relationship between a column's index (represented by the list index), and it's sorting key. + // As such, the order in with they appear is very important! + QList<SortType> m_column_sort_keys = { SortType::ENABLED, + SortType::NAME, + SortType::DATE, + SortType::PROVIDER, + SortType::SIZE }; + QStringList m_column_names = { "Enable", "Name", "Last Modified", "Provider", "Size" }; + QStringList m_column_names_translated = { tr("Enable"), + tr("Name"), + tr("Last Modified"), + tr("Provider"), + tr("Size") }; + QList<QHeaderView::ResizeMode> m_column_resize_modes = { QHeaderView::Interactive, + QHeaderView::Stretch, + QHeaderView::Interactive, + QHeaderView::Interactive, + QHeaderView::Interactive }; + QList<bool> m_columnsHideable = { false, false, true, true, true }; + + QDir m_dir; + BaseInstance* m_instance; + QFileSystemWatcher m_watcher; + bool m_is_watching = false; + + bool m_is_indexed; + bool m_first_folder_load = true; + + Task::Ptr m_current_update_task = nullptr; + bool m_scheduled_update = false; + + QList<Resource::Ptr> m_resources; + + // Represents the relationship between a resource's internal ID and it's row position on the model. + QMap<QString, int> m_resources_index; + + ConcurrentTask m_helper_thread_task; + QMap<int, Task::Ptr> m_active_parse_tasks; + std::atomic<int> m_next_resolution_ticket = 0; +}; diff --git a/archived/projt-launcher/launcher/minecraft/mod/ResourcePack.cpp b/archived/projt-launcher/launcher/minecraft/mod/ResourcePack.cpp new file mode 100644 index 0000000000..e0de7aff25 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/ResourcePack.cpp @@ -0,0 +1,58 @@ +// 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. + */ +#include "ResourcePack.hpp" + +#include <QCoreApplication> +#include <QDebug> +#include <QMap> +#include "MTPixmapCache.h" +#include "Version.h" + +// Values taken from: +// https://minecraft.wiki/w/Pack_format#List_of_resource_pack_formats +static const QMap<int, std::pair<Version, Version>> s_pack_format_versions = { + { 1, { Version("1.6.1"), Version("1.8.9") } }, { 2, { Version("1.9"), Version("1.10.2") } }, + { 3, { Version("1.11"), Version("1.12.2") } }, { 4, { Version("1.13"), Version("1.14.4") } }, + { 5, { Version("1.15"), Version("1.16.1") } }, { 6, { Version("1.16.2"), Version("1.16.5") } }, + { 7, { Version("1.17"), Version("1.17.1") } }, { 8, { Version("1.18"), Version("1.18.2") } }, + { 9, { Version("1.19"), Version("1.19.2") } }, { 11, { Version("22w42a"), Version("22w44a") } }, + { 12, { Version("1.19.3"), Version("1.19.3") } }, { 13, { Version("1.19.4"), Version("1.19.4") } }, + { 14, { Version("23w14a"), Version("23w16a") } }, { 15, { Version("1.20"), Version("1.20.1") } }, + { 16, { Version("23w31a"), Version("23w31a") } }, { 17, { Version("23w32a"), Version("23w35a") } }, + { 18, { Version("1.20.2"), Version("23w16a") } }, { 19, { Version("23w42a"), Version("23w42a") } }, + { 20, { Version("23w43a"), Version("23w44a") } }, { 21, { Version("23w45a"), Version("23w46a") } }, + { 22, { Version("1.20.3-pre1"), Version("23w51b") } }, { 24, { Version("24w03a"), Version("24w04a") } }, + { 25, { Version("24w05a"), Version("24w05b") } }, { 26, { Version("24w06a"), Version("24w07a") } }, + { 28, { Version("24w09a"), Version("24w10a") } }, { 29, { Version("24w11a"), Version("24w11a") } }, + { 30, { Version("24w12a"), Version("23w12a") } }, { 31, { Version("24w13a"), Version("1.20.5-pre3") } }, + { 32, { Version("1.20.5-pre4"), Version("1.20.6") } }, { 33, { Version("24w18a"), Version("24w20a") } }, + { 34, { Version("24w21a"), Version("1.21") } } +}; + +std::pair<Version, Version> ResourcePack::compatibleVersions() const +{ + if (!s_pack_format_versions.contains(m_pack_format)) + { + return { {}, {} }; + } + + return s_pack_format_versions.constFind(m_pack_format).value(); +} diff --git a/archived/projt-launcher/launcher/minecraft/mod/ResourcePack.hpp b/archived/projt-launcher/launcher/minecraft/mod/ResourcePack.hpp new file mode 100644 index 0000000000..4c9c5ccef2 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/ResourcePack.hpp @@ -0,0 +1,48 @@ +// 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. + */ +#pragma once + +#include "Resource.hpp" +#include "minecraft/mod/DataPack.hpp" + +#include <QImage> +#include <QMutex> +#include <QPixmap> +#include <QPixmapCache> + +class Version; + +// Localized descriptions are fully implemented in the DataPack base class. +// ResourcePack inherits m_localized_descriptions, localizedDescription(), +// setLocalizedDescription(), and allLocalizedDescriptions() from DataPack. + +class ResourcePack : public DataPack +{ + Q_OBJECT + public: + ResourcePack(QObject* parent = nullptr) : DataPack(parent) + {} + ResourcePack(QFileInfo file_info) : DataPack(file_info) + {} + + /** Gets, respectively, the lower and upper versions supported by the set pack format. */ + std::pair<Version, Version> compatibleVersions() const override; +}; diff --git a/archived/projt-launcher/launcher/minecraft/mod/ResourcePackFolderModel.cpp b/archived/projt-launcher/launcher/minecraft/mod/ResourcePackFolderModel.cpp new file mode 100644 index 0000000000..a394740515 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/ResourcePackFolderModel.cpp @@ -0,0 +1,203 @@ +// 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> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * 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 "ResourcePackFolderModel.hpp" +#include <qnamespace.h> +#include <qsize.h> + +#include <QIcon> +#include <QStyle> + +#include "Version.h" + +#include "minecraft/mod/tasks/LocalDataPackParseTask.hpp" + +ResourcePackFolderModel::ResourcePackFolderModel(const QDir& dir, + BaseInstance* instance, + bool is_indexed, + bool create_dir, + QObject* parent) + : ResourceFolderModel(dir, instance, is_indexed, create_dir, parent) +{ + m_column_names = QStringList({ "Enable", "Image", "Name", "Pack Format", "Last Modified", "Provider", "Size" }); + m_column_names_translated = QStringList( + { tr("Enable"), tr("Image"), tr("Name"), tr("Pack Format"), tr("Last Modified"), tr("Provider"), tr("Size") }); + m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, SortType::PACK_FORMAT, + SortType::DATE, SortType::PROVIDER, SortType::SIZE }; + m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, + QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive, + QHeaderView::Interactive }; + m_columnsHideable = { false, true, false, true, true, true, true }; +} + +QVariant ResourcePackFolderModel::data(const QModelIndex& index, int role) const +{ + if (!validateIndex(index)) + return {}; + + int row = index.row(); + int column = index.column(); + + switch (role) + { + case Qt::BackgroundRole: return rowBackground(row); + case Qt::DisplayRole: + if (column == PackFormatColumn) + { + auto& resource = at(row); + auto pack_format = resource.packFormat(); + if (pack_format == 0) + return tr("Unrecognized"); + + auto version_bounds = resource.compatibleVersions(); + if (version_bounds.first.toString().isEmpty()) + return QString::number(pack_format); + + return QString("%1 (%2 - %3)") + .arg(QString::number(pack_format), version_bounds.first.toString(), version_bounds.second.toString()); + } + break; + case Qt::DecorationRole: + if (column == ImageColumn) + { + return at(row).image({ 32, 32 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding); + } + break; + case Qt::ToolTipRole: + if (column == PackFormatColumn) + { + return tr("The resource pack format ID, as well as the Minecraft versions it was designed for."); + } + break; + case Qt::SizeHintRole: + if (column == ImageColumn) + { + return QSize(32, 32); + } + break; + default: break; + } + + QModelIndex mappedIndex; + switch (column) + { + case ActiveColumn: mappedIndex = index.siblingAtColumn(ResourceFolderModel::ActiveColumn); break; + case NameColumn: mappedIndex = index.siblingAtColumn(ResourceFolderModel::NameColumn); break; + case DateColumn: mappedIndex = index.siblingAtColumn(ResourceFolderModel::DateColumn); break; + case ProviderColumn: mappedIndex = index.siblingAtColumn(ResourceFolderModel::ProviderColumn); break; + case SizeColumn: mappedIndex = index.siblingAtColumn(ResourceFolderModel::SizeColumn); break; + default: break; + } + + if (mappedIndex.isValid()) + return ResourceFolderModel::data(mappedIndex, role); + + return {}; +} + +QVariant ResourcePackFolderModel::headerData(int section, [[maybe_unused]] Qt::Orientation orientation, int role) const +{ + switch (role) + { + case Qt::DisplayRole: + switch (section) + { + case ActiveColumn: + case NameColumn: + case PackFormatColumn: + case DateColumn: + case ImageColumn: + case ProviderColumn: + case SizeColumn: return columnNames().at(section); + default: return {}; + } + + case Qt::ToolTipRole: + switch (section) + { + case ActiveColumn: return tr("Is the resource pack enabled?"); + case NameColumn: return tr("The name of the resource pack."); + case PackFormatColumn: + //: The string being explained by this is in the format: ID (Lower version - Upper version) + return tr("The resource pack format ID, as well as the Minecraft versions it was designed for."); + case DateColumn: return tr("The date and time this resource pack was last changed (or added)."); + case ProviderColumn: return tr("The source provider of the resource pack."); + case SizeColumn: return tr("The size of the resource pack."); + default: return {}; + } + case Qt::SizeHintRole: + if (section == ImageColumn) + { + return QSize(64, 0); + } + return {}; + default: return {}; + } +} + +int ResourcePackFolderModel::columnCount(const QModelIndex& parent) const +{ + return parent.isValid() ? 0 : NUM_COLUMNS; +} + +Task* ResourcePackFolderModel::createParseTask(Resource& resource) +{ + return new LocalDataPackParseTask(m_next_resolution_ticket, dynamic_cast<ResourcePack*>(&resource)); +} diff --git a/archived/projt-launcher/launcher/minecraft/mod/ResourcePackFolderModel.hpp b/archived/projt-launcher/launcher/minecraft/mod/ResourcePackFolderModel.hpp new file mode 100644 index 0000000000..fafd7796c8 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/ResourcePackFolderModel.hpp @@ -0,0 +1,66 @@ +// 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. + */ +#pragma once + +#include "ResourceFolderModel.hpp" + +#include "ResourcePack.hpp" + +class ResourcePackFolderModel : public ResourceFolderModel +{ + Q_OBJECT + public: + enum Columns + { + ActiveColumn = 0, + ImageColumn, + NameColumn, + PackFormatColumn, + DateColumn, + ProviderColumn, + SizeColumn, + NUM_COLUMNS + }; + + explicit ResourcePackFolderModel(const QDir& dir, + BaseInstance* instance, + bool is_indexed, + bool create_dir, + QObject* parent = nullptr); + + QString id() const override + { + return "resourcepacks"; + } + + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + int columnCount(const QModelIndex& parent) const override; + + [[nodiscard]] Resource* createResource(const QFileInfo& file) override + { + return new ResourcePack(file); + } + [[nodiscard]] Task* createParseTask(Resource&) override; + + RESOURCE_HELPERS(ResourcePack) +}; diff --git a/archived/projt-launcher/launcher/minecraft/mod/ShaderPack.cpp b/archived/projt-launcher/launcher/minecraft/mod/ShaderPack.cpp new file mode 100644 index 0000000000..7814175be6 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/ShaderPack.cpp @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// 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 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ +#include "ShaderPack.hpp" + +void ShaderPack::setPackFormat(ShaderPackFormat new_format) +{ + QMutexLocker locker(&m_data_lock); + + m_pack_format = new_format; +} + +bool ShaderPack::valid() const +{ + return m_pack_format != ShaderPackFormat::INVALID; +} diff --git a/archived/projt-launcher/launcher/minecraft/mod/ShaderPack.hpp b/archived/projt-launcher/launcher/minecraft/mod/ShaderPack.hpp new file mode 100644 index 0000000000..71bce711a4 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/ShaderPack.hpp @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// 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 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#pragma once + +#include "Resource.hpp" + +/* Info: + * Currently For Optifine / Iris shader packs, + * could be expanded to support others should they exist? + * + * This class and enum are mostly here as placeholders for validating + * that a shaderpack exists and is in the right format, + * namely that they contain a folder named 'shaders'. + * + * In the technical sense it would be possible to parse files like `shaders/shaders.properties` + * to get information like the available profiles but this is not all that useful without more knowledge of the + * shader mod used to be able to change settings. + */ + +#include <QMutex> + +enum class ShaderPackFormat +{ + VALID, + INVALID +}; + +class ShaderPack : public Resource +{ + Q_OBJECT + public: + using Ptr = shared_qobject_ptr<Resource>; + + ShaderPackFormat packFormat() const + { + return m_pack_format; + } + + ShaderPack(QObject* parent = nullptr) : Resource(parent) + {} + ShaderPack(QFileInfo file_info) : Resource(file_info) + {} + + /** Thread-safe. */ + void setPackFormat(ShaderPackFormat new_format); + + bool valid() const override; + + protected: + mutable QMutex m_data_lock; + + ShaderPackFormat m_pack_format = ShaderPackFormat::INVALID; +}; diff --git a/archived/projt-launcher/launcher/minecraft/mod/ShaderPackFolderModel.hpp b/archived/projt-launcher/launcher/minecraft/mod/ShaderPackFolderModel.hpp new file mode 100644 index 0000000000..b7fff575a3 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/ShaderPackFolderModel.hpp @@ -0,0 +1,56 @@ +// 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. + */ +#pragma once + +#include "ResourceFolderModel.hpp" +#include "minecraft/mod/ShaderPack.hpp" +#include "minecraft/mod/tasks/LocalShaderPackParseTask.hpp" + +class ShaderPackFolderModel : public ResourceFolderModel +{ + Q_OBJECT + + public: + explicit ShaderPackFolderModel(const QDir& dir, + BaseInstance* instance, + bool is_indexed, + bool create_dir, + QObject* parent = nullptr) + : ResourceFolderModel(dir, instance, is_indexed, create_dir, parent) + {} + + virtual QString id() const override + { + return "shaderpacks"; + } + + [[nodiscard]] Resource* createResource(const QFileInfo& info) override + { + return new ShaderPack(info); + } + + [[nodiscard]] Task* createParseTask(Resource& resource) override + { + return new LocalShaderPackParseTask(m_next_resolution_ticket, static_cast<ShaderPack&>(resource)); + } + + RESOURCE_HELPERS(ShaderPack); +}; diff --git a/archived/projt-launcher/launcher/minecraft/mod/TexturePack.cpp b/archived/projt-launcher/launcher/minecraft/mod/TexturePack.cpp new file mode 100644 index 0000000000..13f644a5d1 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/TexturePack.cpp @@ -0,0 +1,105 @@ +// 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> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * 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. + * + * ======================================================================== */ + +#include "TexturePack.hpp" + +#include <QDebug> +#include <QMap> +#include "MTPixmapCache.h" + +#include "minecraft/mod/tasks/LocalTexturePackParseTask.hpp" + +void TexturePack::setDescription(QString new_description) +{ + QMutexLocker locker(&m_data_lock); + + m_description = new_description; +} + +void TexturePack::setImage(QImage new_image) const +{ + QMutexLocker locker(&m_data_lock); + + Q_ASSERT(!new_image.isNull()); + + if (m_pack_image_cache_key.key.isValid()) + PixmapCache::remove(m_pack_image_cache_key.key); + + // scale the image to avoid flooding the pixmapcache + auto pixmap = QPixmap::fromImage( + new_image.scaled({ 64, 64 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding, Qt::SmoothTransformation)); + + m_pack_image_cache_key.key = PixmapCache::insert(pixmap); + m_pack_image_cache_key.was_ever_used = true; +} + +QPixmap TexturePack::image(QSize size, Qt::AspectRatioMode mode) const +{ + QPixmap cached_image; + if (PixmapCache::find(m_pack_image_cache_key.key, &cached_image)) + { + if (size.isNull()) + return cached_image; + return cached_image.scaled(size, mode, Qt::SmoothTransformation); + } + + // No valid image we can get + if (!m_pack_image_cache_key.was_ever_used) + { + return {}; + } + else + { + qDebug() << "Texture Pack" << name() << "Had it's image evicted from the cache. reloading..."; + PixmapCache::markCacheMissByEviciton(); + } + + // Imaged got evicted from the cache. Re-process it and retry. + TexturePackUtils::processPackPNG(*this); + return image(size); +} + +bool TexturePack::valid() const +{ + return m_description != nullptr; +} diff --git a/archived/projt-launcher/launcher/minecraft/mod/TexturePack.hpp b/archived/projt-launcher/launcher/minecraft/mod/TexturePack.hpp new file mode 100644 index 0000000000..2ac75f6ab3 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/TexturePack.hpp @@ -0,0 +1,100 @@ +// 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> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * 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. + * + * ======================================================================== */ + +#pragma once + +#include "Resource.hpp" + +#include <QImage> +#include <QMutex> +#include <QPixmap> +#include <QPixmapCache> + +class Version; + +class TexturePack : public Resource +{ + Q_OBJECT + public: + using Ptr = shared_qobject_ptr<Resource>; + + TexturePack(QObject* parent = nullptr) : Resource(parent) + {} + TexturePack(QFileInfo file_info) : Resource(file_info) + {} + + /** Gets the description of the texture pack. */ + QString description() const + { + return m_description; + } + + /** Gets the image of the texture pack, converted to a QPixmap for drawing, and scaled to size. */ + QPixmap image(QSize size, Qt::AspectRatioMode mode = Qt::AspectRatioMode::IgnoreAspectRatio) const; + + /** Thread-safe. */ + void setDescription(QString new_description); + + /** Thread-safe. */ + void setImage(QImage new_image) const; + + bool valid() const override; + + protected: + mutable QMutex m_data_lock; + + /** The texture pack's description, as defined in the pack.txt file. + */ + QString m_description; + + /** The texture pack's image file cache key, for access in the QPixmapCache global instance. + * + * The 'was_ever_used' state simply identifies whether the key was never inserted on the cache (true), + * so as to tell whether a cache entry is inexistent or if it was just evicted from the cache. + */ + struct + { + QPixmapCache::Key key; + bool was_ever_used = false; + } mutable m_pack_image_cache_key; +}; diff --git a/archived/projt-launcher/launcher/minecraft/mod/TexturePackFolderModel.cpp b/archived/projt-launcher/launcher/minecraft/mod/TexturePackFolderModel.cpp new file mode 100644 index 0000000000..3f6a7c44e2 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/TexturePackFolderModel.cpp @@ -0,0 +1,166 @@ +// 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> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * 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 "TexturePackFolderModel.hpp" + +#include "minecraft/mod/tasks/LocalTexturePackParseTask.hpp" +#include "minecraft/mod/tasks/ResourceFolderLoadTask.hpp" + +TexturePackFolderModel::TexturePackFolderModel(const QDir& dir, + BaseInstance* instance, + bool is_indexed, + bool create_dir, + QObject* parent) + : ResourceFolderModel(QDir(dir), instance, is_indexed, create_dir, parent) +{ + m_column_names = QStringList({ "Enable", "Image", "Name", "Last Modified", "Provider", "Size" }); + m_column_names_translated = + QStringList({ tr("Enable"), tr("Image"), tr("Name"), tr("Last Modified"), tr("Provider"), tr("Size") }); + m_column_sort_keys = { SortType::ENABLED, SortType::NAME, SortType::NAME, + SortType::DATE, SortType::PROVIDER, SortType::SIZE }; + m_column_resize_modes = { QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Stretch, + QHeaderView::Interactive, QHeaderView::Interactive, QHeaderView::Interactive }; + m_columnsHideable = { false, true, false, true, true, true }; +} + +Task* TexturePackFolderModel::createParseTask(Resource& resource) +{ + return new LocalTexturePackParseTask(m_next_resolution_ticket, static_cast<TexturePack&>(resource)); +} + +QVariant TexturePackFolderModel::data(const QModelIndex& index, int role) const +{ + if (!validateIndex(index)) + return {}; + + int row = index.row(); + int column = index.column(); + + switch (role) + { + case Qt::BackgroundRole: return rowBackground(row); + case Qt::DecorationRole: + if (column == ImageColumn) + { + return at(row).image({ 32, 32 }, Qt::AspectRatioMode::KeepAspectRatioByExpanding); + } + break; + case Qt::SizeHintRole: + if (column == ImageColumn) + { + return QSize(32, 32); + } + break; + default: break; + } + + QModelIndex mappedIndex; + switch (column) + { + case ActiveColumn: mappedIndex = index.siblingAtColumn(ResourceFolderModel::ActiveColumn); break; + case NameColumn: mappedIndex = index.siblingAtColumn(ResourceFolderModel::NameColumn); break; + case DateColumn: mappedIndex = index.siblingAtColumn(ResourceFolderModel::DateColumn); break; + case ProviderColumn: mappedIndex = index.siblingAtColumn(ResourceFolderModel::ProviderColumn); break; + case SizeColumn: mappedIndex = index.siblingAtColumn(ResourceFolderModel::SizeColumn); break; + default: break; + } + + if (mappedIndex.isValid()) + return ResourceFolderModel::data(mappedIndex, role); + + return {}; +} + +QVariant TexturePackFolderModel::headerData(int section, [[maybe_unused]] Qt::Orientation orientation, int role) const +{ + switch (role) + { + case Qt::DisplayRole: + switch (section) + { + case ActiveColumn: + case NameColumn: + case DateColumn: + case ImageColumn: + case ProviderColumn: + case SizeColumn: return columnNames().at(section); + default: return {}; + } + case Qt::ToolTipRole: + { + switch (section) + { + case ActiveColumn: return tr("Is the texture pack enabled?"); + case NameColumn: return tr("The name of the texture pack."); + case DateColumn: return tr("The date and time this texture pack was last changed (or added)."); + case ProviderColumn: return tr("The source provider of the texture pack."); + case SizeColumn: return tr("The size of the texture pack."); + default: return {}; + } + } + default: break; + } + + return {}; +} + +int TexturePackFolderModel::columnCount(const QModelIndex& parent) const +{ + return parent.isValid() ? 0 : NUM_COLUMNS; +} diff --git a/archived/projt-launcher/launcher/minecraft/mod/TexturePackFolderModel.hpp b/archived/projt-launcher/launcher/minecraft/mod/TexturePackFolderModel.hpp new file mode 100644 index 0000000000..43d2c2d039 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/TexturePackFolderModel.hpp @@ -0,0 +1,106 @@ +// 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> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * 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. + * + * ======================================================================== */ + +#pragma once + +#include "ResourceFolderModel.hpp" + +#include "TexturePack.hpp" + +class TexturePackFolderModel : public ResourceFolderModel +{ + Q_OBJECT + + public: + enum Columns + { + ActiveColumn = 0, + ImageColumn, + NameColumn, + DateColumn, + ProviderColumn, + SizeColumn, + NUM_COLUMNS + }; + + explicit TexturePackFolderModel(const QDir& dir, + BaseInstance* instance, + bool is_indexed, + bool create_dir, + QObject* parent = nullptr); + + virtual QString id() const override + { + return "texturepacks"; + } + + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + int columnCount(const QModelIndex& parent) const override; + + [[nodiscard]] Resource* createResource(const QFileInfo& file) override + { + return new TexturePack(file); + } + [[nodiscard]] Task* createParseTask(Resource&) override; + + RESOURCE_HELPERS(TexturePack) +}; diff --git a/archived/projt-launcher/launcher/minecraft/mod/WorldSave.cpp b/archived/projt-launcher/launcher/minecraft/mod/WorldSave.cpp new file mode 100644 index 0000000000..0203ac2159 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/WorldSave.cpp @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// 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 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#include "WorldSave.hpp" + +#include "minecraft/mod/tasks/LocalWorldSaveParseTask.hpp" + +void WorldSave::setSaveFormat(WorldSaveFormat new_save_format) +{ + QMutexLocker locker(&m_data_lock); + + m_save_format = new_save_format; +} + +void WorldSave::setSaveDirName(QString dir_name) +{ + QMutexLocker locker(&m_data_lock); + + m_save_dir_name = dir_name; +} + +bool WorldSave::valid() const +{ + return m_save_format != WorldSaveFormat::INVALID; +} diff --git a/archived/projt-launcher/launcher/minecraft/mod/WorldSave.hpp b/archived/projt-launcher/launcher/minecraft/mod/WorldSave.hpp new file mode 100644 index 0000000000..dff491a710 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/WorldSave.hpp @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// 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 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#pragma once + +#include "Resource.hpp" + +#include <QMutex> + +class Version; + +enum class WorldSaveFormat +{ + SINGLE, + MULTI, + INVALID +}; + +class WorldSave : public Resource +{ + Q_OBJECT + public: + using Ptr = shared_qobject_ptr<Resource>; + + WorldSave(QObject* parent = nullptr) : Resource(parent) + {} + WorldSave(QFileInfo file_info) : Resource(file_info) + {} + + /** Gets the format of the save. */ + WorldSaveFormat saveFormat() const + { + return m_save_format; + } + /** Gets the name of the save dir (first found in multi mode). */ + QString saveDirName() const + { + return m_save_dir_name; + } + + /** Thread-safe. */ + void setSaveFormat(WorldSaveFormat new_save_format); + /** Thread-safe. */ + void setSaveDirName(QString dir_name); + + bool valid() const override; + + protected: + mutable QMutex m_data_lock; + + /** The format in which the save file is in. + * Since saves can be distributed in various slightly different ways, this allows us to treat them separately. + */ + WorldSaveFormat m_save_format = WorldSaveFormat::INVALID; + + QString m_save_dir_name; +}; diff --git a/archived/projt-launcher/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp b/archived/projt-launcher/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp new file mode 100644 index 0000000000..77c60e1a5f --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/tasks/GetModDependenciesTask.cpp @@ -0,0 +1,417 @@ +// 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) 2023 Trial97 <alexandru.tripon97@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. + * + * ======================================================================== */ + +#include "GetModDependenciesTask.hpp" + +#include <QDebug> +#include <algorithm> +#include <memory> +#include "Json.h" +#include "QObjectPtr.h" +#include "minecraft/PackProfile.h" +#include "minecraft/mod/MetadataHandler.hpp" +#include "modplatform/ModIndex.h" +#include "modplatform/ResourceAPI.h" +#include "tasks/SequentialTask.h" +#include "ui/pages/modplatform/ModModel.h" + +static Version mcVersion(BaseInstance* inst) +{ + return static_cast<MinecraftInstance*>(inst)->getPackProfile()->getComponent("net.minecraft")->getVersion(); +} + +static ModPlatform::ModLoaderTypes mcLoaders(BaseInstance* inst) +{ + return static_cast<MinecraftInstance*>(inst)->getPackProfile()->getSupportedModLoaders().value(); +} + +static bool checkDependencies(std::shared_ptr<GetModDependenciesTask::PackDependency> sel, + Version mcVersion, + ModPlatform::ModLoaderTypes loaders) +{ + return (sel->pack->versions.isEmpty() || sel->version.mcVersion.contains(mcVersion.toString())) + && (!loaders || !sel->version.loaders || sel->version.loaders & loaders); +} + +GetModDependenciesTask::GetModDependenciesTask(BaseInstance* instance, + ModFolderModel* folder, + QList<std::shared_ptr<PackDependency>> selected) + : SequentialTask(tr("Get dependencies")), + m_selected(selected), + m_version(mcVersion(instance)), + m_loaderType(mcLoaders(instance)) +{ + for (auto mod : folder->allMods()) + { + m_mods_file_names << mod->fileinfo().fileName(); + if (auto meta = mod->metadata(); meta) + m_mods.append(meta); + } + prepare(); +} + +void GetModDependenciesTask::prepare() +{ + for (auto sel : m_selected) + { + if (checkDependencies(sel, m_version, m_loaderType)) + for (auto dep : getDependenciesForVersion(sel->version, sel->pack->provider)) + { + addTask(prepareDependencyTask(dep, sel->pack->provider, 20)); + } + } +} + +ModPlatform::Dependency GetModDependenciesTask::getOverride(const ModPlatform::Dependency& dep, + const ModPlatform::ResourceProvider providerName) +{ + if (auto isQuilt = m_loaderType & ModPlatform::Quilt; isQuilt || m_loaderType & ModPlatform::Fabric) + { + auto overide = ModPlatform::getOverrideDeps(); + auto over = + std::find_if(overide.cbegin(), + overide.cend(), + [dep, providerName, isQuilt](const auto& o) + { return o.provider == providerName && dep.addonId == (isQuilt ? o.fabric : o.quilt); }); + if (over != overide.cend()) + { + return { isQuilt ? over->quilt : over->fabric, dep.type }; + } + } + return dep; +} + +QList<ModPlatform::Dependency> GetModDependenciesTask::getDependenciesForVersion( + const ModPlatform::IndexedVersion& version, + const ModPlatform::ResourceProvider providerName) +{ + QList<ModPlatform::Dependency> c_dependencies; + for (auto ver_dep : version.dependencies) + { + if (ver_dep.type != ModPlatform::DependencyType::REQUIRED) + continue; + ver_dep = getOverride(ver_dep, providerName); + auto isOnlyVersion = + providerName == ModPlatform::ResourceProvider::MODRINTH && ver_dep.addonId.toString().isEmpty(); + if (auto dep = + std::find_if(c_dependencies.begin(), + c_dependencies.end(), + [&ver_dep, isOnlyVersion](const ModPlatform::Dependency& i) + { return isOnlyVersion ? i.version == ver_dep.version : i.addonId == ver_dep.addonId; }); + dep != c_dependencies.end()) + continue; // check the current dependency list + + if (auto dep = std::find_if(m_selected.begin(), + m_selected.end(), + [&ver_dep, providerName, isOnlyVersion](std::shared_ptr<PackDependency> i) + { + return i->pack->provider == providerName + && (isOnlyVersion ? i->version.version == ver_dep.version + : i->pack->addonId == ver_dep.addonId); + }); + dep != m_selected.end()) + continue; // check the selected versions + + if (auto dep = std::find_if(m_mods.begin(), + m_mods.end(), + [&ver_dep, providerName, isOnlyVersion](std::shared_ptr<Metadata::ModStruct> i) + { + return i->provider == providerName + && (isOnlyVersion ? i->file_id == ver_dep.version + : i->project_id == ver_dep.addonId); + }); + dep != m_mods.end()) + continue; // check the existing mods + + if (auto dep = std::find_if(m_pack_dependencies.begin(), + m_pack_dependencies.end(), + [&ver_dep, providerName, isOnlyVersion](std::shared_ptr<PackDependency> i) + { + return i->pack->provider == providerName + && (isOnlyVersion ? i->version.version == ver_dep.addonId + : i->pack->addonId == ver_dep.addonId); + }); + dep != m_pack_dependencies.end()) // check loaded dependencies + continue; + + c_dependencies.append(ver_dep); + } + return c_dependencies; +} + +Task::Ptr GetModDependenciesTask::getProjectInfoTask(std::shared_ptr<PackDependency> pDep) +{ + auto provider = pDep->pack->provider; + auto responseInfo = std::make_shared<QByteArray>(); + auto info = getAPI(provider)->getProject(pDep->pack->addonId.toString(), responseInfo); + connect(info.get(), + &NetJob::succeeded, + [this, responseInfo, provider, pDep] + { + QJsonParseError parse_error{}; + QJsonDocument doc = QJsonDocument::fromJson(*responseInfo, &parse_error); + if (parse_error.error != QJsonParseError::NoError) + { + removePack(pDep->pack->addonId); + qWarning() << "Error while parsing JSON response for mod info at " << parse_error.offset + << " reason: " << parse_error.errorString(); + qDebug() << *responseInfo; + return; + } + try + { + auto obj = provider == ModPlatform::ResourceProvider::FLAME + ? Json::requireObject(Json::requireObject(doc), "data") + : Json::requireObject(doc); + + getAPI(provider)->loadIndexedPack(*pDep->pack, obj); + } + catch (const JSONValidationError& e) + { + removePack(pDep->pack->addonId); + qDebug() << doc; + qWarning() << "Error while reading mod info: " << e.cause(); + } + }); + return info; +} + +Task::Ptr GetModDependenciesTask::prepareDependencyTask(const ModPlatform::Dependency& dep, + const ModPlatform::ResourceProvider providerName, + int level) +{ + auto pDep = std::make_shared<PackDependency>(); + pDep->dependency = dep; + pDep->pack = std::make_shared<ModPlatform::IndexedPack>(); + pDep->pack->addonId = dep.addonId; + pDep->pack->provider = providerName; + + m_pack_dependencies.append(pDep); + + auto provider = providerName; + + auto tasks = makeShared<SequentialTask>( + QString("DependencyInfo: %1").arg(dep.addonId.toString().isEmpty() ? dep.version : dep.addonId.toString())); + + if (!dep.addonId.toString().isEmpty()) + { + tasks->addTask(getProjectInfoTask(pDep)); + } + + ResourceAPI::DependencySearchArgs args = { dep, m_version, m_loaderType }; + ResourceAPI::Callback<ModPlatform::IndexedVersion> callbacks; + callbacks.on_fail = [](QString reason, int) + { qCritical() << tr("A network error occurred. Could not load project dependencies:%1").arg(reason); }; + callbacks.on_succeed = [dep, provider, pDep, level, this](auto& pack) + { + pDep->version = pack; + if (!pDep->version.addonId.isValid()) + { + if (m_loaderType & ModPlatform::Quilt) + { // falback for quilt + auto overide = ModPlatform::getOverrideDeps(); + auto over = + std::find_if(overide.cbegin(), + overide.cend(), + [dep, provider](auto o) { return o.provider == provider && dep.addonId == o.quilt; }); + if (over != overide.cend()) + { + removePack(dep.addonId); + addTask(prepareDependencyTask({ over->fabric, dep.type }, provider, level)); + return; + } + } + removePack(dep.addonId); + return; + } + pDep->version.is_currently_selected = true; + pDep->pack->versions = { pDep->version }; + pDep->pack->versionsLoaded = true; + + if (level == 0) + { + removePack(dep.addonId); + qWarning() << "Dependency cycle exceeded"; + return; + } + if (dep.addonId.toString().isEmpty() && !pDep->version.addonId.toString().isEmpty()) + { + pDep->pack->addonId = pDep->version.addonId; + auto dep_ = getOverride({ pDep->version.addonId, pDep->dependency.type }, provider); + if (dep_.addonId != pDep->version.addonId) + { + removePack(pDep->version.addonId); + addTask(prepareDependencyTask(dep_, provider, level)); + } + else + { + addTask(getProjectInfoTask(pDep)); + } + } + if (isLocalyInstalled(pDep)) + { + removePack(pDep->version.addonId); + return; + } + for (auto dep_ : getDependenciesForVersion(pDep->version, provider)) + { + addTask(prepareDependencyTask(dep_, provider, level - 1)); + } + }; + + auto version = getAPI(provider)->getDependencyVersion(std::move(args), std::move(callbacks)); + tasks->addTask(version); + return tasks; +} + +void GetModDependenciesTask::removePack(const QVariant& addonId) +{ + auto pred = [addonId](const std::shared_ptr<PackDependency>& v) { return v->pack->addonId == addonId; }; +#if QT_VERSION >= QT_VERSION_CHECK(6, 1, 0) + m_pack_dependencies.removeIf(pred); +#else + for (auto it = m_pack_dependencies.begin(); it != m_pack_dependencies.end();) + if (pred(*it)) + it = m_pack_dependencies.erase(it); + else + ++it; +#endif +} + +auto GetModDependenciesTask::getExtraInfo() -> QHash<QString, PackDependencyExtraInfo> +{ + QHash<QString, PackDependencyExtraInfo> rby; + auto fullList = m_selected + m_pack_dependencies; + for (auto& mod : fullList) + { + auto addonId = mod->pack->addonId; + auto provider = mod->pack->provider; + auto version = mod->version.fileId; + auto req = QStringList(); + for (auto& smod : fullList) + { + if (provider != smod->pack->provider) + continue; + auto deps = smod->version.dependencies; + if (auto dep = std::find_if(deps.begin(), + deps.end(), + [addonId, provider, version](const ModPlatform::Dependency& d) + { + return d.type == ModPlatform::DependencyType::REQUIRED + && (provider == ModPlatform::ResourceProvider::MODRINTH + && d.addonId.toString().isEmpty() + ? version == d.version + : d.addonId == addonId); + }); + dep != deps.end()) + { + req.append(smod->pack->name); + } + } + rby[addonId.toString()] = { maybeInstalled(mod), req }; + } + return rby; +} + +// super lax compare (but not fuzzy) +// convert to lowercase +// convert all speratores to whitespace +// simplify sequence of internal whitespace to a single space +// efectivly compare two strings ignoring all separators and case +auto laxCompare = [](QString fsfilename, QString metadataFilename, bool excludeDigits = false) +{ + // allowed character seperators + QList<QChar> allowedSeperators = { '-', '+', '.', '_' }; + if (excludeDigits) + allowedSeperators.append({ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' }); + + // copy in lowercase + auto fsName = fsfilename.toLower(); + auto metaName = metadataFilename.toLower(); + + // replace all potential allowed seperatores with whitespace + for (auto sep : allowedSeperators) + { + fsName = fsName.replace(sep, ' '); + metaName = metaName.replace(sep, ' '); + } + + // remove extraneous whitespace + fsName = fsName.simplified(); + metaName = metaName.simplified(); + + return fsName.compare(metaName) == 0; +}; + +bool GetModDependenciesTask::isLocalyInstalled(std::shared_ptr<PackDependency> pDep) +{ + return pDep->version.fileName.isEmpty() || + + std::find_if( + m_selected.begin(), + m_selected.end(), + [pDep](std::shared_ptr<PackDependency> i) + { return !i->version.fileName.isEmpty() && laxCompare(i->version.fileName, pDep->version.fileName); }) + != m_selected.end() + || // check the selected versions + + std::find_if(m_mods_file_names.begin(), + m_mods_file_names.end(), + [pDep](QString i) { return !i.isEmpty() && laxCompare(i, pDep->version.fileName); }) + != m_mods_file_names.end() + || // check the existing mods + + std::find_if(m_pack_dependencies.begin(), + m_pack_dependencies.end(), + [pDep](std::shared_ptr<PackDependency> i) + { + return pDep->pack->addonId != i->pack->addonId && !i->version.fileName.isEmpty() + && laxCompare(pDep->version.fileName, i->version.fileName); + }) + != m_pack_dependencies.end(); // check loaded dependencies +} + +bool GetModDependenciesTask::maybeInstalled(std::shared_ptr<PackDependency> pDep) +{ + return std::find_if(m_mods_file_names.begin(), + m_mods_file_names.end(), + [pDep](QString i) { return !i.isEmpty() && laxCompare(i, pDep->version.fileName, true); }) + != m_mods_file_names.end(); // check the existing mods +} diff --git a/archived/projt-launcher/launcher/minecraft/mod/tasks/GetModDependenciesTask.hpp b/archived/projt-launcher/launcher/minecraft/mod/tasks/GetModDependenciesTask.hpp new file mode 100644 index 0000000000..39fdb95c55 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/tasks/GetModDependenciesTask.hpp @@ -0,0 +1,126 @@ +// 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) 2023 Trial97 <alexandru.tripon97@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. + * + * ======================================================================== */ + +#pragma once + +#include <QDir> +#include <QList> +#include <QVariant> +#include <functional> +#include <memory> + +#include "minecraft/mod/MetadataHandler.hpp" +#include "minecraft/mod/ModFolderModel.hpp" +#include "modplatform/ModIndex.h" +#include "modplatform/ResourceAPI.h" +#include "modplatform/flame/FlameAPI.h" +#include "modplatform/modrinth/ModrinthAPI.h" +#include "tasks/SequentialTask.h" +#include "tasks/Task.h" +#include "ui/pages/modplatform/ModModel.h" + +class GetModDependenciesTask : public SequentialTask +{ + Q_OBJECT + public: + using Ptr = shared_qobject_ptr<GetModDependenciesTask>; + + struct PackDependency + { + ModPlatform::Dependency dependency; + ModPlatform::IndexedPack::Ptr pack; + ModPlatform::IndexedVersion version; + PackDependency() = default; + PackDependency(const ModPlatform::IndexedPack::Ptr p, const ModPlatform::IndexedVersion& v) + { + pack = p; + version = v; + } + }; + + struct PackDependencyExtraInfo + { + bool maybe_installed; + QStringList required_by; + }; + + explicit GetModDependenciesTask(BaseInstance* instance, + ModFolderModel* folder, + QList<std::shared_ptr<PackDependency>> selected); + + auto getDependecies() const -> QList<std::shared_ptr<PackDependency>> + { + return m_pack_dependencies; + } + QHash<QString, PackDependencyExtraInfo> getExtraInfo(); + + private: + inline ResourceAPI* getAPI(ModPlatform::ResourceProvider provider) + { + if (provider == ModPlatform::ResourceProvider::FLAME) + return &m_flameAPI; + else + return &m_modrinthAPI; + } + + protected slots: + Task::Ptr prepareDependencyTask(const ModPlatform::Dependency&, ModPlatform::ResourceProvider, int); + QList<ModPlatform::Dependency> getDependenciesForVersion(const ModPlatform::IndexedVersion&, + ModPlatform::ResourceProvider providerName); + void prepare(); + Task::Ptr getProjectInfoTask(std::shared_ptr<PackDependency> pDep); + ModPlatform::Dependency getOverride(const ModPlatform::Dependency&, ModPlatform::ResourceProvider providerName); + void removePack(const QVariant& addonId); + + bool isLocalyInstalled(std::shared_ptr<PackDependency> pDep); + bool maybeInstalled(std::shared_ptr<PackDependency> pDep); + + private: + QList<std::shared_ptr<PackDependency>> m_pack_dependencies; + QList<std::shared_ptr<Metadata::ModStruct>> m_mods; + QList<std::shared_ptr<PackDependency>> m_selected; + QStringList m_mods_file_names; + + Version m_version; + ModPlatform::ModLoaderTypes m_loaderType; + + ModrinthAPI m_modrinthAPI; + FlameAPI m_flameAPI; +}; diff --git a/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp b/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp new file mode 100644 index 0000000000..f2e45edc93 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalDataPackParseTask.cpp @@ -0,0 +1,461 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// 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 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, 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 "LocalDataPackParseTask.hpp" + +#include "FileSystem.h" +#include "Json.h" +#include "minecraft/mod/ResourcePack.hpp" + +#include <quazip/quazip.h> +#include <quazip/quazipdir.h> +#include <quazip/quazipfile.h> + +#include <QCryptographicHash> + +namespace DataPackUtils +{ + + bool process(DataPack* pack, ProcessingLevel level) + { + switch (pack->type()) + { + case ResourceType::FOLDER: return DataPackUtils::processFolder(pack, level); + case ResourceType::ZIPFILE: return DataPackUtils::processZIP(pack, level); + default: qWarning() << "Invalid type for data pack parse task!"; return false; + } + } + + bool processFolder(DataPack* pack, ProcessingLevel level) + { + Q_ASSERT(pack->type() == ResourceType::FOLDER); + + auto mcmeta_invalid = [&pack]() + { + qWarning() << "Data pack at" << pack->fileinfo().filePath() << "does not have a valid pack.mcmeta"; + return false; // the mcmeta is not optional + }; + + QFileInfo mcmeta_file_info(FS::PathCombine(pack->fileinfo().filePath(), "pack.mcmeta")); + if (mcmeta_file_info.exists() && mcmeta_file_info.isFile()) + { + QFile mcmeta_file(mcmeta_file_info.filePath()); + if (!mcmeta_file.open(QIODevice::ReadOnly)) + return mcmeta_invalid(); // can't open mcmeta file + + auto data = mcmeta_file.readAll(); + + bool mcmeta_result = DataPackUtils::processMCMeta(pack, std::move(data)); + + mcmeta_file.close(); + if (!mcmeta_result) + { + return mcmeta_invalid(); // mcmeta invalid + } + } + else + { + return mcmeta_invalid(); // mcmeta file isn't a valid file + } + + if (level == ProcessingLevel::BasicInfoOnly) + { + return true; // only need basic info already checked + } + auto png_invalid = [&pack]() + { + qWarning() << "Data pack at" << pack->fileinfo().filePath() << "does not have a valid pack.png"; + return true; // the png is optional + }; + + QFileInfo image_file_info(FS::PathCombine(pack->fileinfo().filePath(), "pack.png")); + if (image_file_info.exists() && image_file_info.isFile()) + { + QFile pack_png_file(image_file_info.filePath()); + if (!pack_png_file.open(QIODevice::ReadOnly)) + return png_invalid(); // can't open pack.png file + + auto data = pack_png_file.readAll(); + + bool pack_png_result = DataPackUtils::processPackPNG(pack, std::move(data)); + + pack_png_file.close(); + if (!pack_png_result) + { + return png_invalid(); // pack.png invalid + } + } + else + { + return png_invalid(); // pack.png does not exists or is not a valid file. + } + + return true; // all tests passed + } + + bool processZIP(DataPack* pack, ProcessingLevel level) + { + Q_ASSERT(pack->type() == ResourceType::ZIPFILE); + + QuaZip zip(pack->fileinfo().filePath()); + if (!zip.open(QuaZip::mdUnzip)) + return false; // can't open zip file + + QuaZipFile file(&zip); + + auto mcmeta_invalid = [&pack]() + { + qWarning() << "Data pack at" << pack->fileinfo().filePath() << "does not have a valid pack.mcmeta"; + return false; // the mcmeta is not optional + }; + + if (zip.setCurrentFile("pack.mcmeta")) + { + if (!file.open(QIODevice::ReadOnly)) + { + qCritical() << "Failed to open file in zip."; + zip.close(); + return mcmeta_invalid(); + } + + auto data = file.readAll(); + + bool mcmeta_result = DataPackUtils::processMCMeta(pack, std::move(data)); + + file.close(); + if (!mcmeta_result) + { + return mcmeta_invalid(); // mcmeta invalid + } + } + else + { + return mcmeta_invalid(); // could not set pack.mcmeta as current file. + } + + if (level == ProcessingLevel::BasicInfoOnly) + { + zip.close(); + return true; // only need basic info already checked + } + + auto png_invalid = [&pack]() + { + qWarning() << "Data pack at" << pack->fileinfo().filePath() << "does not have a valid pack.png"; + return true; // the png is optional + }; + + if (zip.setCurrentFile("pack.png")) + { + if (!file.open(QIODevice::ReadOnly)) + { + qCritical() << "Failed to open file in zip."; + zip.close(); + return png_invalid(); + } + + auto data = file.readAll(); + + bool pack_png_result = DataPackUtils::processPackPNG(pack, std::move(data)); + + file.close(); + zip.close(); + if (!pack_png_result) + { + return png_invalid(); // pack.png invalid + } + } + else + { + zip.close(); + return png_invalid(); // could not set pack.mcmeta as current file. + } + zip.close(); + + return true; + } + + // https://minecraft.wiki/w/Data_pack#pack.mcmeta + // https://minecraft.wiki/w/Raw_JSON_text_format + // https://minecraft.wiki/w/Tutorials/Creating_a_resource_pack#Formatting_pack.mcmeta + bool processMCMeta(DataPack* pack, QByteArray&& raw_data) + { + try + { + auto json_doc = QJsonDocument::fromJson(raw_data); + auto pack_obj = Json::requireObject(json_doc.object(), "pack", {}); + + pack->setPackFormat(Json::ensureInteger(pack_obj, "pack_format", 0)); + pack->setDescription(DataPackUtils::processComponent(pack_obj.value("description"))); + } + catch (Json::JsonException& e) + { + qWarning() << "JsonException: " << e.what() << e.cause(); + return false; + } + return true; + } + + QString buildStyle(const QJsonObject& obj) + { + QStringList styles; + if (auto color = Json::ensureString(obj, "color"); !color.isEmpty()) + { + styles << QString("color: %1;").arg(color); + } + if (obj.contains("bold")) + { + QString weight = "normal"; + if (Json::ensureBoolean(obj, "bold", false)) + { + weight = "bold"; + } + styles << QString("font-weight: %1;").arg(weight); + } + if (obj.contains("italic")) + { + QString style = "normal"; + if (Json::ensureBoolean(obj, "italic", false)) + { + style = "italic"; + } + styles << QString("font-style: %1;").arg(style); + } + + return styles.isEmpty() ? "" : QString("style=\"%1\"").arg(styles.join(" ")); + } + + QString processComponent(const QJsonArray& value, bool strikethrough, bool underline) + { + QString result; + for (auto current : value) + result += processComponent(current, strikethrough, underline); + return result; + } + + QString processComponent(const QJsonObject& obj, bool strikethrough, bool underline) + { + underline = Json::ensureBoolean(obj, "underlined", underline); + strikethrough = Json::ensureBoolean(obj, "strikethrough", strikethrough); + + QString result = Json::ensureString(obj, "text"); + if (underline) + { + result = QString("<u>%1</u>").arg(result); + } + if (strikethrough) + { + result = QString("<s>%1</s>").arg(result); + } + // the extra needs to be a array + result += processComponent(Json::ensureArray(obj, "extra"), strikethrough, underline); + if (auto style = buildStyle(obj); !style.isEmpty()) + { + result = QString("<span %1>%2</span>").arg(style, result); + } + if (obj.contains("clickEvent")) + { + auto click_event = Json::ensureObject(obj, "clickEvent"); + auto action = Json::ensureString(click_event, "action"); + auto value = Json::ensureString(click_event, "value"); + if (action == "open_url" && !value.isEmpty()) + { + result = QString("<a href=\"%1\">%2</a>").arg(value, result); + } + } + return result; + } + + QString processComponent(const QJsonValue& value, bool strikethrough, bool underline) + { + if (value.isString()) + { + return value.toString(); + } + if (value.isBool()) + { + return value.toBool() ? "true" : "false"; + } + if (value.isDouble()) + { + return QString::number(value.toDouble()); + } + if (value.isArray()) + { + return processComponent(value.toArray(), strikethrough, underline); + } + if (value.isObject()) + { + return processComponent(value.toObject(), strikethrough, underline); + } + qWarning() << "Invalid component type!"; + return {}; + } + + bool processPackPNG(const DataPack* pack, QByteArray&& raw_data) + { + auto img = QImage::fromData(raw_data); + if (!img.isNull()) + { + pack->setImage(img); + } + else + { + qWarning() << "Failed to parse pack.png."; + return false; + } + return true; + } + + bool processPackPNG(const DataPack* pack) + { + auto png_invalid = [&pack]() + { + qWarning() << "Data pack at" << pack->fileinfo().filePath() << "does not have a valid pack.png"; + return false; + }; + + switch (pack->type()) + { + case ResourceType::FOLDER: + { + QFileInfo image_file_info(FS::PathCombine(pack->fileinfo().filePath(), "pack.png")); + if (image_file_info.exists() && image_file_info.isFile()) + { + QFile pack_png_file(image_file_info.filePath()); + if (!pack_png_file.open(QIODevice::ReadOnly)) + return png_invalid(); // can't open pack.png file + + auto data = pack_png_file.readAll(); + + bool pack_png_result = DataPackUtils::processPackPNG(pack, std::move(data)); + + pack_png_file.close(); + if (!pack_png_result) + { + return png_invalid(); // pack.png invalid + } + } + else + { + return png_invalid(); // pack.png does not exists or is not a valid file. + } + return false; + } + case ResourceType::ZIPFILE: + { + QuaZip zip(pack->fileinfo().filePath()); + if (!zip.open(QuaZip::mdUnzip)) + return false; // can't open zip file + + QuaZipFile file(&zip); + if (zip.setCurrentFile("pack.png")) + { + if (!file.open(QIODevice::ReadOnly)) + { + qCritical() << "Failed to open file in zip."; + zip.close(); + return png_invalid(); + } + + auto data = file.readAll(); + + bool pack_png_result = DataPackUtils::processPackPNG(pack, std::move(data)); + + file.close(); + if (!pack_png_result) + { + return png_invalid(); // pack.png invalid + } + } + else + { + return png_invalid(); // could not set pack.mcmeta as current file. + } + return false; + } + default: qWarning() << "Invalid type for data pack parse task!"; return false; + } + } + + bool validate(QFileInfo file) + { + DataPack dp{ file }; + return DataPackUtils::process(&dp, ProcessingLevel::BasicInfoOnly) && dp.valid(); + } + + bool validateResourcePack(QFileInfo file) + { + ResourcePack rp{ file }; + return DataPackUtils::process(&rp, ProcessingLevel::BasicInfoOnly) && rp.valid(); + } + +} // namespace DataPackUtils + +LocalDataPackParseTask::LocalDataPackParseTask(int token, DataPack* dp) : Task(false), m_token(token), m_data_pack(dp) +{} + +void LocalDataPackParseTask::executeTask() +{ + if (!DataPackUtils::process(m_data_pack)) + { + emitFailed("process failed"); + return; + } + + emitSucceeded(); +}
\ No newline at end of file diff --git a/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalDataPackParseTask.hpp b/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalDataPackParseTask.hpp new file mode 100644 index 0000000000..aca6d76595 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalDataPackParseTask.hpp @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// 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 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, 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. + * + * ======================================================================== */ + +#pragma once + +#include <QDebug> +#include <QObject> + +#include "minecraft/mod/DataPack.hpp" + +#include "tasks/Task.h" + +namespace DataPackUtils +{ + + enum class ProcessingLevel + { + Full, + BasicInfoOnly + }; + + bool process(DataPack* pack, ProcessingLevel level = ProcessingLevel::Full); + + bool processZIP(DataPack* pack, ProcessingLevel level = ProcessingLevel::Full); + bool processFolder(DataPack* pack, ProcessingLevel level = ProcessingLevel::Full); + + bool processMCMeta(DataPack* pack, QByteArray&& raw_data); + + QString processComponent(const QJsonValue& value, bool strikethrough = false, bool underline = false); + + bool processPackPNG(const DataPack* pack, QByteArray&& raw_data); + + /// processes ONLY the pack.png (rest of the pack may be invalid) + bool processPackPNG(const DataPack* pack); + + /** Checks whether a file is valid as a data pack or not. */ + bool validate(QFileInfo file); + + /** Checks whether a file is valid as a resource pack or not. */ + bool validateResourcePack(QFileInfo file); + +} // namespace DataPackUtils + +class LocalDataPackParseTask : public Task +{ + Q_OBJECT + public: + LocalDataPackParseTask(int token, DataPack* dp); + + void executeTask() override; + + int token() const + { + return m_token; + } + + private: + int m_token; + + DataPack* m_data_pack; +};
\ No newline at end of file diff --git a/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalModParseTask.cpp b/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalModParseTask.cpp new file mode 100644 index 0000000000..2d74df5565 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalModParseTask.cpp @@ -0,0 +1,942 @@ +// 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. + */ +#include "LocalModParseTask.hpp" + +#include <qdcss.h> +#include <quazip/quazip.h> +#include <quazip/quazipfile.h> +#include <toml++/toml.h> +#include <QJsonArray> +#include <QJsonDocument> +#include <QJsonObject> +#include <QJsonValue> +#include <QRegularExpression> +#include <QString> + +#include "FileSystem.h" +#include "Json.h" +#include "minecraft/mod/ModDetails.hpp" +#include "settings/INIFile.h" + +static const QRegularExpression s_newlineRegex("\r\n|\n|\r"); + +namespace ModUtils +{ + + // NEW format + // https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/c8d8f1929aff9979e322af79a59ce81f3e02db6a + + // OLD format: + // https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/5bf6a2d05145ec79387acc0d45c958642fb049fc + ModDetails ReadMCModInfo(QByteArray contents) + { + auto getInfoFromArray = [](QJsonArray arr) -> ModDetails + { + if (!arr.at(0).isObject()) + { + return {}; + } + ModDetails details; + auto firstObj = arr.at(0).toObject(); + details.mod_id = firstObj.value("modid").toString(); + auto name = firstObj.value("name").toString(); + // NOTE: ignore stupid example mods copies where the author didn't even bother to change the name + if (name != "Example Mod") + { + details.name = name; + } + details.version = firstObj.value("version").toString(); + auto homeurl = firstObj.value("url").toString().trimmed(); + if (!homeurl.isEmpty()) + { + // fix up url. + if (!homeurl.startsWith("http://") && !homeurl.startsWith("https://") && !homeurl.startsWith("ftp://")) + { + homeurl.prepend("http://"); + } + } + details.homeurl = homeurl; + details.description = firstObj.value("description").toString(); + QJsonArray authors = firstObj.value("authorList").toArray(); + if (authors.size() == 0) + { + // NOTE: 'authors' is used as a fallback or alternative key for 'authorList' in some metadata formats. + authors = firstObj.value("authors").toArray(); + } + + if (firstObj.contains("logoFile")) + { + details.icon_file = firstObj.value("logoFile").toString(); + } + + for (auto author : authors) + { + details.authors.append(author.toString()); + } + return details; + }; + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); + // this is the very old format that had just the array + if (jsonDoc.isArray()) + { + return getInfoFromArray(jsonDoc.array()); + } + else if (jsonDoc.isObject()) + { + auto val = jsonDoc.object().value("modinfoversion"); + if (val.isUndefined()) + { + val = jsonDoc.object().value("modListVersion"); + } + + int version = Json::ensureInteger(val, -1); + + // Some mods set the number with "", so it's a String instead + if (version < 0) + version = Json::ensureString(val, "").toInt(); + + if (version != 2) + { + qWarning() << QString( + R"(The value of 'modListVersion' is "%1" (expected "2")! The file may be corrupted.)") + .arg(version); + qWarning() << "The contents of 'mcmod.info' are as follows:"; + qWarning() << contents; + } + + auto arrVal = jsonDoc.object().value("modlist"); + if (arrVal.isUndefined()) + { + arrVal = jsonDoc.object().value("modList"); + } + if (arrVal.isArray()) + { + return getInfoFromArray(arrVal.toArray()); + } + } + return {}; + } + + // https://github.com/MinecraftForge/Documentation/blob/5ab4ba6cf9abc0ac4c0abd96ad187461aefd72af/docs/gettingstarted/structuring.md + ModDetails ReadMCModTOML(QByteArray contents) + { + ModDetails details; + + toml::table tomlData; +#if TOML_EXCEPTIONS + try + { + tomlData = toml::parse(contents.toStdString()); + } + catch ([[maybe_unused]] const toml::parse_error& err) + { + return {}; + } +#else + toml::parse_result result = toml::parse(contents.toStdString()); + if (!result) + { + return {}; + } + tomlData = result.table(); +#endif + + // array defined by [[mods]] + auto tomlModsArr = tomlData["mods"].as_array(); + if (!tomlModsArr) + { + qWarning() << "Corrupted mods.toml? Couldn't find [[mods]] array!"; + return {}; + } + + // we only really care about the first element, since multiple mods in one file is not supported by us at the + // moment + auto tomlModsTable0 = tomlModsArr->get(0); + if (!tomlModsTable0) + { + qWarning() << "Corrupted mods.toml? [[mods]] didn't have an element at index 0!"; + return {}; + } + auto modsTable = tomlModsTable0->as_table(); + if (!modsTable) + { + qWarning() << "Corrupted mods.toml? [[mods]] was not a table!"; + return {}; + } + + // mandatory properties - always in [[mods]] + if (auto modIdDatum = (*modsTable)["modId"].as_string()) + { + details.mod_id = QString::fromStdString(modIdDatum->get()); + } + if (auto versionDatum = (*modsTable)["version"].as_string()) + { + details.version = QString::fromStdString(versionDatum->get()); + } + if (auto displayNameDatum = (*modsTable)["displayName"].as_string()) + { + details.name = QString::fromStdString(displayNameDatum->get()); + } + if (auto descriptionDatum = (*modsTable)["description"].as_string()) + { + details.description = QString::fromStdString(descriptionDatum->get()); + } + + // optional properties - can be in the root table or [[mods]] + QString authors = ""; + if (auto authorsDatum = tomlData["authors"].as_string()) + { + authors = QString::fromStdString(authorsDatum->get()); + } + else if (auto authorsDatumMods = (*modsTable)["authors"].as_string()) + { + authors = QString::fromStdString(authorsDatumMods->get()); + } + if (!authors.isEmpty()) + { + details.authors.append(authors); + } + + QString homeurl = ""; + if (auto homeurlDatum = tomlData["displayURL"].as_string()) + { + homeurl = QString::fromStdString(homeurlDatum->get()); + } + else if (auto homeurlDatumMods = (*modsTable)["displayURL"].as_string()) + { + homeurl = QString::fromStdString(homeurlDatumMods->get()); + } + // fix up url. + if (!homeurl.isEmpty() && !homeurl.startsWith("http://") && !homeurl.startsWith("https://") + && !homeurl.startsWith("ftp://")) + { + homeurl.prepend("http://"); + } + details.homeurl = homeurl; + + QString issueTrackerURL = ""; + if (auto issueTrackerURLDatum = tomlData["issueTrackerURL"].as_string()) + { + issueTrackerURL = QString::fromStdString(issueTrackerURLDatum->get()); + } + else if (auto issueTrackerURLDatumMods = (*modsTable)["issueTrackerURL"].as_string()) + { + issueTrackerURL = QString::fromStdString(issueTrackerURLDatumMods->get()); + } + details.issue_tracker = issueTrackerURL; + + QString license = ""; + if (auto licenseDatum = tomlData["license"].as_string()) + { + license = QString::fromStdString(licenseDatum->get()); + } + else if (auto licenseDatumMods = (*modsTable)["license"].as_string()) + { + license = QString::fromStdString(licenseDatumMods->get()); + } + if (!license.isEmpty()) + details.licenses.append(ModLicense(license)); + + QString logoFile = ""; + if (auto logoFileDatum = tomlData["logoFile"].as_string()) + { + logoFile = QString::fromStdString(logoFileDatum->get()); + } + else if (auto logoFileDatumMods = (*modsTable)["logoFile"].as_string()) + { + logoFile = QString::fromStdString(logoFileDatumMods->get()); + } + details.icon_file = logoFile; + + return details; + } + + // https://fabricmc.net/wiki/documentation:fabric_mod_json + ModDetails ReadFabricModInfo(QByteArray contents) + { + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); + auto object = jsonDoc.object(); + auto schemaVersion = object.contains("schemaVersion") ? object.value("schemaVersion").toInt(0) : 0; + + ModDetails details; + + details.mod_id = object.value("id").toString(); + details.version = object.value("version").toString(); + + details.name = object.contains("name") ? object.value("name").toString() : details.mod_id; + details.description = object.value("description").toString(); + + if (schemaVersion >= 1) + { + QJsonArray authors = object.value("authors").toArray(); + for (auto author : authors) + { + if (author.isObject()) + { + details.authors.append(author.toObject().value("name").toString()); + } + else + { + details.authors.append(author.toString()); + } + } + + if (object.contains("contact")) + { + QJsonObject contact = object.value("contact").toObject(); + + if (contact.contains("homepage")) + { + details.homeurl = contact.value("homepage").toString(); + } + if (contact.contains("issues")) + { + details.issue_tracker = contact.value("issues").toString(); + } + } + + if (object.contains("license")) + { + auto license = object.value("license"); + if (license.isArray()) + { + for (auto l : license.toArray()) + { + if (l.isString()) + { + details.licenses.append(ModLicense(l.toString())); + } + else if (l.isObject()) + { + auto obj = l.toObject(); + details.licenses.append(ModLicense(obj.value("name").toString(), + obj.value("id").toString(), + obj.value("url").toString(), + obj.value("description").toString())); + } + } + } + else if (license.isString()) + { + details.licenses.append(ModLicense(license.toString())); + } + else if (license.isObject()) + { + auto obj = license.toObject(); + details.licenses.append(ModLicense(obj.value("name").toString(), + obj.value("id").toString(), + obj.value("url").toString(), + obj.value("description").toString())); + } + } + + if (object.contains("icon")) + { + auto icon = object.value("icon"); + if (icon.isObject()) + { + auto obj = icon.toObject(); + // take the largest icon + int largest = 0; + for (auto key : obj.keys()) + { + auto size = key.split('x').first().toInt(); + if (size > largest) + { + largest = size; + } + } + if (largest > 0) + { + auto key = QString::number(largest) + "x" + QString::number(largest); + details.icon_file = obj.value(key).toString(); + } + else + { // parsing the sizes failed + // take the first + if (auto it = obj.begin(); it != obj.end()) + { + details.icon_file = it->toString(); + } + } + } + else if (icon.isString()) + { + details.icon_file = icon.toString(); + } + } + } + return details; + } + + // https://github.com/QuiltMC/rfcs/blob/master/specification/0002-quilt.mod.json.md + ModDetails ReadQuiltModInfo(QByteArray contents) + { + ModDetails details; + try + { + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); + auto object = Json::requireObject(jsonDoc, "quilt.mod.json"); + auto schemaVersion = Json::ensureInteger(object.value("schema_version"), 0, "Quilt schema_version"); + + // https://github.com/QuiltMC/rfcs/blob/be6ba280d785395fefa90a43db48e5bfc1d15eb4/specification/0002-quilt.mod.json.md + if (schemaVersion == 1) + { + auto modInfo = Json::requireObject(object.value("quilt_loader"), "Quilt mod info"); + + details.mod_id = Json::requireString(modInfo.value("id"), "Mod ID"); + details.version = Json::requireString(modInfo.value("version"), "Mod version"); + + auto modMetadata = Json::ensureObject(modInfo.value("metadata")); + + details.name = Json::ensureString(modMetadata.value("name"), details.mod_id); + details.description = Json::ensureString(modMetadata.value("description")); + + auto modContributors = Json::ensureObject(modMetadata.value("contributors")); + + // We don't really care about the role of a contributor here + details.authors += modContributors.keys(); + + auto modContact = Json::ensureObject(modMetadata.value("contact")); + + if (modContact.contains("homepage")) + { + details.homeurl = Json::requireString(modContact.value("homepage")); + } + if (modContact.contains("issues")) + { + details.issue_tracker = Json::requireString(modContact.value("issues")); + } + + if (modMetadata.contains("license")) + { + auto license = modMetadata.value("license"); + if (license.isArray()) + { + for (auto l : license.toArray()) + { + if (l.isString()) + { + details.licenses.append(ModLicense(l.toString())); + } + else if (l.isObject()) + { + auto obj = l.toObject(); + details.licenses.append(ModLicense(obj.value("name").toString(), + obj.value("id").toString(), + obj.value("url").toString(), + obj.value("description").toString())); + } + } + } + else if (license.isString()) + { + details.licenses.append(ModLicense(license.toString())); + } + else if (license.isObject()) + { + auto obj = license.toObject(); + details.licenses.append(ModLicense(obj.value("name").toString(), + obj.value("id").toString(), + obj.value("url").toString(), + obj.value("description").toString())); + } + } + + if (modMetadata.contains("icon")) + { + auto icon = modMetadata.value("icon"); + if (icon.isObject()) + { + auto obj = icon.toObject(); + // take the largest icon + int largest = 0; + for (auto key : obj.keys()) + { + auto size = key.split('x').first().toInt(); + if (size > largest) + { + largest = size; + } + } + if (largest > 0) + { + auto key = QString::number(largest) + "x" + QString::number(largest); + details.icon_file = obj.value(key).toString(); + } + else + { // parsing the sizes failed + // take the first + if (auto it = obj.begin(); it != obj.end()) + { + details.icon_file = it->toString(); + } + } + } + else if (icon.isString()) + { + details.icon_file = icon.toString(); + } + } + } + } + catch (const Exception& e) + { + qWarning() << "Unable to parse mod info:" << e.cause(); + } + return details; + } + + ModDetails ReadForgeInfo(QByteArray contents) + { + ModDetails details; + // Read the data + details.name = "Minecraft Forge"; + details.mod_id = "Forge"; + details.homeurl = "http://www.minecraftforge.net/forum/"; + INIFile ini; + if (!ini.loadFile(contents)) + return details; + + QString major = ini.get("forge.major.number", "0").toString(); + QString minor = ini.get("forge.minor.number", "0").toString(); + QString revision = ini.get("forge.revision.number", "0").toString(); + QString build = ini.get("forge.build.number", "0").toString(); + + details.version = major + "." + minor + "." + revision + "." + build; + return details; + } + + ModDetails ReadLiteModInfo(QByteArray contents) + { + ModDetails details; + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); + auto object = jsonDoc.object(); + if (object.contains("name")) + { + details.mod_id = details.name = object.value("name").toString(); + } + if (object.contains("version")) + { + details.version = object.value("version").toString(""); + } + else + { + details.version = object.value("revision").toString(""); + } + details.mcversion = object.value("mcversion").toString(); + auto author = object.value("author").toString(); + if (!author.isEmpty()) + { + details.authors.append(author); + } + details.description = object.value("description").toString(); + details.homeurl = object.value("url").toString(); + return details; + } + + // https://git.sleeping.town/unascribed/NilLoader/src/commit/d7fc87b255fc31019ff90f80d45894927fac6efc/src/main/java/nilloader/api/NilMetadata.java#L64 + ModDetails ReadNilModInfo(QByteArray contents, QString fname) + { + ModDetails details; + + QDCSS cssData = QDCSS(contents); + auto name = cssData.get("@nilmod.name"); + auto desc = cssData.get("@nilmod.description"); + auto authors = cssData.get("@nilmod.authors"); + + if (name->has_value()) + { + details.name = name->value(); + } + if (desc->has_value()) + { + details.description = desc->value(); + } + if (authors->has_value()) + { + details.authors.append(authors->value()); + } + details.version = cssData.get("@nilmod.version")->value_or("?"); + + details.mod_id = fname.remove(".nilmod.css"); + + return details; + } + + bool process(Mod& mod, ProcessingLevel level) + { + switch (mod.type()) + { + case ResourceType::FOLDER: return processFolder(mod, level); + case ResourceType::ZIPFILE: return processZIP(mod, level); + case ResourceType::LITEMOD: return processLitemod(mod); + default: qWarning() << "Invalid type for mod parse task!"; return false; + } + } + + bool processZIP(Mod& mod, [[maybe_unused]] ProcessingLevel level) + { + ModDetails details; + + QuaZip zip(mod.fileinfo().filePath()); + if (!zip.open(QuaZip::mdUnzip)) + return false; + + QuaZipFile file(&zip); + + if (zip.setCurrentFile("META-INF/mods.toml") || zip.setCurrentFile("META-INF/neoforge.mods.toml")) + { + if (!file.open(QIODevice::ReadOnly)) + { + zip.close(); + return false; + } + + details = ReadMCModTOML(file.readAll()); + file.close(); + + // to replace ${file.jarVersion} with the actual version, as needed + if (details.version == "${file.jarVersion}") + { + if (zip.setCurrentFile("META-INF/MANIFEST.MF")) + { + if (!file.open(QIODevice::ReadOnly)) + { + zip.close(); + return false; + } + + // quick and dirty line-by-line parser + auto manifestLines = QString(file.readAll()).split(s_newlineRegex); + QString manifestVersion = ""; + for (auto& line : manifestLines) + { + if (line.startsWith("Implementation-Version: ", Qt::CaseInsensitive)) + { + manifestVersion = line.remove("Implementation-Version: ", Qt::CaseInsensitive); + break; + } + } + + // some mods use ${projectversion} in their build.gradle, causing this mess to show up in + // MANIFEST.MF also keep with forge's behavior of setting the version to "NONE" if none is found + if (manifestVersion.contains("task ':jar' property 'archiveVersion'") || manifestVersion == "") + { + manifestVersion = "NONE"; + } + + details.version = manifestVersion; + + file.close(); + } + } + + zip.close(); + mod.setDetails(details); + + return true; + } + else if (zip.setCurrentFile("mcmod.info")) + { + if (!file.open(QIODevice::ReadOnly)) + { + zip.close(); + return false; + } + + details = ReadMCModInfo(file.readAll()); + file.close(); + zip.close(); + + mod.setDetails(details); + return true; + } + else if (zip.setCurrentFile("quilt.mod.json")) + { + if (!file.open(QIODevice::ReadOnly)) + { + zip.close(); + return false; + } + + details = ReadQuiltModInfo(file.readAll()); + file.close(); + zip.close(); + + mod.setDetails(details); + return true; + } + else if (zip.setCurrentFile("fabric.mod.json")) + { + if (!file.open(QIODevice::ReadOnly)) + { + zip.close(); + return false; + } + + details = ReadFabricModInfo(file.readAll()); + file.close(); + zip.close(); + + mod.setDetails(details); + return true; + } + else if (zip.setCurrentFile("forgeversion.properties")) + { + if (!file.open(QIODevice::ReadOnly)) + { + zip.close(); + return false; + } + + details = ReadForgeInfo(file.readAll()); + file.close(); + zip.close(); + + mod.setDetails(details); + return true; + } + else if (zip.setCurrentFile("META-INF/nil/mappings.json")) + { + // nilloader uses the filename of the metadata file for the modid, so we can't know the exact filename + // thankfully, there is a good file to use as a canary so we don't look for nil meta all the time + + QString foundNilMeta; + for (auto& fname : zip.getFileNameList()) + { + // nilmods can shade nilloader to be able to run as a standalone agent - which includes nilloader's own + // meta file + if (fname.endsWith(".nilmod.css") && fname != "nilloader.nilmod.css") + { + foundNilMeta = fname; + break; + } + } + + if (zip.setCurrentFile(foundNilMeta)) + { + if (!file.open(QIODevice::ReadOnly)) + { + zip.close(); + return false; + } + + details = ReadNilModInfo(file.readAll(), foundNilMeta); + file.close(); + zip.close(); + + mod.setDetails(details); + return true; + } + } + + zip.close(); + return false; // no valid mod found in archive + } + + bool processFolder(Mod& mod, [[maybe_unused]] ProcessingLevel level) + { + ModDetails details; + + QFileInfo mcmod_info(FS::PathCombine(mod.fileinfo().filePath(), "mcmod.info")); + if (mcmod_info.exists() && mcmod_info.isFile()) + { + QFile mcmod(mcmod_info.filePath()); + if (!mcmod.open(QIODevice::ReadOnly)) + return false; + auto data = mcmod.readAll(); + if (data.isEmpty() || data.isNull()) + return false; + details = ReadMCModInfo(data); + + mod.setDetails(details); + return true; + } + + return false; // no valid mcmod.info file found + } + + bool processLitemod(Mod& mod, [[maybe_unused]] ProcessingLevel level) + { + ModDetails details; + + QuaZip zip(mod.fileinfo().filePath()); + if (!zip.open(QuaZip::mdUnzip)) + return false; + + QuaZipFile file(&zip); + + if (zip.setCurrentFile("litemod.json")) + { + if (!file.open(QIODevice::ReadOnly)) + { + zip.close(); + return false; + } + + details = ReadLiteModInfo(file.readAll()); + file.close(); + + mod.setDetails(details); + return true; + } + zip.close(); + + return false; // no valid litemod.json found in archive + } + + /** Checks whether a file is valid as a mod or not. */ + bool validate(QFileInfo file) + { + Mod mod{ file }; + return ModUtils::process(mod, ProcessingLevel::BasicInfoOnly) && mod.valid(); + } + + bool processIconPNG(const Mod& mod, QByteArray&& raw_data, QPixmap* pixmap) + { + auto img = QImage::fromData(raw_data); + if (!img.isNull()) + { + *pixmap = mod.setIcon(img); + } + else + { + qWarning() << "Failed to parse mod logo:" << mod.iconPath() << "from" << mod.name(); + return false; + } + return true; + } + + bool loadIconFile(const Mod& mod, QPixmap* pixmap) + { + if (mod.iconPath().isEmpty()) + { + qWarning() << "No Iconfile set, be sure to parse the mod first"; + return false; + } + + auto png_invalid = [&mod](const QString& reason) + { + qWarning() << "Mod at" << mod.fileinfo().filePath() << "does not have a valid icon:" << reason; + return false; + }; + + switch (mod.type()) + { + case ResourceType::FOLDER: + { + QFileInfo icon_info(FS::PathCombine(mod.fileinfo().filePath(), mod.iconPath())); + if (icon_info.exists() && icon_info.isFile()) + { + QFile icon(icon_info.filePath()); + if (!icon.open(QIODevice::ReadOnly)) + { + return png_invalid("failed to open file " + icon_info.filePath()); + } + auto data = icon.readAll(); + + bool icon_result = ModUtils::processIconPNG(mod, std::move(data), pixmap); + + icon.close(); + + if (!icon_result) + { + return png_invalid("invalid png image"); // icon invalid + } + return true; + } + return png_invalid("file '" + icon_info.filePath() + "' does not exists or is not a file"); + } + case ResourceType::ZIPFILE: + { + QuaZip zip(mod.fileinfo().filePath()); + if (!zip.open(QuaZip::mdUnzip)) + return png_invalid("failed to open '" + mod.fileinfo().filePath() + "' as a zip archive"); + + QuaZipFile file(&zip); + + if (zip.setCurrentFile(mod.iconPath())) + { + if (!file.open(QIODevice::ReadOnly)) + { + qCritical() << "Failed to open file in zip."; + zip.close(); + return png_invalid("Failed to open '" + mod.iconPath() + "' in zip archive"); + } + + auto data = file.readAll(); + + bool icon_result = ModUtils::processIconPNG(mod, std::move(data), pixmap); + + file.close(); + if (!icon_result) + { + return png_invalid("invalid png image"); // icon png invalid + } + return true; + } + return png_invalid("Failed to set '" + mod.iconPath() + "' as current file in zip archive"); // could + // not set + // icon as + // current + // file. + } + case ResourceType::LITEMOD: + { + return png_invalid("litemods do not have icons"); // can lightmods even have icons? + } + default: return png_invalid("Invalid type for mod, can not load icon."); + } + } + +} // namespace ModUtils + +LocalModParseTask::LocalModParseTask(int token, ResourceType type, const QFileInfo& modFile) + : Task(false), + m_token(token), + m_type(type), + m_modFile(modFile), + m_result(new Result()) +{} + +bool LocalModParseTask::abort() +{ + m_aborted.store(true); + return true; +} + +void LocalModParseTask::executeTask() +{ + Mod mod{ m_modFile }; + ModUtils::process(mod, ModUtils::ProcessingLevel::Full); + + m_result->details = mod.details(); + + if (m_aborted) + emitAborted(); + else + emitSucceeded(); +} diff --git a/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalModParseTask.hpp b/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalModParseTask.hpp new file mode 100644 index 0000000000..29d87d5746 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalModParseTask.hpp @@ -0,0 +1,93 @@ +// 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. + */ +#pragma once + +#include <QDebug> +#include <QObject> + +#include "minecraft/mod/Mod.hpp" +#include "minecraft/mod/ModDetails.hpp" + +#include "tasks/Task.h" + +namespace ModUtils +{ + + ModDetails ReadFabricModInfo(QByteArray contents); + ModDetails ReadQuiltModInfo(QByteArray contents); + ModDetails ReadForgeInfo(QByteArray contents); + ModDetails ReadLiteModInfo(QByteArray contents); + + enum class ProcessingLevel + { + Full, + BasicInfoOnly + }; + + bool process(Mod& mod, ProcessingLevel level = ProcessingLevel::Full); + + bool processZIP(Mod& mod, ProcessingLevel level = ProcessingLevel::Full); + bool processFolder(Mod& mod, ProcessingLevel level = ProcessingLevel::Full); + bool processLitemod(Mod& mod, ProcessingLevel level = ProcessingLevel::Full); + + /** Checks whether a file is valid as a mod or not. */ + bool validate(QFileInfo file); + + bool processIconPNG(const Mod& mod, QByteArray&& raw_data, QPixmap* pixmap); + bool loadIconFile(const Mod& mod, QPixmap* pixmap); +} // namespace ModUtils + +class LocalModParseTask : public Task +{ + Q_OBJECT + public: + struct Result + { + ModDetails details; + }; + using ResultPtr = std::shared_ptr<Result>; + ResultPtr result() const + { + return m_result; + } + + bool canAbort() const override + { + return true; + } + bool abort() override; + + LocalModParseTask(int token, ResourceType type, const QFileInfo& modFile); + void executeTask() override; + + int token() const + { + return m_token; + } + + private: + int m_token; + ResourceType m_type; + QFileInfo m_modFile; + ResultPtr m_result; + + std::atomic<bool> m_aborted = false; +}; diff --git a/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalResourceParse.cpp b/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalResourceParse.cpp new file mode 100644 index 0000000000..a2de759c31 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalResourceParse.cpp @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// 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 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, 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 <QObject> + +#include "LocalResourceParse.hpp" + +#include "LocalDataPackParseTask.hpp" +#include "LocalModParseTask.hpp" +#include "LocalShaderPackParseTask.hpp" +#include "LocalTexturePackParseTask.hpp" +#include "LocalWorldSaveParseTask.hpp" +#include "modplatform/ResourceType.h" + +namespace ResourceUtils +{ + ModPlatform::ResourceType identify(QFileInfo file) + { + if (file.exists() && file.isFile()) + { + if (ModUtils::validate(file)) + { + // mods can contain resource and data packs so they must be tested first + qDebug() << file.fileName() << "is a mod"; + return ModPlatform::ResourceType::Mod; + } + else if (DataPackUtils::validateResourcePack(file)) + { + qDebug() << file.fileName() << "is a resource pack"; + return ModPlatform::ResourceType::ResourcePack; + } + else if (TexturePackUtils::validate(file)) + { + qDebug() << file.fileName() << "is a pre 1.6 texture pack"; + return ModPlatform::ResourceType::TexturePack; + } + else if (DataPackUtils::validate(file)) + { + qDebug() << file.fileName() << "is a data pack"; + return ModPlatform::ResourceType::DataPack; + } + else if (WorldSaveUtils::validate(file)) + { + qDebug() << file.fileName() << "is a world save"; + return ModPlatform::ResourceType::World; + } + else if (ShaderPackUtils::validate(file)) + { + qDebug() << file.fileName() << "is a shader pack"; + return ModPlatform::ResourceType::ShaderPack; + } + else + { + qDebug() << "Can't Identify" << file.fileName(); + } + } + else + { + qDebug() << "Can't find" << file.absolutePath(); + } + return ModPlatform::ResourceType::Unknown; + } + +} // namespace ResourceUtils diff --git a/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalResourceParse.hpp b/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalResourceParse.hpp new file mode 100644 index 0000000000..757e4777f1 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalResourceParse.hpp @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// 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 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, 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. + * + * ======================================================================== */ + +#pragma once + +#include <QFileInfo> +#include "modplatform/ResourceType.h" + +namespace ResourceUtils +{ + ModPlatform::ResourceType identify(QFileInfo file); +} // namespace ResourceUtils diff --git a/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalResourceUpdateTask.cpp b/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalResourceUpdateTask.cpp new file mode 100644 index 0000000000..19e47d334f --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalResourceUpdateTask.cpp @@ -0,0 +1,99 @@ +// 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> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * 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. + * + * ======================================================================== */ + +#include "LocalResourceUpdateTask.hpp" + +#include "FileSystem.h" +#include "minecraft/mod/MetadataHandler.hpp" + +#ifdef Q_OS_WIN32 +#include <windows.h> +#endif + +LocalResourceUpdateTask::LocalResourceUpdateTask(QDir index_dir, + ModPlatform::IndexedPack& project, + ModPlatform::IndexedVersion& version) + : m_index_dir(index_dir), + m_project(project), + m_version(version) +{ + // Ensure a '.index' folder exists in the mods folder, and create it if it does not + if (!FS::ensureFolderPathExists(index_dir.path())) + { + emitFailed(QString("Unable to create index directory at %1!").arg(index_dir.absolutePath())); + } + +#ifdef Q_OS_WIN32 + SetFileAttributesW(index_dir.path().toStdWString().c_str(), + FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED); +#endif +} + +void LocalResourceUpdateTask::executeTask() +{ + setStatus(tr("Updating index for resource:\n%1").arg(m_project.name)); + + auto old_metadata = Metadata::get(m_index_dir, m_project.addonId); + if (old_metadata.isValid()) + { + emit hasOldResource(old_metadata.name, old_metadata.filename); + if (m_project.slug.isEmpty()) + m_project.slug = old_metadata.slug; + } + + auto pw_mod = Metadata::create(m_index_dir, m_project, m_version); + if (pw_mod.isValid()) + { + Metadata::update(m_index_dir, pw_mod); + emitSucceeded(); + } + else + { + qCritical() << "Tried to update an invalid resource!"; + emitFailed(tr("Invalid metadata")); + } +} + +auto LocalResourceUpdateTask::abort() -> bool +{ + emitAborted(); + return true; +} diff --git a/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalResourceUpdateTask.hpp b/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalResourceUpdateTask.hpp new file mode 100644 index 0000000000..8c4bc81e78 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalResourceUpdateTask.hpp @@ -0,0 +1,75 @@ +// 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. + * + * ======================================================================== */ + +#pragma once + +#include <QDir> + +#include "modplatform/ModIndex.h" +#include "tasks/Task.h" + +class LocalResourceUpdateTask : public Task +{ + Q_OBJECT + public: + using Ptr = shared_qobject_ptr<LocalResourceUpdateTask>; + + explicit LocalResourceUpdateTask(QDir index_dir, + ModPlatform::IndexedPack& project, + ModPlatform::IndexedVersion& version); + + auto canAbort() const -> bool override + { + return true; + } + auto abort() -> bool override; + + protected slots: + //! Entry point for tasks. + void executeTask() override; + + signals: + void hasOldResource(QString name, QString filename); + + private: + QDir m_index_dir; + ModPlatform::IndexedPack m_project; + ModPlatform::IndexedVersion m_version; +}; diff --git a/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.cpp b/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.cpp new file mode 100644 index 0000000000..376b99506d --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.cpp @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// 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 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#include "LocalShaderPackParseTask.hpp" + +#include "FileSystem.h" + +#include <quazip/quazip.h> +#include <quazip/quazipdir.h> +#include <quazip/quazipfile.h> + +namespace ShaderPackUtils +{ + + bool process(ShaderPack& pack, ProcessingLevel level) + { + switch (pack.type()) + { + case ResourceType::FOLDER: return ShaderPackUtils::processFolder(pack, level); + case ResourceType::ZIPFILE: return ShaderPackUtils::processZIP(pack, level); + default: qWarning() << "Invalid type for shader pack parse task!"; return false; + } + } + + bool processFolder(ShaderPack& pack, ProcessingLevel level) + { + Q_ASSERT(pack.type() == ResourceType::FOLDER); + + QFileInfo shaders_dir_info(FS::PathCombine(pack.fileinfo().filePath(), "shaders")); + if (!shaders_dir_info.exists() || !shaders_dir_info.isDir()) + { + return false; // assets dir does not exists or isn't valid + } + pack.setPackFormat(ShaderPackFormat::VALID); + + if (level == ProcessingLevel::BasicInfoOnly) + { + return true; // only need basic info already checked + } + + return true; // all tests passed + } + + bool processZIP(ShaderPack& pack, ProcessingLevel level) + { + Q_ASSERT(pack.type() == ResourceType::ZIPFILE); + + QuaZip zip(pack.fileinfo().filePath()); + if (!zip.open(QuaZip::mdUnzip)) + return false; // can't open zip file + + QuaZipFile file(&zip); + + QuaZipDir zipDir(&zip); + if (!zipDir.exists("/shaders")) + { + return false; // assets dir does not exists at zip root + } + pack.setPackFormat(ShaderPackFormat::VALID); + + if (level == ProcessingLevel::BasicInfoOnly) + { + zip.close(); + return true; // only need basic info already checked + } + + zip.close(); + + return true; + } + + bool validate(QFileInfo file) + { + ShaderPack sp{ file }; + return ShaderPackUtils::process(sp, ProcessingLevel::BasicInfoOnly) && sp.valid(); + } + +} // namespace ShaderPackUtils + +LocalShaderPackParseTask::LocalShaderPackParseTask(int token, ShaderPack& sp) + : Task(false), + m_token(token), + m_shader_pack(sp) +{} + +bool LocalShaderPackParseTask::abort() +{ + m_aborted = true; + return true; +} + +void LocalShaderPackParseTask::executeTask() +{ + if (!ShaderPackUtils::process(m_shader_pack)) + { + emitFailed("this is not a shader pack"); + return; + } + + if (m_aborted) + emitAborted(); + else + emitSucceeded(); +} diff --git a/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.hpp b/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.hpp new file mode 100644 index 0000000000..2d74ce0116 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalShaderPackParseTask.hpp @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// 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 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#pragma once + +#include <QDebug> +#include <QObject> + +#include "minecraft/mod/ShaderPack.hpp" + +#include "tasks/Task.h" + +namespace ShaderPackUtils +{ + + enum class ProcessingLevel + { + Full, + BasicInfoOnly + }; + + bool process(ShaderPack& pack, ProcessingLevel level = ProcessingLevel::Full); + + bool processZIP(ShaderPack& pack, ProcessingLevel level = ProcessingLevel::Full); + bool processFolder(ShaderPack& pack, ProcessingLevel level = ProcessingLevel::Full); + + /** Checks whether a file is valid as a shader pack or not. */ + bool validate(QFileInfo file); +} // namespace ShaderPackUtils + +class LocalShaderPackParseTask : public Task +{ + Q_OBJECT + public: + LocalShaderPackParseTask(int token, ShaderPack& sp); + + bool canAbort() const override + { + return true; + } + bool abort() override; + + void executeTask() override; + + int token() const + { + return m_token; + } + + private: + int m_token; + + ShaderPack& m_shader_pack; + + bool m_aborted = false; +}; diff --git a/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp b/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp new file mode 100644 index 0000000000..fe7f20b730 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.cpp @@ -0,0 +1,306 @@ +// 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> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * 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. + * + * ======================================================================== */ + +#include "LocalTexturePackParseTask.hpp" + +#include "FileSystem.h" + +#include <quazip/quazip.h> +#include <quazip/quazipfile.h> + +#include <QCryptographicHash> + +namespace TexturePackUtils +{ + + bool process(TexturePack& pack, ProcessingLevel level) + { + switch (pack.type()) + { + case ResourceType::FOLDER: return TexturePackUtils::processFolder(pack, level); + case ResourceType::ZIPFILE: return TexturePackUtils::processZIP(pack, level); + default: qWarning() << "Invalid type for resource pack parse task!"; return false; + } + } + + bool processFolder(TexturePack& pack, ProcessingLevel level) + { + Q_ASSERT(pack.type() == ResourceType::FOLDER); + + QFileInfo mcmeta_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.txt")); + if (mcmeta_file_info.isFile()) + { + QFile mcmeta_file(mcmeta_file_info.filePath()); + if (!mcmeta_file.open(QIODevice::ReadOnly)) + return false; + + auto data = mcmeta_file.readAll(); + + bool packTXT_result = TexturePackUtils::processPackTXT(pack, std::move(data)); + + mcmeta_file.close(); + if (!packTXT_result) + { + return false; + } + } + else + { + return false; + } + + if (level == ProcessingLevel::BasicInfoOnly) + return true; + + QFileInfo image_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.png")); + if (image_file_info.isFile()) + { + QFile mcmeta_file(image_file_info.filePath()); + if (!mcmeta_file.open(QIODevice::ReadOnly)) + return false; + + auto data = mcmeta_file.readAll(); + + bool packPNG_result = TexturePackUtils::processPackPNG(pack, std::move(data)); + + mcmeta_file.close(); + if (!packPNG_result) + { + return false; + } + } + else + { + return false; + } + + return true; + } + + bool processZIP(TexturePack& pack, ProcessingLevel level) + { + Q_ASSERT(pack.type() == ResourceType::ZIPFILE); + + QuaZip zip(pack.fileinfo().filePath()); + if (!zip.open(QuaZip::mdUnzip)) + return false; + + QuaZipFile file(&zip); + + if (zip.setCurrentFile("pack.txt")) + { + if (!file.open(QIODevice::ReadOnly)) + { + qCritical() << "Failed to open file in zip."; + zip.close(); + return false; + } + + auto data = file.readAll(); + + bool packTXT_result = TexturePackUtils::processPackTXT(pack, std::move(data)); + + file.close(); + if (!packTXT_result) + { + return false; + } + } + + if (level == ProcessingLevel::BasicInfoOnly) + { + zip.close(); + return true; + } + + if (zip.setCurrentFile("pack.png")) + { + if (!file.open(QIODevice::ReadOnly)) + { + qCritical() << "Failed to open file in zip."; + zip.close(); + return false; + } + + auto data = file.readAll(); + + bool packPNG_result = TexturePackUtils::processPackPNG(pack, std::move(data)); + + file.close(); + zip.close(); + if (!packPNG_result) + { + return false; + } + } + + zip.close(); + + return true; + } + + bool processPackTXT(TexturePack& pack, QByteArray&& raw_data) + { + pack.setDescription(QString(raw_data)); + return true; + } + + bool processPackPNG(const TexturePack& pack, QByteArray&& raw_data) + { + auto img = QImage::fromData(raw_data); + if (!img.isNull()) + { + pack.setImage(img); + } + else + { + qWarning() << "Failed to parse pack.png."; + return false; + } + return true; + } + + bool processPackPNG(const TexturePack& pack) + { + auto png_invalid = [&pack]() + { + qWarning() << "Texture pack at" << pack.fileinfo().filePath() << "does not have a valid pack.png"; + return false; + }; + + switch (pack.type()) + { + case ResourceType::FOLDER: + { + QFileInfo image_file_info(FS::PathCombine(pack.fileinfo().filePath(), "pack.png")); + if (image_file_info.exists() && image_file_info.isFile()) + { + QFile pack_png_file(image_file_info.filePath()); + if (!pack_png_file.open(QIODevice::ReadOnly)) + return png_invalid(); // can't open pack.png file + + auto data = pack_png_file.readAll(); + + bool pack_png_result = TexturePackUtils::processPackPNG(pack, std::move(data)); + + pack_png_file.close(); + if (!pack_png_result) + { + return png_invalid(); // pack.png invalid + } + } + else + { + return png_invalid(); // pack.png does not exists or is not a valid file. + } + return false; + } + case ResourceType::ZIPFILE: + { + QuaZip zip(pack.fileinfo().filePath()); + if (!zip.open(QuaZip::mdUnzip)) + return false; // can't open zip file + + QuaZipFile file(&zip); + if (zip.setCurrentFile("pack.png")) + { + if (!file.open(QIODevice::ReadOnly)) + { + qCritical() << "Failed to open file in zip."; + zip.close(); + return png_invalid(); + } + + auto data = file.readAll(); + + bool pack_png_result = TexturePackUtils::processPackPNG(pack, std::move(data)); + + file.close(); + if (!pack_png_result) + { + zip.close(); + return png_invalid(); // pack.png invalid + } + } + else + { + zip.close(); + return png_invalid(); // could not set pack.mcmeta as current file. + } + return false; + } + default: qWarning() << "Invalid type for resource pack parse task!"; return false; + } + } + + bool validate(QFileInfo file) + { + TexturePack rp{ file }; + return TexturePackUtils::process(rp, ProcessingLevel::BasicInfoOnly) && rp.valid(); + } + +} // namespace TexturePackUtils + +LocalTexturePackParseTask::LocalTexturePackParseTask(int token, TexturePack& rp) + : Task(false), + m_token(token), + m_texture_pack(rp) +{} + +bool LocalTexturePackParseTask::abort() +{ + m_aborted = true; + return true; +} + +void LocalTexturePackParseTask::executeTask() +{ + if (!TexturePackUtils::process(m_texture_pack)) + { + emitFailed("this is not a texture pack"); + return; + } + + if (m_aborted) + emitAborted(); + else + emitSucceeded(); +} diff --git a/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.hpp b/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.hpp new file mode 100644 index 0000000000..b851928d6b --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalTexturePackParseTask.hpp @@ -0,0 +1,100 @@ +// 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> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * 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. + * + * ======================================================================== */ + +#pragma once + +#include <QDebug> +#include <QObject> + +#include "minecraft/mod/TexturePack.hpp" + +#include "tasks/Task.h" + +namespace TexturePackUtils +{ + + enum class ProcessingLevel + { + Full, + BasicInfoOnly + }; + + bool process(TexturePack& pack, ProcessingLevel level = ProcessingLevel::Full); + + bool processZIP(TexturePack& pack, ProcessingLevel level = ProcessingLevel::Full); + bool processFolder(TexturePack& pack, ProcessingLevel level = ProcessingLevel::Full); + + bool processPackTXT(TexturePack& pack, QByteArray&& raw_data); + bool processPackPNG(const TexturePack& pack, QByteArray&& raw_data); + + /// processes ONLY the pack.png (rest of the pack may be invalid) + bool processPackPNG(const TexturePack& pack); + + /** Checks whether a file is valid as a texture pack or not. */ + bool validate(QFileInfo file); +} // namespace TexturePackUtils + +class LocalTexturePackParseTask : public Task +{ + Q_OBJECT + public: + LocalTexturePackParseTask(int token, TexturePack& rp); + + bool canAbort() const override + { + return true; + } + bool abort() override; + + void executeTask() override; + + int token() const + { + return m_token; + } + + private: + int m_token; + + TexturePack& m_texture_pack; + + bool m_aborted = false; +}; diff --git a/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.cpp b/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.cpp new file mode 100644 index 0000000000..b2cb4d0bc9 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.cpp @@ -0,0 +1,228 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// 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 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#include "LocalWorldSaveParseTask.hpp" + +#include "FileSystem.h" + +#include <quazip/quazip.h> +#include <quazip/quazipdir.h> +#include <quazip/quazipfile.h> + +#include <QDir> +#include <QFileInfo> + +namespace WorldSaveUtils +{ + + bool process(WorldSave& pack, ProcessingLevel level) + { + switch (pack.type()) + { + case ResourceType::FOLDER: return WorldSaveUtils::processFolder(pack, level); + case ResourceType::ZIPFILE: return WorldSaveUtils::processZIP(pack, level); + default: qWarning() << "Invalid type for world save parse task!"; return false; + } + } + + /// @brief checks a folder structure to see if it contains a level.dat + /// @param dir the path to check + /// @param saves used in recursive call if a "saves" dir was found + /// @return std::tuple of ( + /// bool <found level.dat>, + /// QString <name of folder containing level.dat>, + /// bool <saves folder found> + /// ) + static std::tuple<bool, QString, bool> contains_level_dat(QDir dir, bool saves = false) + { + for (auto const& entry : dir.entryInfoList()) + { + if (!entry.isDir()) + { + continue; + } + if (!saves && entry.fileName() == "saves") + { + return contains_level_dat(QDir(entry.filePath()), true); + } + QFileInfo level_dat(FS::PathCombine(entry.filePath(), "level.dat")); + if (level_dat.exists() && level_dat.isFile()) + { + return std::make_tuple(true, entry.fileName(), saves); + } + } + return std::make_tuple(false, "", saves); + } + + bool processFolder(WorldSave& save, ProcessingLevel level) + { + Q_ASSERT(save.type() == ResourceType::FOLDER); + + auto [found, save_dir_name, found_saves_dir] = contains_level_dat(QDir(save.fileinfo().filePath())); + + if (!found) + { + return false; + } + + save.setSaveDirName(save_dir_name); + + if (found_saves_dir) + { + save.setSaveFormat(WorldSaveFormat::MULTI); + } + else + { + save.setSaveFormat(WorldSaveFormat::SINGLE); + } + + if (level == ProcessingLevel::BasicInfoOnly) + { + return true; // only need basic info already checked + } + + // reserved for more intensive processing + + return true; // all tests passed + } + + /// @brief checks a folder structure to see if it contains a level.dat + /// @param zip the zip file to check + /// @return std::tuple of ( + /// bool <found level.dat>, + /// QString <name of folder containing level.dat>, + /// bool <saves folder found> + /// ) + static std::tuple<bool, QString, bool> contains_level_dat(QuaZip& zip) + { + bool saves = false; + QuaZipDir zipDir(&zip); + if (zipDir.exists("/saves")) + { + saves = true; + zipDir.cd("/saves"); + } + + for (auto const& entry : zipDir.entryList()) + { + zipDir.cd(entry); + if (zipDir.exists("level.dat")) + { + return std::make_tuple(true, entry, saves); + } + zipDir.cd(".."); + } + return std::make_tuple(false, "", saves); + } + + bool processZIP(WorldSave& save, ProcessingLevel level) + { + Q_ASSERT(save.type() == ResourceType::ZIPFILE); + + QuaZip zip(save.fileinfo().filePath()); + if (!zip.open(QuaZip::mdUnzip)) + return false; // can't open zip file + + auto [found, save_dir_name, found_saves_dir] = contains_level_dat(zip); + + if (save_dir_name.endsWith("/")) + { + save_dir_name.chop(1); + } + + if (!found) + { + return false; + } + + save.setSaveDirName(save_dir_name); + + if (found_saves_dir) + { + save.setSaveFormat(WorldSaveFormat::MULTI); + } + else + { + save.setSaveFormat(WorldSaveFormat::SINGLE); + } + + if (level == ProcessingLevel::BasicInfoOnly) + { + zip.close(); + return true; // only need basic info already checked + } + + // reserved for more intensive processing + + zip.close(); + + return true; + } + + bool validate(QFileInfo file) + { + WorldSave sp{ file }; + return WorldSaveUtils::process(sp, ProcessingLevel::BasicInfoOnly) && sp.valid(); + } + +} // namespace WorldSaveUtils + +LocalWorldSaveParseTask::LocalWorldSaveParseTask(int token, WorldSave& save) : Task(false), m_token(token), m_save(save) +{} + +bool LocalWorldSaveParseTask::abort() +{ + m_aborted = true; + return true; +} + +void LocalWorldSaveParseTask::executeTask() +{ + if (!WorldSaveUtils::process(m_save)) + { + emitFailed("this is not a world"); + return; + } + + if (m_aborted) + emitAborted(); + else + emitSucceeded(); +} diff --git a/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.hpp b/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.hpp new file mode 100644 index 0000000000..2d1eb1fac1 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/tasks/LocalWorldSaveParseTask.hpp @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: GPL-3.0-only AND Apache-2.0 +// 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 Rachel Powers <508861+Ryex@users.noreply.github.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * ======================================================================== */ + +#pragma once + +#include <QDebug> +#include <QObject> + +#include "minecraft/mod/WorldSave.hpp" + +#include "tasks/Task.h" + +namespace WorldSaveUtils +{ + + enum class ProcessingLevel + { + Full, + BasicInfoOnly + }; + + bool process(WorldSave& save, ProcessingLevel level = ProcessingLevel::Full); + + bool processZIP(WorldSave& pack, ProcessingLevel level = ProcessingLevel::Full); + bool processFolder(WorldSave& pack, ProcessingLevel level = ProcessingLevel::Full); + + bool validate(QFileInfo file); + +} // namespace WorldSaveUtils + +class LocalWorldSaveParseTask : public Task +{ + Q_OBJECT + public: + LocalWorldSaveParseTask(int token, WorldSave& save); + + bool canAbort() const override + { + return true; + } + bool abort() override; + + void executeTask() override; + + int token() const + { + return m_token; + } + + private: + int m_token; + + WorldSave& m_save; + + bool m_aborted = false; +}; diff --git a/archived/projt-launcher/launcher/minecraft/mod/tasks/ResourceFolderLoadTask.cpp b/archived/projt-launcher/launcher/minecraft/mod/tasks/ResourceFolderLoadTask.cpp new file mode 100644 index 0000000000..49088d4dcd --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/tasks/ResourceFolderLoadTask.cpp @@ -0,0 +1,197 @@ +// 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> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * 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 "ResourceFolderLoadTask.hpp" + +#include "Application.h" +#include "FileSystem.h" +#include "minecraft/mod/MetadataHandler.hpp" + +#include <QThread> + +ResourceFolderLoadTask::ResourceFolderLoadTask(const QDir& resource_dir, + const QDir& index_dir, + bool is_indexed, + bool clean_orphan, + std::function<Resource*(const QFileInfo&)> create_function) + : Task(false), + m_resource_dir(resource_dir), + m_index_dir(index_dir), + m_is_indexed(is_indexed), + m_clean_orphan(clean_orphan), + m_create_func(create_function), + m_result(new Result()), + m_thread_to_spawn_into(thread()) +{} + +void ResourceFolderLoadTask::executeTask() +{ + if (thread() != m_thread_to_spawn_into) + connect(this, &Task::finished, this->thread(), &QThread::quit); + + if (m_is_indexed) + { + // Read metadata first + getFromMetadata(); + } + + // Read JAR files that don't have metadata + m_resource_dir.refresh(); + for (auto entry : m_resource_dir.entryInfoList()) + { + // The `.index` directory is an internal metadata store and should never be treated as a resource. + // On Unix it is usually hidden by name (and therefore excluded by QDir filters), but on Windows + // it may be visible if the hidden attribute wasn't applied yet. + if (entry.fileName() == ".index") + { + continue; + } + + auto filePath = entry.absoluteFilePath(); + if (auto app = APPLICATION_DYN; app && app->checkQSavePath(filePath)) + { + continue; + } + auto newFilePath = FS::getUniqueResourceName(filePath); + if (newFilePath != filePath) + { + FS::move(filePath, newFilePath); + entry = QFileInfo(newFilePath); + } + + Resource* resource = m_create_func(entry); + + if (resource->enabled()) + { + if (m_result->resources.contains(resource->internal_id())) + { + m_result->resources[resource->internal_id()]->setStatus(ResourceStatus::INSTALLED); + // Delete the object we just created, since a valid one is already in the mods list. + delete resource; + } + else + { + m_result->resources[resource->internal_id()].reset(resource); + m_result->resources[resource->internal_id()]->setStatus(ResourceStatus::NO_METADATA); + } + } + else + { + QString chopped_id = resource->internal_id().chopped(9); + if (m_result->resources.contains(chopped_id)) + { + m_result->resources[resource->internal_id()].reset(resource); + + auto metadata = m_result->resources[chopped_id]->metadata(); + if (metadata) + { + resource->setMetadata(*metadata); + + m_result->resources[resource->internal_id()]->setStatus(ResourceStatus::INSTALLED); + m_result->resources.remove(chopped_id); + } + } + else + { + m_result->resources[resource->internal_id()].reset(resource); + m_result->resources[resource->internal_id()]->setStatus(ResourceStatus::NO_METADATA); + } + } + } + + // Remove orphan metadata to prevent issues + // See https://github.com/PolyMC/PolyMC/issues/996 + if (m_clean_orphan) + { + QMutableMapIterator iter(m_result->resources); + while (iter.hasNext()) + { + auto resource = iter.next().value(); + if (resource->status() == ResourceStatus::NOT_INSTALLED) + { + resource->destroy(m_index_dir, false, false); + iter.remove(); + } + } + } + + for (auto mod : m_result->resources) + mod->moveToThread(m_thread_to_spawn_into); + + if (m_aborted) + emit finished(); + else + emitSucceeded(); +} + +void ResourceFolderLoadTask::getFromMetadata() +{ + m_index_dir.refresh(); + for (auto entry : m_index_dir.entryList(QDir::Files)) + { + auto metadata = Metadata::get(m_index_dir, entry); + + if (!metadata.isValid()) + continue; + + auto* resource = m_create_func(QFileInfo(m_resource_dir.filePath(metadata.filename))); + resource->setMetadata(metadata); + resource->setStatus(ResourceStatus::NOT_INSTALLED); + m_result->resources[resource->internal_id()].reset(resource); + } +} diff --git a/archived/projt-launcher/launcher/minecraft/mod/tasks/ResourceFolderLoadTask.hpp b/archived/projt-launcher/launcher/minecraft/mod/tasks/ResourceFolderLoadTask.hpp new file mode 100644 index 0000000000..c798e003d3 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/mod/tasks/ResourceFolderLoadTask.hpp @@ -0,0 +1,116 @@ +// 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> + * Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net> + * + * 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. + * + * ======================================================================== */ + +#pragma once + +#include <QDir> +#include <QMap> +#include <QObject> +#include <QRunnable> +#include <memory> +#include "minecraft/mod/Mod.hpp" +#include "tasks/Task.h" + +class ResourceFolderLoadTask : public Task +{ + Q_OBJECT + public: + struct Result + { + QMap<QString, Resource::Ptr> resources; + }; + using ResultPtr = std::shared_ptr<Result>; + ResultPtr result() const + { + return m_result; + } + + public: + ResourceFolderLoadTask(const QDir& resource_dir, + const QDir& index_dir, + bool is_indexed, + bool clean_orphan, + std::function<Resource*(const QFileInfo&)> create_function); + + bool canAbort() const override + { + return true; + } + bool abort() override + { + m_aborted.store(true); + return true; + } + + void executeTask() override; + + private: + void getFromMetadata(); + + private: + QDir m_resource_dir, m_index_dir; + bool m_is_indexed; + bool m_clean_orphan; + std::function<Resource*(QFileInfo const&)> m_create_func; + ResultPtr m_result; + + std::atomic<bool> m_aborted = false; + + /** This is the thread in which we should put new mod objects */ + QThread* m_thread_to_spawn_into; +}; diff --git a/archived/projt-launcher/launcher/minecraft/skins/CapeChange.cpp b/archived/projt-launcher/launcher/minecraft/skins/CapeChange.cpp new file mode 100644 index 0000000000..cdba5e0a14 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/skins/CapeChange.cpp @@ -0,0 +1,97 @@ +// 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 Sefa Eyeoglu <contact@scrumplex.net> + * Copyright (c) 2023 Trial97 <alexandru.tripon97@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 "CapeChange.h" + +#include <memory> + +#include "net/ByteArraySink.h" +#include "net/RawHeaderProxy.h" + +CapeChange::CapeChange(QString cape) : NetRequest(), m_capeId(cape) +{ + logCat = taskMCSkinsLogC; +} + +QNetworkReply* CapeChange::getReply(QNetworkRequest& request) +{ + if (m_capeId.isEmpty()) + { + setStatus(tr("Removing cape")); + return m_network->deleteResource(request); + } + else + { + setStatus(tr("Equipping cape")); + return m_network->put(request, QString("{\"capeId\":\"%1\"}").arg(m_capeId).toUtf8()); + } +} + +CapeChange::Ptr CapeChange::make(QString token, QString capeId) +{ + auto up = makeShared<CapeChange>(capeId); + up->m_url = QUrl("https://api.minecraftservices.com/minecraft/profile/capes/active"); + up->setObjectName(QString("BYTES:") + up->m_url.toString()); + up->m_sink.reset(new Net::ByteArraySink(std::make_shared<QByteArray>())); + up->addHeaderProxy(new Net::RawHeaderProxy(QList<Net::HeaderPair>{ + { "Authorization", QString("Bearer %1").arg(token).toLocal8Bit() }, + })); + return up; +} diff --git a/archived/projt-launcher/launcher/minecraft/skins/CapeChange.h b/archived/projt-launcher/launcher/minecraft/skins/CapeChange.h new file mode 100644 index 0000000000..c9170f973c --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/skins/CapeChange.h @@ -0,0 +1,62 @@ +// 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) 2023 Trial97 <alexandru.tripon97@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. + * + * ======================================================================== */ + +#pragma once + +#include "net/NetRequest.h" + +class CapeChange : public Net::NetRequest +{ + Q_OBJECT + public: + using Ptr = shared_qobject_ptr<CapeChange>; + CapeChange(QString capeId); + virtual ~CapeChange() = default; + + static CapeChange::Ptr make(QString token, QString capeId); + + protected: + virtual QNetworkReply* getReply(QNetworkRequest&) override; + + private: + QString m_capeId; +}; diff --git a/archived/projt-launcher/launcher/minecraft/skins/CapeListModel.cpp b/archived/projt-launcher/launcher/minecraft/skins/CapeListModel.cpp new file mode 100644 index 0000000000..052fd67910 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/skins/CapeListModel.cpp @@ -0,0 +1,287 @@ +// 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. + */ + +#include "CapeListModel.h" + +#include <QDir> +#include <QFileInfo> +#include <QPainter> + +#include "Application.h" +#include "FileSystem.h" +#include "net/Download.h" + +CapeListModel::CapeListModel(QObject* parent) : QAbstractListModel(parent) +{} + +void CapeListModel::loadFromAccount(MinecraftAccountPtr account, const QString& cacheDir) +{ + beginResetModel(); + + m_account = account; + m_cacheDir = cacheDir; + m_capes.clear(); + m_capeImages.clear(); + m_capeIndexMap.clear(); + + if (!m_account || !m_account->accountData()) + { + endResetModel(); + return; + } + + // Ensure cache directory exists + QDir().mkpath(m_cacheDir); + + auto& accountData = *m_account->accountData(); + + // Add "No Cape" option first + CapeInfo noCape; + noCape.id = "no_cape"; + noCape.alias = tr("No Cape"); + noCape.loaded = true; + m_capes.append(noCape); + m_capeIndexMap["no_cape"] = 0; + + // Add all available capes + int index = 1; + for (const auto& cape : accountData.minecraftProfile.capes) + { + CapeInfo info; + info.id = cape.id; + info.alias = cape.alias; + info.url = cape.url; + info.loaded = false; + + // Check if we have embedded data + if (!cape.data.isEmpty()) + { + QImage capeImage; + if (capeImage.loadFromData(cape.data, "PNG")) + { + m_capeImages[cape.id] = capeImage; + info.loaded = true; + + // Also save to cache + QString cachePath = FS::PathCombine(m_cacheDir, cape.id + ".png"); + capeImage.save(cachePath); + } + } + + // Try loading from cache if not embedded + if (!info.loaded) + { + loadCapeFromDisk(cape.id); + if (m_capeImages.contains(cape.id)) + { + info.loaded = true; + } + } + + m_capes.append(info); + m_capeIndexMap[cape.id] = index++; + } + + endResetModel(); + + // Download any missing capes + downloadCapes(); +} + +void CapeListModel::refresh() +{ + if (m_account) + { + loadFromAccount(m_account, m_cacheDir); + } +} + +void CapeListModel::loadCapeFromDisk(const QString& capeId) +{ + QString path = FS::PathCombine(m_cacheDir, capeId + ".png"); + if (QFileInfo::exists(path)) + { + QImage img; + if (img.load(path)) + { + m_capeImages[capeId] = img; + } + } +} + +void CapeListModel::downloadCapes() +{ + QList<int> toDownload; + + for (int i = 0; i < m_capes.size(); ++i) + { + const auto& cape = m_capes[i]; + if (!cape.loaded && !cape.url.isEmpty() && !m_capeImages.contains(cape.id)) + { + toDownload.append(i); + } + } + + if (toDownload.isEmpty()) + { + emit loadingFinished(); + return; + } + + m_downloadJob.reset(new NetJob(tr("Download capes"), APPLICATION->network())); + + for (int idx : toDownload) + { + const auto& cape = m_capes[idx]; + QString cachePath = FS::PathCombine(m_cacheDir, cape.id + ".png"); + m_downloadJob->addNetAction(Net::Download::makeFile(cape.url, cachePath)); + } + + connect(m_downloadJob.get(), &NetJob::succeeded, this, &CapeListModel::onDownloadSucceeded); + connect(m_downloadJob.get(), &NetJob::failed, this, &CapeListModel::onDownloadFailed); + + m_downloadJob->start(); +} + +void CapeListModel::onDownloadSucceeded() +{ + m_downloadJob.reset(); + + // Load all downloaded capes + for (auto& cape : m_capes) + { + if (!cape.loaded && !cape.id.isEmpty() && cape.id != "no_cape") + { + loadCapeFromDisk(cape.id); + if (m_capeImages.contains(cape.id)) + { + cape.loaded = true; + int idx = m_capeIndexMap.value(cape.id, -1); + if (idx >= 0) + { + emit dataChanged(index(idx), index(idx)); + emit capeLoaded(cape.id); + } + } + } + } + + emit loadingFinished(); +} + +void CapeListModel::onDownloadFailed(QString reason) +{ + qWarning() << "Cape download failed:" << reason; + m_downloadJob.reset(); + emit loadingFinished(); +} + +QImage CapeListModel::getCapeImage(const QString& capeId) const +{ + return m_capeImages.value(capeId, QImage()); +} + +int CapeListModel::findCapeIndex(const QString& capeId) const +{ + QString id = capeId; + if (id.isEmpty()) + { + id = "no_cape"; + } + return m_capeIndexMap.value(id, -1); +} + +QString CapeListModel::capeIdAt(int index) const +{ + if (index < 0 || index >= m_capes.size()) + return QString(); + return m_capes[index].id; +} + +int CapeListModel::rowCount(const QModelIndex& parent) const +{ + if (parent.isValid()) + return 0; + return m_capes.size(); +} + +QVariant CapeListModel::data(const QModelIndex& index, int role) const +{ + if (!index.isValid() || index.row() < 0 || index.row() >= m_capes.size()) + return QVariant(); + + const auto& cape = m_capes[index.row()]; + + switch (role) + { + case Qt::DisplayRole: return cape.alias; + + case Qt::DecorationRole: + { + QImage img = m_capeImages.value(cape.id, QImage()); + if (!img.isNull()) + { + return createCapePreview(img, m_elytraMode); + } + return QVariant(); + } + + case CapeIdRole: return cape.id; + + case CapeAliasRole: return cape.alias; + + case CapeImageRole: return m_capeImages.value(cape.id, QImage()); + + case CapeUrlRole: return cape.url; + + default: return QVariant(); + } +} + +QHash<int, QByteArray> CapeListModel::roleNames() const +{ + QHash<int, QByteArray> roles = QAbstractListModel::roleNames(); + roles[CapeIdRole] = "capeId"; + roles[CapeAliasRole] = "capeAlias"; + roles[CapeImageRole] = "capeImage"; + roles[CapeUrlRole] = "capeUrl"; + return roles; +} + +QPixmap CapeListModel::createCapePreview(const QImage& capeImage, bool elytra) const +{ + if (elytra) + { + auto wing = capeImage.copy(34, 0, 12, 22); + QImage mirrored = wing.flipped(Qt::Horizontal); + + QImage combined(wing.width() * 2 - 2, wing.height(), capeImage.format()); + combined.fill(Qt::transparent); + + QPainter painter(&combined); + painter.drawImage(0, 0, wing); + painter.drawImage(wing.width() - 2, 0, mirrored); + painter.end(); + return QPixmap::fromImage(combined.scaled(96, 176, Qt::IgnoreAspectRatio, Qt::FastTransformation)); + } + return QPixmap::fromImage( + capeImage.copy(1, 1, 10, 16).scaled(80, 128, Qt::IgnoreAspectRatio, Qt::FastTransformation)); +} diff --git a/archived/projt-launcher/launcher/minecraft/skins/CapeListModel.h b/archived/projt-launcher/launcher/minecraft/skins/CapeListModel.h new file mode 100644 index 0000000000..ed9b2f4ebb --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/skins/CapeListModel.h @@ -0,0 +1,137 @@ +// 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. + */ + +#pragma once + +#include <QAbstractListModel> +#include <QImage> +#include <QPixmap> +#include <QDir> + +#include "minecraft/auth/MinecraftAccount.hpp" +#include "net/NetJob.h" + +/** + * @brief Model for managing Minecraft capes with on-demand loading + * + * This model provides a list of available capes for a Minecraft account, + * downloading cape images on demand and caching them locally. + */ +class CapeListModel : public QAbstractListModel +{ + Q_OBJECT + + public: + enum Roles + { + CapeIdRole = Qt::UserRole, + CapeAliasRole, + CapeImageRole, + CapeUrlRole + }; + + explicit CapeListModel(QObject* parent = nullptr); + virtual ~CapeListModel() = default; + + /** + * @brief Initialize the model with account data + * @param account The Minecraft account to load capes from + * @param cacheDir Directory to cache cape images + */ + void loadFromAccount(MinecraftAccountPtr account, const QString& cacheDir); + + /** + * @brief Refresh cape data from the account + */ + void refresh(); + + /** + * @brief Get cape image by ID + * @param capeId The cape identifier + * @return Cape image or null image if not loaded + */ + QImage getCapeImage(const QString& capeId) const; + + /** + * @brief Get all loaded cape images + * @return Hash of cape ID to image + */ + QHash<QString, QImage> allCapes() const + { + return m_capeImages; + } + + /** + * @brief Find index of cape by ID + * @param capeId The cape identifier + * @return Index in model or -1 if not found + */ + int findCapeIndex(const QString& capeId) const; + + /** + * @brief Get cape ID at index + * @param index Model index + * @return Cape ID or empty string + */ + QString capeIdAt(int index) const; + + // QAbstractListModel interface + int rowCount(const QModelIndex& parent = QModelIndex()) const override; + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + QHash<int, QByteArray> roleNames() const override; + + signals: + /** + * @brief Emitted when a cape image has been loaded + * @param capeId The cape that was loaded + */ + void capeLoaded(const QString& capeId); + + /** + * @brief Emitted when all capes have finished loading + */ + void loadingFinished(); + + private slots: + void onDownloadSucceeded(); + void onDownloadFailed(QString reason); + + private: + struct CapeInfo + { + QString id; + QString alias; + QString url; + bool loaded = false; + }; + + void downloadCapes(); + void loadCapeFromDisk(const QString& capeId); + QPixmap createCapePreview(const QImage& capeImage, bool elytra = false) const; + + MinecraftAccountPtr m_account; + QString m_cacheDir; + QList<CapeInfo> m_capes; + QHash<QString, QImage> m_capeImages; + QHash<QString, int> m_capeIndexMap; + NetJob::Ptr m_downloadJob; + bool m_elytraMode = false; +}; diff --git a/archived/projt-launcher/launcher/minecraft/skins/SkinDelete.cpp b/archived/projt-launcher/launcher/minecraft/skins/SkinDelete.cpp new file mode 100644 index 0000000000..5675eb6d6f --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/skins/SkinDelete.cpp @@ -0,0 +1,85 @@ +// 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 Sefa Eyeoglu <contact@scrumplex.net> + * Copyright (c) 2023 Trial97 <alexandru.tripon97@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 "SkinDelete.h" + +#include "net/ByteArraySink.h" +#include "net/RawHeaderProxy.h" + +SkinDelete::SkinDelete() : NetRequest() +{ + logCat = taskMCSkinsLogC; +} + +QNetworkReply* SkinDelete::getReply(QNetworkRequest& request) +{ + setStatus(tr("Deleting skin")); + return m_network->deleteResource(request); +} + +SkinDelete::Ptr SkinDelete::make(QString token) +{ + auto up = makeShared<SkinDelete>(); + up->m_url = QUrl("https://api.minecraftservices.com/minecraft/profile/skins/active"); + up->m_sink.reset(new Net::ByteArraySink(std::make_shared<QByteArray>())); + up->addHeaderProxy(new Net::RawHeaderProxy(QList<Net::HeaderPair>{ + { "Authorization", QString("Bearer %1").arg(token).toLocal8Bit() }, + })); + return up; +} diff --git a/archived/projt-launcher/launcher/minecraft/skins/SkinDelete.h b/archived/projt-launcher/launcher/minecraft/skins/SkinDelete.h new file mode 100644 index 0000000000..80d2bb5417 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/skins/SkinDelete.h @@ -0,0 +1,59 @@ +// 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) 2023 Trial97 <alexandru.tripon97@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. + * + * ======================================================================== */ + +#pragma once + +#include "net/NetRequest.h" + +class SkinDelete : public Net::NetRequest +{ + Q_OBJECT + public: + using Ptr = shared_qobject_ptr<SkinDelete>; + SkinDelete(); + virtual ~SkinDelete() = default; + + static SkinDelete::Ptr make(QString token); + + protected: + virtual QNetworkReply* getReply(QNetworkRequest&) override; +}; diff --git a/archived/projt-launcher/launcher/minecraft/skins/SkinList.cpp b/archived/projt-launcher/launcher/minecraft/skins/SkinList.cpp new file mode 100644 index 0000000000..522cee2e78 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/skins/SkinList.cpp @@ -0,0 +1,496 @@ +// 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) 2023 Trial97 <alexandru.tripon97@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. + * + * ======================================================================== */ + +#include "SkinList.h" + +#include <QFileInfo> +#include <QMimeData> + +#include "FileSystem.h" +#include "Json.h" +#include "minecraft/skins/SkinModel.h" + +SkinList::SkinList(QObject* parent, QString path, MinecraftAccountPtr acct) : QAbstractListModel(parent), m_acct(acct) +{ + FS::ensureFolderPathExists(m_dir.absolutePath()); + m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs); + m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); + m_watcher.reset(new QFileSystemWatcher(this)); + m_isWatching = false; + connect(m_watcher.get(), &QFileSystemWatcher::directoryChanged, this, &SkinList::directoryChanged); + connect(m_watcher.get(), &QFileSystemWatcher::fileChanged, this, &SkinList::fileChanged); + directoryChanged(path); +} + +void SkinList::startWatching() +{ + if (m_isWatching) + { + return; + } + update(); + m_isWatching = m_watcher->addPath(m_dir.absolutePath()); + if (m_isWatching) + { + qDebug() << "Started watching " << m_dir.absolutePath(); + } + else + { + qDebug() << "Failed to start watching " << m_dir.absolutePath(); + } +} + +void SkinList::stopWatching() +{ + save(); + if (!m_isWatching) + { + return; + } + m_isWatching = !m_watcher->removePath(m_dir.absolutePath()); + if (!m_isWatching) + { + qDebug() << "Stopped watching " << m_dir.absolutePath(); + } + else + { + qDebug() << "Failed to stop watching " << m_dir.absolutePath(); + } +} + +bool SkinList::update() +{ + QList<SkinModel> newSkins; + m_dir.refresh(); + + auto manifestInfo = QFileInfo(m_dir.absoluteFilePath("index.json")); + if (manifestInfo.exists()) + { + try + { + auto doc = Json::requireDocument(manifestInfo.absoluteFilePath(), "SkinList JSON file"); + const auto root = doc.object(); + auto skins = Json::ensureArray(root, "skins"); + for (auto jSkin : skins) + { + SkinModel s(m_dir, Json::ensureObject(jSkin)); + if (s.isValid()) + { + newSkins << s; + } + } + } + catch (const Exception& e) + { + qCritical() << "Couldn't load skins json:" << e.cause(); + } + } + + bool needsSave = false; + const auto& skin = m_acct->accountData()->minecraftProfile.skin; + if (!skin.url.isEmpty() && !skin.data.isEmpty()) + { + QPixmap skinTexture; + SkinModel* nskin = nullptr; + for (auto i = 0; i < newSkins.size(); i++) + { + if (newSkins[i].getURL() == skin.url) + { + nskin = &newSkins[i]; + break; + } + } + if (!nskin) + { + auto name = m_acct->profileName() + ".png"; + if (QFileInfo(m_dir.absoluteFilePath(name)).exists()) + { + name = QUrl(skin.url).fileName() + ".png"; + } + auto path = m_dir.absoluteFilePath(name); + if (skinTexture.loadFromData(skin.data, "PNG") && skinTexture.save(path)) + { + SkinModel s(path); + s.setModel(skin.variant.toUpper() == "SLIM" ? SkinModel::SLIM : SkinModel::CLASSIC); + s.setCapeId(m_acct->accountData()->minecraftProfile.currentCape); + s.setURL(skin.url); + newSkins << s; + needsSave = true; + } + } + else + { + nskin->setCapeId(m_acct->accountData()->minecraftProfile.currentCape); + nskin->setModel(skin.variant.toUpper() == "SLIM" ? SkinModel::SLIM : SkinModel::CLASSIC); + } + } + + auto folderContents = m_dir.entryInfoList(); + // if there are any untracked files... + for (QFileInfo entry : folderContents) + { + if (!entry.isFile() && entry.suffix() != "png") + continue; + + SkinModel w(entry.absoluteFilePath()); + if (w.isValid()) + { + auto add = true; + for (auto s : newSkins) + { + if (s.name() == w.name()) + { + add = false; + break; + } + } + if (add) + { + newSkins.append(w); + needsSave = true; + } + } + } + std::sort(newSkins.begin(), + newSkins.end(), + [](const SkinModel& a, const SkinModel& b) { return a.getPath().localeAwareCompare(b.getPath()) < 0; }); + beginResetModel(); + m_skinList.swap(newSkins); + endResetModel(); + if (needsSave) + save(); + return true; +} + +void SkinList::directoryChanged(const QString& path) +{ + QDir new_dir(path); + if (!new_dir.exists()) + if (!FS::ensureFolderPathExists(new_dir.absolutePath())) + return; + if (m_dir.absolutePath() != new_dir.absolutePath()) + { + m_dir.setPath(path); + m_dir.refresh(); + if (m_isWatching) + stopWatching(); + startWatching(); + } + update(); +} + +void SkinList::fileChanged(const QString& path) +{ + qDebug() << "Checking " << path; + QFileInfo checkfile(path); + if (!checkfile.exists()) + return; + + for (int i = 0; i < m_skinList.count(); i++) + { + if (m_skinList[i].getPath() == checkfile.absoluteFilePath()) + { + m_skinList[i].refresh(); + dataChanged(index(i), index(i)); + break; + } + } +} + +QStringList SkinList::mimeTypes() const +{ + return { "text/uri-list" }; +} + +Qt::DropActions SkinList::supportedDropActions() const +{ + return Qt::CopyAction; +} + +bool SkinList::dropMimeData(const QMimeData* data, + Qt::DropAction action, + [[maybe_unused]] int row, + [[maybe_unused]] int column, + [[maybe_unused]] const QModelIndex& parent) +{ + if (action == Qt::IgnoreAction) + return true; + // check if the action is supported + if (!data || !(action & supportedDropActions())) + return false; + + // files dropped from outside? + if (data->hasUrls()) + { + auto urls = data->urls(); + QStringList skinFiles; + for (auto url : urls) + { + // only local files may be dropped... + if (!url.isLocalFile()) + continue; + skinFiles << url.toLocalFile(); + } + installSkins(skinFiles); + return true; + } + return false; +} + +Qt::ItemFlags SkinList::flags(const QModelIndex& index) const +{ + Qt::ItemFlags f = Qt::ItemIsDropEnabled | QAbstractListModel::flags(index); + if (index.isValid()) + { + f |= (Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable); + } + return f; +} + +QVariant SkinList::data(const QModelIndex& index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + int row = index.row(); + + if (row < 0 || row >= m_skinList.size()) + return QVariant(); + auto skin = m_skinList[row]; + switch (role) + { + case Qt::DecorationRole: + { + auto preview = skin.getPreview(); + if (preview.isNull()) + { + preview = skin.getTexture(); + } + return preview; + } + case Qt::DisplayRole: return skin.name(); + case Qt::UserRole: return skin.name(); + case Qt::EditRole: return skin.name(); + default: return QVariant(); + } +} + +int SkinList::rowCount(const QModelIndex& parent) const +{ + return parent.isValid() ? 0 : m_skinList.size(); +} + +void SkinList::installSkins(const QStringList& iconFiles) +{ + for (QString file : iconFiles) + installSkin(file); +} + +QString getUniqueFile(const QString& root, const QString& file) +{ + auto result = FS::PathCombine(root, file); + if (!QFileInfo::exists(result)) + { + return result; + } + + QString baseName = QFileInfo(file).completeBaseName(); + QString extension = QFileInfo(file).suffix(); + int tries = 0; + while (QFileInfo::exists(result)) + { + if (++tries > 256) + return {}; + + QString key = QString("%1%2.%3").arg(baseName).arg(tries).arg(extension); + result = FS::PathCombine(root, key); + } + + return result; +} +QString SkinList::installSkin(const QString& file, const QString& name) +{ + if (file.isEmpty()) + return tr("Path is empty."); + QFileInfo fileinfo(file); + if (!fileinfo.exists()) + return tr("File doesn't exist."); + if (!fileinfo.isFile()) + return tr("Not a file."); + if (!fileinfo.isReadable()) + return tr("File is not readable."); + if (fileinfo.suffix() != "png" && !SkinModel(fileinfo.absoluteFilePath()).isValid()) + return tr("Skin images must be 64x64 or 64x32 pixel PNG files."); + + QString target = getUniqueFile(m_dir.absolutePath(), name.isEmpty() ? fileinfo.fileName() : name); + + return QFile::copy(file, target) ? "" : tr("Unable to copy file"); +} + +int SkinList::getSkinIndex(const QString& key) const +{ + for (int i = 0; i < m_skinList.count(); i++) + { + if (m_skinList[i].name() == key) + { + return i; + } + } + return -1; +} + +const SkinModel* SkinList::skin(const QString& key) const +{ + int idx = getSkinIndex(key); + if (idx == -1) + return nullptr; + return &m_skinList[idx]; +} + +SkinModel* SkinList::skin(const QString& key) +{ + int idx = getSkinIndex(key); + if (idx == -1) + return nullptr; + return &m_skinList[idx]; +} + +bool SkinList::deleteSkin(const QString& key, bool trash) +{ + int idx = getSkinIndex(key); + if (idx != -1) + { + auto s = m_skinList[idx]; + if (trash) + { + if (FS::trash(s.getPath(), nullptr)) + { + m_skinList.remove(idx); + save(); + return true; + } + } + else if (QFile::remove(s.getPath())) + { + m_skinList.remove(idx); + save(); + return true; + } + } + return false; +} + +void SkinList::save() +{ + QJsonObject doc; + QJsonArray arr; + for (auto s : m_skinList) + { + arr << s.toJSON(); + } + doc["skins"] = arr; + try + { + Json::write(doc, m_dir.absoluteFilePath("index.json")); + } + catch (const FS::FileSystemException& e) + { + qCritical() << "Failed to write skin index file :" << e.cause(); + } +} + +int SkinList::getSelectedAccountSkin() +{ + const auto& skin = m_acct->accountData()->minecraftProfile.skin; + for (int i = 0; i < m_skinList.count(); i++) + { + if (m_skinList[i].getURL() == skin.url) + { + return i; + } + } + return -1; +} + +bool SkinList::setData(const QModelIndex& idx, const QVariant& value, int role) +{ + if (!idx.isValid() || role != Qt::EditRole) + { + return false; + } + + int row = idx.row(); + if (row < 0 || row >= m_skinList.size()) + return false; + auto& skin = m_skinList[row]; + auto newName = value.toString(); + if (skin.name() != newName) + { + if (!skin.rename(newName)) + return false; + save(); + } + return true; +} + +void SkinList::updateSkin(SkinModel* s) +{ + auto done = false; + for (auto i = 0; i < m_skinList.size(); i++) + { + if (m_skinList[i].getPath() == s->getPath()) + { + m_skinList[i].setCapeId(s->getCapeId()); + m_skinList[i].setModel(s->getModel()); + m_skinList[i].setURL(s->getURL()); + done = true; + break; + } + } + if (!done) + { + beginInsertRows(QModelIndex(), m_skinList.count(), m_skinList.count() + 1); + m_skinList.append(*s); + endInsertRows(); + } + save(); +} diff --git a/archived/projt-launcher/launcher/minecraft/skins/SkinList.h b/archived/projt-launcher/launcher/minecraft/skins/SkinList.h new file mode 100644 index 0000000000..9e5c041f73 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/skins/SkinList.h @@ -0,0 +1,115 @@ +// 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) 2023 Trial97 <alexandru.tripon97@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. + * + * ======================================================================== */ + +#pragma once + +#include <QAbstractListModel> +#include <QDir> +#include <QFileSystemWatcher> + +#include "QObjectPtr.h" +#include "SkinModel.h" +#include "minecraft/auth/MinecraftAccount.hpp" + +class SkinList : public QAbstractListModel +{ + Q_OBJECT + public: + explicit SkinList(QObject* parent, QString path, MinecraftAccountPtr acct); + virtual ~SkinList() + { + save(); + }; + + int getSkinIndex(const QString& key) const; + + virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + bool setData(const QModelIndex& idx, const QVariant& value, int role) override; + virtual int rowCount(const QModelIndex& parent = QModelIndex()) const override; + + virtual QStringList mimeTypes() const override; + virtual Qt::DropActions supportedDropActions() const override; + virtual bool dropMimeData(const QMimeData* data, + Qt::DropAction action, + int row, + int column, + const QModelIndex& parent) override; + virtual Qt::ItemFlags flags(const QModelIndex& index) const override; + + bool deleteSkin(const QString& key, bool trash); + + void installSkins(const QStringList& iconFiles); + QString installSkin(const QString& file, const QString& name = {}); + + const SkinModel* skin(const QString& key) const; + SkinModel* skin(const QString& key); + + void startWatching(); + void stopWatching(); + + QString getDir() const + { + return m_dir.absolutePath(); + } + void save(); + int getSelectedAccountSkin(); + + void updateSkin(SkinModel* s); + + private: + // hide copy constructor + SkinList(const SkinList&) = delete; + // hide assign op + SkinList& operator=(const SkinList&) = delete; + + protected slots: + void directoryChanged(const QString& path); + void fileChanged(const QString& path); + bool update(); + + private: + shared_qobject_ptr<QFileSystemWatcher> m_watcher; + bool m_isWatching; + QList<SkinModel> m_skinList; + QDir m_dir; + MinecraftAccountPtr m_acct; +};
\ No newline at end of file diff --git a/archived/projt-launcher/launcher/minecraft/skins/SkinModel.cpp b/archived/projt-launcher/launcher/minecraft/skins/SkinModel.cpp new file mode 100644 index 0000000000..875399197f --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/skins/SkinModel.cpp @@ -0,0 +1,211 @@ +// 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) 2023 Trial97 <alexandru.tripon97@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. + * + * ======================================================================== */ + +#include "SkinModel.h" +#include <QFileInfo> +#include <QPainter> + +#include "FileSystem.h" +#include "Json.h" + +static QImage improveSkin(QImage skin) +{ + int height = skin.height(); + int width = skin.width(); + if (width != 64 || (height != 32 && height != 64)) + { + return skin; + } + + // It seems some older skins may use this format, which can't be drawn onto + // https://github.com/PrismLauncher/PrismLauncher/issues/4032 + // https://doc.qt.io/qt-6/qpainter.html#begin + if (skin.format() <= QImage::Format_Indexed8 || !skin.hasAlphaChannel()) + { + skin = skin.convertToFormat(QImage::Format_ARGB32); + } + if (skin.size() == QSize(64, 32)) + { + // old format + auto newSkin = QImage(QSize(64, 64), skin.format()); + newSkin.fill(Qt::transparent); + QPainter p(&newSkin); + p.drawImage(QPoint(0, 0), skin.copy(QRect(0, 0, 64, 32))); // copy head + + auto leg = skin.copy(QRect(0, 16, 16, 16)); + p.drawImage(QPoint(16, 48), leg); // copy leg + + auto arm = skin.copy(QRect(40, 16, 16, 16)); + p.drawImage(QPoint(32, 48), arm); // copy arm + return newSkin; + } + return skin; +} +static QImage getSkin(const QString path) +{ + return improveSkin(QImage(path)); +} + +static QImage generatePreviews(QImage texture, bool slim) +{ + QImage preview(36, 36, QImage::Format_ARGB32); + preview.fill(Qt::transparent); + QPainter paint(&preview); + + // head + paint.drawImage(4, 2, texture.copy(8, 8, 8, 8)); + paint.drawImage(4, 2, texture.copy(40, 8, 8, 8)); + // torso + paint.drawImage(4, 10, texture.copy(20, 20, 8, 12)); + paint.drawImage(4, 10, texture.copy(20, 36, 8, 12)); + // right leg + paint.drawImage(4, 22, texture.copy(4, 20, 4, 12)); + paint.drawImage(4, 22, texture.copy(4, 36, 4, 12)); + // left leg + paint.drawImage(8, 22, texture.copy(4, 52, 4, 12)); + paint.drawImage(8, 22, texture.copy(20, 52, 4, 12)); + + auto armWidth = slim ? 3 : 4; + auto armPosX = slim ? 1 : 0; + // right arm + paint.drawImage(armPosX, 10, texture.copy(44, 20, armWidth, 12)); + paint.drawImage(armPosX, 10, texture.copy(44, 36, armWidth, 12)); + // left arm + paint.drawImage(12, 10, texture.copy(36, 52, armWidth, 12)); + paint.drawImage(12, 10, texture.copy(52, 52, armWidth, 12)); + + // back + // head + paint.drawImage(24, 2, texture.copy(24, 8, 8, 8)); + paint.drawImage(24, 2, texture.copy(56, 8, 8, 8)); + // torso + paint.drawImage(24, 10, texture.copy(32, 20, 8, 12)); + paint.drawImage(24, 10, texture.copy(32, 36, 8, 12)); + // right leg + paint.drawImage(24, 22, texture.copy(12, 20, 4, 12)); + paint.drawImage(24, 22, texture.copy(12, 36, 4, 12)); + // left leg + paint.drawImage(28, 22, texture.copy(12, 52, 4, 12)); + paint.drawImage(28, 22, texture.copy(28, 52, 4, 12)); + + // right arm + paint.drawImage(armPosX + 20, 10, texture.copy(48 + armWidth, 20, armWidth, 12)); + paint.drawImage(armPosX + 20, 10, texture.copy(48 + armWidth, 36, armWidth, 12)); + // left arm + paint.drawImage(32, 10, texture.copy(40 + armWidth, 52, armWidth, 12)); + paint.drawImage(32, 10, texture.copy(56 + armWidth, 52, armWidth, 12)); + + return preview; +} +SkinModel::SkinModel(QString path) : m_path(path), m_texture(getSkin(path)), m_model(Model::CLASSIC) +{ + m_preview = generatePreviews(m_texture, false); +} + +SkinModel::SkinModel(QDir skinDir, QJsonObject obj) + : m_capeId(Json::ensureString(obj, "capeId")), + m_model(Model::CLASSIC), + m_url(Json::ensureString(obj, "url")) +{ + auto name = Json::ensureString(obj, "name"); + + if (auto model = Json::ensureString(obj, "model"); model == "SLIM") + { + m_model = Model::SLIM; + } + m_path = skinDir.absoluteFilePath(name) + ".png"; + m_texture = getSkin(m_path); + m_preview = generatePreviews(m_texture, m_model == Model::SLIM); +} + +QString SkinModel::name() const +{ + return QFileInfo(m_path).completeBaseName(); +} + +bool SkinModel::rename(QString newName) +{ + auto info = QFileInfo(m_path); + auto new_path = FS::PathCombine(info.absolutePath(), newName + ".png"); + if (QFileInfo::exists(new_path)) + { + return false; + } + m_path = new_path; + return FS::move(info.absoluteFilePath(), m_path); +} + +QJsonObject SkinModel::toJSON() const +{ + QJsonObject obj; + obj["name"] = name(); + obj["capeId"] = m_capeId; + obj["url"] = m_url; + obj["model"] = getModelString(); + return obj; +} + +QString SkinModel::getModelString() const +{ + switch (m_model) + { + case CLASSIC: return "CLASSIC"; + case SLIM: return "SLIM"; + } + return {}; +} + +bool SkinModel::isValid() const +{ + return !m_texture.isNull() && (m_texture.size().height() == 32 || m_texture.size().height() == 64) + && m_texture.size().width() == 64; +} +void SkinModel::refresh() +{ + m_texture = getSkin(m_path); + m_preview = generatePreviews(m_texture, m_model == Model::SLIM); +} +void SkinModel::setModel(Model model) +{ + m_model = model; + m_preview = generatePreviews(m_texture, m_model == Model::SLIM); +} diff --git a/archived/projt-launcher/launcher/minecraft/skins/SkinModel.h b/archived/projt-launcher/launcher/minecraft/skins/SkinModel.h new file mode 100644 index 0000000000..09b711c1a4 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/skins/SkinModel.h @@ -0,0 +1,112 @@ +// 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) 2023 Trial97 <alexandru.tripon97@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. + * + * ======================================================================== */ + +#pragma once + +#include <QDir> +#include <QImage> +#include <QJsonObject> + +class SkinModel +{ + public: + enum Model + { + CLASSIC, + SLIM + }; + + SkinModel() = default; + SkinModel(QString path); + SkinModel(QDir skinDir, QJsonObject obj); + virtual ~SkinModel() = default; + + QString name() const; + QString getModelString() const; + bool isValid() const; + QString getPath() const + { + return m_path; + } + QImage getTexture() const + { + return m_texture; + } + QImage getPreview() const + { + return m_preview; + } + QString getCapeId() const + { + return m_capeId; + } + Model getModel() const + { + return m_model; + } + QString getURL() const + { + return m_url; + } + + bool rename(QString newName); + void setCapeId(QString capeID) + { + m_capeId = capeID; + } + void setModel(Model model); + void setURL(QString url) + { + m_url = url; + } + void refresh(); + + QJsonObject toJSON() const; + + private: + QString m_path; + QImage m_texture; + QImage m_preview; + QString m_capeId; + Model m_model; + QString m_url; +};
\ No newline at end of file diff --git a/archived/projt-launcher/launcher/minecraft/skins/SkinUpload.cpp b/archived/projt-launcher/launcher/minecraft/skins/SkinUpload.cpp new file mode 100644 index 0000000000..93d77dbeee --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/skins/SkinUpload.cpp @@ -0,0 +1,105 @@ +// 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 Sefa Eyeoglu <contact@scrumplex.net> + * Copyright (c) 2023 Trial97 <alexandru.tripon97@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 "SkinUpload.h" + +#include <QHttpMultiPart> + +#include "FileSystem.h" +#include "net/ByteArraySink.h" +#include "net/RawHeaderProxy.h" + +SkinUpload::SkinUpload(QString path, QString variant) : NetRequest(), m_path(path), m_variant(variant) +{ + logCat = taskMCSkinsLogC; +} + +QNetworkReply* SkinUpload::getReply(QNetworkRequest& request) +{ + QHttpMultiPart* multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType, this); + + QHttpPart skin; + skin.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("image/png")); + skin.setHeader(QNetworkRequest::ContentDispositionHeader, + QVariant("form-data; name=\"file\"; filename=\"skin.png\"")); + + skin.setBody(FS::read(m_path)); + + QHttpPart model; + model.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"variant\"")); + model.setBody(m_variant.toUtf8()); + + multiPart->append(skin); + multiPart->append(model); + setStatus(tr("Uploading skin")); + return m_network->post(request, multiPart); +} + +SkinUpload::Ptr SkinUpload::make(QString token, QString path, QString variant) +{ + auto up = makeShared<SkinUpload>(path, variant); + up->m_url = QUrl("https://api.minecraftservices.com/minecraft/profile/skins"); + up->setObjectName(QString("BYTES:") + up->m_url.toString()); + up->m_sink.reset(new Net::ByteArraySink(std::make_shared<QByteArray>())); + up->addHeaderProxy(new Net::RawHeaderProxy(QList<Net::HeaderPair>{ + { "Authorization", QString("Bearer %1").arg(token).toLocal8Bit() }, + })); + return up; +} diff --git a/archived/projt-launcher/launcher/minecraft/skins/SkinUpload.h b/archived/projt-launcher/launcher/minecraft/skins/SkinUpload.h new file mode 100644 index 0000000000..f39db0a59e --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/skins/SkinUpload.h @@ -0,0 +1,65 @@ +// 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) 2023 Trial97 <alexandru.tripon97@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. + * + * ======================================================================== */ + +#pragma once + +#include "net/NetRequest.h" + +class SkinUpload : public Net::NetRequest +{ + Q_OBJECT + public: + using Ptr = shared_qobject_ptr<SkinUpload>; + + // Note this class takes ownership of the file. + SkinUpload(QString path, QString variant); + virtual ~SkinUpload() = default; + + static SkinUpload::Ptr make(QString token, QString path, QString variant); + + protected: + virtual QNetworkReply* getReply(QNetworkRequest&) override; + + private: + QString m_path; + QString m_variant; +}; diff --git a/archived/projt-launcher/launcher/minecraft/update/AssetUpdateTask.cpp b/archived/projt-launcher/launcher/minecraft/update/AssetUpdateTask.cpp new file mode 100644 index 0000000000..2e205f3763 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/update/AssetUpdateTask.cpp @@ -0,0 +1,139 @@ +// 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. + */ +#include "AssetUpdateTask.h" + +#include "BuildConfig.h" +#include "launch/LaunchStage.hpp" +#include "minecraft/AssetsUtils.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" +#include "net/ChecksumValidator.h" + +#include "Application.h" + +#include "net/ApiDownload.h" + +AssetUpdateTask::AssetUpdateTask(MinecraftInstance* inst) +{ + m_inst = inst; +} + +void AssetUpdateTask::executeTask() +{ + setStatus(tr("Updating assets index...")); + auto components = m_inst->getPackProfile(); + auto profile = components->getProfile(); + auto assets = profile->getMinecraftAssets(); + QUrl indexUrl = assets->url; + QString localPath = assets->id + ".json"; + auto job = makeShared<NetJob>(tr("Asset index for %1").arg(m_inst->name()), APPLICATION->network()); + + auto metacache = APPLICATION->metacache(); + auto entry = metacache->resolveEntry("asset_indexes", localPath); + entry->setStale(true); + auto hexSha1 = assets->sha1.toLatin1(); + qDebug() << "Asset index SHA1:" << hexSha1; + auto dl = Net::ApiDownload::makeCached(indexUrl, entry); + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, assets->sha1)); + job->addNetAction(dl); + + downloadJob.reset(job); + + connect(downloadJob.get(), &NetJob::succeeded, this, &AssetUpdateTask::assetIndexFinished); + connect(downloadJob.get(), &NetJob::failed, this, &AssetUpdateTask::assetIndexFailed); + connect(downloadJob.get(), &NetJob::aborted, this, &AssetUpdateTask::emitAborted); + connect(downloadJob.get(), &NetJob::progress, this, &AssetUpdateTask::progress); + connect(downloadJob.get(), &NetJob::stepProgress, this, &AssetUpdateTask::propagateStepProgress); + + qDebug() << m_inst->name() << ": Starting asset index download"; + downloadJob->start(); +} + +bool AssetUpdateTask::canAbort() const +{ + return true; +} + +void AssetUpdateTask::assetIndexFinished() +{ + AssetsIndex index; + qDebug() << m_inst->name() << ": Finished asset index download"; + + auto components = m_inst->getPackProfile(); + auto profile = components->getProfile(); + auto assets = profile->getMinecraftAssets(); + + QString asset_fname = "assets/indexes/" + assets->id + ".json"; + // NOTE: Current validation is done via AssetsUtils::loadAssetsIndexJson. + // Future improvement: Implement a generic validator based on JSON schema for more robust checks. + if (!AssetsUtils::loadAssetsIndexJson(assets->id, asset_fname, index)) + { + auto metacache = APPLICATION->metacache(); + auto entry = metacache->resolveEntry("asset_indexes", assets->id + ".json"); + metacache->evictEntry(entry); + emitFailed(tr("Failed to read the assets index!")); + } + + auto job = index.getDownloadJob(); + if (job) + { + QString resourceURLStr = APPLICATION->settings()->get("ResourceURL").toString(); + QString source = tr("Mojang"); + if (!resourceURLStr.isEmpty() && resourceURLStr != BuildConfig.DEFAULT_RESOURCE_BASE) + { + source = QUrl(resourceURLStr).host(); + } + setStatus(tr("Getting the asset files from %1...").arg(source)); + downloadJob = job; + connect(downloadJob.get(), &NetJob::succeeded, this, &AssetUpdateTask::emitSucceeded); + connect(downloadJob.get(), &NetJob::failed, this, &AssetUpdateTask::assetsFailed); + connect(downloadJob.get(), &NetJob::aborted, this, &AssetUpdateTask::emitAborted); + connect(downloadJob.get(), &NetJob::progress, this, &AssetUpdateTask::progress); + connect(downloadJob.get(), &NetJob::stepProgress, this, &AssetUpdateTask::propagateStepProgress); + downloadJob->start(); + return; + } + emitSucceeded(); +} + +void AssetUpdateTask::assetIndexFailed(QString reason) +{ + qDebug() << m_inst->name() << ": Failed asset index download"; + emitFailed(tr("Failed to download the assets index:\n%1").arg(reason)); +} + +void AssetUpdateTask::assetsFailed(QString reason) +{ + emitFailed(tr("Failed to download assets:\n%1").arg(reason)); +} + +bool AssetUpdateTask::abort() +{ + if (downloadJob) + { + return downloadJob->abort(); + } + else + { + qWarning() << "Prematurely aborted AssetUpdateTask"; + } + return true; +} diff --git a/archived/projt-launcher/launcher/minecraft/update/AssetUpdateTask.h b/archived/projt-launcher/launcher/minecraft/update/AssetUpdateTask.h new file mode 100644 index 0000000000..65c9263a47 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/update/AssetUpdateTask.h @@ -0,0 +1,48 @@ +// 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. + */ +#pragma once +#include "net/NetJob.h" +#include "tasks/Task.h" +class MinecraftInstance; + +class AssetUpdateTask : public Task +{ + Q_OBJECT + public: + AssetUpdateTask(MinecraftInstance* inst); + virtual ~AssetUpdateTask() = default; + + void executeTask() override; + + bool canAbort() const override; + + private slots: + void assetIndexFinished(); + void assetIndexFailed(QString reason); + void assetsFailed(QString reason); + + public slots: + bool abort() override; + + private: + MinecraftInstance* m_inst; + NetJob::Ptr downloadJob; +}; diff --git a/archived/projt-launcher/launcher/minecraft/update/FMLLibrariesTask.cpp b/archived/projt-launcher/launcher/minecraft/update/FMLLibrariesTask.cpp new file mode 100644 index 0000000000..5aa2c6dd36 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/update/FMLLibrariesTask.cpp @@ -0,0 +1,159 @@ +// 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. + */ +#include "FMLLibrariesTask.h" + +#include "FileSystem.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" +#include "minecraft/VersionFilterData.h" + +#include "Application.h" +#include "BuildConfig.h" + +#include "net/ApiDownload.h" + +FMLLibrariesTask::FMLLibrariesTask(MinecraftInstance* inst) +{ + m_inst = inst; +} +void FMLLibrariesTask::executeTask() +{ + // Get the mod list + MinecraftInstance* inst = (MinecraftInstance*)m_inst; + auto components = inst->getPackProfile(); + auto profile = components->getProfile(); + + if (!profile->hasTrait("legacyFML")) + { + emitSucceeded(); + return; + } + + QString version = components->getComponentVersion("net.minecraft"); + auto& fmlLibsMapping = g_VersionFilterData.fmlLibsMapping; + if (!fmlLibsMapping.contains(version)) + { + emitSucceeded(); + return; + } + + auto& libList = fmlLibsMapping[version]; + + // determine if we need some libs for FML or forge + setStatus(tr("Checking for FML libraries...")); + if (!components->getComponent("net.minecraftforge")) + { + emitSucceeded(); + return; + } + + // now check the lib folder inside the instance for files. + for (auto& lib : libList) + { + QFileInfo libInfo(FS::PathCombine(inst->libDir(), lib.filename)); + if (libInfo.exists()) + continue; + fmlLibsToProcess.append(lib); + } + + // if everything is in place, there's nothing to do here... + if (fmlLibsToProcess.isEmpty()) + { + emitSucceeded(); + return; + } + + // download missing libs to our place + setStatus(tr("Downloading FML libraries...")); + NetJob::Ptr dljob{ new NetJob("FML libraries", APPLICATION->network()) }; + auto metacache = APPLICATION->metacache(); + Net::Download::Options options = Net::Download::Option::MakeEternal; + for (auto& lib : fmlLibsToProcess) + { + auto entry = metacache->resolveEntry("fmllibs", lib.filename); + QString urlString = BuildConfig.FMLLIBS_BASE_URL + lib.filename; + dljob->addNetAction(Net::ApiDownload::makeCached(QUrl(urlString), entry, options)); + } + + connect(dljob.get(), &NetJob::succeeded, this, &FMLLibrariesTask::fmllibsFinished); + connect(dljob.get(), &NetJob::failed, this, &FMLLibrariesTask::fmllibsFailed); + connect(dljob.get(), &NetJob::aborted, this, &FMLLibrariesTask::emitAborted); + connect(dljob.get(), &NetJob::progress, this, &FMLLibrariesTask::progress); + connect(dljob.get(), &NetJob::stepProgress, this, &FMLLibrariesTask::propagateStepProgress); + downloadJob.reset(dljob); + downloadJob->start(); +} + +bool FMLLibrariesTask::canAbort() const +{ + return true; +} + +void FMLLibrariesTask::fmllibsFinished() +{ + downloadJob.reset(); + if (!fmlLibsToProcess.isEmpty()) + { + setStatus(tr("Copying FML libraries into the instance...")); + MinecraftInstance* inst = (MinecraftInstance*)m_inst; + auto metacache = APPLICATION->metacache(); + int index = 0; + for (auto& lib : fmlLibsToProcess) + { + progress(index, fmlLibsToProcess.size()); + auto entry = metacache->resolveEntry("fmllibs", lib.filename); + auto path = FS::PathCombine(inst->libDir(), lib.filename); + if (!FS::ensureFilePathExists(path)) + { + emitFailed(tr("Failed creating FML library folder inside the instance.")); + return; + } + if (!QFile::copy(entry->getFullPath(), FS::PathCombine(inst->libDir(), lib.filename))) + { + emitFailed(tr("Failed copying Forge/FML library: %1.").arg(lib.filename)); + return; + } + index++; + } + progress(index, fmlLibsToProcess.size()); + } + emitSucceeded(); +} +void FMLLibrariesTask::fmllibsFailed(QString reason) +{ + QStringList failed = downloadJob->getFailedFiles(); + QString failed_all = failed.join("\n"); + emitFailed( + tr("Failed to download the following files:\n%1\n\nReason:%2\nPlease try again.").arg(failed_all, reason)); +} + +bool FMLLibrariesTask::abort() +{ + if (downloadJob) + { + return downloadJob->abort(); + } + else + { + qWarning() << "Prematurely aborted FMLLibrariesTask"; + } + return true; +} diff --git a/archived/projt-launcher/launcher/minecraft/update/FMLLibrariesTask.h b/archived/projt-launcher/launcher/minecraft/update/FMLLibrariesTask.h new file mode 100644 index 0000000000..b96db03c1c --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/update/FMLLibrariesTask.h @@ -0,0 +1,50 @@ +// 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. + */ +#pragma once +#include "minecraft/VersionFilterData.h" +#include "net/NetJob.h" +#include "tasks/Task.h" + +class MinecraftInstance; + +class FMLLibrariesTask : public Task +{ + Q_OBJECT + public: + FMLLibrariesTask(MinecraftInstance* inst); + virtual ~FMLLibrariesTask() = default; + + void executeTask() override; + + bool canAbort() const override; + + private slots: + void fmllibsFinished(); + void fmllibsFailed(QString reason); + + public slots: + bool abort() override; + + private: + MinecraftInstance* m_inst; + NetJob::Ptr downloadJob; + QList<FMLlib> fmlLibsToProcess; +}; diff --git a/archived/projt-launcher/launcher/minecraft/update/FoldersTask.cpp b/archived/projt-launcher/launcher/minecraft/update/FoldersTask.cpp new file mode 100644 index 0000000000..7177ed22d2 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/update/FoldersTask.cpp @@ -0,0 +1,79 @@ +// 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 Sefa Eyeoglu <contact@scrumplex.net> + * + * 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 "FoldersTask.h" +#include <QDir> +#include "minecraft/MinecraftInstance.h" + +FoldersTask::FoldersTask(MinecraftInstance* inst) +{ + m_inst = inst; +} + +void FoldersTask::executeTask() +{ + // Make directories + QDir mcDir(m_inst->gameRoot()); + if (!mcDir.exists() && !mcDir.mkpath(".")) + { + emitFailed(tr("Failed to create folder for Minecraft binaries.")); + return; + } + emitSucceeded(); +} diff --git a/archived/projt-launcher/launcher/minecraft/update/FoldersTask.h b/archived/projt-launcher/launcher/minecraft/update/FoldersTask.h new file mode 100644 index 0000000000..426c162d78 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/update/FoldersTask.h @@ -0,0 +1,37 @@ +// 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. + */ +#pragma once + +#include "tasks/Task.h" + +class MinecraftInstance; +class FoldersTask : public Task +{ + Q_OBJECT + public: + FoldersTask(MinecraftInstance* inst); + virtual ~FoldersTask() = default; + + void executeTask() override; + + private: + MinecraftInstance* m_inst; +}; diff --git a/archived/projt-launcher/launcher/minecraft/update/LibrariesTask.cpp b/archived/projt-launcher/launcher/minecraft/update/LibrariesTask.cpp new file mode 100644 index 0000000000..6fa5796d0f --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/update/LibrariesTask.cpp @@ -0,0 +1,123 @@ +// 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. + */ +#include "LibrariesTask.h" + +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" + +#include "Application.h" + +LibrariesTask::LibrariesTask(MinecraftInstance* inst) +{ + m_inst = inst; +} + +void LibrariesTask::executeTask() +{ + setStatus(tr("Downloading required library files...")); + qDebug() << m_inst->name() << ": downloading libraries"; + MinecraftInstance* inst = (MinecraftInstance*)m_inst; + + // Build a list of URLs that will need to be downloaded. + auto components = inst->getPackProfile(); + auto profile = components->getProfile(); + + NetJob::Ptr job{ new NetJob(tr("Libraries for instance %1").arg(inst->name()), APPLICATION->network()) }; + downloadJob.reset(job); + + auto metacache = APPLICATION->metacache(); + + auto processArtifactPool = + [this, inst, metacache](const QList<LibraryPtr>& pool, QStringList& errors, const QString& localPath) + { + for (auto lib : pool) + { + if (!lib) + { + emitFailed(tr("Null jar is specified in the metadata, aborting.")); + return false; + } + auto dls = lib->getDownloads(inst->runtimeContext(), metacache.get(), errors, localPath); + for (auto dl : dls) + { + downloadJob->addNetAction(dl); + } + } + return true; + }; + + QStringList failedLocalLibraries; + QList<LibraryPtr> libArtifactPool; + libArtifactPool.append(profile->getLibraries()); + libArtifactPool.append(profile->getNativeLibraries()); + libArtifactPool.append(profile->getMavenFiles()); + for (auto agent : profile->getAgents()) + { + libArtifactPool.append(agent->library()); + } + libArtifactPool.append(profile->getMainJar()); + processArtifactPool(libArtifactPool, failedLocalLibraries, inst->getLocalLibraryPath()); + + QStringList failedLocalJarMods; + processArtifactPool(profile->getJarMods(), failedLocalJarMods, inst->jarModsDir()); + + if (!failedLocalJarMods.empty() || !failedLocalLibraries.empty()) + { + downloadJob.reset(); + QString failed_all = (failedLocalLibraries + failedLocalJarMods).join("\n"); + emitFailed(tr("Some artifacts marked as 'local' are missing their files:\n%1\n\nYou need to either add the " + "files, or removed the " + "packages that require them.\nYou'll have to correct this problem manually.") + .arg(failed_all)); + return; + } + + connect(downloadJob.get(), &NetJob::succeeded, this, &LibrariesTask::emitSucceeded); + connect(downloadJob.get(), &NetJob::failed, this, &LibrariesTask::jarlibFailed); + connect(downloadJob.get(), &NetJob::aborted, this, &LibrariesTask::emitAborted); + connect(downloadJob.get(), &NetJob::progress, this, &LibrariesTask::progress); + connect(downloadJob.get(), &NetJob::stepProgress, this, &LibrariesTask::propagateStepProgress); + + downloadJob->start(); +} + +bool LibrariesTask::canAbort() const +{ + return true; +} + +void LibrariesTask::jarlibFailed(QString reason) +{ + emitFailed(tr("Game update failed: it was impossible to fetch the required libraries.\nReason:\n%1").arg(reason)); +} + +bool LibrariesTask::abort() +{ + if (downloadJob) + { + return downloadJob->abort(); + } + else + { + qWarning() << "Prematurely aborted LibrariesTask"; + } + return true; +} diff --git a/archived/projt-launcher/launcher/minecraft/update/LibrariesTask.h b/archived/projt-launcher/launcher/minecraft/update/LibrariesTask.h new file mode 100644 index 0000000000..7c4c9c54a0 --- /dev/null +++ b/archived/projt-launcher/launcher/minecraft/update/LibrariesTask.h @@ -0,0 +1,46 @@ +// 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. + */ +#pragma once +#include "net/NetJob.h" +#include "tasks/Task.h" +class MinecraftInstance; + +class LibrariesTask : public Task +{ + Q_OBJECT + public: + LibrariesTask(MinecraftInstance* inst); + virtual ~LibrariesTask() = default; + + void executeTask() override; + + bool canAbort() const override; + + private slots: + void jarlibFailed(QString reason); + + public slots: + bool abort() override; + + private: + MinecraftInstance* m_inst; + NetJob::Ptr downloadJob; +}; |
