summaryrefslogtreecommitdiff
path: root/meshmc/launcher/minecraft
diff options
context:
space:
mode:
authorMehmet Samet Duman <yongdohyun@projecttick.org>2026-04-02 18:45:07 +0300
committerMehmet Samet Duman <yongdohyun@projecttick.org>2026-04-02 18:45:07 +0300
commit31b9a8949ed0a288143e23bf739f2eb64fdc63be (patch)
tree8a984fa143c38fccad461a77792d6864f3e82cd3 /meshmc/launcher/minecraft
parent934382c8a1ce738589dee9ee0f14e1cec812770e (diff)
parentfad6a1066616b69d7f5fef01178efdf014c59537 (diff)
downloadProject-Tick-31b9a8949ed0a288143e23bf739f2eb64fdc63be.tar.gz
Project-Tick-31b9a8949ed0a288143e23bf739f2eb64fdc63be.zip
Add 'meshmc/' from commit 'fad6a1066616b69d7f5fef01178efdf014c59537'
git-subtree-dir: meshmc git-subtree-mainline: 934382c8a1ce738589dee9ee0f14e1cec812770e git-subtree-split: fad6a1066616b69d7f5fef01178efdf014c59537
Diffstat (limited to 'meshmc/launcher/minecraft')
-rw-r--r--meshmc/launcher/minecraft/AssetsUtils.cpp344
-rw-r--r--meshmc/launcher/minecraft/AssetsUtils.h76
-rw-r--r--meshmc/launcher/minecraft/Component.cpp408
-rw-r--r--meshmc/launcher/minecraft/Component.h140
-rw-r--r--meshmc/launcher/minecraft/ComponentUpdateTask.cpp667
-rw-r--r--meshmc/launcher/minecraft/ComponentUpdateTask.h55
-rw-r--r--meshmc/launcher/minecraft/ComponentUpdateTask_p.h46
-rw-r--r--meshmc/launcher/minecraft/GradleSpecifier.h175
-rw-r--r--meshmc/launcher/minecraft/GradleSpecifier_test.cpp99
-rw-r--r--meshmc/launcher/minecraft/LaunchProfile.cpp316
-rw-r--r--meshmc/launcher/minecraft/LaunchProfile.h123
-rw-r--r--meshmc/launcher/minecraft/Library.cpp282
-rw-r--r--meshmc/launcher/minecraft/Library.h242
-rw-r--r--meshmc/launcher/minecraft/Library_test.cpp377
-rw-r--r--meshmc/launcher/minecraft/MinecraftInstance.cpp1108
-rw-r--r--meshmc/launcher/minecraft/MinecraftInstance.h162
-rw-r--r--meshmc/launcher/minecraft/MinecraftLoadAndCheck.cpp71
-rw-r--r--meshmc/launcher/minecraft/MinecraftLoadAndCheck.h70
-rw-r--r--meshmc/launcher/minecraft/MinecraftUpdate.cpp200
-rw-r--r--meshmc/launcher/minecraft/MinecraftUpdate.h78
-rw-r--r--meshmc/launcher/minecraft/MojangDownloadInfo.h95
-rw-r--r--meshmc/launcher/minecraft/MojangVersionFormat.cpp387
-rw-r--r--meshmc/launcher/minecraft/MojangVersionFormat.h50
-rw-r--r--meshmc/launcher/minecraft/MojangVersionFormat_test.cpp77
-rw-r--r--meshmc/launcher/minecraft/OneSixVersionFormat.cpp386
-rw-r--r--meshmc/launcher/minecraft/OneSixVersionFormat.h62
-rw-r--r--meshmc/launcher/minecraft/OpSys.cpp68
-rw-r--r--meshmc/launcher/minecraft/OpSys.h54
-rw-r--r--meshmc/launcher/minecraft/PackProfile.cpp1191
-rw-r--r--meshmc/launcher/minecraft/PackProfile.h178
-rw-r--r--meshmc/launcher/minecraft/PackProfile_p.h61
-rw-r--r--meshmc/launcher/minecraft/ParseUtils.cpp55
-rw-r--r--meshmc/launcher/minecraft/ParseUtils.h30
-rw-r--r--meshmc/launcher/minecraft/ParseUtils_test.cpp57
-rw-r--r--meshmc/launcher/minecraft/ProfileUtils.cpp204
-rw-r--r--meshmc/launcher/minecraft/ProfileUtils.h50
-rw-r--r--meshmc/launcher/minecraft/Rule.cpp114
-rw-r--r--meshmc/launcher/minecraft/Rule.h117
-rw-r--r--meshmc/launcher/minecraft/VersionFile.cpp80
-rw-r--r--meshmc/launcher/minecraft/VersionFile.h141
-rw-r--r--meshmc/launcher/minecraft/VersionFilterData.cpp98
-rw-r--r--meshmc/launcher/minecraft/VersionFilterData.h54
-rw-r--r--meshmc/launcher/minecraft/World.cpp478
-rw-r--r--meshmc/launcher/minecraft/World.h133
-rw-r--r--meshmc/launcher/minecraft/WorldList.cpp380
-rw-r--r--meshmc/launcher/minecraft/WorldList.h148
-rw-r--r--meshmc/launcher/minecraft/auth/AccountData.cpp362
-rw-r--r--meshmc/launcher/minecraft/auth/AccountData.h103
-rw-r--r--meshmc/launcher/minecraft/auth/AccountList.cpp722
-rw-r--r--meshmc/launcher/minecraft/auth/AccountList.h187
-rw-r--r--meshmc/launcher/minecraft/auth/AccountTask.cpp129
-rw-r--r--meshmc/launcher/minecraft/auth/AccountTask.h101
-rw-r--r--meshmc/launcher/minecraft/auth/AuthRequest.cpp162
-rw-r--r--meshmc/launcher/minecraft/auth/AuthRequest.h93
-rw-r--r--meshmc/launcher/minecraft/auth/AuthSession.cpp57
-rw-r--r--meshmc/launcher/minecraft/auth/AuthSession.h71
-rw-r--r--meshmc/launcher/minecraft/auth/AuthStep.cpp26
-rw-r--r--meshmc/launcher/minecraft/auth/AuthStep.h54
-rw-r--r--meshmc/launcher/minecraft/auth/MinecraftAccount.cpp257
-rw-r--r--meshmc/launcher/minecraft/auth/MinecraftAccount.h198
-rw-r--r--meshmc/launcher/minecraft/auth/Parsers.cpp366
-rw-r--r--meshmc/launcher/minecraft/auth/Parsers.h42
-rw-r--r--meshmc/launcher/minecraft/auth/flows/AuthFlow.cpp93
-rw-r--r--meshmc/launcher/minecraft/auth/flows/AuthFlow.h64
-rw-r--r--meshmc/launcher/minecraft/auth/flows/MSA.cpp65
-rw-r--r--meshmc/launcher/minecraft/auth/flows/MSA.h37
-rw-r--r--meshmc/launcher/minecraft/auth/steps/EntitlementsStep.cpp80
-rw-r--r--meshmc/launcher/minecraft/auth/steps/EntitlementsStep.h47
-rw-r--r--meshmc/launcher/minecraft/auth/steps/GetSkinStep.cpp64
-rw-r--r--meshmc/launcher/minecraft/auth/steps/GetSkinStep.h44
-rw-r--r--meshmc/launcher/minecraft/auth/steps/MSAStep.cpp161
-rw-r--r--meshmc/launcher/minecraft/auth/steps/MSAStep.h55
-rw-r--r--meshmc/launcher/minecraft/auth/steps/MeshMCLoginStep.cpp98
-rw-r--r--meshmc/launcher/minecraft/auth/steps/MeshMCLoginStep.h44
-rw-r--r--meshmc/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp101
-rw-r--r--meshmc/launcher/minecraft/auth/steps/MinecraftProfileStep.h44
-rw-r--r--meshmc/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp191
-rw-r--r--meshmc/launcher/minecraft/auth/steps/XboxAuthorizationStep.h55
-rw-r--r--meshmc/launcher/minecraft/auth/steps/XboxProfileStep.cpp96
-rw-r--r--meshmc/launcher/minecraft/auth/steps/XboxProfileStep.h44
-rw-r--r--meshmc/launcher/minecraft/auth/steps/XboxUserStep.cpp93
-rw-r--r--meshmc/launcher/minecraft/auth/steps/XboxUserStep.h44
-rw-r--r--meshmc/launcher/minecraft/gameoptions/GameOptions.cpp155
-rw-r--r--meshmc/launcher/minecraft/gameoptions/GameOptions.h56
-rw-r--r--meshmc/launcher/minecraft/launch/ClaimAccount.cpp49
-rw-r--r--meshmc/launcher/minecraft/launch/ClaimAccount.h61
-rw-r--r--meshmc/launcher/minecraft/launch/CreateGameFolders.cpp50
-rw-r--r--meshmc/launcher/minecraft/launch/CreateGameFolders.h58
-rw-r--r--meshmc/launcher/minecraft/launch/DirectJavaLaunch.cpp173
-rw-r--r--meshmc/launcher/minecraft/launch/DirectJavaLaunch.h80
-rw-r--r--meshmc/launcher/minecraft/launch/ExtractNatives.cpp133
-rw-r--r--meshmc/launcher/minecraft/launch/ExtractNatives.h59
-rw-r--r--meshmc/launcher/minecraft/launch/MeshMCPartLaunch.cpp237
-rw-r--r--meshmc/launcher/minecraft/launch/MeshMCPartLaunch.h83
-rw-r--r--meshmc/launcher/minecraft/launch/MinecraftServerTarget.cpp85
-rw-r--r--meshmc/launcher/minecraft/launch/MinecraftServerTarget.h52
-rw-r--r--meshmc/launcher/minecraft/launch/ModMinecraftJar.cpp103
-rw-r--r--meshmc/launcher/minecraft/launch/ModMinecraftJar.h60
-rw-r--r--meshmc/launcher/minecraft/launch/PrintInstanceInfo.cpp166
-rw-r--r--meshmc/launcher/minecraft/launch/PrintInstanceInfo.h66
-rw-r--r--meshmc/launcher/minecraft/launch/ReconstructAssets.cpp61
-rw-r--r--meshmc/launcher/minecraft/launch/ReconstructAssets.h56
-rw-r--r--meshmc/launcher/minecraft/launch/ScanModFolders.cpp85
-rw-r--r--meshmc/launcher/minecraft/launch/ScanModFolders.h66
-rw-r--r--meshmc/launcher/minecraft/launch/VerifyJavaInstall.cpp374
-rw-r--r--meshmc/launcher/minecraft/launch/VerifyJavaInstall.h61
-rw-r--r--meshmc/launcher/minecraft/legacy/LegacyInstance.cpp270
-rw-r--r--meshmc/launcher/minecraft/legacy/LegacyInstance.h172
-rw-r--r--meshmc/launcher/minecraft/legacy/LegacyModList.cpp150
-rw-r--r--meshmc/launcher/minecraft/legacy/LegacyModList.h69
-rw-r--r--meshmc/launcher/minecraft/legacy/LegacyUpgradeTask.cpp151
-rw-r--r--meshmc/launcher/minecraft/legacy/LegacyUpgradeTask.h49
-rw-r--r--meshmc/launcher/minecraft/mod/LocalModParseTask.cpp423
-rw-r--r--meshmc/launcher/minecraft/mod/LocalModParseTask.h59
-rw-r--r--meshmc/launcher/minecraft/mod/Mod.cpp158
-rw-r--r--meshmc/launcher/minecraft/mod/Mod.h140
-rw-r--r--meshmc/launcher/minecraft/mod/ModDetails.h37
-rw-r--r--meshmc/launcher/minecraft/mod/ModFolderLoadTask.cpp38
-rw-r--r--meshmc/launcher/minecraft/mod/ModFolderLoadTask.h52
-rw-r--r--meshmc/launcher/minecraft/mod/ModFolderModel.cpp573
-rw-r--r--meshmc/launcher/minecraft/mod/ModFolderModel.h169
-rw-r--r--meshmc/launcher/minecraft/mod/ModFolderModel_test.cpp71
-rw-r--r--meshmc/launcher/minecraft/mod/ResourcePackFolderModel.cpp50
-rw-r--r--meshmc/launcher/minecraft/mod/ResourcePackFolderModel.h35
-rw-r--r--meshmc/launcher/minecraft/mod/TexturePackFolderModel.cpp50
-rw-r--r--meshmc/launcher/minecraft/mod/TexturePackFolderModel.h35
-rw-r--r--meshmc/launcher/minecraft/services/CapeChange.cpp96
-rw-r--r--meshmc/launcher/minecraft/services/CapeChange.h52
-rw-r--r--meshmc/launcher/minecraft/services/SkinDelete.cpp66
-rw-r--r--meshmc/launcher/minecraft/services/SkinDelete.h47
-rw-r--r--meshmc/launcher/minecraft/services/SkinUpload.cpp96
-rw-r--r--meshmc/launcher/minecraft/services/SkinUpload.h56
-rw-r--r--meshmc/launcher/minecraft/testdata/1.9-simple.json198
-rw-r--r--meshmc/launcher/minecraft/testdata/1.9.json529
-rw-r--r--meshmc/launcher/minecraft/testdata/codecwav-20101023.jar1
-rw-r--r--meshmc/launcher/minecraft/testdata/lib-native-arch.json46
-rw-r--r--meshmc/launcher/minecraft/testdata/lib-native.json52
-rw-r--r--meshmc/launcher/minecraft/testdata/lib-simple.json11
-rw-r--r--meshmc/launcher/minecraft/testdata/testname-testversion-linux-32.jar1
-rw-r--r--meshmc/launcher/minecraft/update/AssetUpdateTask.cpp133
-rw-r--r--meshmc/launcher/minecraft/update/AssetUpdateTask.h49
-rw-r--r--meshmc/launcher/minecraft/update/FMLLibrariesTask.cpp147
-rw-r--r--meshmc/launcher/minecraft/update/FMLLibrariesTask.h51
-rw-r--r--meshmc/launcher/minecraft/update/FoldersTask.cpp40
-rw-r--r--meshmc/launcher/minecraft/update/FoldersTask.h38
-rw-r--r--meshmc/launcher/minecraft/update/LibrariesTask.cpp122
-rw-r--r--meshmc/launcher/minecraft/update/LibrariesTask.h47
147 files changed, 21295 insertions, 0 deletions
diff --git a/meshmc/launcher/minecraft/AssetsUtils.cpp b/meshmc/launcher/minecraft/AssetsUtils.cpp
new file mode 100644
index 0000000000..9781a18648
--- /dev/null
+++ b/meshmc/launcher/minecraft/AssetsUtils.cpp
@@ -0,0 +1,344 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * 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 <QFileInfo>
+#include <QDir>
+#include <QDirIterator>
+#include <QCryptographicHash>
+#include <QJsonParseError>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QVariant>
+#include <QDebug>
+
+#include "AssetsUtils.h"
+#include "FileSystem.h"
+#include "net/Download.h"
+#include "net/ChecksumValidator.h"
+#include "BuildConfig.h"
+
+#include "Application.h"
+
+namespace
+{
+ 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.
+ // TODO: We should probably report this error to the user.
+ if (!file.open(QIODevice::ReadOnly)) {
+ qCritical() << "Failed to read assets index file" << path;
+ 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.toDouble();
+ }
+ }
+
+ index.objects.insert(iter.key(), object);
+ }
+
+ return true;
+ }
+
+ // FIXME: ugly code duplication
+ QDir getAssetsDir(const QString& assetsId, const QString& resourcesFolder)
+ {
+ QDir assetsDir = QDir("assets/");
+ QDir indexDir = QDir(FS::PathCombine(assetsDir.path(), "indexes"));
+ QDir objectDir = QDir(FS::PathCombine(assetsDir.path(), "objects"));
+ QDir virtualDir = QDir(FS::PathCombine(assetsDir.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 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;
+ }
+
+ // FIXME: ugly code duplication
+ bool reconstructAssets(QString assetsId, QString resourcesFolder)
+ {
+ QDir assetsDir = QDir("assets/");
+ QDir indexDir = QDir(FS::PathCombine(assetsDir.path(), "indexes"));
+ QDir objectDir = QDir(FS::PathCombine(assetsDir.path(), "objects"));
+ QDir virtualDir = QDir(FS::PathCombine(assetsDir.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" << assetsDir.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);
+ }
+ }
+
+ // TODO: Write last used time to virtualRoot/.lastused
+ if (removeLeftovers) {
+ for (auto& file : presentFiles) {
+ qDebug() << "Would remove" << file;
+ }
+ }
+ }
+ return true;
+ }
+
+} // namespace AssetsUtils
+
+NetAction::Ptr AssetObject::getDownloadAction()
+{
+ QFileInfo objectFile(getLocalPath());
+ if ((!objectFile.isFile()) || (objectFile.size() != size)) {
+ auto objectDL =
+ Net::Download::makeFile(getUrl(), objectFile.filePath());
+ if (hash.size()) {
+ auto rawHash = QByteArray::fromHex(hash.toLatin1());
+ objectDL->addValidator(
+ new Net::ChecksumValidator(QCryptographicHash::Sha1, rawHash));
+ }
+ objectDL->m_total_progress = size;
+ return objectDL;
+ }
+ return nullptr;
+}
+
+QString AssetObject::getLocalPath()
+{
+ return "assets/objects/" + getRelPath();
+}
+
+QUrl AssetObject::getUrl()
+{
+ return BuildConfig.RESOURCE_BASE + getRelPath();
+}
+
+QString AssetObject::getRelPath()
+{
+ return hash.left(2) + "/" + hash;
+}
+
+NetJob::Ptr AssetsIndex::getDownloadJob()
+{
+ auto job = new NetJob(QObject::tr("Assets for %1").arg(id),
+ APPLICATION->network());
+ for (auto& object : objects.values()) {
+ auto dl = object.getDownloadAction();
+ if (dl) {
+ job->addNetAction(dl);
+ }
+ }
+ if (job->size())
+ return job;
+ return nullptr;
+}
diff --git a/meshmc/launcher/minecraft/AssetsUtils.h b/meshmc/launcher/minecraft/AssetsUtils.h
new file mode 100644
index 0000000000..68e88a5e3f
--- /dev/null
+++ b/meshmc/launcher/minecraft/AssetsUtils.h
@@ -0,0 +1,76 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * 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 <QString>
+#include <QMap>
+#include "net/NetAction.h"
+#include "net/NetJob.h"
+
+struct AssetObject {
+ QString getRelPath();
+ QUrl getUrl();
+ QString getLocalPath();
+ NetAction::Ptr getDownloadAction();
+
+ QString hash;
+ qint64 size;
+};
+
+struct AssetsIndex {
+ NetJob::Ptr getDownloadJob();
+
+ QString id;
+ QMap<QString, AssetObject> objects;
+ bool isVirtual = false;
+ bool mapToResources = false;
+};
+
+/// FIXME: this is absolutely horrendous. REDO!!!!
+namespace AssetsUtils
+{
+ bool loadAssetsIndexJson(const QString& id, const QString& file,
+ AssetsIndex& index);
+
+ QDir getAssetsDir(const QString& assetsId, const QString& resourcesFolder);
+
+ /// Reconstruct a virtual assets folder for the given assets ID and return
+ /// the folder
+ bool reconstructAssets(QString assetsId, QString resourcesFolder);
+} // namespace AssetsUtils
diff --git a/meshmc/launcher/minecraft/Component.cpp b/meshmc/launcher/minecraft/Component.cpp
new file mode 100644
index 0000000000..e11c0fa492
--- /dev/null
+++ b/meshmc/launcher/minecraft/Component.cpp
@@ -0,0 +1,408 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <meta/VersionList.h>
+#include <meta/Index.h>
+#include "Component.h"
+
+#include <QSaveFile>
+
+#include "meta/Version.h"
+#include "VersionFile.h"
+#include "minecraft/PackProfile.h"
+#include "FileSystem.h"
+#include "OneSixVersionFormat.h"
+#include "Application.h"
+
+#include <assert.h>
+
+Component::Component(PackProfile* parent, const QString& uid)
+{
+ assert(parent);
+ m_parent = parent;
+
+ m_uid = uid;
+}
+
+Component::Component(PackProfile* parent,
+ std::shared_ptr<Meta::Version> version)
+{
+ assert(parent);
+ m_parent = parent;
+
+ m_metaVersion = version;
+ m_uid = version->uid();
+ m_version = m_cachedVersion = version->version();
+ m_cachedName = version->name();
+ m_loaded = version->isLoaded();
+}
+
+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<Meta::Version> 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);
+ } else {
+ profile->applyProblemSeverity(getProblemSeverity());
+ }
+}
+
+std::shared_ptr<class VersionFile> Component::getVersionFile() const
+{
+ if (m_metaVersion) {
+ if (!m_metaVersion->isLoaded()) {
+ m_metaVersion->load(Net::Mode::Online);
+ }
+ return m_metaVersion->data();
+ } else {
+ return m_file;
+ }
+}
+
+std::shared_ptr<class Meta::VersionList> Component::getVersionList() const
+{
+ // FIXME: what if the metadata index isn't loaded yet?
+ if (APPLICATION->metadataIndex()->hasUid(m_uid)) {
+ return APPLICATION->metadataIndex()->get(m_uid);
+ }
+ return nullptr;
+}
+
+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->time();
+ }
+ auto vfile = getVersionFile();
+ if (vfile) {
+ return vfile->releaseTime;
+ }
+ // FIXME: fake
+ 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()
+{
+ if (m_metaVersion) {
+ if (getVersionFile()) {
+ return true;
+ }
+ }
+ return false;
+}
+bool Component::isRemovable()
+{
+ return !m_important;
+}
+bool Component::isRevertible()
+{
+ if (isCustom()) {
+ if (APPLICATION->metadataIndex()->hasUid(m_uid)) {
+ return true;
+ }
+ }
+ return false;
+}
+bool Component::isMoveable()
+{
+ // HACK, FIXME: this was too dumb and wouldn't follow dependency constraints
+ // anyway. For now hardcoded to 'true'.
+ return true;
+}
+bool Component::isVersionChangeable()
+{
+ auto list = getVersionList();
+ if (list) {
+ if (!list->isLoaded()) {
+ list->load(Net::Mode::Online);
+ }
+ return list->count() != 0;
+ }
+ return false;
+}
+
+void Component::setImportant(bool state)
+{
+ if (m_important != state) {
+ m_important = state;
+ emit dataChanged();
+ }
+}
+
+ProblemSeverity Component::getProblemSeverity() const
+{
+ auto file = getVersionFile();
+ if (file) {
+ return file->getProblemSeverity();
+ }
+ return ProblemSeverity::Error;
+}
+
+const QList<PatchProblem> Component::getProblems() const
+{
+ auto file = getVersionFile();
+ if (file) {
+ return file->getProblems();
+ }
+ return {{ProblemSeverity::Error, QObject::tr("Patch is not loaded yet.")}};
+}
+
+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()->get(m_uid, version);
+ if (metaVersion->isLoaded()) {
+ // 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;
+ }
+ // FIXME: get rid of this try-catch.
+ 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 = QFile::remove(filename);
+ }
+ if (result) {
+ // file gone...
+ m_file.reset();
+
+ // check local cache for metadata...
+ auto version = APPLICATION->metadataIndex()->get(m_uid, m_version);
+ if (version->isLoaded()) {
+ 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 std::set<Meta::Require>& a,
+ const std::set<Meta::Require>& b)
+{
+ // NOTE: this needs to be rewritten if the type of Meta::RequireSet 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->requirements)) {
+ m_cachedRequires = file->requirements;
+ 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();
+ }
+}
diff --git a/meshmc/launcher/minecraft/Component.h b/meshmc/launcher/minecraft/Component.h
new file mode 100644
index 0000000000..6214fb623b
--- /dev/null
+++ b/meshmc/launcher/minecraft/Component.h
@@ -0,0 +1,140 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <memory>
+#include <QList>
+#include <QJsonDocument>
+#include <QDateTime>
+#include "meta/JsonFormat.h"
+#include "ProblemProvider.h"
+#include "QObjectPtr.h"
+
+class PackProfile;
+class LaunchProfile;
+namespace Meta
+{
+ class Version;
+ class VersionList;
+} // namespace Meta
+class VersionFile;
+
+class Component : public QObject, public ProblemProvider
+{
+ Q_OBJECT
+ public:
+ Component(PackProfile* parent, const QString& uid);
+
+ // DEPRECATED: remove these constructors?
+ Component(PackProfile* parent, std::shared_ptr<Meta::Version> version);
+ Component(PackProfile* parent, const QString& uid,
+ std::shared_ptr<VersionFile> file);
+
+ virtual ~Component() {};
+ void applyTo(LaunchProfile* profile);
+
+ bool isEnabled();
+ bool setEnabled(bool state);
+ bool canBeDisabled();
+
+ bool isMoveable();
+ bool isCustomizable();
+ bool isRevertible();
+ bool isRemovable();
+ bool isCustom();
+ bool isVersionChangeable();
+
+ // DEPRECATED: explicit numeric order values, used for loading old
+ // non-component config. TODO: refactor and move to migration code
+ void setOrder(int order);
+ int getOrder();
+
+ QString getID();
+ QString getName();
+ QString getVersion();
+ std::shared_ptr<Meta::Version> getMeta();
+ QDateTime getReleaseDateTime();
+
+ QString getFilename();
+
+ std::shared_ptr<class VersionFile> getVersionFile() const;
+ std::shared_ptr<class Meta::VersionList> getVersionList() const;
+
+ void setImportant(bool state);
+
+ const QList<PatchProblem> getProblems() const override;
+ ProblemSeverity getProblemSeverity() const override;
+
+ void setVersion(const QString& version);
+ bool customize();
+ bool revert();
+
+ void updateCachedData();
+
+ 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)
+ Meta::RequireSet m_cachedRequires;
+ Meta::RequireSet 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. TODO: refactor and move to migration code
+ bool m_orderOverride = false;
+ int m_order = 0;
+
+ // load state
+ std::shared_ptr<Meta::Version> m_metaVersion;
+ std::shared_ptr<VersionFile> m_file;
+ bool m_loaded = false;
+};
+
+typedef shared_qobject_ptr<Component> ComponentPtr;
diff --git a/meshmc/launcher/minecraft/ComponentUpdateTask.cpp b/meshmc/launcher/minecraft/ComponentUpdateTask.cpp
new file mode 100644
index 0000000000..33d549cfd6
--- /dev/null
+++ b/meshmc/launcher/minecraft/ComponentUpdateTask.cpp
@@ -0,0 +1,667 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "ComponentUpdateTask.h"
+
+#include "PackProfile_p.h"
+#include "PackProfile.h"
+#include "Component.h"
+#include "meta/Index.h"
+#include "meta/VersionList.h"
+#include "meta/Version.h"
+#include "ComponentUpdateTask_p.h"
+#include "cassert"
+#include "Version.h"
+#include "net/Mode.h"
+#include "OneSixVersionFormat.h"
+
+#include "Application.h"
+
+/*
+ * This is responsible for loading the components of a component list AND
+ * resolving dependency issues between them
+ */
+
+/*
+ * FIXME: the 'one shot async task' nature of this does not fit the intended
+ * usage Really, it should be a reactor/state machine that receives input from
+ * the application and dynamically adapts to changing requirements...
+ *
+ * The reactor should be the only entry into manipulating the PackProfile.
+ * See: https://en.wikipedia.org/wiki/Reactor_pattern
+ */
+
+/*
+ * 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, QObject* parent)
+ : Task(parent)
+{
+ d.reset(new ComponentUpdateTaskData);
+ d->m_list = list;
+ d->mode = mode;
+ d->netmode = netmode;
+}
+
+ComponentUpdateTask::~ComponentUpdateTask() {}
+
+void ComponentUpdateTask::executeTask()
+{
+ qDebug() << "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) {
+ qDebug() << 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) {
+ // FIXME: @QUALITY do not ignore return value
+ ProfileUtils::saveJsonFile(
+ OneSixVersionFormat::versionFileToJson(file),
+ customPatchFilename);
+ }
+
+ component->m_file = file;
+ component->m_loaded = true;
+ result = LoadResult::LoadedLocal;
+ } else {
+ auto metaVersion = APPLICATION->metadataIndex()->get(
+ component->m_uid, component->m_version);
+ component->m_metaVersion = metaVersion;
+ if (metaVersion->isLoaded()) {
+ component->m_loaded = true;
+ result = LoadResult::LoadedLocal;
+ } else {
+ metaVersion->load(netmode);
+ loadTask = metaVersion->getCurrentTask();
+ if (loadTask)
+ result = LoadResult::RequiresRemote;
+ else if (metaVersion->isLoaded())
+ result = LoadResult::LoadedLocal;
+ else
+ result = LoadResult::Failed;
+ }
+ }
+ return result;
+ }
+
+ // FIXME: dead code. determine if this can still be useful?
+ /*
+ static LoadResult loadPackProfile(ComponentPtr component, Task::Ptr&
+ loadTask, Net::Mode netmode)
+ {
+ if(component->m_loaded)
+ {
+ qDebug() << component->getName() << "is already loaded";
+ return LoadResult::LoadedLocal;
+ }
+
+ LoadResult result = LoadResult::Failed;
+ auto metaList = APPLICATION->metadataIndex()->get(component->m_uid);
+ if(metaList->isLoaded())
+ {
+ component->m_loaded = true;
+ result = LoadResult::LoadedLocal;
+ }
+ else
+ {
+ metaList->load(netmode);
+ loadTask = metaList->getCurrentTask();
+ result = LoadResult::RequiresRemote;
+ }
+ return result;
+ }
+ */
+
+ static LoadResult loadIndex(Task::Ptr& loadTask, Net::Mode netmode)
+ {
+ // FIXME: DECIDE. do we want to run the update task anyway?
+ if (APPLICATION->metadataIndex()->isLoaded()) {
+ qDebug() << "Index is already loaded";
+ return LoadResult::LoadedLocal;
+ }
+ APPLICATION->metadataIndex()->load(netmode);
+ loadTask = APPLICATION->metadataIndex()->getCurrentTask();
+ if (loadTask) {
+ return LoadResult::RequiresRemote;
+ }
+ // FIXME: this is assuming the load succeeded... did it really?
+ return LoadResult::LoadedLocal;
+ }
+} // namespace
+
+void ComponentUpdateTask::loadComponents()
+{
+ LoadResult result = LoadResult::LoadedLocal;
+ size_t taskIndex = 0;
+ size_t componentIndex = 0;
+ d->remoteLoadSuccessful = true;
+ // load the main index (it is needed to determine if components can revert)
+ {
+ // FIXME: tear out as a method? or lambda?
+ Task::Ptr indexLoadTask;
+ auto singleResult = loadIndex(indexLoadTask, d->netmode);
+ result = composeLoadResult(result, singleResult);
+ if (indexLoadTask) {
+ qDebug() << "Remote loading is being run for metadata index";
+ RemoteLoadStatus status;
+ status.type = RemoteLoadStatus::Type::Index;
+ d->remoteLoadStatusList.append(status);
+ connect(indexLoadTask.get(), &Task::succeeded,
+ [this, taskIndex]() { remoteLoadSucceeded(taskIndex); });
+ connect(indexLoadTask.get(), &Task::failed,
+ [this, taskIndex](const QString& error) {
+ remoteLoadFailed(taskIndex, error);
+ });
+ taskIndex++;
+ }
+ }
+ // load all the components OR their lists...
+ for (auto component : d->m_list->d->components) {
+ Task::Ptr loadTask;
+ LoadResult singleResult;
+ RemoteLoadStatus::Type loadType;
+ // FIXME: to do this right, we need to load the lists and decide on
+ // which versions to use during dependency resolution. For now, ignore
+ // all that...
+#if 0
+ 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;
+ }
+ }
+#else
+ singleResult = loadComponent(component, loadTask, d->netmode);
+ loadType = RemoteLoadStatus::Type::Version;
+#endif
+ if (singleResult == LoadResult::LoadedLocal) {
+ component->updateCachedData();
+ }
+ result = composeLoadResult(result, singleResult);
+ if (loadTask) {
+ qDebug() << "Remote loading is being run for"
+ << component->getName();
+ connect(loadTask.get(), &Task::succeeded,
+ [this, taskIndex]() { remoteLoadSucceeded(taskIndex); });
+ connect(loadTask.get(), &Task::failed,
+ [this, taskIndex](const QString& error) {
+ remoteLoadFailed(taskIndex, error);
+ });
+ RemoteLoadStatus status;
+ status.type = loadType;
+ status.PackProfileIndex = componentIndex;
+ d->remoteLoadStatusList.append(status);
+ taskIndex++;
+ }
+ componentIndex++;
+ }
+ d->remoteTasksInProgress = taskIndex;
+ switch (result) {
+ case LoadResult::LoadedLocal: {
+ // Everything got loaded. Advance to dependency resolution.
+ resolveDependencies(d->mode == Mode::Launch ||
+ 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 Meta::Require {
+ 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 {
+ // FIXME: mark error as explicit version conflict
+ 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 Meta::Require& 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 {
+ qCritical()
+ << "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:
+ qCritical() << "No decision for" << reqStr;
+ succeeded = false;
+ break;
+ case Decision::Met:
+ qDebug() << reqStr << "Is met.";
+ break;
+ case Decision::Missing:
+ qDebug() << reqStr << "Is missing and should be added at"
+ << req.indexOfFirstDependee;
+ toAdd.insert(req);
+ break;
+ case Decision::VersionNotSame:
+ qDebug()
+ << reqStr
+ << "already has different version that can be changed.";
+ toChange.insert(req);
+ break;
+ case Decision::LockedVersionNotSame:
+ qDebug()
+ << reqStr
+ << "already has different version that cannot be changed.";
+ succeeded = false;
+ break;
+ }
+ }
+ return succeeded;
+}
+
+// FIXME, TODO: decouple dependency resolution from loading
+// FIXME: This works directly with the PackProfile internals. It shouldn't! It
+// needs richer data types than PackProfile uses.
+// FIXME: throw all this away and use a graph
+void ComponentUpdateTask::resolveDependencies(bool checkOnly)
+{
+ qDebug() << "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_list->d->components;
+ auto& componentIndex = d->m_list->d->componentIndex;
+
+ RequireExSet allRequires;
+ QStringList toRemove;
+ do {
+ allRequires.clear();
+ toRemove.clear();
+ if (!gatherRequirementsFromComponents(components, allRequires)) {
+ emitFailed(tr("Conflicting requirements detected during dependency "
+ "checking!"));
+ return;
+ }
+ getTrivialRemovals(components, allRequires, toRemove);
+ if (!toRemove.isEmpty()) {
+ qDebug() << "Removing obsolete components...";
+ for (auto& remove : toRemove) {
+ qDebug() << "Removing" << remove;
+ d->m_list->remove(remove);
+ }
+ }
+ } while (!toRemove.isEmpty());
+ RequireExSet toAdd;
+ RequireExSet toChange;
+ bool succeeded = getTrivialComponentChanges(componentIndex, allRequires,
+ toAdd, toChange);
+ if (!succeeded) {
+ emitFailed(tr("Instance has conflicting dependencies."));
+ return;
+ }
+ if (checkOnly) {
+ 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) {
+ ComponentPtr component = new Component(d->m_list, add.uid);
+ if (!add.equalsVersion.isEmpty()) {
+ // exact version
+ qDebug() << "Adding" << add.uid << "version"
+ << add.equalsVersion << "at position"
+ << add.indexOfFirstDependee;
+ component->m_version = add.equalsVersion;
+ } else {
+ // version needs to be decided
+ qDebug() << "Adding" << add.uid << "at position"
+ << add.indexOfFirstDependee;
+ // ############################################################################################################
+ // HACK HACK HACK HACK FIXME: this is a placeholder for deciding
+ // what version to use. For now, it is hardcoded.
+ if (!add.suggests.isEmpty()) {
+ component->m_version = add.suggests;
+ } else {
+ if (add.uid == "org.lwjgl") {
+ component->m_version = "2.9.1";
+ } else if (add.uid == "org.lwjgl3") {
+ component->m_version = "3.1.2";
+ } else if (add.uid == "net.fabricmc.intermediary") {
+ auto minecraft = std::find_if(
+ components.begin(), components.end(),
+ [](ComponentPtr& cmp) {
+ return cmp->getID() == "net.minecraft";
+ });
+ if (minecraft != components.end()) {
+ component->m_version = (*minecraft)->getVersion();
+ }
+ }
+ }
+ // HACK HACK HACK HACK FIXME: this is a placeholder for deciding
+ // what version to use. For now, it is hardcoded.
+ // ############################################################################################################
+ }
+ component->m_dependencyOnly = true;
+ // FIXME: this should not work directly with the component list
+ d->m_list->insertComponent(add.indexOfFirstDependee, component);
+ componentIndex[add.uid] = component;
+ }
+ recursionNeeded = true;
+ }
+ if (toChange.size()) {
+ // change a version of something that exists
+ for (auto& change : toChange) {
+ // FIXME: this should not work directly with the component list
+ qDebug() << "Setting version of " << change.uid << "to"
+ << change.equalsVersion;
+ auto component = componentIndex[change.uid];
+ component->setVersion(change.equalsVersion);
+ }
+ recursionNeeded = true;
+ }
+
+ if (recursionNeeded) {
+ loadComponents();
+ } else {
+ emitSucceeded();
+ }
+}
+
+void ComponentUpdateTask::remoteLoadSucceeded(size_t taskIndex)
+{
+ auto& taskSlot = d->remoteLoadStatusList[taskIndex];
+ if (taskSlot.finished) {
+ qWarning() << "Got multiple results from remote load task" << taskIndex;
+ return;
+ }
+ qDebug() << "Remote task" << taskIndex << "succeeded";
+ taskSlot.succeeded = false;
+ 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_list->getComponent(taskSlot.PackProfileIndex);
+ component->m_loaded = true;
+ component->updateCachedData();
+ }
+ checkIfAllFinished();
+}
+
+void ComponentUpdateTask::remoteLoadFailed(size_t taskIndex, const QString& msg)
+{
+ auto& taskSlot = d->remoteLoadStatusList[taskIndex];
+ if (taskSlot.finished) {
+ qWarning() << "Got multiple results from remote load task" << taskIndex;
+ return;
+ }
+ qDebug() << "Remote task" << taskIndex << "failed: " << msg;
+ d->remoteLoadSuccessful = false;
+ taskSlot.succeeded = false;
+ taskSlot.finished = true;
+ taskSlot.error = msg;
+ 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();
+ resolveDependencies(d->mode == Mode::Launch);
+ } else {
+ // remote load failed... report error and bail
+ QStringList allErrorsList;
+ for (auto& item : d->remoteLoadStatusList) {
+ if (!item.succeeded) {
+ allErrorsList.append(item.error);
+ }
+ }
+ 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/meshmc/launcher/minecraft/ComponentUpdateTask.h b/meshmc/launcher/minecraft/ComponentUpdateTask.h
new file mode 100644
index 0000000000..b50b2b65f2
--- /dev/null
+++ b/meshmc/launcher/minecraft/ComponentUpdateTask.h
@@ -0,0 +1,55 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "tasks/Task.h"
+#include "net/Mode.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, QObject* parent = 0);
+ virtual ~ComponentUpdateTask();
+
+ protected:
+ void executeTask();
+
+ private:
+ void loadComponents();
+ void resolveDependencies(bool checkOnly);
+
+ void remoteLoadSucceeded(size_t index);
+ void remoteLoadFailed(size_t index, const QString& msg);
+ void checkIfAllFinished();
+
+ private:
+ std::unique_ptr<ComponentUpdateTaskData> d;
+};
diff --git a/meshmc/launcher/minecraft/ComponentUpdateTask_p.h b/meshmc/launcher/minecraft/ComponentUpdateTask_p.h
new file mode 100644
index 0000000000..a3e6b62b7f
--- /dev/null
+++ b/meshmc/launcher/minecraft/ComponentUpdateTask_p.h
@@ -0,0 +1,46 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <cstddef>
+#include <QString>
+#include <QList>
+#include "net/Mode.h"
+
+class PackProfile;
+
+struct RemoteLoadStatus {
+ enum class Type { Index, List, Version } type = Type::Version;
+ size_t PackProfileIndex = 0;
+ bool finished = false;
+ bool succeeded = false;
+ QString error;
+};
+
+struct ComponentUpdateTaskData {
+ PackProfile* m_list = nullptr;
+ QList<RemoteLoadStatus> remoteLoadStatusList;
+ bool remoteLoadSuccessful = true;
+ size_t remoteTasksInProgress = 0;
+ ComponentUpdateTask::Mode mode;
+ Net::Mode netmode;
+};
diff --git a/meshmc/launcher/minecraft/GradleSpecifier.h b/meshmc/launcher/minecraft/GradleSpecifier.h
new file mode 100644
index 0000000000..cc0f8ebd42
--- /dev/null
+++ b/meshmc/launcher/minecraft/GradleSpecifier.h
@@ -0,0 +1,175 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <QString>
+#include <QStringList>
+#include <QRegularExpression>
+#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"
+ */
+ QRegularExpression matcher("^([^:@]+):([^:@]+):([^:@]+)"
+ "(?::([^:@]+))?"
+ "(?:@([^:@]+))?$");
+ auto match = matcher.match(value);
+ m_valid = match.hasMatch();
+ if (!m_valid) {
+ m_invalidValue = value;
+ return *this;
+ }
+ m_groupId = match.captured(1);
+ m_artifactId = match.captured(2);
+ m_version = match.captured(3);
+ m_classifier = match.captured(4);
+ auto ext = match.captured(5);
+ if (!ext.isEmpty()) {
+ m_extension = ext;
+ }
+ 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
+ {
+ // Classifiers differentiate otherwise identical coordinates (e.g. the
+ // base lwjgl-glfw:3.3.2 jar vs lwjgl-glfw:3.3.2:natives-linux). Two
+ // entries with different classifiers must be treated as distinct
+ // library entries.
+ return other.artifactId() == artifactId() &&
+ other.groupId() == groupId() &&
+ other.m_classifier == m_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/meshmc/launcher/minecraft/GradleSpecifier_test.cpp b/meshmc/launcher/minecraft/GradleSpecifier_test.cpp
new file mode 100644
index 0000000000..bea37b69db
--- /dev/null
+++ b/meshmc/launcher/minecraft/GradleSpecifier_test.cpp
@@ -0,0 +1,99 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <QTest>
+#include "TestUtil.h"
+
+#include "minecraft/GradleSpecifier.h"
+
+class GradleSpecifierTest : public QObject
+{
+ Q_OBJECT
+ private slots:
+ void initTestCase() {}
+ void cleanupTestCase() {}
+
+ void test_Positive_data()
+ {
+ QTest::addColumn<QString>("through");
+
+ QTest::newRow("3 parter") << "org.gradle.test.classifiers:service:1.0";
+ QTest::newRow("classifier")
+ << "org.gradle.test.classifiers:service:1.0:jdk15";
+ QTest::newRow("jarextension")
+ << "org.gradle.test.classifiers:service:1.0@jar";
+ QTest::newRow("jarboth")
+ << "org.gradle.test.classifiers:service:1.0:jdk15@jar";
+ QTest::newRow("packxz")
+ << "org.gradle.test.classifiers:service:1.0:jdk15@jar.pack.xz";
+ }
+ void test_Positive()
+ {
+ QFETCH(QString, through);
+
+ QString converted = GradleSpecifier(through).serialize();
+
+ QCOMPARE(converted, through);
+ }
+
+ void test_Path_data()
+ {
+ QTest::addColumn<QString>("spec");
+ QTest::addColumn<QString>("expected");
+
+ QTest::newRow("3 parter") << "group.id:artifact:1.0"
+ << "group/id/artifact/1.0/artifact-1.0.jar";
+ QTest::newRow("doom") << "id.software:doom:1.666:demons@wad"
+ << "id/software/doom/1.666/doom-1.666-demons.wad";
+ }
+ void test_Path()
+ {
+ QFETCH(QString, spec);
+ QFETCH(QString, expected);
+
+ QString converted = GradleSpecifier(spec).toPath();
+
+ QCOMPARE(converted, expected);
+ }
+ void test_Negative_data()
+ {
+ QTest::addColumn<QString>("input");
+
+ QTest::newRow("too many :")
+ << "org:gradle.test:class:::ifiers:service:1.0::";
+ QTest::newRow("nonsense") << "I like turtles";
+ QTest::newRow("empty string") << "";
+ QTest::newRow("missing version") << "herp.derp:artifact";
+ }
+ void test_Negative()
+ {
+ QFETCH(QString, input);
+
+ GradleSpecifier spec(input);
+ QVERIFY(!spec.valid());
+ QCOMPARE(spec.serialize(), input);
+ QCOMPARE(spec.toPath(), QString());
+ }
+};
+
+QTEST_GUILESS_MAIN(GradleSpecifierTest)
+
+#include "GradleSpecifier_test.moc"
diff --git a/meshmc/launcher/minecraft/LaunchProfile.cpp b/meshmc/launcher/minecraft/LaunchProfile.cpp
new file mode 100644
index 0000000000..a94a6d83a6
--- /dev/null
+++ b/meshmc/launcher/minecraft/LaunchProfile.cpp
@@ -0,0 +1,316 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "LaunchProfile.h"
+#include <Version.h>
+
+void LaunchProfile::clear()
+{
+ m_minecraftVersion.clear();
+ m_minecraftVersionType.clear();
+ m_minecraftAssets.reset();
+ m_minecraftArguments.clear();
+ m_tweakers.clear();
+ m_mainClass.clear();
+ m_appletClass.clear();
+ m_libraries.clear();
+ m_mavenFiles.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::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::applyLibrary(LibraryPtr library)
+{
+ if (!library->isActive()) {
+ 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)
+{
+ if (!mavenFile->isActive()) {
+ 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));
+}
+
+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
+{
+ // FIXME: implement something that actually makes sense here
+ return {};
+}
+
+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 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;
+}
+
+void LaunchProfile::getLibraryFiles(const QString& architecture,
+ QStringList& jars, QStringList& nativeJars,
+ const QString& overridePath,
+ const QString& tempPath) const
+{
+ QStringList native32, native64;
+ jars.clear();
+ nativeJars.clear();
+ for (auto lib : getLibraries()) {
+ lib->getApplicableFiles(currentSystem, jars, nativeJars, native32,
+ native64, overridePath);
+ }
+ // NOTE: order is important here, add main jar last to the lists
+ if (m_mainJar) {
+ // FIXME: HACK!! jar modding is weird and unsystematic!
+ if (m_jarMods.size()) {
+ QDir tempDir(tempPath);
+ jars.append(tempDir.absoluteFilePath("minecraft.jar"));
+ } else {
+ m_mainJar->getApplicableFiles(currentSystem, jars, nativeJars,
+ native32, native64, overridePath);
+ }
+ }
+ for (auto lib : getNativeLibraries()) {
+ lib->getApplicableFiles(currentSystem, jars, nativeJars, native32,
+ native64, overridePath);
+ }
+ if (architecture == "32") {
+ nativeJars.append(native32);
+ } else if (architecture == "64") {
+ nativeJars.append(native64);
+ }
+}
diff --git a/meshmc/launcher/minecraft/LaunchProfile.h b/meshmc/launcher/minecraft/LaunchProfile.h
new file mode 100644
index 0000000000..b508c23d14
--- /dev/null
+++ b/meshmc/launcher/minecraft/LaunchProfile.h
@@ -0,0 +1,123 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+#include <QString>
+#include "Library.h"
+#include <ProblemProvider.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 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);
+ void applyMavenFile(LibraryPtr library);
+ 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 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 LibraryPtr getMainJar() const;
+ void getLibraryFiles(const QString& architecture, QStringList& jars,
+ QStringList& nativeJars, const QString& overridePath,
+ const QString& tempPath) 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;
+
+ /// 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 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;
+
+ ProblemSeverity m_problemSeverity = ProblemSeverity::None;
+};
diff --git a/meshmc/launcher/minecraft/Library.cpp b/meshmc/launcher/minecraft/Library.cpp
new file mode 100644
index 0000000000..8ecc32d200
--- /dev/null
+++ b/meshmc/launcher/minecraft/Library.cpp
@@ -0,0 +1,282 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "Library.h"
+#include "MinecraftInstance.h"
+
+#include <net/Download.h>
+#include <net/ChecksumValidator.h>
+#include <FileSystem.h>
+#include <BuildConfig.h>
+
+void Library::getApplicableFiles(OpSys system, QStringList& jar,
+ QStringList& native, QStringList& native32,
+ QStringList& native64,
+ const QString& overridePath) const
+{
+ bool local = isLocal();
+ auto actualPath = [&](QString 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(system);
+ 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);
+ }
+}
+
+QList<NetAction::Ptr> Library::getDownloads(OpSys system,
+ class HttpMetaCache* cache,
+ QStringList& failedLocalFiles,
+ const QString& overridePath) const
+{
+ QList<NetAction::Ptr> out;
+ bool stale = isAlwaysStale();
+ bool local = isLocal();
+
+ auto check_local_file = [&](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;
+ };
+
+ auto add_download = [&](QString storage, QString url, QString sha1) {
+ if (local) {
+ return check_local_file(storage);
+ }
+ 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;
+ }
+
+ if (sha1.size()) {
+ auto rawSha1 = QByteArray::fromHex(sha1.toLatin1());
+ auto dl = Net::Download::makeCached(url, entry, options);
+ dl->addValidator(
+ new Net::ChecksumValidator(QCryptographicHash::Sha1, rawSha1));
+ qDebug() << "Checksummed Download for:" << rawName().serialize()
+ << "storage:" << storage << "url:" << url;
+ out.append(dl);
+ } else {
+ out.append(Net::Download::makeCached(url, entry, options));
+ qDebug() << "Download for:" << rawName().serialize()
+ << "storage:" << storage << "url:" << url;
+ }
+ return true;
+ };
+
+ QString raw_storage = storageSuffix(system);
+ if (m_mojangDownloads) {
+ if (isNative()) {
+ if (m_nativeClassifiers.contains(system)) {
+ auto nativeClassifier = m_nativeClassifiers[system];
+ 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 = [&]() {
+ 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;
+}
+
+bool Library::isActive() const
+{
+ bool result = true;
+ if (m_rules.empty()) {
+ result = true;
+ } else {
+ RuleAction ruleResult = Disallow;
+ for (auto rule : m_rules) {
+ RuleAction temp = rule->apply(this);
+ if (temp != Defer)
+ ruleResult = temp;
+ }
+ result = result && (ruleResult == Allow);
+ }
+ if (isNative()) {
+ result = result && m_nativeClassifiers.contains(currentSystem);
+ }
+ return result;
+}
+
+bool Library::isLocal() const
+{
+ return m_hint == "local";
+}
+
+bool Library::isAlwaysStale() const
+{
+ return m_hint == "always-stale";
+}
+
+void Library::setStoragePrefix(QString prefix)
+{
+ m_storagePrefix = prefix;
+}
+
+QString Library::defaultStoragePrefix()
+{
+ return "libraries/";
+}
+
+QString Library::storagePrefix() const
+{
+ if (m_storagePrefix.isEmpty()) {
+ return defaultStoragePrefix();
+ }
+ return m_storagePrefix;
+}
+
+QString Library::filename(OpSys system) 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;
+ if (m_nativeClassifiers.contains(system)) {
+ nativeSpec.setClassifier(m_nativeClassifiers[system]);
+ } else {
+ nativeSpec.setClassifier("INVALID");
+ }
+ return nativeSpec.getFileName();
+}
+
+QString Library::displayName(OpSys system) const
+{
+ if (!m_displayname.isEmpty())
+ return m_displayname;
+ return filename(system);
+}
+
+QString Library::storageSuffix(OpSys system) 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;
+ if (m_nativeClassifiers.contains(system)) {
+ nativeSpec.setClassifier(m_nativeClassifiers[system]);
+ } else {
+ nativeSpec.setClassifier("INVALID");
+ }
+ return nativeSpec.toPath(m_filename);
+}
diff --git a/meshmc/launcher/minecraft/Library.h b/meshmc/launcher/minecraft/Library.h
new file mode 100644
index 0000000000..60e59f9df3
--- /dev/null
+++ b/meshmc/launcher/minecraft/Library.h
@@ -0,0 +1,242 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+#include <QString>
+#include <net/NetAction.h>
+#include <QPair>
+#include <QList>
+#include <QStringList>
+#include <QMap>
+#include <QDir>
+#include <QUrl>
+#include <memory>
+
+#include "Rule.h"
+#include "minecraft/OpSys.h"
+#include "GradleSpecifier.h"
+#include "MojangDownloadInfo.h"
+
+class Library;
+class MinecraftInstance;
+
+typedef std::shared_ptr<Library> LibraryPtr;
+
+class Library
+{
+ friend class OneSixVersionFormat;
+ friend class MojangVersionFormat;
+ friend class LibraryTest;
+
+ public:
+ Library() {}
+ Library(const QString& name)
+ {
+ m_name = name;
+ }
+ /// limited copy without some data. TODO: why?
+ 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;
+ 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(OpSys system, 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(OpSys system) const;
+
+ // DEPRECATED: set a display name, used by jar mods only
+ void setDisplayName(const QString& displayName)
+ {
+ m_displayname = displayName;
+ }
+
+ /// Get the file name of the library
+ QString displayName(OpSys system) const;
+
+ void setMojangDownloadInfo(MojangLibraryDownloadInfo::Ptr info)
+ {
+ m_mojangDownloads = info;
+ }
+
+ void setHint(const QString& hint)
+ {
+ m_hint = hint;
+ }
+
+ /// Set the load rules
+ void setRules(QList<std::shared_ptr<Rule>> rules)
+ {
+ m_rules = rules;
+ }
+
+ /// Returns true if the library should be loaded (or extracted, in case of
+ /// natives)
+ bool isActive() 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<NetAction::Ptr> getDownloads(OpSys system, class HttpMetaCache* cache,
+ QStringList& failedLocalFiles,
+ const QString& overridePath) const;
+
+ private: /* methods */
+ /// the default storage prefix used by MeshMC
+ 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(OpSys system) 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: MeshMC-specific absolute URL. takes precedence over the
+ /// implicit maven repo URL, if defined
+ QString m_absoluteURL;
+
+ /// MeshMC extension - filename override
+ QString m_filename;
+
+ /// DEPRECATED MeshMC extension - display name
+ QString m_displayname;
+
+ /**
+ * MeshMC-specific type hint - modifies how the library is treated
+ */
+ QString m_hint;
+
+ /**
+ * storage - by default the local libraries folder in meshmc, but could be
+ * elsewhere MeshMC 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<OpSys, QString> m_nativeClassifiers;
+
+ /// true if the library had a rules section (even empty)
+ bool applyRules = false;
+
+ /// rules associated with the library
+ QList<std::shared_ptr<Rule>> m_rules;
+
+ /// MOJANG: container with Mojang style download info
+ MojangLibraryDownloadInfo::Ptr m_mojangDownloads;
+};
diff --git a/meshmc/launcher/minecraft/Library_test.cpp b/meshmc/launcher/minecraft/Library_test.cpp
new file mode 100644
index 0000000000..ad6acc32bd
--- /dev/null
+++ b/meshmc/launcher/minecraft/Library_test.cpp
@@ -0,0 +1,377 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <QTest>
+#include "TestUtil.h"
+
+#include "minecraft/MojangVersionFormat.h"
+#include "minecraft/OneSixVersionFormat.h"
+#include "minecraft/Library.h"
+#include "net/HttpMetaCache.h"
+#include "FileSystem.h"
+
+class LibraryTest : public QObject
+{
+ Q_OBJECT
+ private:
+ LibraryPtr readMojangJson(const char* file)
+ {
+ auto path = QFINDTESTDATA(file);
+ QFile jsonFile(path);
+ if (!jsonFile.open(QIODevice::ReadOnly))
+ return nullptr;
+ auto data = jsonFile.readAll();
+ jsonFile.close();
+ ProblemContainer problems;
+ return MojangVersionFormat::libraryFromJson(
+ problems, QJsonDocument::fromJson(data).object(), file);
+ }
+ // get absolute path to expected storage, assuming default cache prefix
+ QStringList getStorage(QString relative)
+ {
+ return {FS::PathCombine(cache->getBasePath("libraries"), relative)};
+ }
+ private slots:
+ void initTestCase()
+ {
+ cache.reset(new HttpMetaCache());
+ cache->addBase("libraries", QDir("libraries").absolutePath());
+ dataDir = QDir("data").absolutePath();
+ }
+ void test_legacy()
+ {
+ Library test("test.package:testname:testversion");
+ QCOMPARE(test.artifactPrefix(), QString("test.package:testname"));
+ QCOMPARE(test.isNative(), false);
+
+ QStringList jar, native, native32, native64;
+ test.getApplicableFiles(currentSystem, jar, native, native32, native64,
+ QString());
+ QCOMPARE(
+ jar,
+ getStorage(
+ "test/package/testname/testversion/testname-testversion.jar"));
+ QCOMPARE(native, {});
+ QCOMPARE(native32, {});
+ QCOMPARE(native64, {});
+ }
+ void test_legacy_url()
+ {
+ QStringList failedFiles;
+ Library test("test.package:testname:testversion");
+ test.setRepositoryURL("file://foo/bar");
+ auto downloads = test.getDownloads(currentSystem, cache.get(),
+ failedFiles, QString());
+ QCOMPARE(downloads.size(), 1);
+ QCOMPARE(failedFiles, {});
+ NetAction::Ptr dl = downloads[0];
+ QCOMPARE(dl->m_url, QUrl("file://foo/bar/test/package/testname/"
+ "testversion/testname-testversion.jar"));
+ }
+ void test_legacy_url_local_broken()
+ {
+ Library test("test.package:testname:testversion");
+ QCOMPARE(test.isNative(), false);
+ QStringList failedFiles;
+ test.setHint("local");
+ auto downloads = test.getDownloads(currentSystem, cache.get(),
+ failedFiles, QString());
+ QCOMPARE(downloads.size(), 0);
+ QCOMPARE(failedFiles, {"testname-testversion.jar"});
+ }
+ void test_legacy_url_local_override()
+ {
+ Library test("com.paulscode:codecwav:20101023");
+ QCOMPARE(test.isNative(), false);
+ QStringList failedFiles;
+ test.setHint("local");
+ auto downloads = test.getDownloads(currentSystem, cache.get(),
+ failedFiles, QString("data"));
+ QCOMPARE(downloads.size(), 0);
+ qDebug() << failedFiles;
+ QCOMPARE(failedFiles.size(), 0);
+
+ QStringList jar, native, native32, native64;
+ test.getApplicableFiles(currentSystem, jar, native, native32, native64,
+ QString("data"));
+ QCOMPARE(jar,
+ {QFileInfo("data/codecwav-20101023.jar").absoluteFilePath()});
+ QCOMPARE(native, {});
+ QCOMPARE(native32, {});
+ QCOMPARE(native64, {});
+ }
+ void test_legacy_native()
+ {
+ Library test("test.package:testname:testversion");
+ test.m_nativeClassifiers[OpSys::Os_Linux] = "linux";
+ QCOMPARE(test.isNative(), true);
+ test.setRepositoryURL("file://foo/bar");
+ {
+ QStringList jar, native, native32, native64;
+ test.getApplicableFiles(Os_Linux, jar, native, native32, native64,
+ QString());
+ QCOMPARE(jar, {});
+ QCOMPARE(native, getStorage("test/package/testname/testversion/"
+ "testname-testversion-linux.jar"));
+ QCOMPARE(native32, {});
+ QCOMPARE(native64, {});
+ QStringList failedFiles;
+ auto dls = test.getDownloads(Os_Linux, cache.get(), failedFiles,
+ QString());
+ QCOMPARE(dls.size(), 1);
+ QCOMPARE(failedFiles, {});
+ auto dl = dls[0];
+ QCOMPARE(dl->m_url,
+ QUrl("file://foo/bar/test/package/testname/testversion/"
+ "testname-testversion-linux.jar"));
+ }
+ }
+ void test_legacy_native_arch()
+ {
+ Library test("test.package:testname:testversion");
+ test.m_nativeClassifiers[OpSys::Os_Linux] = "linux-${arch}";
+ test.m_nativeClassifiers[OpSys::Os_OSX] = "osx-${arch}";
+ test.m_nativeClassifiers[OpSys::Os_Windows] = "windows-${arch}";
+ QCOMPARE(test.isNative(), true);
+ test.setRepositoryURL("file://foo/bar");
+ {
+ QStringList jar, native, native32, native64;
+ test.getApplicableFiles(Os_Linux, jar, native, native32, native64,
+ QString());
+ QCOMPARE(jar, {});
+ QCOMPARE(native, {});
+ QCOMPARE(native32, getStorage("test/package/testname/testversion/"
+ "testname-testversion-linux-32.jar"));
+ QCOMPARE(native64, getStorage("test/package/testname/testversion/"
+ "testname-testversion-linux-64.jar"));
+ QStringList failedFiles;
+ auto dls = test.getDownloads(Os_Linux, cache.get(), failedFiles,
+ QString());
+ QCOMPARE(dls.size(), 2);
+ QCOMPARE(failedFiles, {});
+ QCOMPARE(dls[0]->m_url,
+ QUrl("file://foo/bar/test/package/testname/testversion/"
+ "testname-testversion-linux-32.jar"));
+ QCOMPARE(dls[1]->m_url,
+ QUrl("file://foo/bar/test/package/testname/testversion/"
+ "testname-testversion-linux-64.jar"));
+ }
+ {
+ QStringList jar, native, native32, native64;
+ test.getApplicableFiles(Os_Windows, jar, native, native32, native64,
+ QString());
+ QCOMPARE(jar, {});
+ QCOMPARE(native, {});
+ QCOMPARE(native32,
+ getStorage("test/package/testname/testversion/"
+ "testname-testversion-windows-32.jar"));
+ QCOMPARE(native64,
+ getStorage("test/package/testname/testversion/"
+ "testname-testversion-windows-64.jar"));
+ QStringList failedFiles;
+ auto dls = test.getDownloads(Os_Windows, cache.get(), failedFiles,
+ QString());
+ QCOMPARE(dls.size(), 2);
+ QCOMPARE(failedFiles, {});
+ QCOMPARE(dls[0]->m_url,
+ QUrl("file://foo/bar/test/package/testname/testversion/"
+ "testname-testversion-windows-32.jar"));
+ QCOMPARE(dls[1]->m_url,
+ QUrl("file://foo/bar/test/package/testname/testversion/"
+ "testname-testversion-windows-64.jar"));
+ }
+ {
+ QStringList jar, native, native32, native64;
+ test.getApplicableFiles(Os_OSX, jar, native, native32, native64,
+ QString());
+ QCOMPARE(jar, {});
+ QCOMPARE(native, {});
+ QCOMPARE(native32, getStorage("test/package/testname/testversion/"
+ "testname-testversion-osx-32.jar"));
+ QCOMPARE(native64, getStorage("test/package/testname/testversion/"
+ "testname-testversion-osx-64.jar"));
+ QStringList failedFiles;
+ auto dls =
+ test.getDownloads(Os_OSX, cache.get(), failedFiles, QString());
+ QCOMPARE(dls.size(), 2);
+ QCOMPARE(failedFiles, {});
+ QCOMPARE(dls[0]->m_url,
+ QUrl("file://foo/bar/test/package/testname/testversion/"
+ "testname-testversion-osx-32.jar"));
+ QCOMPARE(dls[1]->m_url,
+ QUrl("file://foo/bar/test/package/testname/testversion/"
+ "testname-testversion-osx-64.jar"));
+ }
+ }
+ void test_legacy_native_arch_local_override()
+ {
+ Library test("test.package:testname:testversion");
+ test.m_nativeClassifiers[OpSys::Os_Linux] = "linux-${arch}";
+ test.setHint("local");
+ QCOMPARE(test.isNative(), true);
+ test.setRepositoryURL("file://foo/bar");
+ {
+ QStringList jar, native, native32, native64;
+ test.getApplicableFiles(Os_Linux, jar, native, native32, native64,
+ QString("data"));
+ QCOMPARE(jar, {});
+ QCOMPARE(native, {});
+ QCOMPARE(native32,
+ {QFileInfo("data/testname-testversion-linux-32.jar")
+ .absoluteFilePath()});
+ QCOMPARE(native64,
+ {QFileInfo("data/testname-testversion-linux-64.jar")
+ .absoluteFilePath()});
+ QStringList failedFiles;
+ auto dls = test.getDownloads(Os_Linux, cache.get(), failedFiles,
+ QString("data"));
+ QCOMPARE(dls.size(), 0);
+ QCOMPARE(failedFiles, {"data/testname-testversion-linux-64.jar"});
+ }
+ }
+ void test_onenine()
+ {
+ auto test = readMojangJson("data/lib-simple.json");
+ {
+ QStringList jar, native, native32, native64;
+ test->getApplicableFiles(Os_OSX, jar, native, native32, native64,
+ QString());
+ QCOMPARE(
+ jar,
+ getStorage(
+ "com/paulscode/codecwav/20101023/codecwav-20101023.jar"));
+ QCOMPARE(native, {});
+ QCOMPARE(native32, {});
+ QCOMPARE(native64, {});
+ }
+ {
+ QStringList failedFiles;
+ auto dls = test->getDownloads(Os_Linux, cache.get(), failedFiles,
+ QString());
+ QCOMPARE(dls.size(), 1);
+ QCOMPARE(failedFiles, {});
+ QCOMPARE(dls[0]->m_url,
+ QUrl("https://libraries.minecraft.net/com/paulscode/"
+ "codecwav/20101023/codecwav-20101023.jar"));
+ }
+ test->setHint("local");
+ {
+ QStringList jar, native, native32, native64;
+ test->getApplicableFiles(Os_OSX, jar, native, native32, native64,
+ QString("data"));
+ QCOMPARE(
+ jar,
+ {QFileInfo("data/codecwav-20101023.jar").absoluteFilePath()});
+ QCOMPARE(native, {});
+ QCOMPARE(native32, {});
+ QCOMPARE(native64, {});
+ }
+ {
+ QStringList failedFiles;
+ auto dls = test->getDownloads(Os_Linux, cache.get(), failedFiles,
+ QString("data"));
+ QCOMPARE(dls.size(), 0);
+ QCOMPARE(failedFiles, {});
+ }
+ }
+ void test_onenine_local_override()
+ {
+ auto test = readMojangJson("data/lib-simple.json");
+ test->setHint("local");
+ {
+ QStringList jar, native, native32, native64;
+ test->getApplicableFiles(Os_OSX, jar, native, native32, native64,
+ QString("data"));
+ QCOMPARE(
+ jar,
+ {QFileInfo("data/codecwav-20101023.jar").absoluteFilePath()});
+ QCOMPARE(native, {});
+ QCOMPARE(native32, {});
+ QCOMPARE(native64, {});
+ }
+ {
+ QStringList failedFiles;
+ auto dls = test->getDownloads(Os_Linux, cache.get(), failedFiles,
+ QString("data"));
+ QCOMPARE(dls.size(), 0);
+ QCOMPARE(failedFiles, {});
+ }
+ }
+ void test_onenine_native()
+ {
+ auto test = readMojangJson("data/lib-native.json");
+ QStringList jar, native, native32, native64;
+ test->getApplicableFiles(Os_OSX, jar, native, native32, native64,
+ QString());
+ QCOMPARE(jar, QStringList());
+ QCOMPARE(native,
+ getStorage(
+ "org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/"
+ "lwjgl-platform-2.9.4-nightly-20150209-natives-osx.jar"));
+ QCOMPARE(native32, {});
+ QCOMPARE(native64, {});
+ QStringList failedFiles;
+ auto dls =
+ test->getDownloads(Os_OSX, cache.get(), failedFiles, QString());
+ QCOMPARE(dls.size(), 1);
+ QCOMPARE(failedFiles, {});
+ QCOMPARE(dls[0]->m_url,
+ QUrl("https://libraries.minecraft.net/org/lwjgl/lwjgl/"
+ "lwjgl-platform/2.9.4-nightly-20150209/"
+ "lwjgl-platform-2.9.4-nightly-20150209-natives-osx.jar"));
+ }
+ void test_onenine_native_arch()
+ {
+ auto test = readMojangJson("data/lib-native-arch.json");
+ QStringList jar, native, native32, native64;
+ test->getApplicableFiles(Os_Windows, jar, native, native32, native64,
+ QString());
+ QCOMPARE(jar, {});
+ QCOMPARE(native, {});
+ QCOMPARE(native32,
+ getStorage("tv/twitch/twitch-platform/5.16/"
+ "twitch-platform-5.16-natives-windows-32.jar"));
+ QCOMPARE(native64,
+ getStorage("tv/twitch/twitch-platform/5.16/"
+ "twitch-platform-5.16-natives-windows-64.jar"));
+ QStringList failedFiles;
+ auto dls =
+ test->getDownloads(Os_Windows, cache.get(), failedFiles, QString());
+ QCOMPARE(dls.size(), 2);
+ QCOMPARE(failedFiles, {});
+ QCOMPARE(
+ dls[0]->m_url,
+ QUrl("https://libraries.minecraft.net/tv/twitch/twitch-platform/"
+ "5.16/twitch-platform-5.16-natives-windows-32.jar"));
+ QCOMPARE(
+ dls[1]->m_url,
+ QUrl("https://libraries.minecraft.net/tv/twitch/twitch-platform/"
+ "5.16/twitch-platform-5.16-natives-windows-64.jar"));
+ }
+
+ private:
+ std::unique_ptr<HttpMetaCache> cache;
+ QString dataDir;
+};
+
+QTEST_GUILESS_MAIN(LibraryTest)
+
+#include "Library_test.moc"
diff --git a/meshmc/launcher/minecraft/MinecraftInstance.cpp b/meshmc/launcher/minecraft/MinecraftInstance.cpp
new file mode 100644
index 0000000000..d0a7ab740f
--- /dev/null
+++ b/meshmc/launcher/minecraft/MinecraftInstance.cpp
@@ -0,0 +1,1108 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "MinecraftInstance.h"
+#include "minecraft/launch/CreateGameFolders.h"
+#include "minecraft/launch/ExtractNatives.h"
+#include "minecraft/launch/PrintInstanceInfo.h"
+#include "settings/Setting.h"
+#include "settings/SettingsObject.h"
+#include "Application.h"
+#include <QRegularExpression>
+
+#include "MMCStrings.h"
+#include "pathmatcher/RegexpMatcher.h"
+#include "pathmatcher/MultiMatcher.h"
+#include "FileSystem.h"
+#include "java/JavaVersion.h"
+#include "MMCTime.h"
+
+#include "launch/LaunchTask.h"
+#include "launch/steps/LookupServerAddress.h"
+#include "launch/steps/PostLaunchCommand.h"
+#include "launch/steps/Update.h"
+#include "launch/steps/PreLaunchCommand.h"
+#include "launch/steps/TextPrint.h"
+#include "launch/steps/CheckJava.h"
+
+#include "minecraft/launch/MeshMCPartLaunch.h"
+#include "minecraft/launch/DirectJavaLaunch.h"
+#include "minecraft/launch/ModMinecraftJar.h"
+#include "minecraft/launch/ClaimAccount.h"
+#include "minecraft/launch/ReconstructAssets.h"
+#include "minecraft/launch/ScanModFolders.h"
+#include "minecraft/launch/VerifyJavaInstall.h"
+
+#include "java/JavaUtils.h"
+
+#include "meta/Index.h"
+#include "meta/VersionList.h"
+
+#include "icons/IconList.h"
+
+#include "mod/ModFolderModel.h"
+#include "mod/ResourcePackFolderModel.h"
+#include "mod/TexturePackFolderModel.h"
+
+#include "WorldList.h"
+
+#include "PackProfile.h"
+#include "AssetsUtils.h"
+#include "MinecraftUpdate.h"
+#include "MinecraftLoadAndCheck.h"
+#include "minecraft/gameoptions/GameOptions.h"
+#include "minecraft/update/FoldersTask.h"
+
+#define IBUS "@im=ibus"
+
+// 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)
+{
+ // Java Settings
+ auto javaOverride = m_settings->registerSetting("OverrideJava", false);
+ auto locationOverride =
+ m_settings->registerSetting("OverrideJavaLocation", false);
+ auto argsOverride = m_settings->registerSetting("OverrideJavaArgs", false);
+
+ // combinations
+ auto javaOrLocation = std::make_shared<OrSetting>(
+ "JavaOrLocationOverride", javaOverride, locationOverride);
+ auto javaOrArgs = std::make_shared<OrSetting>("JavaOrArgsOverride",
+ javaOverride, argsOverride);
+
+ m_settings->registerOverride(globalSettings->getSetting("JavaPath"),
+ javaOrLocation);
+ m_settings->registerOverride(globalSettings->getSetting("JvmArgs"),
+ javaOrArgs);
+
+ // special!
+ m_settings->registerPassthrough(globalSettings->getSetting("JavaTimestamp"),
+ javaOrLocation);
+ m_settings->registerPassthrough(globalSettings->getSetting("JavaVersion"),
+ javaOrLocation);
+ m_settings->registerPassthrough(
+ globalSettings->getSetting("JavaArchitecture"), javaOrLocation);
+
+ // Window Size
+ auto windowSetting = m_settings->registerSetting("OverrideWindow", false);
+ m_settings->registerOverride(globalSettings->getSetting("LaunchMaximized"),
+ windowSetting);
+ m_settings->registerOverride(
+ globalSettings->getSetting("MinecraftWinWidth"), windowSetting);
+ m_settings->registerOverride(
+ globalSettings->getSetting("MinecraftWinHeight"), windowSetting);
+
+ // Memory
+ auto memorySetting = m_settings->registerSetting("OverrideMemory", false);
+ m_settings->registerOverride(globalSettings->getSetting("MinMemAlloc"),
+ memorySetting);
+ m_settings->registerOverride(globalSettings->getSetting("MaxMemAlloc"),
+ memorySetting);
+ m_settings->registerOverride(globalSettings->getSetting("PermGen"),
+ memorySetting);
+
+ // Minecraft launch method
+ auto launchMethodOverride =
+ m_settings->registerSetting("OverrideMCLaunchMethod", false);
+ m_settings->registerOverride(globalSettings->getSetting("MCLaunchMethod"),
+ launchMethodOverride);
+
+ // Native library workarounds
+ auto nativeLibraryWorkaroundsOverride =
+ m_settings->registerSetting("OverrideNativeWorkarounds", false);
+ m_settings->registerOverride(globalSettings->getSetting("UseNativeOpenAL"),
+ nativeLibraryWorkaroundsOverride);
+ m_settings->registerOverride(globalSettings->getSetting("UseNativeGLFW"),
+ nativeLibraryWorkaroundsOverride);
+
+ // Game time
+ auto gameTimeOverride =
+ m_settings->registerSetting("OverrideGameTime", false);
+ m_settings->registerOverride(globalSettings->getSetting("ShowGameTime"),
+ gameTimeOverride);
+ m_settings->registerOverride(globalSettings->getSetting("RecordGameTime"),
+ gameTimeOverride);
+
+ // Join server on launch, this does not have a global override
+ m_settings->registerSetting("JoinServerOnLaunch", false);
+ m_settings->registerSetting("JoinServerOnLaunchAddress", "");
+
+ // DEPRECATED: Read what versions the user configuration thinks should be
+ // used
+ m_settings->registerSetting({"IntendedVersion", "MinecraftVersion"}, "");
+ m_settings->registerSetting("LWJGLVersion", "");
+ m_settings->registerSetting("ForgeVersion", "");
+ m_settings->registerSetting("LiteloaderVersion", "");
+
+ m_components.reset(new PackProfile(this));
+ m_components->setOldConfigVersion(
+ "net.minecraft", m_settings->get("IntendedVersion").toString());
+ auto setting = m_settings->getSetting("LWJGLVersion");
+ m_components->setOldConfigVersion(
+ "org.lwjgl", m_settings->get("LWJGLVersion").toString());
+ m_components->setOldConfigVersion(
+ "net.minecraftforge", m_settings->get("ForgeVersion").toString());
+ m_components->setOldConfigVersion(
+ "com.mumfrey.liteloader",
+ m_settings->get("LiteloaderVersion").toString());
+}
+
+void MinecraftInstance::saveNow()
+{
+ m_components->saveNow();
+}
+
+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 (mcDir.exists() && !dotMCDir.exists())
+ return mcDir.filePath();
+ else
+ return dotMCDir.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();
+}
+
+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::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() const
+{
+ QStringList jars, nativeJars;
+ auto javaArchitecture = settings()->get("JavaArchitecture").toString();
+ auto profile = m_components->getProfile();
+ profile->getLibraryFiles(javaArchitecture, jars, nativeJars,
+ getLocalLibraryPath(), binRoot());
+ return jars;
+}
+
+QString MinecraftInstance::getMainClass() const
+{
+ auto profile = m_components->getProfile();
+ return profile->getMainClass();
+}
+
+QStringList MinecraftInstance::getNativeJars() const
+{
+ QStringList jars, nativeJars;
+ auto javaArchitecture = settings()->get("JavaArchitecture").toString();
+ auto profile = m_components->getProfile();
+ profile->getLibraryFiles(javaArchitecture, jars, nativeJars,
+ getLocalLibraryPath(), binRoot());
+ return nativeJars;
+}
+
+QStringList MinecraftInstance::extraArguments() const
+{
+ auto list = BaseInstance::extraArguments();
+ auto version = getPackProfile();
+ if (!version)
+ return list;
+ auto jarMods = getJarMods();
+ if (!jarMods.isEmpty()) {
+ list.append({"-Dfml.ignoreInvalidMinecraftCertificates=true",
+ "-Dfml.ignorePatchDiscrepancies=true"});
+ }
+ return list;
+}
+
+QStringList MinecraftInstance::javaArguments() const
+{
+ 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.
+ JavaVersion javaVersion = getJavaVersion();
+ if (javaVersion.requiresPermGen()) {
+ auto permgen = settings()->get("PermGen").toInt();
+ if (permgen != 64) {
+ args << QString("-XX:PermSize=%1m").arg(permgen);
+ }
+ }
+
+ args << "-Duser.language=en";
+
+ return args;
+}
+
+QMap<QString, QString> MinecraftInstance::getVariables() const
+{
+ QMap<QString, QString> out;
+ out.insert("INST_NAME", name());
+ out.insert("INST_ID", id());
+ out.insert("INST_DIR", QDir(instanceRoot()).absolutePath());
+ out.insert("INST_MC_DIR", QDir(gameRoot()).absolutePath());
+ out.insert("INST_JAVA", settings()->get("JavaPath").toString());
+ out.insert("INST_JAVA_ARGS", javaArguments().join(' '));
+ return out;
+}
+
+QProcessEnvironment MinecraftInstance::createEnvironment()
+{
+ // prepare the process environment
+ QProcessEnvironment env = CleanEnviroment();
+
+ // export some infos
+ auto variables = getVariables();
+ for (auto it = variables.begin(); it != variables.end(); ++it) {
+ env.insert(it.key(), it.value());
+ }
+ return env;
+}
+
+static QString replaceTokensIn(QString text, QMap<QString, QString> with)
+{
+ QString result;
+ QRegularExpression token_regexp("\\$\\{(.+?)\\}");
+ QRegularExpressionMatchIterator it = token_regexp.globalMatch(text);
+ int tail = 0;
+ while (it.hasNext()) {
+ QRegularExpressionMatch match = it.next();
+ result.append(text.mid(tail, match.capturedStart() - tail));
+ QString key = match.captured(1);
+ auto iter = with.find(key);
+ if (iter != with.end()) {
+ result.append(*iter);
+ }
+ tail = match.capturedEnd();
+ }
+ result.append(text.mid(tail));
+ return result;
+}
+
+QStringList MinecraftInstance::processMinecraftArgs(
+ AuthSessionPtr session, MinecraftServerTargetPtr serverToJoin) const
+{
+ auto profile = m_components->getProfile();
+ QString args_pattern = profile->getMinecraftArguments();
+ for (auto tweaker : profile->getTweakers()) {
+ args_pattern += " --tweakClass " + tweaker;
+ }
+
+ if (serverToJoin && !serverToJoin->address.isEmpty()) {
+ args_pattern += " --server " + serverToJoin->address;
+ args_pattern += " --port " + QString::number(serverToJoin->port);
+ }
+
+ 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->demo) {
+ args_pattern += " --demo";
+ }
+ }
+
+ // blatant self-promotion.
+ token_mapping["profile_name"] = token_mapping["version_name"] = "MeshMC";
+
+ 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,
+ MinecraftServerTargetPtr serverToJoin)
+{
+ 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 (serverToJoin && !serverToJoin->address.isEmpty()) {
+ launchScript += "serverAddress " + serverToJoin->address + "\n";
+ launchScript +=
+ "serverPort " + QString::number(serverToJoin->port) + "\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())
+ windowParams = "max";
+ else
+ windowParams =
+ QString("%1x%2")
+ .arg(settings()->get("MinecraftWinWidth").toInt())
+ .arg(settings()->get("MinecraftWinHeight").toInt());
+ launchScript += "windowTitle " + windowTitle() + "\n";
+ launchScript += "windowParams " + windowParams + "\n";
+ }
+
+ // legacy auth
+ if (session) {
+ launchScript += "userName " + session->player_name + "\n";
+ launchScript += "sessionId " + session->session + "\n";
+ }
+
+ // libraries and class path.
+ {
+ QStringList jars, nativeJars;
+ auto javaArchitecture = settings()->get("JavaArchitecture").toString();
+ profile->getLibraryFiles(javaArchitecture, jars, nativeJars,
+ getLocalLibraryPath(), binRoot());
+ for (auto file : jars) {
+ launchScript += "cp " + file + "\n";
+ }
+ for (auto file : nativeJars) {
+ launchScript += "ext " + file + "\n";
+ }
+ launchScript += "natives " + getNativePath() + "\n";
+ }
+
+ for (auto trait : profile->getTraits()) {
+ launchScript += "traits " + trait + "\n";
+ }
+
+ // Decide between legacy (in-process, applet-based) and modern (subprocess)
+ // launch. Instances with the "legacyLaunch" or "alphaLaunch" trait use the
+ // classic OneSix applet launcher path which runs everything in-process. All
+ // other instances use the ModernMeshMC which spawns the game as a child
+ // process with the configured Java binary, allowing Minecraft versions that
+ // require Java 21, 25 or newer to run even when MeshMC library JVM is an
+ // older version.
+ auto profileTraits = profile->getTraits();
+ bool isLegacyApplet = profileTraits.contains("legacyLaunch") ||
+ profileTraits.contains("alphaLaunch");
+
+ if (isLegacyApplet) {
+ launchScript += "launcher onesix\n";
+ } else {
+ // Pass the configured Java binary path so ModernMeshMC can spawn the
+ // game process with the correct JVM.
+ launchScript +=
+ "javaPath " + settings()->get("JavaPath").toString() + "\n";
+
+ // Forward all JVM arguments (memory settings, platform flags, extra
+ // args, …) so the child process inherits the same tuning as the current
+ // process.
+ for (auto arg : javaArguments()) {
+ launchScript += "jvmArg " + arg + "\n";
+ }
+
+ launchScript += "launcher modern\n";
+ }
+
+ // qDebug() << "Generated launch script:" << launchScript;
+ return launchScript;
+}
+
+QStringList
+MinecraftInstance::verboseDescription(AuthSessionPtr session,
+ MinecraftServerTargetPtr serverToJoin)
+{
+ QStringList out;
+ out << "Main Class:" << " " + getMainClass() << "";
+ out << "Native path:" << " " + getNativePath() << "";
+
+ auto profile = m_components->getProfile();
+
+ auto alltraits = traits();
+ if (alltraits.size()) {
+ out << "Traits:";
+ for (auto trait : alltraits) {
+ out << "traits " + trait;
+ }
+ out << "";
+ }
+
+ 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;
+ auto javaArchitecture = settings->get("JavaArchitecture").toString();
+ profile->getLibraryFiles(javaArchitecture, jars, nativeJars,
+ getLocalLibraryPath(), binRoot());
+ auto printLibFile = [&](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 << "";
+ }
+
+ auto printModList = [&](const QString& label, ModFolderModel& model) {
+ if (model.size()) {
+ out << QString("%1:").arg(label);
+ auto modList = model.allMods();
+ std::sort(modList.begin(), modList.end(), [](Mod& a, Mod& b) {
+ auto aName = a.filename().completeBaseName();
+ auto bName = b.filename().completeBaseName();
+ return aName.localeAwareCompare(bName) < 0;
+ });
+ for (auto& mod : modList) {
+ if (mod.type() == Mod::MOD_FOLDER) {
+ out << u8" [📁] " + mod.filename().completeBaseName() +
+ " (folder)";
+ continue;
+ }
+
+ if (mod.enabled()) {
+ out << u8" [✔️] " + mod.filename().completeBaseName();
+ } else {
+ out << u8" [❌] " + mod.filename().completeBaseName() +
+ " (disabled)";
+ }
+ }
+ out << "";
+ }
+ };
+
+ printModList("Mods", *(loaderModList().get()));
+ printModList("Core Mods", *(coreModList().get()));
+
+ auto& jarMods = profile->getJarMods();
+ if (jarMods.size()) {
+ out << "Jar Mods:";
+ for (auto& jarmod : jarMods) {
+ auto displayname = jarmod->displayName(currentSystem);
+ auto realname = jarmod->filename(currentSystem);
+ if (displayname != realname) {
+ out << " " + displayname + " (" + realname + ")";
+ } else {
+ out << " " + realname;
+ }
+ }
+ out << "";
+ }
+
+ auto params = processMinecraftArgs(nullptr, serverToJoin);
+ out << "Params:";
+ out << " " + params.join(' ');
+ out << "";
+
+ 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 << "";
+ 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>"));
+ }
+ addToFilter(sessionRef.access_token, tr("<ACCESS TOKEN>"));
+ if (sessionRef.client_token.size()) {
+ addToFilter(sessionRef.client_token, tr("<CLIENT TOKEN>"));
+ }
+ addToFilter(sessionRef.uuid, tr("<PROFILE ID>"));
+
+ return filter;
+}
+
+MessageLevel::Enum MinecraftInstance::guessLevel(const QString& line,
+ MessageLevel::Enum level)
+{
+ QRegularExpression re(
+ "\\[(?<timestamp>[0-9:]+)\\] \\[[^/]+/(?<level>[^\\]]+)\\]");
+ auto match = re.match(line);
+ if (match.hasMatch()) {
+ // New style logs from log4j
+ QString timestamp = match.captured("timestamp");
+ QString levelStr = match.captured("level");
+ if (levelStr == "INFO")
+ level = MessageLevel::Message;
+ if (levelStr == "WARN")
+ level = MessageLevel::Warning;
+ if (levelStr == "ERROR")
+ level = MessageLevel::Error;
+ if (levelStr == "FATAL")
+ level = MessageLevel::Fatal;
+ if (levelStr == "TRACE" || levelStr == "DEBUG")
+ level = MessageLevel::Debug;
+ } else {
+ // Old style forge logs
+ if (line.contains("[INFO]") || line.contains("[CONFIG]") ||
+ line.contains("[FINE]") || line.contains("[FINER]") ||
+ line.contains("[FINEST]"))
+ level = MessageLevel::Message;
+ if (line.contains("[SEVERE]") || line.contains("[STDERR]"))
+ level = MessageLevel::Error;
+ if (line.contains("[WARNING]"))
+ level = MessageLevel::Warning;
+ if (line.contains("[DEBUG]"))
+ level = MessageLevel::Debug;
+ }
+ if (line.contains("overwriting existing"))
+ return MessageLevel::Fatal;
+ // NOTE: this diverges from the real regexp. no unicode, the first section
+ // is + instead of *
+ static const QString javaSymbol =
+ "([a-zA-Z_$][a-zA-Z\\d_$]*\\.)+[a-zA-Z_$][a-zA-Z\\d_$]*";
+ if (line.contains("Exception in thread") ||
+ line.contains(QRegularExpression("\\s+at " + javaSymbol)) ||
+ line.contains(QRegularExpression("Caused by: " + javaSymbol)) ||
+ line.contains(
+ QRegularExpression("([a-zA-Z_$][a-zA-Z\\d_$]*\\.)+[a-zA-Z_$]?[a-zA-"
+ "Z\\d_$]*(Exception|Error|Throwable)")) ||
+ line.contains(QRegularExpression("... \\d+ more$")))
+ return MessageLevel::Error;
+ return level;
+}
+
+IPathMatcher::Ptr MinecraftInstance::getLogFileMatcher()
+{
+ auto combined = std::make_shared<MultiMatcher>();
+ combined->add(
+ std::make_shared<RegexpMatcher>(".*\\.log(\\.[0-9]*)?(\\.gz)?$"));
+ combined->add(std::make_shared<RegexpMatcher>("crash-.*\\.txt"));
+ combined->add(std::make_shared<RegexpMatcher>("IDMap dump.*\\.txt$"));
+ combined->add(std::make_shared<RegexpMatcher>("ModLoader\\.txt(\\..*)?$"));
+ return combined;
+}
+
+QString MinecraftInstance::getLogFileRoot()
+{
+ return gameRoot();
+}
+
+QString MinecraftInstance::getStatusbarDescription()
+{
+ QStringList traits;
+ if (hasVersionBroken()) {
+ traits.append(tr("broken"));
+ }
+
+ QString description;
+ description.append(
+ tr("Minecraft %1 (%2)")
+ .arg(m_components->getComponentVersion("net.minecraft"))
+ .arg(typeName()));
+ if (m_settings->get("ShowGameTime").toBool()) {
+ if (lastTimePlayed() > 0) {
+ description.append(
+ tr(", last played for %1")
+ .arg(Time::prettifyDuration(lastTimePlayed())));
+ }
+
+ if (totalTimePlayed() > 0) {
+ description.append(
+ tr(", total played for %1")
+ .arg(Time::prettifyDuration(totalTimePlayed())));
+ }
+ }
+ if (hasCrashed()) {
+ description.append(tr(", has crashed."));
+ }
+ return description;
+}
+
+Task::Ptr MinecraftInstance::createUpdateTask(Net::Mode mode)
+{
+ switch (mode) {
+ case Net::Mode::Offline: {
+ return Task::Ptr(new MinecraftLoadAndCheck(this));
+ }
+ case Net::Mode::Online: {
+ return Task::Ptr(new MinecraftUpdate(this));
+ }
+ }
+ return nullptr;
+}
+
+shared_qobject_ptr<LaunchTask>
+MinecraftInstance::createLaunchTask(AuthSessionPtr session,
+ MinecraftServerTargetPtr serverToJoin)
+{
+ // FIXME: get rid of shared_from_this ...
+ auto process = LaunchTask::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->appendStep(
+ new TextPrint(pptr, "Minecraft folder is:\n" + gameRoot() + "\n\n",
+ MessageLevel::MeshMC));
+ }
+
+ // check java
+ {
+ process->appendStep(new CheckJava(pptr));
+ }
+
+ // check launch method
+ QStringList validMethods = {"MeshMCPart", "DirectJava"};
+ QString method = launchMethod();
+ if (!validMethods.contains(method)) {
+ process->appendStep(new TextPrint(
+ pptr, "Selected launch method \"" + method + "\" is not valid.\n",
+ MessageLevel::Fatal));
+ return process;
+ }
+
+ // create the .minecraft folder and server-resource-packs (workaround for
+ // Minecraft bug MCL-3732)
+ {
+ process->appendStep(new CreateGameFolders(pptr));
+ }
+
+ if (!serverToJoin && m_settings->get("JoinServerOnLaunch").toBool()) {
+ QString fullAddress =
+ m_settings->get("JoinServerOnLaunchAddress").toString();
+ serverToJoin.reset(new MinecraftServerTarget(
+ MinecraftServerTarget::parse(fullAddress)));
+ }
+
+ if (serverToJoin && serverToJoin->port == 25565) {
+ // Resolve server address to join on launch
+ auto* step = new LookupServerAddress(pptr);
+ step->setLookupAddress(serverToJoin->address);
+ step->setOutputAddressPtr(serverToJoin);
+ process->appendStep(step);
+ }
+
+ // run pre-launch command if that's needed
+ if (getPreLaunchCommand().size()) {
+ auto step = new PreLaunchCommand(pptr);
+ step->setWorkingDirectory(gameRoot());
+ process->appendStep(step);
+ }
+
+ // if we aren't in offline mode,.
+ if (session->status != AuthSession::PlayableOffline) {
+ if (!session->demo) {
+ process->appendStep(new ClaimAccount(pptr, session));
+ }
+ process->appendStep(new Update(pptr, Net::Mode::Online));
+ } else {
+ process->appendStep(new Update(pptr, Net::Mode::Offline));
+ }
+
+ // if there are any jar mods
+ {
+ process->appendStep(new ModMinecraftJar(pptr));
+ }
+
+ // Scan mods folders for mods
+ {
+ process->appendStep(new ScanModFolders(pptr));
+ }
+
+ // print some instance info here...
+ {
+ process->appendStep(new PrintInstanceInfo(pptr, session, serverToJoin));
+ }
+
+ // extract native jars if needed
+ {
+ process->appendStep(new ExtractNatives(pptr));
+ }
+
+ // reconstruct assets if needed
+ {
+ process->appendStep(new ReconstructAssets(pptr));
+ }
+
+ // verify that minimum Java requirements are met
+ {
+ process->appendStep(new VerifyJavaInstall(pptr));
+ }
+
+ {
+ // actually launch the game
+ auto method = launchMethod();
+ if (method == "MeshMCPart") {
+ auto step = new MeshMCPartLaunch(pptr);
+ step->setWorkingDirectory(gameRoot());
+ step->setAuthSession(session);
+ step->setServerToJoin(serverToJoin);
+ process->appendStep(step);
+ } else if (method == "DirectJava") {
+ auto step = new DirectJavaLaunch(pptr);
+ step->setWorkingDirectory(gameRoot());
+ step->setAuthSession(session);
+ step->setServerToJoin(serverToJoin);
+ process->appendStep(step);
+ }
+ }
+
+ // run post-exit command if that's needed
+ if (getPostExitCommand().size()) {
+ auto step = new PostLaunchCommand(pptr);
+ step->setWorkingDirectory(gameRoot());
+ process->appendStep(step);
+ }
+ if (session) {
+ process->setCensorFilter(createCensorFilterFromSession(session));
+ }
+ m_launchProcess = process;
+ emit launchTaskChanged(m_launchProcess);
+ return m_launchProcess;
+}
+
+QString MinecraftInstance::launchMethod()
+{
+ return m_settings->get("MCLaunchMethod").toString();
+}
+
+JavaVersion MinecraftInstance::getJavaVersion() const
+{
+ return JavaVersion(settings()->get("JavaVersion").toString());
+}
+
+std::shared_ptr<ModFolderModel> MinecraftInstance::loaderModList() const
+{
+ if (!m_loader_mod_list) {
+ m_loader_mod_list.reset(new ModFolderModel(modsRoot()));
+ m_loader_mod_list->disableInteraction(isRunning());
+ connect(this, &BaseInstance::runningStatusChanged,
+ m_loader_mod_list.get(), &ModFolderModel::disableInteraction);
+ }
+ return m_loader_mod_list;
+}
+
+std::shared_ptr<ModFolderModel> MinecraftInstance::coreModList() const
+{
+ if (!m_core_mod_list) {
+ m_core_mod_list.reset(new ModFolderModel(coreModsDir()));
+ m_core_mod_list->disableInteraction(isRunning());
+ connect(this, &BaseInstance::runningStatusChanged,
+ m_core_mod_list.get(), &ModFolderModel::disableInteraction);
+ }
+ return m_core_mod_list;
+}
+
+std::shared_ptr<ModFolderModel> MinecraftInstance::resourcePackList() const
+{
+ if (!m_resource_pack_list) {
+ m_resource_pack_list.reset(
+ new ResourcePackFolderModel(resourcePacksDir()));
+ m_resource_pack_list->disableInteraction(isRunning());
+ connect(this, &BaseInstance::runningStatusChanged,
+ m_resource_pack_list.get(),
+ &ModFolderModel::disableInteraction);
+ }
+ return m_resource_pack_list;
+}
+
+std::shared_ptr<ModFolderModel> MinecraftInstance::texturePackList() const
+{
+ if (!m_texture_pack_list) {
+ m_texture_pack_list.reset(
+ new TexturePackFolderModel(texturePacksDir()));
+ m_texture_pack_list->disableInteraction(isRunning());
+ connect(this, &BaseInstance::runningStatusChanged,
+ m_texture_pack_list.get(), &ModFolderModel::disableInteraction);
+ }
+ return m_texture_pack_list;
+}
+
+std::shared_ptr<ModFolderModel> MinecraftInstance::shaderPackList() const
+{
+ if (!m_shader_pack_list) {
+ m_shader_pack_list.reset(new ResourcePackFolderModel(shaderPacksDir()));
+ m_shader_pack_list->disableInteraction(isRunning());
+ connect(this, &BaseInstance::runningStatusChanged,
+ m_shader_pack_list.get(), &ModFolderModel::disableInteraction);
+ }
+ return m_shader_pack_list;
+}
+
+std::shared_ptr<WorldList> MinecraftInstance::worldList() const
+{
+ if (!m_world_list) {
+ m_world_list.reset(new WorldList(worldDir()));
+ }
+ return m_world_list;
+}
+
+std::shared_ptr<GameOptions> MinecraftInstance::gameOptionsModel() const
+{
+ if (!m_game_options) {
+ m_game_options.reset(
+ new GameOptions(FS::PathCombine(gameRoot(), "options.txt")));
+ }
+ return m_game_options;
+}
+
+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(currentSystem, jar, temp1, temp2, temp3,
+ jarmodsPath().absolutePath());
+ // QString filePath =
+ // jarmodsPath().absoluteFilePath(jarmod->filename(currentSystem));
+ mods.push_back(Mod(QFileInfo(jar[0])));
+ }
+ return mods;
+}
+
+#include "MinecraftInstance.moc"
diff --git a/meshmc/launcher/minecraft/MinecraftInstance.h b/meshmc/launcher/minecraft/MinecraftInstance.h
new file mode 100644
index 0000000000..1d54e85c00
--- /dev/null
+++ b/meshmc/launcher/minecraft/MinecraftInstance.h
@@ -0,0 +1,162 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+#include "BaseInstance.h"
+#include <java/JavaVersion.h>
+#include "minecraft/mod/Mod.h"
+#include <QProcess>
+#include <QDir>
+#include "minecraft/launch/MinecraftServerTarget.h"
+
+class ModFolderModel;
+class WorldList;
+class GameOptions;
+class LaunchStep;
+class PackProfile;
+
+class MinecraftInstance : public BaseInstance
+{
+ Q_OBJECT
+ public:
+ MinecraftInstance(SettingsObjectPtr globalSettings,
+ SettingsObjectPtr settings, const QString& rootDir);
+ virtual ~MinecraftInstance() {};
+ virtual void saveNow() override;
+
+ // FIXME: remove
+ QString typeName() const override;
+ // FIXME: remove
+ QSet<QString> traits() const override;
+
+ bool canEdit() const override
+ {
+ return true;
+ }
+
+ bool canExport() const override
+ {
+ return true;
+ }
+
+ ////// Directories and files //////
+ QString jarModsDir() const;
+ QString resourcePacksDir() const;
+ QString texturePacksDir() const;
+ QString shaderPacksDir() const;
+ QString modsRoot() const override;
+ QString coreModsDir() const;
+ 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;
+
+ ////// Profile management //////
+ std::shared_ptr<PackProfile> getPackProfile() const;
+
+ ////// Mod Lists //////
+ std::shared_ptr<ModFolderModel> loaderModList() const;
+ std::shared_ptr<ModFolderModel> coreModList() const;
+ std::shared_ptr<ModFolderModel> resourcePackList() const;
+ std::shared_ptr<ModFolderModel> texturePackList() const;
+ std::shared_ptr<ModFolderModel> shaderPackList() const;
+ std::shared_ptr<WorldList> worldList() const;
+ std::shared_ptr<GameOptions> gameOptionsModel() const;
+
+ ////// Launch stuff //////
+ Task::Ptr createUpdateTask(Net::Mode mode) override;
+ shared_qobject_ptr<LaunchTask>
+ createLaunchTask(AuthSessionPtr account,
+ MinecraftServerTargetPtr serverToJoin) override;
+ QStringList extraArguments() const override;
+ QStringList
+ verboseDescription(AuthSessionPtr session,
+ MinecraftServerTargetPtr serverToJoin) override;
+ QList<Mod> getJarMods() const;
+ QString createLaunchScript(AuthSessionPtr session,
+ MinecraftServerTargetPtr serverToJoin);
+ /// get arguments passed to java
+ QStringList javaArguments() const;
+
+ /// get variables for launch command variable substitution/environment
+ QMap<QString, QString> getVariables() const override;
+
+ /// create an environment for launching processes
+ QProcessEnvironment createEnvironment() override;
+
+ /// guess log level from a line of minecraft log
+ MessageLevel::Enum guessLevel(const QString& line,
+ MessageLevel::Enum level) override;
+
+ IPathMatcher::Ptr getLogFileMatcher() override;
+
+ QString getLogFileRoot() override;
+
+ QString getStatusbarDescription() override;
+
+ // FIXME: remove
+ virtual QStringList getClassPath() const;
+ // FIXME: remove
+ virtual QStringList getNativeJars() const;
+ // FIXME: remove
+ virtual QString getMainClass() const;
+
+ // FIXME: remove
+ virtual QStringList
+ processMinecraftArgs(AuthSessionPtr account,
+ MinecraftServerTargetPtr serverToJoin) const;
+
+ virtual JavaVersion getJavaVersion() const;
+
+ protected:
+ QMap<QString, QString>
+ createCensorFilterFromSession(AuthSessionPtr session);
+ QStringList validLaunchMethods();
+ QString launchMethod();
+
+ 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_resource_pack_list;
+ mutable std::shared_ptr<ModFolderModel> m_shader_pack_list;
+ mutable std::shared_ptr<ModFolderModel> m_texture_pack_list;
+ mutable std::shared_ptr<WorldList> m_world_list;
+ mutable std::shared_ptr<GameOptions> m_game_options;
+};
+
+typedef std::shared_ptr<MinecraftInstance> MinecraftInstancePtr;
diff --git a/meshmc/launcher/minecraft/MinecraftLoadAndCheck.cpp b/meshmc/launcher/minecraft/MinecraftLoadAndCheck.cpp
new file mode 100644
index 0000000000..2ab43aebf5
--- /dev/null
+++ b/meshmc/launcher/minecraft/MinecraftLoadAndCheck.cpp
@@ -0,0 +1,71 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "MinecraftLoadAndCheck.h"
+#include "MinecraftInstance.h"
+#include "PackProfile.h"
+
+MinecraftLoadAndCheck::MinecraftLoadAndCheck(MinecraftInstance* inst,
+ QObject* parent)
+ : Task(parent), m_inst(inst)
+{
+}
+
+void MinecraftLoadAndCheck::executeTask()
+{
+ // add offline metadata load task
+ auto components = m_inst->getPackProfile();
+ components->reload(Net::Mode::Offline);
+ m_task = components->getCurrentTask();
+
+ if (!m_task) {
+ emitSucceeded();
+ return;
+ }
+ connect(m_task.get(), &Task::succeeded, this,
+ &MinecraftLoadAndCheck::subtaskSucceeded);
+ connect(m_task.get(), &Task::failed, this,
+ &MinecraftLoadAndCheck::subtaskFailed);
+ connect(m_task.get(), &Task::progress, this,
+ &MinecraftLoadAndCheck::progress);
+ connect(m_task.get(), &Task::status, this,
+ &MinecraftLoadAndCheck::setStatus);
+}
+
+void MinecraftLoadAndCheck::subtaskSucceeded()
+{
+ if (isFinished()) {
+ qCritical() << "MinecraftUpdate: Subtask" << sender()
+ << "succeeded, but work was already done!";
+ return;
+ }
+ emitSucceeded();
+}
+
+void MinecraftLoadAndCheck::subtaskFailed(QString error)
+{
+ if (isFinished()) {
+ qCritical() << "MinecraftUpdate: Subtask" << sender()
+ << "failed, but work was already done!";
+ return;
+ }
+ emitFailed(error);
+}
diff --git a/meshmc/launcher/minecraft/MinecraftLoadAndCheck.h b/meshmc/launcher/minecraft/MinecraftLoadAndCheck.h
new file mode 100644
index 0000000000..7ec79bb48e
--- /dev/null
+++ b/meshmc/launcher/minecraft/MinecraftLoadAndCheck.h
@@ -0,0 +1,70 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * 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 <QObject>
+#include <QList>
+#include <QUrl>
+
+#include "tasks/Task.h"
+
+#include "QObjectPtr.h"
+
+class MinecraftVersion;
+class MinecraftInstance;
+
+class MinecraftLoadAndCheck : public Task
+{
+ Q_OBJECT
+ public:
+ explicit MinecraftLoadAndCheck(MinecraftInstance* inst,
+ QObject* parent = 0);
+ virtual ~MinecraftLoadAndCheck() {};
+ void executeTask() override;
+
+ private slots:
+ void subtaskSucceeded();
+ void subtaskFailed(QString error);
+
+ private:
+ MinecraftInstance* m_inst = nullptr;
+ Task::Ptr m_task;
+ QString m_preFailure;
+ QString m_fail_reason;
+};
diff --git a/meshmc/launcher/minecraft/MinecraftUpdate.cpp b/meshmc/launcher/minecraft/MinecraftUpdate.cpp
new file mode 100644
index 0000000000..356d717ed3
--- /dev/null
+++ b/meshmc/launcher/minecraft/MinecraftUpdate.cpp
@@ -0,0 +1,200 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * 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 "MinecraftUpdate.h"
+#include "MinecraftInstance.h"
+
+#include <QFile>
+#include <QFileInfo>
+#include <QTextStream>
+#include <QDataStream>
+
+#include "BaseInstance.h"
+#include "minecraft/PackProfile.h"
+#include "minecraft/Library.h"
+#include <FileSystem.h>
+
+#include "update/FoldersTask.h"
+#include "update/LibrariesTask.h"
+#include "update/FMLLibrariesTask.h"
+#include "update/AssetUpdateTask.h"
+
+#include <meta/Index.h>
+#include <meta/Version.h>
+
+MinecraftUpdate::MinecraftUpdate(MinecraftInstance* inst, QObject* parent)
+ : Task(parent), m_inst(inst)
+{
+}
+
+void MinecraftUpdate::executeTask()
+{
+ m_tasks.clear();
+ // create folders
+ {
+ m_tasks.append(std::make_shared<FoldersTask>(m_inst));
+ }
+
+ // add metadata update task if necessary
+ {
+ auto components = m_inst->getPackProfile();
+ components->reload(Net::Mode::Online);
+ auto task = components->getCurrentTask();
+ if (task) {
+ m_tasks.append(task.unwrap());
+ }
+ }
+
+ // libraries download
+ {
+ m_tasks.append(std::make_shared<LibrariesTask>(m_inst));
+ }
+
+ // FML libraries download and copy into the instance
+ {
+ m_tasks.append(std::make_shared<FMLLibrariesTask>(m_inst));
+ }
+
+ // assets update
+ {
+ m_tasks.append(std::make_shared<AssetUpdateTask>(m_inst));
+ }
+
+ if (!m_preFailure.isEmpty()) {
+ emitFailed(m_preFailure);
+ return;
+ }
+ next();
+}
+
+void MinecraftUpdate::next()
+{
+ if (m_abort) {
+ emitFailed(tr("Aborted by user."));
+ return;
+ }
+ if (m_failed_out_of_order) {
+ emitFailed(m_fail_reason);
+ return;
+ }
+ m_currentTask++;
+ if (m_currentTask > 0) {
+ auto task = m_tasks[m_currentTask - 1];
+ disconnect(task.get(), &Task::succeeded, this,
+ &MinecraftUpdate::subtaskSucceeded);
+ disconnect(task.get(), &Task::failed, this,
+ &MinecraftUpdate::subtaskFailed);
+ disconnect(task.get(), &Task::progress, this,
+ &MinecraftUpdate::progress);
+ disconnect(task.get(), &Task::status, this,
+ &MinecraftUpdate::setStatus);
+ }
+ if (m_currentTask == m_tasks.size()) {
+ emitSucceeded();
+ return;
+ }
+ auto task = m_tasks[m_currentTask];
+ // if the task is already finished by the time we look at it, skip it
+ if (task->isFinished()) {
+ qCritical() << "MinecraftUpdate: Skipping finished subtask"
+ << m_currentTask << ":" << task.get();
+ next();
+ }
+ connect(task.get(), &Task::succeeded, this,
+ &MinecraftUpdate::subtaskSucceeded);
+ connect(task.get(), &Task::failed, this, &MinecraftUpdate::subtaskFailed);
+ connect(task.get(), &Task::progress, this, &MinecraftUpdate::progress);
+ connect(task.get(), &Task::status, this, &MinecraftUpdate::setStatus);
+ // if the task is already running, do not start it again
+ if (!task->isRunning()) {
+ task->start();
+ }
+}
+
+void MinecraftUpdate::subtaskSucceeded()
+{
+ if (isFinished()) {
+ qCritical() << "MinecraftUpdate: Subtask" << sender()
+ << "succeeded, but work was already done!";
+ return;
+ }
+ auto senderTask = QObject::sender();
+ auto currentTask = m_tasks[m_currentTask].get();
+ if (senderTask != currentTask) {
+ qDebug() << "MinecraftUpdate: Subtask" << sender()
+ << "succeeded out of order.";
+ return;
+ }
+ next();
+}
+
+void MinecraftUpdate::subtaskFailed(QString error)
+{
+ if (isFinished()) {
+ qCritical() << "MinecraftUpdate: Subtask" << sender()
+ << "failed, but work was already done!";
+ return;
+ }
+ auto senderTask = QObject::sender();
+ auto currentTask = m_tasks[m_currentTask].get();
+ if (senderTask != currentTask) {
+ qDebug() << "MinecraftUpdate: Subtask" << sender()
+ << "failed out of order.";
+ m_failed_out_of_order = true;
+ m_fail_reason = error;
+ return;
+ }
+ emitFailed(error);
+}
+
+bool MinecraftUpdate::abort()
+{
+ if (!m_abort) {
+ m_abort = true;
+ auto task = m_tasks[m_currentTask];
+ if (task->canAbort()) {
+ return task->abort();
+ }
+ }
+ return true;
+}
+
+bool MinecraftUpdate::canAbort() const
+{
+ return true;
+}
diff --git a/meshmc/launcher/minecraft/MinecraftUpdate.h b/meshmc/launcher/minecraft/MinecraftUpdate.h
new file mode 100644
index 0000000000..cec09a3244
--- /dev/null
+++ b/meshmc/launcher/minecraft/MinecraftUpdate.h
@@ -0,0 +1,78 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * 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 <QObject>
+#include <QList>
+#include <QUrl>
+
+#include "net/NetJob.h"
+#include "tasks/Task.h"
+#include "minecraft/VersionFilterData.h"
+
+class MinecraftVersion;
+class MinecraftInstance;
+
+class MinecraftUpdate : public Task
+{
+ Q_OBJECT
+ public:
+ explicit MinecraftUpdate(MinecraftInstance* inst, QObject* parent = 0);
+ virtual ~MinecraftUpdate() {};
+
+ void executeTask() override;
+ bool canAbort() const override;
+
+ private slots:
+ bool abort() override;
+ void subtaskSucceeded();
+ void subtaskFailed(QString error);
+
+ private:
+ void next();
+
+ private:
+ MinecraftInstance* m_inst = nullptr;
+ QList<std::shared_ptr<Task>> m_tasks;
+ QString m_preFailure;
+ int m_currentTask = -1;
+ bool m_abort = false;
+ bool m_failed_out_of_order = false;
+ QString m_fail_reason;
+};
diff --git a/meshmc/launcher/minecraft/MojangDownloadInfo.h b/meshmc/launcher/minecraft/MojangDownloadInfo.h
new file mode 100644
index 0000000000..d62e4747e8
--- /dev/null
+++ b/meshmc/launcher/minecraft/MojangDownloadInfo.h
@@ -0,0 +1,95 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+#include <QString>
+#include <QMap>
+#include <memory>
+
+struct MojangDownloadInfo {
+ // types
+ typedef std::shared_ptr<MojangDownloadInfo> Ptr;
+
+ // 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
+ typedef std::shared_ptr<MojangLibraryDownloadInfo> Ptr;
+
+ // 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
+ typedef std::shared_ptr<MojangAssetIndexInfo> Ptr;
+
+ // 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://launchermeta.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/meshmc/launcher/minecraft/MojangVersionFormat.cpp b/meshmc/launcher/minecraft/MojangVersionFormat.cpp
new file mode 100644
index 0000000000..0943da3e56
--- /dev/null
+++ b/meshmc/launcher/minecraft/MojangVersionFormat.cpp
@@ -0,0 +1,387 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "MojangVersionFormat.h"
+#include "OneSixVersionFormat.h"
+#include "MojangDownloadInfo.h"
+
+#include "Json.h"
+using namespace Json;
+#include "ParseUtils.h"
+#include <BuildConfig.h>
+
+static const int CURRENT_MINIMUM_MESHMC_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.size()) {
+ 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);
+ if (out->minecraftArguments.isEmpty()) {
+ QString processArguments;
+ Bits::readString(in, "processArguments", processArguments);
+ QString toCompare = processArguments.toLower();
+ if (toCompare == "legacy") {
+ out->minecraftArguments = " ${auth_player_name} ${auth_session}";
+ } else if (toCompare == "username_session") {
+ out->minecraftArguments =
+ "--username ${auth_player_name} --session ${auth_session}";
+ } else if (toCompare == "username_session_version") {
+ out->minecraftArguments =
+ "--username ${auth_player_name} --session ${auth_session} "
+ "--version ${profile_name}";
+ } else if (!toCompare.isEmpty()) {
+ out->addProblem(
+ ProblemSeverity::Error,
+ QObject::tr("processArguments is set to unknown value '%1'")
+ .arg(processArguments));
+ }
+ }
+ 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->minimumMeshMCVersion =
+ requireInteger(in.value("minimumLauncherVersion"));
+ if (out->minimumMeshMCVersion > CURRENT_MINIMUM_MESHMC_VERSION) {
+ out->addProblem(
+ ProblemSeverity::Warning,
+ QObject::tr("The 'minimumMeshMCVersion' value of this version "
+ "(%1) is higher than supported by %3 (%2). It "
+ "might not work properly!")
+ .arg(out->minimumMeshMCVersion)
+ .arg(CURRENT_MINIMUM_MESHMC_VERSION)
+ .arg(BuildConfig.MESHMC_NAME));
+ }
+ }
+ 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->minimumMeshMCVersion != -1) {
+ out.insert("minimumLauncherVersion", in->minimumMeshMCVersion);
+ }
+ writeString(out, "assets", in->assets);
+ if (in->mojangAssetIndex && in->mojangAssetIndex->known) {
+ out.insert("assetIndex", assetIndexToJson(in->mojangAssetIndex));
+ }
+ if (in->mojangDownloads.size()) {
+ QJsonObject downloadsOut;
+ for (auto iter = in->mojangDownloads.begin();
+ iter != in->mojangDownloads.end(); iter++) {
+ downloadsOut.insert(iter.key(), downloadInfoToJson(iter.value()));
+ }
+ out.insert("downloads", downloadsOut);
+ }
+}
+
+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)";
+ }
+ OpSys opSys = OpSys_fromString(it.key());
+ if (opSys != Os_Other) {
+ out->m_nativeClassifiers[opSys] = it.value().toString();
+ }
+ }
+ }
+ if (libObj.contains("rules")) {
+ out->applyRules = true;
+ out->m_rules = rulesFromJsonV4(libObj);
+ }
+ 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(OpSys_toString(iter.key()), iter.value());
+ iter++;
+ }
+ libRoot.insert("natives", nativeList);
+ if (library->m_extractExcludes.size()) {
+ 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.size()) {
+ 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/meshmc/launcher/minecraft/MojangVersionFormat.h b/meshmc/launcher/minecraft/MojangVersionFormat.h
new file mode 100644
index 0000000000..7d47cc3b16
--- /dev/null
+++ b/meshmc/launcher/minecraft/MojangVersionFormat.h
@@ -0,0 +1,50 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <minecraft/VersionFile.h>
+#include <minecraft/Library.h>
+#include <QJsonDocument>
+#include <ProblemProvider.h>
+
+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/meshmc/launcher/minecraft/MojangVersionFormat_test.cpp b/meshmc/launcher/minecraft/MojangVersionFormat_test.cpp
new file mode 100644
index 0000000000..ba506778a2
--- /dev/null
+++ b/meshmc/launcher/minecraft/MojangVersionFormat_test.cpp
@@ -0,0 +1,77 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <QTest>
+#include <QDebug>
+#include "TestUtil.h"
+
+#include "minecraft/MojangVersionFormat.h"
+
+class MojangVersionFormatTest : public QObject
+{
+ Q_OBJECT
+
+ static QJsonDocument readJson(const char* file)
+ {
+ auto path = QFINDTESTDATA(file);
+ QFile jsonFile(path);
+ if (!jsonFile.open(QIODevice::ReadOnly))
+ return QJsonDocument();
+ auto data = jsonFile.readAll();
+ jsonFile.close();
+ return QJsonDocument::fromJson(data);
+ }
+ static void writeJson(const char* file, QJsonDocument doc)
+ {
+ QFile jsonFile(file);
+ if (!jsonFile.open(QIODevice::WriteOnly | QIODevice::Text))
+ return;
+ auto data = doc.toJson(QJsonDocument::Indented);
+ qDebug() << QString::fromUtf8(data);
+ jsonFile.write(data);
+ jsonFile.close();
+ }
+
+ private slots:
+ void test_Through_Simple()
+ {
+ QJsonDocument doc = readJson("data/1.9-simple.json");
+ auto vfile =
+ MojangVersionFormat::versionFileFromJson(doc, "1.9-simple.json");
+ auto doc2 = MojangVersionFormat::versionFileToJson(vfile);
+ writeJson("1.9-simple-passthorugh.json", doc2);
+
+ QCOMPARE(doc.toJson(), doc2.toJson());
+ }
+
+ void test_Through()
+ {
+ QJsonDocument doc = readJson("data/1.9.json");
+ auto vfile = MojangVersionFormat::versionFileFromJson(doc, "1.9.json");
+ auto doc2 = MojangVersionFormat::versionFileToJson(vfile);
+ writeJson("1.9-passthorugh.json", doc2);
+ QCOMPARE(doc.toJson(), doc2.toJson());
+ }
+};
+
+QTEST_GUILESS_MAIN(MojangVersionFormatTest)
+
+#include "MojangVersionFormat_test.moc"
diff --git a/meshmc/launcher/minecraft/OneSixVersionFormat.cpp b/meshmc/launcher/minecraft/OneSixVersionFormat.cpp
new file mode 100644
index 0000000000..990b845d95
--- /dev/null
+++ b/meshmc/launcher/minecraft/OneSixVersionFormat.cpp
@@ -0,0 +1,386 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "OneSixVersionFormat.h"
+#include <Json.h>
+#include "minecraft/ParseUtils.h"
+#include <minecraft/MojangVersionFormat.h>
+
+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.size())
+ libRoot.insert("MMC-absoluteUrl", library->m_absoluteURL);
+ if (library->m_hint.size())
+ libRoot.insert("MMC-hint", library->m_hint);
+ if (library->m_filename.size())
+ libRoot.insert("MMC-filename", library->m_filename);
+ if (library->m_displayname.size())
+ 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();
+
+ Meta::MetadataVersion formatVersion = Meta::parseFormatVersion(root, false);
+ switch (formatVersion) {
+ case Meta::MetadataVersion::InitialRelease:
+ break;
+ case Meta::MetadataVersion::Invalid:
+ 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 {
+ // FIXME: evaluate if we don't want to throw exceptions here instead
+ qCritical() << filename << "doesn't contain an order field";
+ }
+ }
+
+ out->name = root.value("name").toString();
+
+ if (root.contains("uid")) {
+ out->uid = root.value("uid").toString();
+ } else {
+ out->uid = root.value("fileId").toString();
+ }
+
+ out->version = root.value("version").toString();
+
+ MojangVersionFormat::readVersionProperties(root, out.get());
+
+ // added for legacy Minecraft window embedding, TODO: remove
+ readString(root, "appletClass", out->appletClass);
+
+ 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("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 = [&](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 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")) {
+ Meta::parseRequires(root, &out->requirements);
+ }
+ QString dependsOnMinecraftVersion = root.value("mcVersion").toString();
+ if (!dependsOnMinecraftVersion.isEmpty()) {
+ Meta::Require mcReq;
+ mcReq.uid = "net.minecraft";
+ mcReq.equalsVersion = dependsOnMinecraftVersion;
+ if (out->requirements.count(mcReq) == 0) {
+ out->requirements.insert(mcReq);
+ }
+ }
+ if (root.contains("conflicts")) {
+ Meta::parseRequires(root, &out->conflicts);
+ }
+ if (root.contains("volatile")) {
+ out->m_volatile = requireBoolean(root, "volatile");
+ }
+
+ /* 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);
+
+ Meta::serializeFormatVersion(root, Meta::MetadataVersion::InitialRelease);
+
+ 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());
+ 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->requirements.empty()) {
+ Meta::serializeRequires(root, &patch->requirements, "requires");
+ }
+ if (!patch->conflicts.empty()) {
+ Meta::serializeRequires(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(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.
+ auto uuid = QUuid::createUuid();
+ QString id = uuid.toString().remove('{').remove('}');
+ out->setRawName(GradleSpecifier("org.projecttick.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/meshmc/launcher/minecraft/OneSixVersionFormat.h b/meshmc/launcher/minecraft/OneSixVersionFormat.h
new file mode 100644
index 0000000000..64d7bbf40d
--- /dev/null
+++ b/meshmc/launcher/minecraft/OneSixVersionFormat.h
@@ -0,0 +1,62 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <minecraft/VersionFile.h>
+#include <minecraft/PackProfile.h>
+#include <minecraft/Library.h>
+#include <QJsonDocument>
+#include <ProblemProvider.h>
+
+class OneSixVersionFormat
+{
+ public:
+ // version files / profile patches
+ static VersionFilePtr versionFileFromJson(const QJsonDocument& doc,
+ const QString& filename,
+ const 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/meshmc/launcher/minecraft/OpSys.cpp b/meshmc/launcher/minecraft/OpSys.cpp
new file mode 100644
index 0000000000..98375854cd
--- /dev/null
+++ b/meshmc/launcher/minecraft/OpSys.cpp
@@ -0,0 +1,68 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * 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 "OpSys.h"
+
+OpSys OpSys_fromString(QString name)
+{
+ if (name == "freebsd")
+ return Os_FreeBSD;
+ if (name == "linux")
+ return Os_Linux;
+ if (name == "windows")
+ return Os_Windows;
+ if (name == "osx")
+ return Os_OSX;
+ return Os_Other;
+}
+
+QString OpSys_toString(OpSys name)
+{
+ switch (name) {
+ case Os_FreeBSD:
+ return "freebsd";
+ case Os_Linux:
+ return "linux";
+ case Os_OSX:
+ return "osx";
+ case Os_Windows:
+ return "windows";
+ default:
+ return "other";
+ }
+} \ No newline at end of file
diff --git a/meshmc/launcher/minecraft/OpSys.h b/meshmc/launcher/minecraft/OpSys.h
new file mode 100644
index 0000000000..49a5404a41
--- /dev/null
+++ b/meshmc/launcher/minecraft/OpSys.h
@@ -0,0 +1,54 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * 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 <QString>
+enum OpSys { Os_Windows, Os_FreeBSD, Os_Linux, Os_OSX, Os_Other };
+
+OpSys OpSys_fromString(QString);
+QString OpSys_toString(OpSys);
+
+#ifdef Q_OS_WIN32
+#define currentSystem Os_Windows
+#elif defined Q_OS_MAC
+#define currentSystem Os_OSX
+#elif defined Q_OS_FREEBSD
+#define currentSystem Os_FreeBSD
+#else
+#define currentSystem Os_Linux
+#endif
diff --git a/meshmc/launcher/minecraft/PackProfile.cpp b/meshmc/launcher/minecraft/PackProfile.cpp
new file mode 100644
index 0000000000..0bff119c66
--- /dev/null
+++ b/meshmc/launcher/minecraft/PackProfile.cpp
@@ -0,0 +1,1191 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * 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 <QFile>
+#include <QCryptographicHash>
+#include <Version.h>
+#include <QDir>
+#include <QJsonDocument>
+#include <QJsonArray>
+#include <QDebug>
+#include <QSaveFile>
+#include <QUuid>
+#include <QTimer>
+
+#include "Exception.h"
+#include "minecraft/OneSixVersionFormat.h"
+#include "FileSystem.h"
+#include "meta/Index.h"
+#include "minecraft/MinecraftInstance.h"
+#include "Json.h"
+
+#include "PackProfile.h"
+#include "PackProfile_p.h"
+#include "ComponentUpdateTask.h"
+
+#include "Application.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);
+ }
+ Meta::serializeRequires(obj, &component->m_cachedRequires,
+ "cachedRequires");
+ Meta::serializeRequires(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 = new 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
+ // TODO @RESILIENCE: ignore invalid values/structure here?
+ component->m_cachedVersion = Json::ensureString(obj.value("cachedVersion"));
+ component->m_cachedName = Json::ensureString(obj.value("cachedName"));
+ Meta::parseRequires(obj, &component->m_cachedRequires, "cachedRequires");
+ Meta::parseRequires(obj, &component->m_cachedConflicts, "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)) {
+ qCritical() << "Couldn't open" << outFile.fileName()
+ << "for writing:" << outFile.errorString();
+ return false;
+ }
+ auto data = QJsonDocument(obj).toJson(QJsonDocument::Indented);
+ if (outFile.write(data) != data.size()) {
+ qCritical() << "Couldn't write all the data into" << outFile.fileName()
+ << "because:" << outFile.errorString();
+ return false;
+ }
+ if (!outFile.commit()) {
+ qCritical() << "Couldn't save" << outFile.fileName()
+ << "because:" << outFile.errorString();
+ }
+ return true;
+}
+
+// Read the given file into component containers
+static bool loadPackProfile(PackProfile* parent, const QString& filename,
+ const QString& componentJsonPattern,
+ ComponentContainer& container)
+{
+ QFile componentsFile(filename);
+ if (!componentsFile.exists()) {
+ qWarning()
+ << "Components file doesn't exist. This should never happen.";
+ return false;
+ }
+ if (!componentsFile.open(QFile::ReadOnly)) {
+ qCritical() << "Couldn't open" << componentsFile.fileName()
+ << " for reading:" << componentsFile.errorString();
+ qWarning() << "Ignoring overriden order";
+ return false;
+ }
+
+ // and it's valid JSON
+ QJsonParseError error;
+ QJsonDocument doc =
+ QJsonDocument::fromJson(componentsFile.readAll(), &error);
+ if (error.error != QJsonParseError::NoError) {
+ qCritical() << "Couldn't parse" << componentsFile.fileName() << ":"
+ << error.errorString();
+ qWarning() << "Ignoring overriden 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("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 obj =
+ Json::requireObject(item, "Component must be an object.");
+ container.append(
+ componentFromJsonV1(parent, componentJsonPattern, obj));
+ }
+ } catch (const JSONValidationError& err) {
+ qCritical() << "Couldn't parse" << componentsFile.fileName()
+ << ": bad file format";
+ container.clear();
+ return false;
+ }
+ return true;
+}
+
+// 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() << "Component list should never save if it didn't "
+ "successfully load, instance:"
+ << d->m_instance->name();
+ return;
+ }
+ if (!d->dirty) {
+ d->dirty = true;
+ qDebug() << "Component list save is scheduled for"
+ << d->m_instance->name();
+ }
+ d->m_saveTimer.start();
+}
+
+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() << "Component list save performed now for"
+ << d->m_instance->name();
+ auto filename = componentsFilePath();
+ savePackProfile(filename, d->components);
+ d->dirty = false;
+}
+
+bool PackProfile::load()
+{
+ auto filename = componentsFilePath();
+ QFile componentsFile(filename);
+
+ // migrate old config to new one, if needed
+ if (!componentsFile.exists()) {
+ if (!migratePreComponentConfig()) {
+ // FIXME: the user should be notified...
+ qCritical()
+ << "Failed to convert old pre-component config for instance"
+ << d->m_instance->name();
+ return false;
+ }
+ }
+
+ // load the new component list and swap it with the current one...
+ ComponentContainer newComponents;
+ if (!loadPackProfile(this, filename, patchesPattern(), newComponents)) {
+ qCritical() << "Failed to load the component config for instance"
+ << d->m_instance->name();
+ return false;
+ } else {
+ // FIXME: 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() << "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 true;
+ }
+}
+
+void PackProfile::reload(Net::Mode netmode)
+{
+ // Do not reload when the update/resolve task is running. It is in control.
+ if (d->m_updateTask) {
+ return;
+ }
+
+ // flush any scheduled saves to not lose state
+ saveNow();
+
+ // FIXME: differentiate when a reapply is required by propagating state from
+ // components
+ invalidateLaunchProfile();
+
+ if (load()) {
+ resolve(netmode);
+ }
+}
+
+Task::Ptr PackProfile::getCurrentTask()
+{
+ return d->m_updateTask;
+}
+
+void PackProfile::resolve(Net::Mode netmode)
+{
+ auto updateTask = new ComponentUpdateTask(
+ ComponentUpdateTask::Mode::Resolution, netmode, this);
+ d->m_updateTask.reset(updateTask);
+ connect(updateTask, &ComponentUpdateTask::succeeded, this,
+ &PackProfile::updateSucceeded);
+ connect(updateTask, &ComponentUpdateTask::failed, this,
+ &PackProfile::updateFailed);
+ d->m_updateTask->start();
+}
+
+void PackProfile::updateSucceeded()
+{
+ qDebug() << "Component list update/resolve task succeeded for"
+ << d->m_instance->name();
+ d->m_updateTask.reset();
+ invalidateLaunchProfile();
+}
+
+void PackProfile::updateFailed(const QString& error)
+{
+ qDebug() << "Component list update/resolve task failed for"
+ << d->m_instance->name() << "Reason:" << error;
+ d->m_updateTask.reset();
+ invalidateLaunchProfile();
+}
+
+// NOTE this is really old stuff, and only needs to be used when loading the old
+// hardcoded component-unaware format (loadPreComponentConfig).
+static void upgradeDeprecatedFiles(QString root, QString instanceName)
+{
+ auto versionJsonPath = FS::PathCombine(root, "version.json");
+ auto customJsonPath = FS::PathCombine(root, "custom.json");
+ auto mcJson = FS::PathCombine(root, "patches", "net.minecraft.json");
+
+ QString sourceFile;
+ QString renameFile;
+
+ // convert old crap.
+ if (QFile::exists(customJsonPath)) {
+ sourceFile = customJsonPath;
+ renameFile = versionJsonPath;
+ } else if (QFile::exists(versionJsonPath)) {
+ sourceFile = versionJsonPath;
+ }
+ if (!sourceFile.isEmpty() && !QFile::exists(mcJson)) {
+ if (!FS::ensureFilePathExists(mcJson)) {
+ qWarning() << "Couldn't create patches folder for" << instanceName;
+ return;
+ }
+ if (!renameFile.isEmpty() && QFile::exists(renameFile)) {
+ if (!QFile::rename(renameFile, renameFile + ".old")) {
+ qWarning() << "Couldn't rename" << renameFile << "to"
+ << renameFile + ".old" << "in" << instanceName;
+ return;
+ }
+ }
+ auto file = ProfileUtils::parseJsonFile(QFileInfo(sourceFile), false);
+ ProfileUtils::removeLwjglFromPatch(file);
+ file->uid = "net.minecraft";
+ file->version = file->minecraftVersion;
+ file->name = "Minecraft";
+
+ Meta::Require needsLwjgl;
+ needsLwjgl.uid = "org.lwjgl";
+ file->requirements.insert(needsLwjgl);
+
+ if (!ProfileUtils::saveJsonFile(
+ OneSixVersionFormat::versionFileToJson(file), mcJson)) {
+ return;
+ }
+ if (!QFile::rename(sourceFile, sourceFile + ".old")) {
+ qWarning() << "Couldn't rename" << sourceFile << "to"
+ << sourceFile + ".old" << "in" << instanceName;
+ return;
+ }
+ }
+}
+
+/*
+ * Migrate old layout to the component based one...
+ * - Part of the version information is taken from `instance.cfg` (fed to this
+ * class from outside).
+ * - Part is taken from the old order.json file.
+ * - Part is loaded from loose json files in the instance's `patches` directory.
+ */
+bool PackProfile::migratePreComponentConfig()
+{
+ // upgrade the very old files from the beginnings of MeshMC
+ upgradeDeprecatedFiles(d->m_instance->instanceRoot(),
+ d->m_instance->name());
+
+ QList<ComponentPtr> components;
+ QSet<QString> loaded;
+
+ auto addBuiltinPatch =
+ [&](const QString& uid, bool asDependency, const QString& emptyVersion,
+ const Meta::Require& req, const Meta::Require& conflict) {
+ auto jsonFilePath = FS::PathCombine(d->m_instance->instanceRoot(),
+ "patches", uid + ".json");
+ auto intendedVersion = d->getOldConfigVersion(uid);
+ // load up the base minecraft patch
+ ComponentPtr component;
+ if (QFile::exists(jsonFilePath)) {
+ if (intendedVersion.isEmpty()) {
+ intendedVersion = emptyVersion;
+ }
+ auto file =
+ ProfileUtils::parseJsonFile(QFileInfo(jsonFilePath), false);
+ // fix uid
+ file->uid = uid;
+ // if version is missing, add it from the outside.
+ if (file->version.isEmpty()) {
+ file->version = intendedVersion;
+ }
+ // if this is a dependency (LWJGL), mark it also as volatile
+ if (asDependency) {
+ file->m_volatile = true;
+ }
+ // insert requirements if needed
+ if (!req.uid.isEmpty()) {
+ file->requirements.insert(req);
+ }
+ // insert conflicts if needed
+ if (!conflict.uid.isEmpty()) {
+ file->conflicts.insert(conflict);
+ }
+ // FIXME: @QUALITY do not ignore return value
+ ProfileUtils::saveJsonFile(
+ OneSixVersionFormat::versionFileToJson(file), jsonFilePath);
+ component = new Component(this, uid, file);
+ component->m_version = intendedVersion;
+ } else if (!intendedVersion.isEmpty()) {
+ auto metaVersion =
+ APPLICATION->metadataIndex()->get(uid, intendedVersion);
+ component = new Component(this, metaVersion);
+ } else {
+ return;
+ }
+ component->m_dependencyOnly = asDependency;
+ component->m_important = !asDependency;
+ components.append(component);
+ };
+ // TODO: insert depends and conflicts here if these are customized files...
+ Meta::Require reqLwjgl;
+ reqLwjgl.uid = "org.lwjgl";
+ reqLwjgl.suggests = "2.9.1";
+ Meta::Require conflictLwjgl3;
+ conflictLwjgl3.uid = "org.lwjgl3";
+ Meta::Require nullReq;
+ addBuiltinPatch("org.lwjgl", true, "2.9.1", nullReq, conflictLwjgl3);
+ addBuiltinPatch("net.minecraft", false, QString(), reqLwjgl, nullReq);
+
+ // first, collect all other file-based patches and load them
+ QMap<QString, ComponentPtr> loadedComponents;
+ QDir patchesDir(FS::PathCombine(d->m_instance->instanceRoot(), "patches"));
+ for (auto info :
+ patchesDir.entryInfoList(QStringList() << "*.json", QDir::Files)) {
+ // parse the file
+ qDebug() << "Reading" << info.fileName();
+ auto file = ProfileUtils::parseJsonFile(info, true);
+
+ // correct missing or wrong uid based on the file name
+ QString uid = info.completeBaseName();
+
+ // ignore builtins, they've been handled already
+ if (uid == "net.minecraft")
+ continue;
+ if (uid == "org.lwjgl")
+ continue;
+
+ // handle horrible corner cases
+ if (uid.isEmpty()) {
+ // if you have a file named '.json', make it just go away.
+ // FIXME: @QUALITY do not ignore return value
+ QFile::remove(info.absoluteFilePath());
+ continue;
+ }
+ file->uid = uid;
+ // FIXME: @QUALITY do not ignore return value
+ ProfileUtils::saveJsonFile(OneSixVersionFormat::versionFileToJson(file),
+ info.absoluteFilePath());
+
+ auto component = new Component(this, file->uid, file);
+ auto version = d->getOldConfigVersion(file->uid);
+ if (!version.isEmpty()) {
+ component->m_version = version;
+ }
+ loadedComponents[file->uid] = component;
+ }
+ // try to load the other 'hardcoded' patches (forge, liteloader), if they
+ // weren't loaded from files
+ auto loadSpecial = [&](const QString& uid, int order) {
+ auto patchVersion = d->getOldConfigVersion(uid);
+ if (!patchVersion.isEmpty() && !loadedComponents.contains(uid)) {
+ auto patch = new Component(
+ this, APPLICATION->metadataIndex()->get(uid, patchVersion));
+ patch->setOrder(order);
+ loadedComponents[uid] = patch;
+ }
+ };
+ loadSpecial("net.minecraftforge", 5);
+ loadSpecial("com.mumfrey.liteloader", 10);
+
+ // load the old order.json file, if present
+ ProfileUtils::PatchOrder userOrder;
+ ProfileUtils::readOverrideOrders(
+ FS::PathCombine(d->m_instance->instanceRoot(), "order.json"),
+ userOrder);
+
+ // now add all the patches by user sort order
+ for (auto uid : userOrder) {
+ // ignore builtins
+ if (uid == "net.minecraft")
+ continue;
+ if (uid == "org.lwjgl")
+ continue;
+ // ordering has a patch that is gone?
+ if (!loadedComponents.contains(uid)) {
+ continue;
+ }
+ components.append(loadedComponents.take(uid));
+ }
+
+ // is there anything left to sort? - this is used when there are leftover
+ // components that aren't part of the order.json
+ if (!loadedComponents.isEmpty()) {
+ // inserting into multimap by order number as key sorts the patches and
+ // detects duplicates
+ QMultiMap<int, ComponentPtr> files;
+ auto iter = loadedComponents.begin();
+ while (iter != loadedComponents.end()) {
+ files.insert((*iter)->getOrder(), *iter);
+ iter++;
+ }
+
+ // then just extract the patches and put them in the list
+ for (auto order : files.keys()) {
+ const auto& values = files.values(order);
+ for (auto& value : values) {
+ // TODO: put back the insertion of problem messages here, so the
+ // user knows about the id duplication
+ components.append(value);
+ }
+ }
+ }
+ // new we have a complete list of components...
+ return savePackProfile(componentsFilePath(), components);
+}
+
+// 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()) {
+ qWarning() << "Attempt to add a component with empty ID!";
+ return;
+ }
+ if (d->componentIndex.contains(id)) {
+ qWarning() << "Attempt to add a component that is already present!";
+ return;
+ }
+ beginInsertRows(QModelIndex(), index, 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) {
+ qWarning()
+ << "PackProfile got dataChenged 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++;
+ }
+ qWarning() << "PackProfile got dataChenged signal from a Component which "
+ "does not belong to it!";
+}
+
+bool PackProfile::remove(const int index)
+{
+ auto patch = getComponent(index);
+ if (!patch->isRemovable()) {
+ qWarning() << "Patch" << patch->getID() << "is non-removable";
+ return false;
+ }
+
+ if (!removeComponent_internal(patch)) {
+ qCritical() << "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()) {
+ qDebug() << "Patch" << patch->getID() << "is not customizable";
+ return false;
+ }
+ if (!patch->customize()) {
+ qCritical() << "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()) {
+ qDebug() << "Patch" << patch->getID() << "is not revertible";
+ return false;
+ }
+ if (!patch->revert()) {
+ qCritical() << "Patch" << patch->getID() << "could not be reverted";
+ return false;
+ }
+ invalidateLaunchProfile();
+ scheduleSave();
+ return true;
+}
+
+Component* PackProfile::getComponent(const QString& id)
+{
+ auto iter = d->componentIndex.find(id);
+ if (iter == d->componentIndex.end()) {
+ return nullptr;
+ }
+ return (*iter).get();
+}
+
+Component* PackProfile::getComponent(int index)
+{
+ if (index < 0 || index >= d->components.size()) {
+ return nullptr;
+ }
+ return d->components[index].get();
+}
+
+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: {
+ switch (column) {
+ case NameColumn: {
+ return patch->isEnabled() ? Qt::Checked : Qt::Unchecked;
+ }
+ default:
+ 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: {
+ switch (column) {
+ case NameColumn: {
+ auto severity = patch->getProblemSeverity();
+ switch (severity) {
+ case ProblemSeverity::Warning:
+ return "warning";
+ case ProblemSeverity::Error:
+ return "error";
+ default:
+ return QVariant();
+ }
+ }
+ default: {
+ return QVariant();
+ }
+ }
+ }
+ }
+ return QVariant();
+}
+
+bool PackProfile::setData(const QModelIndex& index, const QVariant& value,
+ int role)
+{
+ if (!index.isValid() || index.row() < 0 || index.row() >= rowCount(index)) {
+ 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();
+}
+
+// FIXME: zero precision mess
+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);
+ // TODO: this will need fine-tuning later...
+ if (patch->canBeDisabled() && !d->interactionDisabled) {
+ outFlags |= Qt::ItemIsUserCheckable;
+ }
+ return outFlags;
+}
+
+int PackProfile::rowCount(const QModelIndex& parent) const
+{
+ return d->components.size();
+}
+
+int PackProfile::columnCount(const QModelIndex& parent) const
+{
+ return 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::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)) {
+ qCritical() << "Error opening" << file.fileName()
+ << "for reading:" << file.errorString();
+ return false;
+ }
+ file.write(OneSixVersionFormat::versionFileToJson(f).toJson());
+ file.close();
+
+ appendComponent(new 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()) {
+ qCritical() << "File" << fileName << "could not be removed because:"
+ << patchFile.errorString();
+ return false;
+ }
+ }
+
+ // FIXME: we need a generic way of removing local resources, not just jar
+ // mods...
+ auto preRemoveJarMod = [&](LibraryPtr jarMod) -> bool {
+ if (!jarMod->isLocal()) {
+ return true;
+ }
+ QStringList jar, temp1, temp2, temp3;
+ jarMod->getApplicableFiles(currentSystem, jar, temp1, temp2, temp3,
+ d->m_instance->jarmodsPath().absolutePath());
+ QFileInfo finfo(jar[0]);
+ if (finfo.exists()) {
+ QFile jarModFile(jar[0]);
+ if (!jarModFile.remove()) {
+ qCritical()
+ << "File" << jar[0] << "could not be removed because:"
+ << jarModFile.errorString();
+ return false;
+ }
+ return true;
+ }
+ return true;
+ };
+
+ auto vFile = patch->getVersionFile();
+ if (vFile) {
+ auto& jarMods = vFile->jarMods;
+ for (auto& jarmod : jarMods) {
+ ok &= preRemoveJarMod(jarmod);
+ }
+ }
+ return ok;
+}
+
+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);
+ auto uuid = QUuid::createUuid();
+ QString id = uuid.toString().remove('{').remove('}');
+ QString target_filename = id + ".jar";
+ QString target_id = "org.projecttick.jarmod." + id;
+ QString target_name = sourceInfo.completeBaseName() + " (jar mod)";
+ QString finalPath =
+ FS::PathCombine(d->m_instance->jarModsDir(), target_filename);
+
+ QFileInfo targetInfo(finalPath);
+ if (targetInfo.exists()) {
+ return false;
+ }
+
+ 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("org.projecttick.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)) {
+ qCritical() << "Error opening" << file.fileName()
+ << "for reading:" << file.errorString();
+ return false;
+ }
+ file.write(OneSixVersionFormat::versionFileToJson(f).toJson());
+ file.close();
+
+ appendComponent(new 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("org.projecttick: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 (!QFile::remove(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)) {
+ qCritical() << "Error opening" << file.fileName()
+ << "for reading:" << file.errorString();
+ return false;
+ }
+ file.write(OneSixVersionFormat::versionFileToJson(f).toJson());
+ file.close();
+
+ appendComponent(new Component(this, f->uid, f));
+
+ 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) {
+ qDebug() << "Applying" << file->getID()
+ << (file->getProblemSeverity() ==
+ ProblemSeverity::Error
+ ? "ERROR"
+ : "GOOD");
+ file->applyTo(profile.get());
+ }
+ d->m_profile = profile;
+ } catch (const Exception& error) {
+ qWarning() << "Couldn't apply profile patches because: "
+ << error.cause();
+ }
+ }
+ return d->m_profile;
+}
+
+void PackProfile::setOldConfigVersion(const QString& uid,
+ const QString& version)
+{
+ if (version.isEmpty()) {
+ return;
+ }
+ d->m_oldConfigVersions[uid] = version;
+}
+
+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()) {
+ component->setVersion(version);
+ component->setImportant(important);
+ return true;
+ }
+ return false;
+ } else {
+ // add new
+ auto component = new 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));
+ }
+ }
+}
diff --git a/meshmc/launcher/minecraft/PackProfile.h b/meshmc/launcher/minecraft/PackProfile.h
new file mode 100644
index 0000000000..cba387d461
--- /dev/null
+++ b/meshmc/launcher/minecraft/PackProfile.h
@@ -0,0 +1,178 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * 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 <QString>
+#include <QList>
+#include <memory>
+
+#include "Library.h"
+#include "LaunchProfile.h"
+#include "Component.h"
+#include "ProfileUtils.h"
+#include "BaseVersion.h"
+#include "MojangDownloadInfo.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 };
+
+ 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);
+
+ enum MoveDirection { MoveUp, MoveDown };
+ /// move component file # up or down the list
+ void move(const int index, const MoveDirection direction);
+
+ /// remove component file # - including files/records
+ bool remove(const 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
+ void 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();
+
+ signals:
+ void minecraftChanged();
+
+ public:
+ /// get the profile component by id
+ Component* getComponent(const QString& id);
+
+ /// get the profile component by index
+ Component* getComponent(int index);
+
+ /// Add the component to the internal list of patches
+ // todo(merged): is this the best approach
+ void appendComponent(ComponentPtr component);
+
+ private:
+ void scheduleSave();
+ bool saveIsScheduled() const;
+
+ /// apply the component patches. Catches all the errors and returns
+ /// true/false for success/failure
+ void invalidateLaunchProfile();
+
+ /// 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:
+ bool load();
+ bool installJarMods_internal(QStringList filepaths);
+ bool installCustomJar_internal(QString filepath);
+ bool removeComponent_internal(ComponentPtr patch);
+
+ bool migratePreComponentConfig();
+
+ private: /* data */
+ std::unique_ptr<PackProfileData> d;
+};
diff --git a/meshmc/launcher/minecraft/PackProfile_p.h b/meshmc/launcher/minecraft/PackProfile_p.h
new file mode 100644
index 0000000000..b3dedc64a3
--- /dev/null
+++ b/meshmc/launcher/minecraft/PackProfile_p.h
@@ -0,0 +1,61 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "Component.h"
+#include <map>
+#include <QTimer>
+#include <QList>
+#include <QMap>
+
+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;
+
+ // version information migrated from instance.cfg file. Single use on
+ // migration!
+ std::map<QString, QString> m_oldConfigVersions;
+ QString getOldConfigVersion(const QString& uid) const
+ {
+ const auto iter = m_oldConfigVersions.find(uid);
+ if (iter != m_oldConfigVersions.cend()) {
+ return (*iter).second;
+ }
+ return QString();
+ }
+
+ // 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/meshmc/launcher/minecraft/ParseUtils.cpp b/meshmc/launcher/minecraft/ParseUtils.cpp
new file mode 100644
index 0000000000..3a25cd684c
--- /dev/null
+++ b/meshmc/launcher/minecraft/ParseUtils.cpp
@@ -0,0 +1,55 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <QDateTime>
+#include <QString>
+#include "ParseUtils.h"
+#include <QDebug>
+#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/meshmc/launcher/minecraft/ParseUtils.h b/meshmc/launcher/minecraft/ParseUtils.h
new file mode 100644
index 0000000000..f2a4ba717a
--- /dev/null
+++ b/meshmc/launcher/minecraft/ParseUtils.h
@@ -0,0 +1,30 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+#include <QString>
+#include <QDateTime>
+
+/// 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/meshmc/launcher/minecraft/ParseUtils_test.cpp b/meshmc/launcher/minecraft/ParseUtils_test.cpp
new file mode 100644
index 0000000000..413e6d9b40
--- /dev/null
+++ b/meshmc/launcher/minecraft/ParseUtils_test.cpp
@@ -0,0 +1,57 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <QTest>
+#include "TestUtil.h"
+
+#include "minecraft/ParseUtils.h"
+
+class ParseUtilsTest : public QObject
+{
+ Q_OBJECT
+ private slots:
+ void test_Through_data()
+ {
+ QTest::addColumn<QString>("timestamp");
+ const char* timestamps[] = {
+ "2016-02-29T13:49:54+01:00", "2016-02-26T15:21:11+00:01",
+ "2016-02-24T15:52:36+01:13", "2016-02-18T17:41:00+00:00",
+ "2016-02-17T15:23:19+00:00", "2016-02-16T15:22:39+09:22",
+ "2016-02-10T15:06:41+00:00", "2016-02-04T15:28:02-05:33"};
+ for (unsigned i = 0; i < (sizeof(timestamps) / sizeof(const char*));
+ i++) {
+ QTest::newRow(timestamps[i]) << QString(timestamps[i]);
+ }
+ }
+ void test_Through()
+ {
+ QFETCH(QString, timestamp);
+
+ auto time_parsed = timeFromS3Time(timestamp);
+ auto time_serialized = timeToS3Time(time_parsed);
+
+ QCOMPARE(time_serialized, timestamp);
+ }
+};
+
+QTEST_GUILESS_MAIN(ParseUtilsTest)
+
+#include "ParseUtils_test.moc"
diff --git a/meshmc/launcher/minecraft/ProfileUtils.cpp b/meshmc/launcher/minecraft/ProfileUtils.cpp
new file mode 100644
index 0000000000..2bc6650ed5
--- /dev/null
+++ b/meshmc/launcher/minecraft/ProfileUtils.cpp
@@ -0,0 +1,204 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "ProfileUtils.h"
+#include "minecraft/VersionFilterData.h"
+#include "minecraft/OneSixVersionFormat.h"
+#include "Json.h"
+#include <QDebug>
+
+#include <QJsonDocument>
+#include <QJsonArray>
+#include <QRegularExpression>
+#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 overriden 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 overriden 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 (const JSONValidationError& err) {
+ qCritical() << "Couldn't parse" << orderFile.fileName()
+ << ": bad file format";
+ qWarning() << "Ignoring overriden 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;
+ }
+
+ VersionFilePtr parseBinaryJsonFile(const QFileInfo& fileInfo)
+ {
+ 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);
+ }
+ QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
+ file.close();
+ if (doc.isNull()) {
+ file.remove();
+ throw JSONValidationError(
+ QObject::tr("Unable to process the version file %1.")
+ .arg(fileInfo.fileName()));
+ }
+ return guardedParseJson(doc, fileInfo.completeBaseName(),
+ fileInfo.absoluteFilePath(), false);
+ }
+
+ 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/meshmc/launcher/minecraft/ProfileUtils.h b/meshmc/launcher/minecraft/ProfileUtils.h
new file mode 100644
index 0000000000..1b285f079d
--- /dev/null
+++ b/meshmc/launcher/minecraft/ProfileUtils.h
@@ -0,0 +1,50 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+#include "Library.h"
+#include "VersionFile.h"
+
+namespace ProfileUtils
+{
+ typedef QStringList PatchOrder;
+
+ /// 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,
+ const bool requireOrder);
+
+ /// Save a JSON file (in any format)
+ bool saveJsonFile(const QJsonDocument doc, const QString& filename);
+
+ /// Parse a version file in binary JSON format
+ VersionFilePtr parseBinaryJsonFile(const QFileInfo& fileInfo);
+
+ /// Remove LWJGL from a patch file. This is applied to all Mojang-like
+ /// profile files.
+ void removeLwjglFromPatch(VersionFilePtr patch);
+
+} // namespace ProfileUtils
diff --git a/meshmc/launcher/minecraft/Rule.cpp b/meshmc/launcher/minecraft/Rule.cpp
new file mode 100644
index 0000000000..c77193790f
--- /dev/null
+++ b/meshmc/launcher/minecraft/Rule.cpp
@@ -0,0 +1,114 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * 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 <QJsonObject>
+#include <QJsonArray>
+
+#include "Rule.h"
+
+RuleAction RuleAction_fromString(QString name)
+{
+ if (name == "allow")
+ return Allow;
+ if (name == "disallow")
+ return Disallow;
+ return Defer;
+}
+
+QList<std::shared_ptr<Rule>> rulesFromJsonV4(const QJsonObject& objectWithRules)
+{
+ QList<std::shared_ptr<Rule>> rules;
+ auto rulesVal = objectWithRules.value("rules");
+ if (!rulesVal.isArray())
+ return rules;
+
+ QJsonArray ruleList = rulesVal.toArray();
+ for (auto ruleVal : ruleList) {
+ std::shared_ptr<Rule> rule;
+ if (!ruleVal.isObject())
+ continue;
+ auto ruleObj = ruleVal.toObject();
+ auto actionVal = ruleObj.value("action");
+ if (!actionVal.isString())
+ continue;
+ auto action = RuleAction_fromString(actionVal.toString());
+ if (action == Defer)
+ continue;
+
+ auto osVal = ruleObj.value("os");
+ if (!osVal.isObject()) {
+ // add a new implicit action rule
+ rules.append(ImplicitRule::create(action));
+ continue;
+ }
+
+ auto osObj = osVal.toObject();
+ auto osNameVal = osObj.value("name");
+ if (!osNameVal.isString())
+ continue;
+ OpSys requiredOs = OpSys_fromString(osNameVal.toString());
+ QString versionRegex = osObj.value("version").toString();
+ // add a new OS rule
+ rules.append(OsRule::create(action, requiredOs, versionRegex));
+ }
+ return rules;
+}
+
+QJsonObject ImplicitRule::toJson()
+{
+ QJsonObject ruleObj;
+ ruleObj.insert("action",
+ m_result == Allow ? QString("allow") : QString("disallow"));
+ return ruleObj;
+}
+
+QJsonObject OsRule::toJson()
+{
+ QJsonObject ruleObj;
+ ruleObj.insert("action",
+ m_result == Allow ? QString("allow") : QString("disallow"));
+ QJsonObject osObj;
+ {
+ osObj.insert("name", OpSys_toString(m_system));
+ if (!m_version_regexp.isEmpty()) {
+ osObj.insert("version", m_version_regexp);
+ }
+ }
+ ruleObj.insert("os", osObj);
+ return ruleObj;
+}
diff --git a/meshmc/launcher/minecraft/Rule.h b/meshmc/launcher/minecraft/Rule.h
new file mode 100644
index 0000000000..fa36b73b94
--- /dev/null
+++ b/meshmc/launcher/minecraft/Rule.h
@@ -0,0 +1,117 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * 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 <QString>
+#include <QList>
+#include <QJsonObject>
+#include <memory>
+#include "OpSys.h"
+
+class Library;
+class Rule;
+
+enum RuleAction { Allow, Disallow, Defer };
+
+QList<std::shared_ptr<Rule>>
+rulesFromJsonV4(const QJsonObject& objectWithRules);
+
+class Rule
+{
+ protected:
+ RuleAction m_result;
+ virtual bool applies(const Library* parent) = 0;
+
+ public:
+ Rule(RuleAction result) : m_result(result) {}
+ virtual ~Rule() {};
+ virtual QJsonObject toJson() = 0;
+ RuleAction apply(const Library* parent)
+ {
+ if (applies(parent))
+ return m_result;
+ else
+ return Defer;
+ }
+};
+
+class OsRule : public Rule
+{
+ private:
+ // the OS
+ OpSys m_system;
+ // the OS version regexp
+ QString m_version_regexp;
+
+ protected:
+ virtual bool applies(const Library*)
+ {
+ return (m_system == currentSystem);
+ }
+ OsRule(RuleAction result, OpSys system, QString version_regexp)
+ : Rule(result), m_system(system), m_version_regexp(version_regexp)
+ {
+ }
+
+ public:
+ virtual QJsonObject toJson();
+ static std::shared_ptr<OsRule> create(RuleAction result, OpSys system,
+ QString version_regexp)
+ {
+ return std::shared_ptr<OsRule>(
+ new OsRule(result, system, version_regexp));
+ }
+};
+
+class ImplicitRule : public Rule
+{
+ protected:
+ virtual bool applies(const Library*)
+ {
+ return true;
+ }
+ ImplicitRule(RuleAction result) : Rule(result) {}
+
+ public:
+ virtual QJsonObject toJson();
+ static std::shared_ptr<ImplicitRule> create(RuleAction result)
+ {
+ return std::shared_ptr<ImplicitRule>(new ImplicitRule(result));
+ }
+};
diff --git a/meshmc/launcher/minecraft/VersionFile.cpp b/meshmc/launcher/minecraft/VersionFile.cpp
new file mode 100644
index 0000000000..59fb779b28
--- /dev/null
+++ b/meshmc/launcher/minecraft/VersionFile.cpp
@@ -0,0 +1,80 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <QJsonArray>
+#include <QJsonDocument>
+
+#include <QDebug>
+
+#include "minecraft/VersionFile.h"
+#include "minecraft/Library.h"
+#include "minecraft/PackProfile.h"
+#include "ParseUtils.h"
+
+#include <Version.h>
+
+static bool isMinecraftVersion(const QString& uid)
+{
+ return uid == "net.minecraft";
+}
+
+void VersionFile::applyTo(LaunchProfile* profile)
+{
+ // Only real Minecraft can set those. Don't let anything override them.
+ if (isMinecraftVersion(uid)) {
+ profile->applyMinecraftVersion(minecraftVersion);
+ 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->applyTweakers(addTweakers);
+ profile->applyJarMods(jarMods);
+ profile->applyMods(mods);
+ profile->applyTraits(traits);
+
+ for (auto library : libraries) {
+ profile->applyLibrary(library);
+ }
+ for (auto mavenFile : mavenFiles) {
+ profile->applyMavenFile(mavenFile);
+ }
+ profile->applyProblemSeverity(getProblemSeverity());
+}
+
+/*
+ auto theirVersion = profile->getMinecraftVersion();
+ if (!theirVersion.isNull() && !dependsOnMinecraftVersion.isNull())
+ {
+ if (QRegExp(dependsOnMinecraftVersion, Qt::CaseInsensitive,
+ QRegExp::Wildcard).indexIn(theirVersion) == -1)
+ {
+ throw MinecraftVersionMismatch(uid, dependsOnMinecraftVersion,
+ theirVersion);
+ }
+ }
+*/
diff --git a/meshmc/launcher/minecraft/VersionFile.h b/meshmc/launcher/minecraft/VersionFile.h
new file mode 100644
index 0000000000..fdca5e54c6
--- /dev/null
+++ b/meshmc/launcher/minecraft/VersionFile.h
@@ -0,0 +1,141 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <QString>
+#include <QStringList>
+#include <QDateTime>
+#include <QSet>
+
+#include <memory>
+#include "minecraft/OpSys.h"
+#include "minecraft/Rule.h"
+#include "ProblemProvider.h"
+#include "Library.h"
+#include <meta/JsonFormat.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);
+
+ public: /* data */
+ /// MeshMC: order hint for this version file if no explicit order is set
+ int order = 0;
+
+ /// MeshMC: human readable name of this package
+ QString name;
+
+ /// MeshMC: package ID of this package
+ QString uid;
+
+ /// MeshMC: version of this package
+ QString version;
+
+ /// MeshMC: DEPRECATED dependency on a Minecraft version
+ QString dependsOnMinecraftVersion;
+
+ /// Mojang: DEPRECATED used to version the Mojang version format
+ int minimumMeshMCVersion = -1;
+
+ /// Mojang: DEPRECATED version of Minecraft this is
+ QString minecraftVersion;
+
+ /// Mojang: class to launch Minecraft with
+ QString mainClass;
+
+ /// MeshMC: 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;
+
+ /// 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;
+
+ /// MeshMC: list of tweaker mod arguments for launchwrapper
+ QStringList addTweakers;
+
+ /// Mojang: list of libraries to add to the version
+ QList<LibraryPtr> libraries;
+
+ /// MeshMC: list of maven files to put in the libraries folder, but not in
+ /// classpath
+ QList<LibraryPtr> mavenFiles;
+
+ /// The main jar (Minecraft version library, normally)
+ LibraryPtr mainJar;
+
+ /// MeshMC: list of attached traits of this version file - used to enable
+ /// features
+ QSet<QString> traits;
+
+ /// MeshMC: list of jar mods added to this version
+ QList<LibraryPtr> jarMods;
+
+ /// MeshMC: list of mods added to this version
+ QList<LibraryPtr> mods;
+
+ /**
+ * MeshMC: set of packages this depends on
+ * NOTE: this is shared with the meta format!!!
+ */
+ Meta::RequireSet requirements;
+
+ /**
+ * MeshMC: set of packages this conflicts with
+ * NOTE: this is shared with the meta format!!!
+ */
+ Meta::RequireSet conflicts;
+
+ /// is volatile -- may be removed as soon as it is no longer needed by
+ /// something else
+ bool m_volatile = false;
+
+ 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/meshmc/launcher/minecraft/VersionFilterData.cpp b/meshmc/launcher/minecraft/VersionFilterData.cpp
new file mode 100644
index 0000000000..6fb146b690
--- /dev/null
+++ b/meshmc/launcher/minecraft/VersionFilterData.cpp
@@ -0,0 +1,98 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "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"});
+
+ // FIXME: remove, used for deciding when core mods should display
+ 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");
+ java21BeginsDate = timeFromS3Time("2024-04-03T00:00:00+00:00");
+ java25BeginsDate = timeFromS3Time("2025-10-15T00:00:00+00:00");
+}
diff --git a/meshmc/launcher/minecraft/VersionFilterData.h b/meshmc/launcher/minecraft/VersionFilterData.h
new file mode 100644
index 0000000000..7cd22f4683
--- /dev/null
+++ b/meshmc/launcher/minecraft/VersionFilterData.h
@@ -0,0 +1,54 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+#include <QMap>
+#include <QString>
+#include <QSet>
+#include <QDateTime>
+
+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;
+ // release date of first version to require Java 21 (24w14a / 1.20.5)
+ QDateTime java21BeginsDate;
+ // release date of first version to require Java 25
+ QDateTime java25BeginsDate;
+};
+extern VersionFilterData g_VersionFilterData;
diff --git a/meshmc/launcher/minecraft/World.cpp b/meshmc/launcher/minecraft/World.cpp
new file mode 100644
index 0000000000..4ae59afaf5
--- /dev/null
+++ b/meshmc/launcher/minecraft/World.cpp
@@ -0,0 +1,478 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * 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.
+ */
+
+#include <QDir>
+#include <QString>
+#include <QDebug>
+#include <QSaveFile>
+#include "World.h"
+
+#include "GZip.h"
+#include <MMCZip.h>
+#include <FileSystem.h>
+#include <sstream>
+#include <io/stream_reader.h>
+#include <tag_string.h>
+#include <tag_primitive.h>
+
+#include <QCoreApplication>
+
+#include <nonstd/optional>
+
+using nonstd::nullopt;
+using nonstd::optional;
+
+GameType::GameType(nonstd::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 getLevelDatFromFS(const QFileInfo& file)
+{
+ QDir worldDir(file.filePath());
+ if (!file.isDir() || !worldDir.exists("level.dat")) {
+ return QString();
+ }
+ return worldDir.absoluteFilePath("level.dat");
+}
+
+QByteArray getLevelDatDataFromFS(const QFileInfo& file)
+{
+ auto fullFilePath = getLevelDatFromFS(file);
+ if (fullFilePath.isNull()) {
+ return QByteArray();
+ }
+ QFile f(fullFilePath);
+ if (!f.open(QIODevice::ReadOnly)) {
+ return QByteArray();
+ }
+ return f.readAll();
+}
+
+bool putLevelDatDataToFS(const QFileInfo& file, QByteArray& data)
+{
+ auto fullFilePath = getLevelDatFromFS(file);
+ if (fullFilePath.isNull()) {
+ return false;
+ }
+ QSaveFile 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;
+}
+
+void World::readFromFS(const QFileInfo& file)
+{
+ auto bytes = getLevelDatDataFromFS(file);
+ if (bytes.isEmpty()) {
+ is_valid = false;
+ return;
+ }
+ loadFromLevelDat(bytes);
+ levelDatTime = file.lastModified();
+}
+
+void World::readFromZip(const QFileInfo& file)
+{
+ QString zipPath = file.absoluteFilePath();
+ auto location = MMCZip::findFolderOfFileInZip(zipPath, "level.dat");
+ is_valid = !location.isEmpty();
+ if (!is_valid) {
+ return;
+ }
+ m_containerOffsetPath = location;
+ QByteArray levelDatData =
+ MMCZip::readFileFromZip(zipPath, location + "level.dat");
+ is_valid = !levelDatData.isEmpty();
+ if (!is_valid) {
+ return;
+ }
+ levelDatTime = MMCZip::getEntryModTime(zipPath, location + "level.dat");
+ loadFromLevelDat(levelDatData);
+}
+
+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()) {
+ auto result = MMCZip::extractSubDir(m_containerFile.absoluteFilePath(),
+ m_containerOffsetPath, finalPath);
+ ok = result.has_value();
+ } else if (m_containerFile.isDir()) {
+ QString from = m_containerFile.filePath();
+ ok = FS::copy(from, finalPath)();
+ }
+
+ if (ok && !name.isEmpty() && m_actualName != name) {
+ World newWorld{QFileInfo(finalPath)};
+ 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 (const std::out_of_range& e) {
+ // fallback for old world formats
+ qWarning() << "String NBT tag" << name << "could not be found.";
+ return nullopt;
+ } catch (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 (const std::out_of_range& e) {
+ // fallback for old world formats
+ qWarning() << "Long NBT tag" << name << "could not be found.";
+ return nullopt;
+ } catch (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 (const std::out_of_range& e) {
+ // fallback for old world formats
+ qWarning() << "Int NBT tag" << name << "could not be found.";
+ return nullopt;
+ } catch (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
+
+void World::loadFromLevelDat(QByteArray data)
+{
+ auto levelData = parseLevelDat(data);
+ if (!levelData) {
+ is_valid = 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();
+ is_valid = false;
+ return;
+ }
+ nbt::value& val = *valPtr;
+
+ is_valid = val.get_type() == nbt::tag_type::Compound;
+ if (!is_valid)
+ 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) : 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() << "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 (!is_valid)
+ return false;
+ 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 is_valid == other.is_valid && folderName() == other.folderName();
+}
diff --git a/meshmc/launcher/minecraft/World.h b/meshmc/launcher/minecraft/World.h
new file mode 100644
index 0000000000..b873a8e884
--- /dev/null
+++ b/meshmc/launcher/minecraft/World.h
@@ -0,0 +1,133 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * 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 <QFileInfo>
+#include <QDateTime>
+#include <nonstd/optional>
+
+struct GameType {
+ GameType() = default;
+ GameType(nonstd::optional<int> original);
+
+ QString toTranslatedString() const;
+ QString toLogString() const;
+
+ enum {
+ Unknown = -1,
+ Survival = 0,
+ Creative,
+ Adventure,
+ Spectator
+ } type = Unknown;
+ nonstd::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;
+ }
+ QDateTime lastPlayed() const
+ {
+ return m_lastPlayed;
+ }
+ GameType gameType() const
+ {
+ return m_gameType;
+ }
+ int64_t seed() const
+ {
+ return m_randomSeed;
+ }
+ bool isValid() const
+ {
+ return is_valid;
+ }
+ 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());
+
+ // WEAK compare operator - used for replacing worlds
+ bool operator==(const World& other) const;
+
+ 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 levelDatTime;
+ QDateTime m_lastPlayed;
+ int64_t m_randomSeed = 0;
+ GameType m_gameType;
+ bool is_valid = false;
+};
diff --git a/meshmc/launcher/minecraft/WorldList.cpp b/meshmc/launcher/minecraft/WorldList.cpp
new file mode 100644
index 0000000000..b09b2423b6
--- /dev/null
+++ b/meshmc/launcher/minecraft/WorldList.cpp
@@ -0,0 +1,380 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * 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.
+ */
+
+#include "WorldList.h"
+#include <FileSystem.h>
+#include <QMimeData>
+#include <QUrl>
+#include <QUuid>
+#include <QString>
+#include <QFileSystemWatcher>
+#include <QDebug>
+
+WorldList::WorldList(const QString& dir) : QAbstractListModel(), 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);
+ is_watching = false;
+ connect(m_watcher, SIGNAL(directoryChanged(QString)), this,
+ SLOT(directoryChanged(QString)));
+}
+
+void WorldList::startWatching()
+{
+ if (is_watching) {
+ return;
+ }
+ update();
+ is_watching = m_watcher->addPath(m_dir.absolutePath());
+ if (is_watching) {
+ qDebug() << "Started watching " << m_dir.absolutePath();
+ } else {
+ qDebug() << "Failed to start watching " << m_dir.absolutePath();
+ }
+}
+
+void WorldList::stopWatching()
+{
+ if (!is_watching) {
+ return;
+ }
+ is_watching = !m_watcher->removePath(m_dir.absolutePath());
+ if (!is_watching) {
+ 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();
+ worlds.swap(newWorlds);
+ endResetModel();
+ return true;
+}
+
+void WorldList::directoryChanged(QString path)
+{
+ update();
+}
+
+bool WorldList::isValid()
+{
+ return m_dir.exists() && m_dir.isReadable();
+}
+
+bool WorldList::deleteWorld(int index)
+{
+ if (index >= worlds.size() || index < 0)
+ return false;
+ World& m = worlds[index];
+ if (m.destroy()) {
+ beginRemoveRows(QModelIndex(), index, index);
+ 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 = worlds[i];
+ m.destroy();
+ }
+ beginRemoveRows(QModelIndex(), first, last);
+ worlds.erase(worlds.begin() + first, worlds.begin() + last + 1);
+ endRemoveRows();
+ emit changed();
+ return true;
+}
+
+bool WorldList::resetIcon(int row)
+{
+ if (row >= worlds.size() || row < 0)
+ return false;
+ World& 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 3;
+}
+
+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 >= worlds.size())
+ return QVariant();
+
+ auto& world = 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();
+
+ default:
+ return QVariant();
+ }
+
+ case Qt::ToolTipRole: {
+ 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 IconFileRole: {
+ return world.iconFile();
+ }
+ default:
+ return QVariant();
+ }
+}
+
+QVariant WorldList::headerData(int section, 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");
+ 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.");
+ default:
+ return QVariant();
+ }
+ default:
+ return QVariant();
+ }
+ return QVariant();
+}
+
+QStringList WorldList::mimeTypes() const
+{
+ QStringList types;
+ types << "text/uri-list";
+ return types;
+}
+
+class WorldMimeData : public QMimeData
+{
+ Q_OBJECT
+
+ public:
+ WorldMimeData(QList<World> worlds)
+ {
+ m_worlds = worlds;
+ }
+ QStringList formats() const override
+ {
+ return QMimeData::formats() << "text/uri-list";
+ }
+
+ protected:
+ QVariant retrieveData(const QString& mimetype,
+ QMetaType type) const override
+ {
+ QList<QUrl> urls;
+ for (auto& world : m_worlds) {
+ if (!world.isValid() || !world.isOnFS())
+ continue;
+ QString worldPath = world.container().absoluteFilePath();
+ qDebug() << worldPath;
+ urls.append(QUrl::fromLocalFile(worldPath));
+ }
+ const_cast<WorldMimeData*>(this)->setUrls(urls);
+ return QMimeData::retrieveData(mimetype, type);
+ }
+
+ private:
+ QList<World> m_worlds;
+};
+
+QMimeData* WorldList::mimeData(const QModelIndexList& indexes) const
+{
+ if (indexes.size() == 0)
+ return new QMimeData();
+
+ QList<World> worlds;
+ for (auto idx : indexes) {
+ if (idx.column() != 0)
+ continue;
+ int row = idx.row();
+ if (row < 0 || row >= this->worlds.size())
+ continue;
+ worlds.append(this->worlds[row]);
+ }
+ if (!worlds.size()) {
+ return new QMimeData();
+ }
+ return new WorldMimeData(worlds);
+}
+
+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,
+ int row, int column, 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 = is_watching;
+ 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;
+}
+
+#include "WorldList.moc"
diff --git a/meshmc/launcher/minecraft/WorldList.h b/meshmc/launcher/minecraft/WorldList.h
new file mode 100644
index 0000000000..3df0ee1a6e
--- /dev/null
+++ b/meshmc/launcher/minecraft/WorldList.h
@@ -0,0 +1,148 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * This file incorporates work covered by the following copyright and
+ * permission notice:
+ *
+ * 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 <QList>
+#include <QString>
+#include <QDir>
+#include <QAbstractListModel>
+#include <QMimeData>
+#include "minecraft/World.h"
+
+class QFileSystemWatcher;
+
+class WorldList : public QAbstractListModel
+{
+ Q_OBJECT
+ public:
+ enum Columns { NameColumn, GameModeColumn, LastPlayedColumn };
+
+ enum Roles {
+ ObjectRole = Qt::UserRole + 1,
+ FolderRole,
+ SeedRole,
+ NameRole,
+ GameModeRole,
+ LastPlayedRole,
+ IconFileRole
+ };
+
+ WorldList(const QString& dir);
+
+ virtual QVariant data(const QModelIndex& index,
+ int role = Qt::DisplayRole) const;
+
+ virtual int rowCount(const QModelIndex& parent = QModelIndex()) const
+ {
+ return 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 worlds.size();
+ };
+ bool empty() const
+ {
+ return size() == 0;
+ }
+ World& operator[](size_t index)
+ {
+ return 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;
+ }
+
+ const QList<World>& allWorlds() const
+ {
+ return worlds;
+ }
+
+ private slots:
+ void directoryChanged(QString path);
+
+ signals:
+ void changed();
+
+ protected:
+ QFileSystemWatcher* m_watcher;
+ bool is_watching;
+ QDir m_dir;
+ QList<World> worlds;
+};
diff --git a/meshmc/launcher/minecraft/auth/AccountData.cpp b/meshmc/launcher/minecraft/auth/AccountData.cpp
new file mode 100644
index 0000000000..74d67c0478
--- /dev/null
+++ b/meshmc/launcher/minecraft/auth/AccountData.cpp
@@ -0,0 +1,362 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "AccountData.h"
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonArray>
+#include <QDebug>
+#include <QUuid>
+#include <QRegularExpression>
+
+namespace
+{
+ void tokenToJSONV3(QJsonObject& parent, Katabasis::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;
+ }
+ }
+
+ Katabasis::Token tokenFromJSONV3(const QJsonObject& parent,
+ const char* tokenName)
+ {
+ Katabasis::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 = Katabasis::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.variant = variantV.toString();
+
+ // data for skin is optional
+ auto dataV = skinObj.value("data");
+ if (dataV.isString()) {
+ // TODO: validate base64
+ out.skin.data =
+ QByteArray::fromBase64(dataV.toString().toLatin1());
+ } 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.alias = aliasV.toString();
+
+ // data for cape is optional.
+ auto dataV = capeObj.value("data");
+ if (dataV.isString()) {
+ // TODO: validate base64
+ cape.data =
+ QByteArray::fromBase64(dataV.toString().toLatin1());
+ } 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 = Katabasis::Validity::Assumed;
+ return out;
+ }
+
+ void entitlementToJSONV3(QJsonObject& parent, MinecraftEntitlement p)
+ {
+ if (p.validity == Katabasis::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 = Katabasis::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 {
+ qWarning() << "Failed to parse account data: type is not recognized "
+ "(only MSA is supported).";
+ return false;
+ }
+
+ msaToken = tokenFromJSONV3(data, "msa");
+ userToken = tokenFromJSONV3(data, "utoken");
+ xboxApiToken = tokenFromJSONV3(data, "xrp-main");
+ mojangservicesToken = tokenFromJSONV3(data, "xrp-mc");
+
+ yggdrasilToken = tokenFromJSONV3(data, "ygg");
+ minecraftProfile = profileFromJSONV3(data, "profile");
+ if (!entitlementFromJSONV3(data, minecraftEntitlement)) {
+ if (minecraftProfile.validity != Katabasis::Validity::None) {
+ minecraftEntitlement.canPlayMinecraft = true;
+ minecraftEntitlement.ownsMinecraft = true;
+ minecraftEntitlement.validity = Katabasis::Validity::Assumed;
+ }
+ }
+
+ validity_ = minecraftProfile.validity;
+ return true;
+}
+
+QJsonObject AccountData::saveState() const
+{
+ QJsonObject output;
+ output["type"] = "MSA";
+ tokenToJSONV3(output, msaToken, "msa");
+ tokenToJSONV3(output, userToken, "utoken");
+ tokenToJSONV3(output, xboxApiToken, "xrp-main");
+ tokenToJSONV3(output, mojangservicesToken, "xrp-mc");
+
+ 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
+{
+ if (xboxApiToken.extra.contains("gtg")) {
+ return xboxApiToken.extra["gtg"].toString();
+ }
+ return "Xbox profile missing";
+}
+
+QString AccountData::lastError() const
+{
+ return errorString;
+}
diff --git a/meshmc/launcher/minecraft/auth/AccountData.h b/meshmc/launcher/minecraft/auth/AccountData.h
new file mode 100644
index 0000000000..9e791c568e
--- /dev/null
+++ b/meshmc/launcher/minecraft/auth/AccountData.h
@@ -0,0 +1,103 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+#include <QString>
+#include <QByteArray>
+#include <QVector>
+#include <katabasis/Bits.h>
+#include <QJsonObject>
+
+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;
+ Katabasis::Validity validity = Katabasis::Validity::None;
+};
+
+struct MinecraftProfile {
+ QString id;
+ QString name;
+ Skin skin;
+ QString currentCape;
+ QMap<QString, Cape> capes;
+ Katabasis::Validity validity = Katabasis::Validity::None;
+};
+
+enum class AccountType { MSA };
+
+enum class AccountState {
+ Unchecked,
+ Offline,
+ Working,
+ Online,
+ Errored,
+ Expired,
+ Gone
+};
+
+struct AccountData {
+ QJsonObject saveState() const;
+ bool resumeStateFromV3(QJsonObject data);
+
+ //! 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;
+
+ Katabasis::Token msaToken;
+ Katabasis::Token userToken;
+ Katabasis::Token xboxApiToken;
+ Katabasis::Token mojangservicesToken;
+
+ Katabasis::Token yggdrasilToken;
+ MinecraftProfile minecraftProfile;
+ MinecraftEntitlement minecraftEntitlement;
+ Katabasis::Validity validity_ = Katabasis::Validity::None;
+
+ // runtime only information (not saved with the account)
+ QString internalId;
+ QString errorString;
+ AccountState accountState = AccountState::Unchecked;
+};
diff --git a/meshmc/launcher/minecraft/auth/AccountList.cpp b/meshmc/launcher/minecraft/auth/AccountList.cpp
new file mode 100644
index 0000000000..f12a00815c
--- /dev/null
+++ b/meshmc/launcher/minecraft/auth/AccountList.cpp
@@ -0,0 +1,722 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * 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.h"
+#include "AccountData.h"
+#include "AccountTask.h"
+
+#include <QIODevice>
+#include <QFile>
+#include <QTextStream>
+#include <QJsonDocument>
+#include <QJsonArray>
+#include <QJsonObject>
+#include <QJsonParseError>
+#include <QDir>
+#include <QTimer>
+
+#include <QDebug>
+
+#include <FileSystem.h>
+#include <QSaveFile>
+
+#include <chrono>
+
+enum AccountListVersion { MojangOnly = 2, 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
+ if (m_accounts.contains(account)) {
+ 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) {
+ 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();
+ 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)
+ // TODO: Alert the user if this fails.
+ saveList();
+
+ emit listChanged();
+}
+
+void AccountList::onDefaultAccountChanged()
+{
+ if (m_autosave)
+ saveList();
+
+ emit defaultAccountChanged();
+}
+
+int AccountList::count() const
+{
+ return m_accounts.count();
+}
+
+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 NameColumn:
+ return account->accountDisplayString();
+
+ case TypeColumn: {
+ auto typeStr = account->typeString();
+ typeStr[0] = typeStr[0].toUpper();
+ return typeStr;
+ }
+
+ case StatusColumn: {
+ switch (account->accountState()) {
+ case AccountState::Unchecked: {
+ return tr("Unchecked", "Account status");
+ }
+ case AccountState::Offline: {
+ return tr("Offline", "Account status");
+ }
+ case AccountState::Online: {
+ return tr("Online", "Account status");
+ }
+ case AccountState::Working: {
+ return tr("Working", "Account status");
+ }
+ case AccountState::Errored: {
+ return tr("Errored", "Account status");
+ }
+ case AccountState::Expired: {
+ return tr("Expired", "Account status");
+ }
+ case AccountState::Gone: {
+ return tr("Gone", "Account status");
+ }
+ }
+ }
+
+ case ProfileNameColumn: {
+ return account->profileName();
+ }
+
+ default:
+ return QVariant();
+ }
+
+ case Qt::ToolTipRole:
+ return account->accountDisplayString();
+
+ case PointerRole:
+ return QVariant::fromValue(account);
+
+ case Qt::CheckStateRole:
+ switch (index.column()) {
+ case NameColumn:
+ return account == m_defaultAccount ? Qt::Checked
+ : Qt::Unchecked;
+ }
+
+ default:
+ return QVariant();
+ }
+}
+
+QVariant AccountList::headerData(int section, Qt::Orientation orientation,
+ int role) const
+{
+ switch (role) {
+ case Qt::DisplayRole:
+ switch (section) {
+ case NameColumn:
+ return tr("Account");
+ case TypeColumn:
+ return tr("Type");
+ case StatusColumn:
+ return tr("Status");
+ case ProfileNameColumn:
+ return tr("Profile");
+ default:
+ return QVariant();
+ }
+
+ case Qt::ToolTipRole:
+ switch (section) {
+ case NameColumn:
+ return tr("User name of the account.");
+ case TypeColumn:
+ return tr("Type of the account.");
+ case StatusColumn:
+ return tr("Current status of the account.");
+ case ProfileNameColumn:
+ return tr("Name of the Minecraft profile associated with "
+ "the account.");
+ default:
+ return QVariant();
+ }
+
+ default:
+ return QVariant();
+ }
+}
+
+int AccountList::rowCount(const QModelIndex&) const
+{
+ // Return count
+ return count();
+}
+
+int AccountList::columnCount(const QModelIndex&) const
+{
+ return NUM_COLUMNS;
+}
+
+Qt::ItemFlags AccountList::flags(const QModelIndex& index) const
+{
+ if (index.row() < 0 || index.row() >= rowCount(index) || !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) || !idx.isValid()) {
+ return false;
+ }
+
+ if (role == Qt::CheckStateRole) {
+ if (value == Qt::Checked) {
+ MinecraftAccountPtr account = at(idx.row());
+ setDefaultAccount(account);
+ }
+ }
+
+ emit dataChanged(idx, index(idx.row(), columnCount(QModelIndex()) - 1));
+ return true;
+}
+
+bool AccountList::loadList()
+{
+ if (m_listFilePath.isEmpty()) {
+ qCritical() << "Can't load 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.
+ // TODO: We should probably report this error to the user.
+ if (!file.open(QIODevice::ReadOnly)) {
+ qCritical() << QString("Failed to read the account list file (%1).")
+ .arg(m_listFilePath)
+ .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();
+ switch (listVersion) {
+ case AccountListVersion::MojangMSA: {
+ return loadV3(root);
+ } break;
+ default: {
+ 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 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.
+ // TODO: We should probably report this error to the user.
+ if (!file.open(QIODevice::WriteOnly)) {
+ qCritical() << QString("Failed to read the account list file (%1).")
+ .arg(m_listFilePath)
+ .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(), &AccountTask::succeeded, this,
+ &AccountList::authSucceeded);
+ connect(m_currentTask.get(), &AccountTask::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/meshmc/launcher/minecraft/auth/AccountList.h b/meshmc/launcher/minecraft/auth/AccountList.h
new file mode 100644
index 0000000000..2d352532a8
--- /dev/null
+++ b/meshmc/launcher/minecraft/auth/AccountList.h
@@ -0,0 +1,187 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * 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.h"
+
+#include <QObject>
+#include <QVariant>
+#include <QAbstractListModel>
+#include <QSharedPointer>
+
+/*!
+ * List of available Mojang accounts.
+ * This should be loaded in the background by MeshMC on startup.
+ */
+class AccountList : public QAbstractListModel
+{
+ Q_OBJECT
+ public:
+ enum ModelRoles { PointerRole = 0x34B1CB48 };
+
+ enum VListColumns {
+ // TODO: Add icon column.
+ NameColumn = 0,
+ ProfileNameColumn,
+ 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(const 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);
+
+ 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<AccountTask> 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/meshmc/launcher/minecraft/auth/AccountTask.cpp b/meshmc/launcher/minecraft/auth/AccountTask.cpp
new file mode 100644
index 0000000000..ddcf1918db
--- /dev/null
+++ b/meshmc/launcher/minecraft/auth/AccountTask.cpp
@@ -0,0 +1,129 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * 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 "AccountTask.h"
+#include "MinecraftAccount.h"
+
+#include <QObject>
+#include <QString>
+#include <QJsonObject>
+#include <QJsonDocument>
+#include <QNetworkReply>
+#include <QByteArray>
+
+#include <QDebug>
+
+AccountTask::AccountTask(AccountData* data, QObject* parent)
+ : Task(parent), m_data(data)
+{
+ changeState(AccountTaskState::STATE_CREATED);
+}
+
+QString AccountTask::getStateMessage() const
+{
+ switch (m_taskState) {
+ case AccountTaskState::STATE_CREATED:
+ return "Waiting...";
+ case AccountTaskState::STATE_WORKING:
+ return tr("Sending request to auth servers...");
+ case AccountTaskState::STATE_SUCCEEDED:
+ return tr("Authentication task succeeded.");
+ case AccountTaskState::STATE_OFFLINE:
+ return tr("Failed to contact the authentication server.");
+ case AccountTaskState::STATE_FAILED_SOFT:
+ return tr("Encountered an error during authentication.");
+ case AccountTaskState::STATE_FAILED_HARD:
+ return tr("Failed to authenticate. The session has expired.");
+ case AccountTaskState::STATE_FAILED_GONE:
+ return tr("Failed to authenticate. The account no longer exists.");
+ default:
+ return tr("...");
+ }
+}
+
+bool AccountTask::changeState(AccountTaskState newState, QString reason)
+{
+ m_taskState = newState;
+ setStatus(getStateMessage());
+ switch (newState) {
+ case AccountTaskState::STATE_CREATED: {
+ m_data->errorString.clear();
+ return true;
+ }
+ case AccountTaskState::STATE_WORKING: {
+ m_data->accountState = AccountState::Working;
+ return true;
+ }
+ case AccountTaskState::STATE_SUCCEEDED: {
+ m_data->accountState = AccountState::Online;
+ emitSucceeded();
+ return false;
+ }
+ case AccountTaskState::STATE_OFFLINE: {
+ m_data->errorString = reason;
+ m_data->accountState = AccountState::Offline;
+ emitFailed(reason);
+ return false;
+ }
+ case AccountTaskState::STATE_FAILED_SOFT: {
+ m_data->errorString = reason;
+ m_data->accountState = AccountState::Errored;
+ emitFailed(reason);
+ return false;
+ }
+ case AccountTaskState::STATE_FAILED_HARD: {
+ m_data->errorString = reason;
+ m_data->accountState = AccountState::Expired;
+ emitFailed(reason);
+ return false;
+ }
+ case AccountTaskState::STATE_FAILED_GONE: {
+ m_data->errorString = reason;
+ m_data->accountState = AccountState::Gone;
+ emitFailed(reason);
+ return false;
+ }
+ default: {
+ QString error =
+ tr("Unknown account task state: %1").arg(int(newState));
+ m_data->accountState = AccountState::Errored;
+ emitFailed(error);
+ return false;
+ }
+ }
+}
diff --git a/meshmc/launcher/minecraft/auth/AccountTask.h b/meshmc/launcher/minecraft/auth/AccountTask.h
new file mode 100644
index 0000000000..184b8b4c01
--- /dev/null
+++ b/meshmc/launcher/minecraft/auth/AccountTask.h
@@ -0,0 +1,101 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * 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 <tasks/Task.h>
+
+#include <QString>
+#include <QJsonObject>
+#include <QTimer>
+#include <qsslerror.h>
+
+#include "MinecraftAccount.h"
+
+class QNetworkReply;
+
+/**
+ * Enum for describing the state of the current task.
+ * Used by the getStateMessage function to determine what the status message
+ * should be.
+ */
+enum class AccountTaskState {
+ STATE_CREATED,
+ STATE_WORKING,
+ STATE_SUCCEEDED,
+ STATE_FAILED_SOFT, //!< soft failure. authentication went through partially
+ STATE_FAILED_HARD, //!< hard failure. main tokens are invalid
+ STATE_FAILED_GONE, //!< hard failure. main tokens are invalid, and the
+ //!< account no longer exists
+ STATE_OFFLINE //!< soft failure. authentication failed in the first step in
+ //!< a 'soft' way
+};
+
+class AccountTask : public Task
+{
+ Q_OBJECT
+ public:
+ explicit AccountTask(AccountData* data, QObject* parent = 0);
+ virtual ~AccountTask() {};
+
+ AccountTaskState m_taskState = AccountTaskState::STATE_CREATED;
+
+ AccountTaskState taskState()
+ {
+ return m_taskState;
+ }
+
+ signals:
+ void authorizeWithBrowser(const QUrl& url);
+
+ protected:
+ /**
+ * Returns the state message for the given state.
+ * Used to set the status message for the task.
+ * Should be overridden by subclasses that want to change messages for a
+ * given state.
+ */
+ virtual QString getStateMessage() const;
+
+ protected slots:
+ // NOTE: true -> non-terminal state, false -> terminal state
+ bool changeState(AccountTaskState newState, QString reason = QString());
+
+ protected:
+ AccountData* m_data = nullptr;
+};
diff --git a/meshmc/launcher/minecraft/auth/AuthRequest.cpp b/meshmc/launcher/minecraft/auth/AuthRequest.cpp
new file mode 100644
index 0000000000..9edf238c57
--- /dev/null
+++ b/meshmc/launcher/minecraft/auth/AuthRequest.cpp
@@ -0,0 +1,162 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <cassert>
+
+#include <QDebug>
+#include <QTimer>
+#include <QBuffer>
+#include <QUrlQuery>
+
+#include "Application.h"
+#include "AuthRequest.h"
+#include "katabasis/Globals.h"
+
+AuthRequest::AuthRequest(QObject* parent) : QObject(parent) {}
+
+AuthRequest::~AuthRequest() {}
+
+void AuthRequest::get(const QNetworkRequest& req, int timeout /* = 60*1000*/)
+{
+ setup(req, QNetworkAccessManager::GetOperation);
+ reply_ = APPLICATION->network()->get(request_);
+ status_ = Requesting;
+ timedReplies_.add(new Katabasis::Reply(reply_, timeout));
+ connect(reply_, SIGNAL(errorOccurred(QNetworkReply::NetworkError)), this,
+ SLOT(onRequestError(QNetworkReply::NetworkError)));
+ connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished()));
+ connect(reply_, &QNetworkReply::sslErrors, this, &AuthRequest::onSslErrors);
+}
+
+void AuthRequest::post(const QNetworkRequest& req, const QByteArray& data,
+ int timeout /* = 60*1000*/)
+{
+ setup(req, QNetworkAccessManager::PostOperation);
+ data_ = data;
+ status_ = Requesting;
+ reply_ = APPLICATION->network()->post(request_, data_);
+ timedReplies_.add(new Katabasis::Reply(reply_, timeout));
+ connect(reply_, SIGNAL(errorOccurred(QNetworkReply::NetworkError)), this,
+ SLOT(onRequestError(QNetworkReply::NetworkError)));
+ connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished()));
+ connect(reply_, &QNetworkReply::sslErrors, this, &AuthRequest::onSslErrors);
+ connect(reply_, SIGNAL(uploadProgress(qint64, qint64)), this,
+ SLOT(onUploadProgress(qint64, qint64)));
+}
+
+void AuthRequest::onRequestFinished()
+{
+ if (status_ == Idle) {
+ return;
+ }
+ if (reply_ != qobject_cast<QNetworkReply*>(sender())) {
+ return;
+ }
+ httpStatus_ =
+ reply_->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+ finish();
+}
+
+void AuthRequest::onRequestError(QNetworkReply::NetworkError error)
+{
+ qWarning() << "AuthRequest::onRequestError: Error" << (int)error;
+ if (status_ == Idle) {
+ return;
+ }
+ if (reply_ != qobject_cast<QNetworkReply*>(sender())) {
+ return;
+ }
+ errorString_ = reply_->errorString();
+ httpStatus_ =
+ reply_->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+ error_ = error;
+ qWarning() << "AuthRequest::onRequestError: Error string: " << errorString_;
+ qWarning() << "AuthRequest::onRequestError: HTTP status" << httpStatus_
+ << reply_->attribute(QNetworkRequest::HttpReasonPhraseAttribute)
+ .toString();
+
+ // QTimer::singleShot(10, this, SLOT(finish()));
+}
+
+void AuthRequest::onSslErrors(QList<QSslError> errors)
+{
+ int i = 1;
+ for (auto error : errors) {
+ qCritical() << "LOGIN SSL Error #" << i << " : " << error.errorString();
+ auto cert = error.certificate();
+ qCritical() << "Certificate in question:\n" << cert.toText();
+ i++;
+ }
+}
+
+void AuthRequest::onUploadProgress(qint64 uploaded, qint64 total)
+{
+ if (status_ == Idle) {
+ qWarning() << "AuthRequest::onUploadProgress: No pending request";
+ return;
+ }
+ if (reply_ != qobject_cast<QNetworkReply*>(sender())) {
+ return;
+ }
+ // Restart timeout because request in progress
+ Katabasis::Reply* o2Reply = timedReplies_.find(reply_);
+ if (o2Reply) {
+ o2Reply->start();
+ }
+ emit uploadProgress(uploaded, total);
+}
+
+void AuthRequest::setup(const QNetworkRequest& req,
+ QNetworkAccessManager::Operation operation,
+ const QByteArray& verb)
+{
+ request_ = req;
+ operation_ = operation;
+ url_ = req.url();
+
+ QUrl url = url_;
+ request_.setUrl(url);
+
+ if (!verb.isEmpty()) {
+ request_.setRawHeader(Katabasis::HTTP_HTTP_HEADER, verb);
+ }
+
+ status_ = Requesting;
+ error_ = QNetworkReply::NoError;
+ errorString_.clear();
+ httpStatus_ = 0;
+}
+
+void AuthRequest::finish()
+{
+ QByteArray data;
+ if (status_ == Idle) {
+ qWarning() << "AuthRequest::finish: No pending request";
+ return;
+ }
+ data = reply_->readAll();
+ status_ = Idle;
+ timedReplies_.remove(reply_);
+ reply_->disconnect(this);
+ reply_->deleteLater();
+ QList<QNetworkReply::RawHeaderPair> headers = reply_->rawHeaderPairs();
+ emit finished(error_, data, headers);
+}
diff --git a/meshmc/launcher/minecraft/auth/AuthRequest.h b/meshmc/launcher/minecraft/auth/AuthRequest.h
new file mode 100644
index 0000000000..cd57fa34db
--- /dev/null
+++ b/meshmc/launcher/minecraft/auth/AuthRequest.h
@@ -0,0 +1,93 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+#include <QObject>
+#include <QNetworkRequest>
+#include <QNetworkReply>
+#include <QNetworkAccessManager>
+#include <QUrl>
+#include <QByteArray>
+
+#include "katabasis/Reply.h"
+
+/// Makes authentication requests.
+class AuthRequest : public QObject
+{
+ Q_OBJECT
+
+ public:
+ explicit AuthRequest(QObject* parent = 0);
+ ~AuthRequest();
+
+ public slots:
+ void get(const QNetworkRequest& req, int timeout = 60 * 1000);
+ void post(const QNetworkRequest& req, const QByteArray& data,
+ int timeout = 60 * 1000);
+
+ signals:
+
+ /// Emitted when a request has been completed or failed.
+ void finished(QNetworkReply::NetworkError error, QByteArray data,
+ QList<QNetworkReply::RawHeaderPair> headers);
+
+ /// Emitted when an upload has progressed.
+ void uploadProgress(qint64 bytesSent, qint64 bytesTotal);
+
+ protected slots:
+
+ /// Handle request finished.
+ void onRequestFinished();
+
+ /// Handle request error.
+ void onRequestError(QNetworkReply::NetworkError error);
+
+ /// Handle ssl errors.
+ void onSslErrors(QList<QSslError> errors);
+
+ /// Finish the request, emit finished() signal.
+ void finish();
+
+ /// Handle upload progress.
+ void onUploadProgress(qint64 uploaded, qint64 total);
+
+ public:
+ QNetworkReply::NetworkError error_;
+ int httpStatus_ = 0;
+ QString errorString_;
+
+ protected:
+ void setup(const QNetworkRequest& request,
+ QNetworkAccessManager::Operation operation,
+ const QByteArray& verb = QByteArray());
+
+ enum Status { Idle, Requesting, ReRequesting };
+
+ QNetworkRequest request_;
+ QByteArray data_;
+ QNetworkReply* reply_;
+ Status status_;
+ QNetworkAccessManager::Operation operation_;
+ QUrl url_;
+ Katabasis::ReplyList timedReplies_;
+
+ QTimer* timer_;
+};
diff --git a/meshmc/launcher/minecraft/auth/AuthSession.cpp b/meshmc/launcher/minecraft/auth/AuthSession.cpp
new file mode 100644
index 0000000000..53366077ae
--- /dev/null
+++ b/meshmc/launcher/minecraft/auth/AuthSession.cpp
@@ -0,0 +1,57 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "AuthSession.h"
+#include <QJsonObject>
+#include <QJsonArray>
+#include <QJsonDocument>
+#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)
+{
+ if (status != PlayableOffline && status != PlayableOnline) {
+ return false;
+ }
+ session = "-";
+ player_name = offline_playername;
+ status = PlayableOffline;
+ return true;
+}
+
+void AuthSession::MakeDemo()
+{
+ player_name = "Player";
+ demo = true;
+}
diff --git a/meshmc/launcher/minecraft/auth/AuthSession.h b/meshmc/launcher/minecraft/auth/AuthSession.h
new file mode 100644
index 0000000000..80525bb972
--- /dev/null
+++ b/meshmc/launcher/minecraft/auth/AuthSession.h
@@ -0,0 +1,71 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <QString>
+#include <QMultiMap>
+#include <memory>
+#include "QObjectPtr.h"
+
+class MinecraftAccount;
+class QNetworkAccessManager;
+
+struct AuthSession {
+ bool MakeOffline(QString offline_playername);
+ void MakeDemo();
+
+ QString serializeUserProperties();
+
+ enum Status {
+ Undetermined,
+ RequiresOAuth,
+ RequiresPassword,
+ RequiresProfileSetup,
+ PlayableOffline,
+ PlayableOnline,
+ GoneOrMigrated
+ } status = Undetermined;
+
+ // client token
+ QString client_token;
+ // account user name
+ QString username;
+ // 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;
+ // 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;
+};
+
+typedef std::shared_ptr<AuthSession> AuthSessionPtr;
diff --git a/meshmc/launcher/minecraft/auth/AuthStep.cpp b/meshmc/launcher/minecraft/auth/AuthStep.cpp
new file mode 100644
index 0000000000..459d74d63d
--- /dev/null
+++ b/meshmc/launcher/minecraft/auth/AuthStep.cpp
@@ -0,0 +1,26 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "AuthStep.h"
+
+AuthStep::AuthStep(AccountData* data) : QObject(nullptr), m_data(data) {}
+
+AuthStep::~AuthStep() noexcept = default;
diff --git a/meshmc/launcher/minecraft/auth/AuthStep.h b/meshmc/launcher/minecraft/auth/AuthStep.h
new file mode 100644
index 0000000000..0c9c758b4e
--- /dev/null
+++ b/meshmc/launcher/minecraft/auth/AuthStep.h
@@ -0,0 +1,54 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+#include <QObject>
+#include <QList>
+#include <QNetworkReply>
+
+#include "QObjectPtr.h"
+#include "minecraft/auth/AccountData.h"
+#include "AccountTask.h"
+
+class AuthStep : public QObject
+{
+ Q_OBJECT
+
+ public:
+ using Ptr = shared_qobject_ptr<AuthStep>;
+
+ public:
+ explicit AuthStep(AccountData* data);
+ virtual ~AuthStep() noexcept;
+
+ virtual QString describe() = 0;
+
+ public slots:
+ virtual void perform() = 0;
+ virtual void rehydrate() = 0;
+
+ signals:
+ void finished(AccountTaskState resultingState, QString message);
+ void authorizeWithBrowser(const QUrl& url);
+
+ protected:
+ AccountData* m_data;
+};
diff --git a/meshmc/launcher/minecraft/auth/MinecraftAccount.cpp b/meshmc/launcher/minecraft/auth/MinecraftAccount.cpp
new file mode 100644
index 0000000000..53e77bbed0
--- /dev/null
+++ b/meshmc/launcher/minecraft/auth/MinecraftAccount.cpp
@@ -0,0 +1,257 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * 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.h"
+
+#include <QUuid>
+#include <QJsonObject>
+#include <QJsonArray>
+#include <QRegularExpression>
+#include <QStringList>
+#include <QJsonDocument>
+
+#include <QDebug>
+
+#include <QPainter>
+
+#include "flows/MSA.h"
+
+MinecraftAccount::MinecraftAccount(QObject* parent) : QObject(parent)
+{
+ data.internalId =
+ QUuid::createUuid().toString().remove(QRegularExpression("[{}-]"));
+}
+
+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;
+}
+
+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);
+ 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<AccountTask> MinecraftAccount::loginMSA()
+{
+ Q_ASSERT(m_currentTask.get() == nullptr);
+
+ m_currentTask.reset(new MSAInteractive(&data));
+ connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded()));
+ connect(m_currentTask.get(), SIGNAL(failed(QString)),
+ SLOT(authFailed(QString)));
+ emit activityChanged(true);
+ return m_currentTask;
+}
+
+shared_qobject_ptr<AccountTask> MinecraftAccount::refresh()
+{
+ if (m_currentTask) {
+ return m_currentTask;
+ }
+
+ m_currentTask.reset(new MSASilent(&data));
+
+ connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded()));
+ connect(m_currentTask.get(), SIGNAL(failed(QString)),
+ SLOT(authFailed(QString)));
+ emit activityChanged(true);
+ return m_currentTask;
+}
+
+shared_qobject_ptr<AccountTask> MinecraftAccount::currentTask()
+{
+ return m_currentTask;
+}
+
+void MinecraftAccount::authSucceeded()
+{
+ m_currentTask.reset();
+ emit changed();
+ emit activityChanged(false);
+}
+
+void MinecraftAccount::authFailed(QString reason)
+{
+ switch (m_currentTask->taskState()) {
+ case AccountTaskState::STATE_OFFLINE:
+ case AccountTaskState::STATE_FAILED_SOFT: {
+ // NOTE: this doesn't do much. There was an error of some sort.
+ } break;
+ case AccountTaskState::STATE_FAILED_HARD: {
+ data.msaToken.token = QString();
+ data.msaToken.refresh_token = QString();
+ data.msaToken.validity = Katabasis::Validity::None;
+ data.validity_ = Katabasis::Validity::None;
+ emit changed();
+ } break;
+ case AccountTaskState::STATE_FAILED_GONE: {
+ data.validity_ = Katabasis::Validity::None;
+ emit changed();
+ } break;
+ case AccountTaskState::STATE_CREATED:
+ case AccountTaskState::STATE_WORKING:
+ case AccountTaskState::STATE_SUCCEEDED: {
+ // Not reachable here, as they are not failures.
+ }
+ }
+ m_currentTask.reset();
+ emit activityChanged(false);
+}
+
+bool MinecraftAccount::isActive() const
+{
+ return m_currentTask;
+}
+
+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 Katabasis::Validity::Certain: {
+ break;
+ }
+ case Katabasis::Validity::None: {
+ return false;
+ }
+ case Katabasis::Validity::Assumed: {
+ return true;
+ }
+ }
+ auto now = QDateTime::currentDateTimeUtc();
+ auto issuedTimestamp = data.msaToken.issueInstant;
+ auto expiresTimestamp = data.msaToken.notAfter;
+
+ if (!expiresTimestamp.isValid()) {
+ expiresTimestamp = issuedTimestamp.addSecs(24 * 3600);
+ }
+ if (now.secsTo(expiresTimestamp) < (12 * 3600)) {
+ return true;
+ }
+ return false;
+}
+
+void MinecraftAccount::fillSession(AuthSessionPtr session)
+{
+ if (ownsMinecraft() && !hasProfile()) {
+ session->status = AuthSession::RequiresProfileSetup;
+ } else {
+ if (session->wants_online) {
+ session->status = AuthSession::PlayableOnline;
+ } else {
+ session->status = AuthSession::PlayableOffline;
+ }
+ }
+
+ // the user name
+ session->username = data.profileName();
+ // volatile auth token
+ session->access_token = data.accessToken();
+ // the semi-permanent client token
+ session->client_token = QString();
+ // profile name
+ session->player_name = data.profileName();
+ // profile ID
+ session->uuid = data.profileId();
+ // '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();
+ // FIXME: we now need a better way to identify accounts...
+ qWarning() << "Profile" << data.profileId() << "is no longer in use.";
+ }
+}
+
+void MinecraftAccount::incrementUses()
+{
+ bool wasInUse = isInUse();
+ Usable::incrementUses();
+ if (!wasInUse) {
+ emit changed();
+ // FIXME: we now need a better way to identify accounts...
+ qWarning() << "Profile" << data.profileId() << "is now in use.";
+ }
+}
diff --git a/meshmc/launcher/minecraft/auth/MinecraftAccount.h b/meshmc/launcher/minecraft/auth/MinecraftAccount.h
new file mode 100644
index 0000000000..1d25fa7d57
--- /dev/null
+++ b/meshmc/launcher/minecraft/auth/MinecraftAccount.h
@@ -0,0 +1,198 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * 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 <QObject>
+#include <QString>
+#include <QList>
+#include <QJsonObject>
+#include <QPair>
+#include <QMap>
+#include <QPixmap>
+
+#include <memory>
+
+#include "AuthSession.h"
+#include "Usable.h"
+#include "AccountData.h"
+#include "QObjectPtr.h"
+
+class Task;
+class AccountTask;
+class MinecraftAccount;
+
+typedef shared_qobject_ptr<MinecraftAccount> MinecraftAccountPtr;
+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 MeshMC 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 loadFromJsonV3(const QJsonObject& json);
+
+ //! Saves a MinecraftAccount to a JSON object and returns it.
+ QJsonObject saveToJson() const;
+
+ public: /* manipulation */
+ shared_qobject_ptr<AccountTask> loginMSA();
+
+ shared_qobject_ptr<AccountTask> refresh();
+
+ shared_qobject_ptr<AccountTask> 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();
+ }
+
+ bool isActive() const;
+
+ bool isMSA() const
+ {
+ return data.type == AccountType::MSA;
+ }
+
+ bool ownsMinecraft() const
+ {
+ return data.minecraftEntitlement.ownsMinecraft;
+ }
+
+ bool hasProfile() const
+ {
+ return data.profileId().size() != 0;
+ }
+
+ QString typeString() const
+ {
+ return "msa";
+ }
+
+ 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);
+
+ // TODO: better signalling for the various possible state changes -
+ // especially errors
+
+ protected: /* variables */
+ AccountData data;
+
+ // current task we are executing here
+ shared_qobject_ptr<AccountTask> m_currentTask;
+
+ protected: /* methods */
+ void incrementUses() override;
+ void decrementUses() override;
+
+ private slots:
+ void authSucceeded();
+ void authFailed(QString reason);
+};
diff --git a/meshmc/launcher/minecraft/auth/Parsers.cpp b/meshmc/launcher/minecraft/auth/Parsers.cpp
new file mode 100644
index 0000000000..6a4690942c
--- /dev/null
+++ b/meshmc/launcher/minecraft/auth/Parsers.cpp
@@ -0,0 +1,366 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "Parsers.h"
+
+#include <QJsonDocument>
+#include <QJsonArray>
+#include <QDebug>
+
+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"
+ }
+ ]
+ }
+ }
+ */
+ // TODO: handle error responses ...
+ /*
+ {
+ "Identity":"0",
+ "XErr":2148916238,
+ "Message":"",
+ "Redirect":"https://start.ui.xboxlive.com/AddChildToFamily"
+ }
+ // 2148916233 = missing XBox account
+ // 2148916238 = child account not linked to a family
+ */
+
+ bool parseXTokenResponse(QByteArray& data, Katabasis::Token& output,
+ QString name)
+ {
+ qDebug() << "Parsing" << name << ":";
+#ifndef NDEBUG
+ qDebug() << data;
+#endif
+ 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 = Katabasis::Validity::Certain;
+ qDebug() << name << "is valid.";
+ return true;
+ }
+
+ bool parseMinecraftProfile(QByteArray& data, MinecraftProfile& output)
+ {
+ qDebug() << "Parsing Minecraft profile...";
+#ifndef NDEBUG
+ qDebug() << data;
+#endif
+
+ 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;
+ }
+ 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;
+ }
+ if (!getString(capeObj.value("alias"), capeOut.alias)) {
+ continue;
+ }
+
+ output.capes[capeOut.id] = capeOut;
+ }
+ output.currentCape = currentCape;
+ output.validity = Katabasis::Validity::Certain;
+ return true;
+ }
+
+ bool parseMinecraftEntitlements(QByteArray& data,
+ MinecraftEntitlement& output)
+ {
+ qDebug() << "Parsing Minecraft entitlements...";
+#ifndef NDEBUG
+ qDebug() << data;
+#endif
+
+ 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 = Katabasis::Validity::Certain;
+ return true;
+ }
+
+ bool parseRolloutResponse(QByteArray& data, bool& result)
+ {
+ qDebug() << "Parsing Rollout response...";
+#ifndef NDEBUG
+ qDebug() << data;
+#endif
+
+ 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, Katabasis::Token& output)
+ {
+ QJsonParseError jsonError;
+ qDebug() << "Parsing Mojang response...";
+#ifndef NDEBUG
+ qDebug() << data;
+#endif
+ 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;
+ }
+
+ // TODO: it's a JWT... validate it?
+ if (!getString(obj.value("access_token"), output.token)) {
+ qWarning() << "access_token is not valid";
+ return false;
+ }
+ output.validity = Katabasis::Validity::Certain;
+ qDebug() << "Mojang response is valid.";
+ return true;
+ }
+
+} // namespace Parsers
diff --git a/meshmc/launcher/minecraft/auth/Parsers.h b/meshmc/launcher/minecraft/auth/Parsers.h
new file mode 100644
index 0000000000..62fd056b92
--- /dev/null
+++ b/meshmc/launcher/minecraft/auth/Parsers.h
@@ -0,0 +1,42 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "AccountData.h"
+
+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, Katabasis::Token& output,
+ QString name);
+ bool parseMojangResponse(QByteArray& data, Katabasis::Token& output);
+
+ bool parseMinecraftProfile(QByteArray& data, MinecraftProfile& output);
+ bool parseMinecraftEntitlements(QByteArray& data,
+ MinecraftEntitlement& output);
+ bool parseRolloutResponse(QByteArray& data, bool& result);
+} // namespace Parsers
diff --git a/meshmc/launcher/minecraft/auth/flows/AuthFlow.cpp b/meshmc/launcher/minecraft/auth/flows/AuthFlow.cpp
new file mode 100644
index 0000000000..ef29e9e77f
--- /dev/null
+++ b/meshmc/launcher/minecraft/auth/flows/AuthFlow.cpp
@@ -0,0 +1,93 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <QNetworkAccessManager>
+#include <QNetworkRequest>
+#include <QNetworkReply>
+#include <QDebug>
+
+#include "AuthFlow.h"
+#include "katabasis/Globals.h"
+
+#include <Application.h>
+
+AuthFlow::AuthFlow(AccountData* data, QObject* parent)
+ : AccountTask(data, parent)
+{
+}
+
+void AuthFlow::succeed()
+{
+ m_data->validity_ = Katabasis::Validity::Certain;
+ changeState(AccountTaskState::STATE_SUCCEEDED,
+ tr("Finished all authentication steps"));
+}
+
+void AuthFlow::executeTask()
+{
+ if (m_currentStep) {
+ return;
+ }
+ changeState(AccountTaskState::STATE_WORKING, tr("Initializing"));
+ nextStep();
+}
+
+void AuthFlow::nextStep()
+{
+ if (m_steps.size() == 0) {
+ // we got to the end without an incident... assume this is all.
+ m_currentStep.reset();
+ succeed();
+ return;
+ }
+ m_currentStep = m_steps.front();
+ qDebug() << "AuthFlow:" << m_currentStep->describe();
+ m_steps.pop_front();
+ connect(m_currentStep.get(), &AuthStep::finished, this,
+ &AuthFlow::stepFinished);
+ connect(m_currentStep.get(), &AuthStep::authorizeWithBrowser, this,
+ &AuthFlow::authorizeWithBrowser);
+
+ m_currentStep->perform();
+}
+
+QString AuthFlow::getStateMessage() const
+{
+ switch (m_taskState) {
+ case AccountTaskState::STATE_WORKING: {
+ if (m_currentStep) {
+ return m_currentStep->describe();
+ } else {
+ return tr("Working...");
+ }
+ }
+ default: {
+ return AccountTask::getStateMessage();
+ }
+ }
+}
+
+void AuthFlow::stepFinished(AccountTaskState resultingState, QString message)
+{
+ if (changeState(resultingState, message)) {
+ nextStep();
+ }
+}
diff --git a/meshmc/launcher/minecraft/auth/flows/AuthFlow.h b/meshmc/launcher/minecraft/auth/flows/AuthFlow.h
new file mode 100644
index 0000000000..0a4a431b71
--- /dev/null
+++ b/meshmc/launcher/minecraft/auth/flows/AuthFlow.h
@@ -0,0 +1,64 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <QObject>
+#include <QList>
+#include <QVector>
+#include <QSet>
+#include <QNetworkReply>
+#include <QImage>
+
+#include "minecraft/auth/AccountData.h"
+#include "minecraft/auth/AccountTask.h"
+#include "minecraft/auth/AuthStep.h"
+
+class AuthFlow : public AccountTask
+{
+ Q_OBJECT
+
+ public:
+ explicit AuthFlow(AccountData* data, QObject* parent = 0);
+
+ Katabasis::Validity validity()
+ {
+ return m_data->validity_;
+ };
+
+ QString getStateMessage() const override;
+
+ void executeTask() override;
+
+ signals:
+ // No extra signals needed - authorizeWithBrowser is on AccountTask
+
+ private slots:
+ void stepFinished(AccountTaskState resultingState, QString message);
+
+ protected:
+ void succeed();
+ void nextStep();
+
+ protected:
+ QList<AuthStep::Ptr> m_steps;
+ AuthStep::Ptr m_currentStep;
+};
diff --git a/meshmc/launcher/minecraft/auth/flows/MSA.cpp b/meshmc/launcher/minecraft/auth/flows/MSA.cpp
new file mode 100644
index 0000000000..2b5908932a
--- /dev/null
+++ b/meshmc/launcher/minecraft/auth/flows/MSA.cpp
@@ -0,0 +1,65 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "MSA.h"
+
+#include "minecraft/auth/steps/MSAStep.h"
+#include "minecraft/auth/steps/XboxUserStep.h"
+#include "minecraft/auth/steps/XboxAuthorizationStep.h"
+#include "minecraft/auth/steps/MeshMCLoginStep.h"
+#include "minecraft/auth/steps/XboxProfileStep.h"
+#include "minecraft/auth/steps/EntitlementsStep.h"
+#include "minecraft/auth/steps/MinecraftProfileStep.h"
+#include "minecraft/auth/steps/GetSkinStep.h"
+
+MSASilent::MSASilent(AccountData* data, QObject* parent)
+ : AuthFlow(data, parent)
+{
+ m_steps.append(new MSAStep(m_data, MSAStep::Action::Refresh));
+ m_steps.append(new XboxUserStep(m_data));
+ m_steps.append(new XboxAuthorizationStep(m_data, &m_data->xboxApiToken,
+ "http://xboxlive.com", "Xbox"));
+ m_steps.append(
+ new XboxAuthorizationStep(m_data, &m_data->mojangservicesToken,
+ "rp://api.minecraftservices.com/", "Mojang"));
+ m_steps.append(new MeshMCLoginStep(m_data));
+ m_steps.append(new XboxProfileStep(m_data));
+ m_steps.append(new EntitlementsStep(m_data));
+ m_steps.append(new MinecraftProfileStep(m_data));
+ m_steps.append(new GetSkinStep(m_data));
+}
+
+MSAInteractive::MSAInteractive(AccountData* data, QObject* parent)
+ : AuthFlow(data, parent)
+{
+ m_steps.append(new MSAStep(m_data, MSAStep::Action::Login));
+ m_steps.append(new XboxUserStep(m_data));
+ m_steps.append(new XboxAuthorizationStep(m_data, &m_data->xboxApiToken,
+ "http://xboxlive.com", "Xbox"));
+ m_steps.append(
+ new XboxAuthorizationStep(m_data, &m_data->mojangservicesToken,
+ "rp://api.minecraftservices.com/", "Mojang"));
+ m_steps.append(new MeshMCLoginStep(m_data));
+ m_steps.append(new XboxProfileStep(m_data));
+ m_steps.append(new EntitlementsStep(m_data));
+ m_steps.append(new MinecraftProfileStep(m_data));
+ m_steps.append(new GetSkinStep(m_data));
+}
diff --git a/meshmc/launcher/minecraft/auth/flows/MSA.h b/meshmc/launcher/minecraft/auth/flows/MSA.h
new file mode 100644
index 0000000000..cdeb1ff30f
--- /dev/null
+++ b/meshmc/launcher/minecraft/auth/flows/MSA.h
@@ -0,0 +1,37 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+#include "AuthFlow.h"
+
+class MSAInteractive : public AuthFlow
+{
+ Q_OBJECT
+ public:
+ explicit MSAInteractive(AccountData* data, QObject* parent = 0);
+};
+
+class MSASilent : public AuthFlow
+{
+ Q_OBJECT
+ public:
+ explicit MSASilent(AccountData* data, QObject* parent = 0);
+};
diff --git a/meshmc/launcher/minecraft/auth/steps/EntitlementsStep.cpp b/meshmc/launcher/minecraft/auth/steps/EntitlementsStep.cpp
new file mode 100644
index 0000000000..8d0418042a
--- /dev/null
+++ b/meshmc/launcher/minecraft/auth/steps/EntitlementsStep.cpp
@@ -0,0 +1,80 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "EntitlementsStep.h"
+
+#include <QNetworkRequest>
+#include <QUuid>
+
+#include "minecraft/auth/AuthRequest.h"
+#include "minecraft/auth/Parsers.h"
+
+EntitlementsStep::EntitlementsStep(AccountData* data) : AuthStep(data) {}
+
+EntitlementsStep::~EntitlementsStep() noexcept = default;
+
+QString EntitlementsStep::describe()
+{
+ return tr("Determining game ownership.");
+}
+
+void EntitlementsStep::perform()
+{
+ auto uuid = QUuid::createUuid();
+ m_entitlementsRequestId = uuid.toString().remove('{').remove('}');
+ auto url =
+ "https://api.minecraftservices.com/entitlements/license?requestId=" +
+ m_entitlementsRequestId;
+ QNetworkRequest request = QNetworkRequest(url);
+ request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
+ request.setRawHeader("Accept", "application/json");
+ request.setRawHeader(
+ "Authorization",
+ QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8());
+ AuthRequest* requestor = new AuthRequest(this);
+ connect(requestor, &AuthRequest::finished, this,
+ &EntitlementsStep::onRequestDone);
+ requestor->get(request);
+ qDebug() << "Getting entitlements...";
+}
+
+void EntitlementsStep::rehydrate()
+{
+ // NOOP, for now. We only save bools and there's nothing to check.
+}
+
+void EntitlementsStep::onRequestDone(
+ QNetworkReply::NetworkError error, QByteArray data,
+ QList<QNetworkReply::RawHeaderPair> headers)
+{
+ auto requestor = qobject_cast<AuthRequest*>(QObject::sender());
+ requestor->deleteLater();
+
+#ifndef NDEBUG
+ qDebug() << data;
+#endif
+
+ // TODO: check presence of same entitlementsRequestId?
+ // TODO: validate JWTs?
+ Parsers::parseMinecraftEntitlements(data, m_data->minecraftEntitlement);
+
+ emit finished(AccountTaskState::STATE_WORKING, tr("Got entitlements"));
+}
diff --git a/meshmc/launcher/minecraft/auth/steps/EntitlementsStep.h b/meshmc/launcher/minecraft/auth/steps/EntitlementsStep.h
new file mode 100644
index 0000000000..bd97a1c59e
--- /dev/null
+++ b/meshmc/launcher/minecraft/auth/steps/EntitlementsStep.h
@@ -0,0 +1,47 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+#include <QObject>
+
+#include "QObjectPtr.h"
+#include "minecraft/auth/AuthStep.h"
+
+class EntitlementsStep : public AuthStep
+{
+ Q_OBJECT
+
+ public:
+ explicit EntitlementsStep(AccountData* data);
+ virtual ~EntitlementsStep() noexcept;
+
+ void perform() override;
+ void rehydrate() override;
+
+ QString describe() override;
+
+ private slots:
+ void onRequestDone(QNetworkReply::NetworkError, QByteArray,
+ QList<QNetworkReply::RawHeaderPair>);
+
+ private:
+ QString m_entitlementsRequestId;
+};
diff --git a/meshmc/launcher/minecraft/auth/steps/GetSkinStep.cpp b/meshmc/launcher/minecraft/auth/steps/GetSkinStep.cpp
new file mode 100644
index 0000000000..abf5db950f
--- /dev/null
+++ b/meshmc/launcher/minecraft/auth/steps/GetSkinStep.cpp
@@ -0,0 +1,64 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "GetSkinStep.h"
+
+#include <QNetworkRequest>
+
+#include "minecraft/auth/AuthRequest.h"
+#include "minecraft/auth/Parsers.h"
+
+GetSkinStep::GetSkinStep(AccountData* data) : AuthStep(data) {}
+
+GetSkinStep::~GetSkinStep() noexcept = default;
+
+QString GetSkinStep::describe()
+{
+ return tr("Getting skin.");
+}
+
+void GetSkinStep::perform()
+{
+ auto url = QUrl(m_data->minecraftProfile.skin.url);
+ QNetworkRequest request = QNetworkRequest(url);
+ AuthRequest* requestor = new AuthRequest(this);
+ connect(requestor, &AuthRequest::finished, this,
+ &GetSkinStep::onRequestDone);
+ requestor->get(request);
+}
+
+void GetSkinStep::rehydrate()
+{
+ // NOOP, for now.
+}
+
+void GetSkinStep::onRequestDone(QNetworkReply::NetworkError error,
+ QByteArray data,
+ QList<QNetworkReply::RawHeaderPair> headers)
+{
+ auto requestor = qobject_cast<AuthRequest*>(QObject::sender());
+ requestor->deleteLater();
+
+ if (error == QNetworkReply::NoError) {
+ m_data->minecraftProfile.skin.data = data;
+ }
+ emit finished(AccountTaskState::STATE_SUCCEEDED, tr("Got skin"));
+}
diff --git a/meshmc/launcher/minecraft/auth/steps/GetSkinStep.h b/meshmc/launcher/minecraft/auth/steps/GetSkinStep.h
new file mode 100644
index 0000000000..ed6a288cdb
--- /dev/null
+++ b/meshmc/launcher/minecraft/auth/steps/GetSkinStep.h
@@ -0,0 +1,44 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+#include <QObject>
+
+#include "QObjectPtr.h"
+#include "minecraft/auth/AuthStep.h"
+
+class GetSkinStep : public AuthStep
+{
+ Q_OBJECT
+
+ public:
+ explicit GetSkinStep(AccountData* data);
+ virtual ~GetSkinStep() noexcept;
+
+ void perform() override;
+ void rehydrate() override;
+
+ QString describe() override;
+
+ private slots:
+ void onRequestDone(QNetworkReply::NetworkError, QByteArray,
+ QList<QNetworkReply::RawHeaderPair>);
+};
diff --git a/meshmc/launcher/minecraft/auth/steps/MSAStep.cpp b/meshmc/launcher/minecraft/auth/steps/MSAStep.cpp
new file mode 100644
index 0000000000..9be4761549
--- /dev/null
+++ b/meshmc/launcher/minecraft/auth/steps/MSAStep.cpp
@@ -0,0 +1,161 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "MSAStep.h"
+
+#include <QNetworkRequest>
+#include <QDesktopServices>
+
+#include "minecraft/auth/AuthRequest.h"
+#include "minecraft/auth/Parsers.h"
+
+#include "Application.h"
+
+MSAStep::MSAStep(AccountData* data, Action action)
+ : AuthStep(data), m_action(action)
+{
+ m_replyHandler = new QOAuthHttpServerReplyHandler(this);
+ m_replyHandler->setCallbackText(
+ tr("Login successful! You can close this page and return to MeshMC."));
+
+ m_oauth2 = new QOAuth2AuthorizationCodeFlow(this);
+ m_oauth2->setClientIdentifier(APPLICATION->msaClientId());
+ m_oauth2->setAuthorizationUrl(QUrl(
+ "https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize"));
+ m_oauth2->setTokenUrl(
+ QUrl("https://login.microsoftonline.com/consumers/oauth2/v2.0/token"));
+ m_oauth2->setScope("XboxLive.signin offline_access");
+ m_oauth2->setReplyHandler(m_replyHandler);
+ m_oauth2->setNetworkAccessManager(APPLICATION->network().get());
+
+ connect(m_oauth2, &QOAuth2AuthorizationCodeFlow::granted, this,
+ &MSAStep::onGranted);
+ connect(m_oauth2, &QOAuth2AuthorizationCodeFlow::requestFailed, this,
+ &MSAStep::onRequestFailed);
+ connect(m_oauth2, &QOAuth2AuthorizationCodeFlow::authorizeWithBrowser, this,
+ &MSAStep::onOpenBrowser);
+}
+
+MSAStep::~MSAStep() noexcept = default;
+
+QString MSAStep::describe()
+{
+ return tr("Logging in with Microsoft account.");
+}
+
+void MSAStep::rehydrate()
+{
+ switch (m_action) {
+ case Refresh: {
+ // TODO: check the tokens and see if they are old (older than a day)
+ return;
+ }
+ case Login: {
+ // NOOP
+ return;
+ }
+ }
+}
+
+void MSAStep::perform()
+{
+ switch (m_action) {
+ case Refresh: {
+ // Load the refresh token from stored account data
+ m_oauth2->setRefreshToken(m_data->msaToken.refresh_token);
+ m_oauth2->refreshTokens();
+ return;
+ }
+ case Login: {
+ *m_data = AccountData();
+ if (!m_replyHandler->isListening()) {
+ if (!m_replyHandler->listen(QHostAddress::LocalHost)) {
+ emit finished(AccountTaskState::STATE_FAILED_HARD,
+ tr("Failed to start local HTTP server for "
+ "OAuth2 callback."));
+ return;
+ }
+ }
+ m_oauth2->setModifyParametersFunction(
+ [](QAbstractOAuth::Stage stage,
+ QMultiMap<QString, QVariant>* parameters) {
+ if (stage ==
+ QAbstractOAuth::Stage::RequestingAuthorization) {
+ parameters->insert("prompt", "select_account");
+ }
+ });
+ m_oauth2->grant();
+ return;
+ }
+ }
+}
+
+void MSAStep::onOpenBrowser(const QUrl& url)
+{
+ emit authorizeWithBrowser(url);
+ QDesktopServices::openUrl(url);
+}
+
+void MSAStep::onGranted()
+{
+ m_replyHandler->close();
+
+ // Store the tokens in account data
+ m_data->msaToken.token = m_oauth2->token();
+ m_data->msaToken.refresh_token = m_oauth2->refreshToken();
+ m_data->msaToken.issueInstant = QDateTime::currentDateTimeUtc();
+ m_data->msaToken.notAfter = m_oauth2->expirationAt();
+ if (!m_data->msaToken.notAfter.isValid()) {
+ m_data->msaToken.notAfter = m_data->msaToken.issueInstant.addSecs(3600);
+ }
+ m_data->msaToken.validity = Katabasis::Validity::Certain;
+
+ emit finished(AccountTaskState::STATE_WORKING, tr("Got MSA token."));
+}
+
+void MSAStep::onRequestFailed(QAbstractOAuth::Error error)
+{
+ m_replyHandler->close();
+
+ switch (error) {
+ case QAbstractOAuth::Error::NetworkError:
+ emit finished(
+ AccountTaskState::STATE_OFFLINE,
+ tr("Microsoft authentication failed due to a network error."));
+ return;
+ case QAbstractOAuth::Error::ServerError:
+ case QAbstractOAuth::Error::OAuthTokenNotFoundError:
+ case QAbstractOAuth::Error::OAuthTokenSecretNotFoundError:
+ case QAbstractOAuth::Error::OAuthCallbackNotVerified:
+ emit finished(AccountTaskState::STATE_FAILED_HARD,
+ tr("Microsoft authentication failed."));
+ return;
+ case QAbstractOAuth::Error::ExpiredError:
+ emit finished(AccountTaskState::STATE_FAILED_GONE,
+ tr("Microsoft authentication token expired."));
+ return;
+ default:
+ emit finished(AccountTaskState::STATE_FAILED_HARD,
+ tr("Microsoft authentication failed with an "
+ "unrecognized error."));
+ return;
+ }
+}
diff --git a/meshmc/launcher/minecraft/auth/steps/MSAStep.h b/meshmc/launcher/minecraft/auth/steps/MSAStep.h
new file mode 100644
index 0000000000..2e223024e3
--- /dev/null
+++ b/meshmc/launcher/minecraft/auth/steps/MSAStep.h
@@ -0,0 +1,55 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+#include <QObject>
+
+#include "QObjectPtr.h"
+#include "minecraft/auth/AuthStep.h"
+
+#include <QOAuth2AuthorizationCodeFlow>
+#include <QOAuthHttpServerReplyHandler>
+
+class MSAStep : public AuthStep
+{
+ Q_OBJECT
+ public:
+ enum Action { Refresh, Login };
+
+ public:
+ explicit MSAStep(AccountData* data, Action action);
+ virtual ~MSAStep() noexcept;
+
+ void perform() override;
+ void rehydrate() override;
+
+ QString describe() override;
+
+ private slots:
+ void onGranted();
+ void onRequestFailed(QAbstractOAuth::Error error);
+ void onOpenBrowser(const QUrl& url);
+
+ private:
+ QOAuth2AuthorizationCodeFlow* m_oauth2 = nullptr;
+ QOAuthHttpServerReplyHandler* m_replyHandler = nullptr;
+ Action m_action;
+};
diff --git a/meshmc/launcher/minecraft/auth/steps/MeshMCLoginStep.cpp b/meshmc/launcher/minecraft/auth/steps/MeshMCLoginStep.cpp
new file mode 100644
index 0000000000..19afcda3fc
--- /dev/null
+++ b/meshmc/launcher/minecraft/auth/steps/MeshMCLoginStep.cpp
@@ -0,0 +1,98 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "MeshMCLoginStep.h"
+
+#include <QNetworkRequest>
+
+#include "minecraft/auth/AuthRequest.h"
+#include "minecraft/auth/Parsers.h"
+#include "minecraft/auth/AccountTask.h"
+
+MeshMCLoginStep::MeshMCLoginStep(AccountData* data) : AuthStep(data) {}
+
+MeshMCLoginStep::~MeshMCLoginStep() noexcept = default;
+
+QString MeshMCLoginStep::describe()
+{
+ return tr("Accessing Mojang services.");
+}
+
+void MeshMCLoginStep::perform()
+{
+ auto requestURL = "https://api.minecraftservices.com/launcher/login";
+ auto uhs = m_data->mojangservicesToken.extra["uhs"].toString();
+ auto xToken = m_data->mojangservicesToken.token;
+
+ QString mc_auth_template = R"XXX(
+{
+ "xtoken": "XBL3.0 x=%1;%2",
+ "platform": "PC_LAUNCHER"
+}
+)XXX";
+ auto requestBody = mc_auth_template.arg(uhs, xToken);
+
+ QNetworkRequest request = QNetworkRequest(QUrl(requestURL));
+ request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
+ request.setRawHeader("Accept", "application/json");
+ AuthRequest* requestor = new AuthRequest(this);
+ connect(requestor, &AuthRequest::finished, this,
+ &MeshMCLoginStep::onRequestDone);
+ requestor->post(request, requestBody.toUtf8());
+ qDebug() << "Getting Minecraft access token...";
+}
+
+void MeshMCLoginStep::rehydrate()
+{
+ // TODO: check the token validity
+}
+
+void MeshMCLoginStep::onRequestDone(QNetworkReply::NetworkError error,
+ QByteArray data,
+ QList<QNetworkReply::RawHeaderPair> headers)
+{
+ auto requestor = qobject_cast<AuthRequest*>(QObject::sender());
+ requestor->deleteLater();
+
+ qDebug() << data;
+ if (error != QNetworkReply::NoError) {
+ qWarning() << "Reply error:" << error;
+#ifndef NDEBUG
+ qDebug() << data;
+#endif
+ emit finished(AccountTaskState::STATE_FAILED_SOFT,
+ tr("Failed to get Minecraft access token: %1")
+ .arg(requestor->errorString_));
+ return;
+ }
+
+ if (!Parsers::parseMojangResponse(data, m_data->yggdrasilToken)) {
+ qWarning() << "Could not parse login_with_xbox response...";
+#ifndef NDEBUG
+ qDebug() << data;
+#endif
+ emit finished(
+ AccountTaskState::STATE_FAILED_SOFT,
+ tr("Failed to parse the Minecraft access token response."));
+ return;
+ }
+ emit finished(AccountTaskState::STATE_WORKING, tr(""));
+}
diff --git a/meshmc/launcher/minecraft/auth/steps/MeshMCLoginStep.h b/meshmc/launcher/minecraft/auth/steps/MeshMCLoginStep.h
new file mode 100644
index 0000000000..859ae867f3
--- /dev/null
+++ b/meshmc/launcher/minecraft/auth/steps/MeshMCLoginStep.h
@@ -0,0 +1,44 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+#include <QObject>
+
+#include "QObjectPtr.h"
+#include "minecraft/auth/AuthStep.h"
+
+class MeshMCLoginStep : public AuthStep
+{
+ Q_OBJECT
+
+ public:
+ explicit MeshMCLoginStep(AccountData* data);
+ virtual ~MeshMCLoginStep() noexcept;
+
+ void perform() override;
+ void rehydrate() override;
+
+ QString describe() override;
+
+ private slots:
+ void onRequestDone(QNetworkReply::NetworkError, QByteArray,
+ QList<QNetworkReply::RawHeaderPair>);
+};
diff --git a/meshmc/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp b/meshmc/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp
new file mode 100644
index 0000000000..9955ff9738
--- /dev/null
+++ b/meshmc/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp
@@ -0,0 +1,101 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "MinecraftProfileStep.h"
+
+#include <QNetworkRequest>
+
+#include "minecraft/auth/AuthRequest.h"
+#include "minecraft/auth/Parsers.h"
+
+MinecraftProfileStep::MinecraftProfileStep(AccountData* data) : AuthStep(data)
+{
+}
+
+MinecraftProfileStep::~MinecraftProfileStep() noexcept = default;
+
+QString MinecraftProfileStep::describe()
+{
+ return tr("Fetching the Minecraft profile.");
+}
+
+void MinecraftProfileStep::perform()
+{
+ auto url = QUrl("https://api.minecraftservices.com/minecraft/profile");
+ QNetworkRequest request = QNetworkRequest(url);
+ request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
+ request.setRawHeader(
+ "Authorization",
+ QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8());
+
+ AuthRequest* requestor = new AuthRequest(this);
+ connect(requestor, &AuthRequest::finished, this,
+ &MinecraftProfileStep::onRequestDone);
+ requestor->get(request);
+}
+
+void MinecraftProfileStep::rehydrate()
+{
+ // NOOP, for now. We only save bools and there's nothing to check.
+}
+
+void MinecraftProfileStep::onRequestDone(
+ QNetworkReply::NetworkError error, QByteArray data,
+ QList<QNetworkReply::RawHeaderPair> headers)
+{
+ auto requestor = qobject_cast<AuthRequest*>(QObject::sender());
+ requestor->deleteLater();
+
+#ifndef NDEBUG
+ qDebug() << data;
+#endif
+ if (error == QNetworkReply::ContentNotFoundError) {
+ // NOTE: Succeed even if we do not have a profile. This is a valid
+ // account state.
+ m_data->minecraftProfile = MinecraftProfile();
+ emit finished(AccountTaskState::STATE_SUCCEEDED,
+ tr("Account has no Minecraft profile."));
+ return;
+ }
+ if (error != QNetworkReply::NoError) {
+ qWarning() << "Error getting profile:";
+ qWarning() << " HTTP Status: " << requestor->httpStatus_;
+ qWarning() << " Internal error no.: " << error;
+ qWarning() << " Error string: " << requestor->errorString_;
+
+ qWarning() << " Response:";
+ qWarning() << QString::fromUtf8(data);
+
+ emit finished(AccountTaskState::STATE_FAILED_SOFT,
+ tr("Minecraft Java profile acquisition failed."));
+ return;
+ }
+ if (!Parsers::parseMinecraftProfile(data, m_data->minecraftProfile)) {
+ m_data->minecraftProfile = MinecraftProfile();
+ emit finished(
+ AccountTaskState::STATE_FAILED_SOFT,
+ tr("Minecraft Java profile response could not be parsed"));
+ return;
+ }
+
+ emit finished(AccountTaskState::STATE_WORKING,
+ tr("Minecraft Java profile acquisition succeeded."));
+}
diff --git a/meshmc/launcher/minecraft/auth/steps/MinecraftProfileStep.h b/meshmc/launcher/minecraft/auth/steps/MinecraftProfileStep.h
new file mode 100644
index 0000000000..eb0594bdf8
--- /dev/null
+++ b/meshmc/launcher/minecraft/auth/steps/MinecraftProfileStep.h
@@ -0,0 +1,44 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+#include <QObject>
+
+#include "QObjectPtr.h"
+#include "minecraft/auth/AuthStep.h"
+
+class MinecraftProfileStep : public AuthStep
+{
+ Q_OBJECT
+
+ public:
+ explicit MinecraftProfileStep(AccountData* data);
+ virtual ~MinecraftProfileStep() noexcept;
+
+ void perform() override;
+ void rehydrate() override;
+
+ QString describe() override;
+
+ private slots:
+ void onRequestDone(QNetworkReply::NetworkError, QByteArray,
+ QList<QNetworkReply::RawHeaderPair>);
+};
diff --git a/meshmc/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp b/meshmc/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp
new file mode 100644
index 0000000000..b54ad2a32b
--- /dev/null
+++ b/meshmc/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp
@@ -0,0 +1,191 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "XboxAuthorizationStep.h"
+
+#include <QNetworkRequest>
+#include <QJsonParseError>
+#include <QJsonDocument>
+
+#include "minecraft/auth/AuthRequest.h"
+#include "minecraft/auth/Parsers.h"
+
+XboxAuthorizationStep::XboxAuthorizationStep(AccountData* data,
+ Katabasis::Token* token,
+ QString relyingParty,
+ QString authorizationKind)
+ : AuthStep(data), m_token(token), m_relyingParty(relyingParty),
+ m_authorizationKind(authorizationKind)
+{
+}
+
+XboxAuthorizationStep::~XboxAuthorizationStep() noexcept = default;
+
+QString XboxAuthorizationStep::describe()
+{
+ return tr("Getting authorization to access %1 services.")
+ .arg(m_authorizationKind);
+}
+
+void XboxAuthorizationStep::rehydrate()
+{
+ // FIXME: check if the tokens are good?
+}
+
+void XboxAuthorizationStep::perform()
+{
+ QString xbox_auth_template = R"XXX(
+{
+ "Properties": {
+ "SandboxId": "RETAIL",
+ "UserTokens": [
+ "%1"
+ ]
+ },
+ "RelyingParty": "%2",
+ "TokenType": "JWT"
+}
+)XXX";
+ auto xbox_auth_data =
+ xbox_auth_template.arg(m_data->userToken.token, m_relyingParty);
+ // http://xboxlive.com
+ QNetworkRequest request =
+ QNetworkRequest(QUrl("https://xsts.auth.xboxlive.com/xsts/authorize"));
+ request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
+ request.setRawHeader("Accept", "application/json");
+ AuthRequest* requestor = new AuthRequest(this);
+ connect(requestor, &AuthRequest::finished, this,
+ &XboxAuthorizationStep::onRequestDone);
+ requestor->post(request, xbox_auth_data.toUtf8());
+ qDebug() << "Getting authorization token for " << m_relyingParty;
+}
+
+void XboxAuthorizationStep::onRequestDone(
+ QNetworkReply::NetworkError error, QByteArray data,
+ QList<QNetworkReply::RawHeaderPair> headers)
+{
+ auto requestor = qobject_cast<AuthRequest*>(QObject::sender());
+ requestor->deleteLater();
+
+#ifndef NDEBUG
+ qDebug() << data;
+#endif
+ if (error != QNetworkReply::NoError) {
+ qWarning() << "Reply error:" << error;
+ if (!processSTSError(error, data, headers)) {
+ emit finished(
+ AccountTaskState::STATE_FAILED_SOFT,
+ tr("Failed to get authorization for %1 services. Error %1.")
+ .arg(m_authorizationKind, error));
+ }
+ return;
+ }
+
+ Katabasis::Token temp;
+ if (!Parsers::parseXTokenResponse(data, temp, m_authorizationKind)) {
+ emit finished(AccountTaskState::STATE_FAILED_SOFT,
+ tr("Could not parse authorization response for access to "
+ "%1 services.")
+ .arg(m_authorizationKind));
+ return;
+ }
+
+ if (temp.extra["uhs"] != m_data->userToken.extra["uhs"]) {
+ emit finished(AccountTaskState::STATE_FAILED_SOFT,
+ tr("Server has changed %1 authorization user hash in the "
+ "reply. Something is wrong.")
+ .arg(m_authorizationKind));
+ return;
+ }
+ auto& token = *m_token;
+ token = temp;
+
+ emit finished(AccountTaskState::STATE_WORKING,
+ tr("Got authorization to access %1").arg(m_relyingParty));
+}
+
+bool XboxAuthorizationStep::processSTSError(
+ QNetworkReply::NetworkError error, QByteArray data,
+ QList<QNetworkReply::RawHeaderPair> headers)
+{
+ if (error == QNetworkReply::AuthenticationRequiredError) {
+ QJsonParseError jsonError;
+ QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError);
+ if (jsonError.error) {
+ qWarning() << "Cannot parse error XSTS response as JSON: "
+ << jsonError.errorString();
+ emit finished(
+ AccountTaskState::STATE_FAILED_SOFT,
+ tr("Cannot parse %1 authorization error response as JSON: %2")
+ .arg(m_authorizationKind, jsonError.errorString()));
+ return true;
+ }
+
+ int64_t errorCode = -1;
+ auto obj = doc.object();
+ if (!Parsers::getNumber(obj.value("XErr"), errorCode)) {
+ emit finished(AccountTaskState::STATE_FAILED_SOFT,
+ tr("XErr element is missing from %1 authorization "
+ "error response.")
+ .arg(m_authorizationKind));
+ return true;
+ }
+ switch (errorCode) {
+ case 2148916233: {
+ emit finished(
+ AccountTaskState::STATE_FAILED_SOFT,
+ tr("This Microsoft account does not have an XBox Live "
+ "profile. Buy the game on %1 first.")
+ .arg("<a "
+ "href=\"https://www.minecraft.net/en-us/store/"
+ "minecraft-java-edition\">minecraft.net</a>"));
+ return true;
+ }
+ case 2148916235: {
+ // NOTE: this is the Grulovia error
+ emit finished(AccountTaskState::STATE_FAILED_SOFT,
+ tr("XBox Live is not available in your country. "
+ "You've been blocked."));
+ return true;
+ }
+ case 2148916238: {
+ emit finished(
+ AccountTaskState::STATE_FAILED_SOFT,
+ tr("This Microsoft account is underaged and is not linked "
+ "to a family.\n\nPlease set up your account according "
+ "to %1.")
+ .arg(
+ "<a "
+ "href=\"https://help.minecraft.net/hc/en-us/"
+ "articles/4403181904525\">help.minecraft.net</a>"));
+ return true;
+ }
+ default: {
+ emit finished(AccountTaskState::STATE_FAILED_SOFT,
+ tr("XSTS authentication ended with unrecognized "
+ "error(s):\n\n%1")
+ .arg(errorCode));
+ return true;
+ }
+ }
+ }
+ return false;
+}
diff --git a/meshmc/launcher/minecraft/auth/steps/XboxAuthorizationStep.h b/meshmc/launcher/minecraft/auth/steps/XboxAuthorizationStep.h
new file mode 100644
index 0000000000..a8413c939f
--- /dev/null
+++ b/meshmc/launcher/minecraft/auth/steps/XboxAuthorizationStep.h
@@ -0,0 +1,55 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+#include <QObject>
+
+#include "QObjectPtr.h"
+#include "minecraft/auth/AuthStep.h"
+
+class XboxAuthorizationStep : public AuthStep
+{
+ Q_OBJECT
+
+ public:
+ explicit XboxAuthorizationStep(AccountData* data, Katabasis::Token* token,
+ QString relyingParty,
+ QString authorizationKind);
+ virtual ~XboxAuthorizationStep() noexcept;
+
+ void perform() override;
+ void rehydrate() override;
+
+ QString describe() override;
+
+ private:
+ bool processSTSError(QNetworkReply::NetworkError error, QByteArray data,
+ QList<QNetworkReply::RawHeaderPair> headers);
+
+ private slots:
+ void onRequestDone(QNetworkReply::NetworkError, QByteArray,
+ QList<QNetworkReply::RawHeaderPair>);
+
+ private:
+ Katabasis::Token* m_token;
+ QString m_relyingParty;
+ QString m_authorizationKind;
+};
diff --git a/meshmc/launcher/minecraft/auth/steps/XboxProfileStep.cpp b/meshmc/launcher/minecraft/auth/steps/XboxProfileStep.cpp
new file mode 100644
index 0000000000..aae94b0403
--- /dev/null
+++ b/meshmc/launcher/minecraft/auth/steps/XboxProfileStep.cpp
@@ -0,0 +1,96 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "XboxProfileStep.h"
+
+#include <QNetworkRequest>
+#include <QUrlQuery>
+
+#include "minecraft/auth/AuthRequest.h"
+#include "minecraft/auth/Parsers.h"
+
+XboxProfileStep::XboxProfileStep(AccountData* data) : AuthStep(data) {}
+
+XboxProfileStep::~XboxProfileStep() noexcept = default;
+
+QString XboxProfileStep::describe()
+{
+ return tr("Fetching Xbox profile.");
+}
+
+void XboxProfileStep::rehydrate()
+{
+ // NOOP, for now. We only save bools and there's nothing to check.
+}
+
+void XboxProfileStep::perform()
+{
+ auto url = QUrl("https://profile.xboxlive.com/users/me/profile/settings");
+ QUrlQuery q;
+ q.addQueryItem(
+ "settings",
+ "GameDisplayName,AppDisplayName,AppDisplayPicRaw,GameDisplayPicRaw,"
+ "PublicGamerpic,ShowUserAsAvatar,Gamerscore,Gamertag,ModernGamertag,"
+ "ModernGamertagSuffix,"
+ "UniqueModernGamertag,AccountTier,TenureLevel,XboxOneRep,"
+ "PreferredColor,Location,Bio,Watermarks,"
+ "RealName,RealNameOverride,IsQuarantined");
+ url.setQuery(q);
+
+ QNetworkRequest request = QNetworkRequest(url);
+ request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
+ request.setRawHeader("Accept", "application/json");
+ request.setRawHeader("x-xbl-contract-version", "3");
+ request.setRawHeader("Authorization",
+ QString("XBL3.0 x=%1;%2")
+ .arg(m_data->userToken.extra["uhs"].toString(),
+ m_data->xboxApiToken.token)
+ .toUtf8());
+ AuthRequest* requestor = new AuthRequest(this);
+ connect(requestor, &AuthRequest::finished, this,
+ &XboxProfileStep::onRequestDone);
+ requestor->get(request);
+ qDebug() << "Getting Xbox profile...";
+}
+
+void XboxProfileStep::onRequestDone(QNetworkReply::NetworkError error,
+ QByteArray data,
+ QList<QNetworkReply::RawHeaderPair> headers)
+{
+ auto requestor = qobject_cast<AuthRequest*>(QObject::sender());
+ requestor->deleteLater();
+
+ if (error != QNetworkReply::NoError) {
+ qWarning() << "Reply error:" << error;
+#ifndef NDEBUG
+ qDebug() << data;
+#endif
+ finished(AccountTaskState::STATE_FAILED_SOFT,
+ tr("Failed to retrieve the Xbox profile."));
+ return;
+ }
+
+#ifndef NDEBUG
+ qDebug() << "XBox profile: " << data;
+#endif
+
+ emit finished(AccountTaskState::STATE_WORKING, tr("Got Xbox profile"));
+}
diff --git a/meshmc/launcher/minecraft/auth/steps/XboxProfileStep.h b/meshmc/launcher/minecraft/auth/steps/XboxProfileStep.h
new file mode 100644
index 0000000000..cf2c0c3c9b
--- /dev/null
+++ b/meshmc/launcher/minecraft/auth/steps/XboxProfileStep.h
@@ -0,0 +1,44 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+#include <QObject>
+
+#include "QObjectPtr.h"
+#include "minecraft/auth/AuthStep.h"
+
+class XboxProfileStep : public AuthStep
+{
+ Q_OBJECT
+
+ public:
+ explicit XboxProfileStep(AccountData* data);
+ virtual ~XboxProfileStep() noexcept;
+
+ void perform() override;
+ void rehydrate() override;
+
+ QString describe() override;
+
+ private slots:
+ void onRequestDone(QNetworkReply::NetworkError, QByteArray,
+ QList<QNetworkReply::RawHeaderPair>);
+};
diff --git a/meshmc/launcher/minecraft/auth/steps/XboxUserStep.cpp b/meshmc/launcher/minecraft/auth/steps/XboxUserStep.cpp
new file mode 100644
index 0000000000..77afa17fb9
--- /dev/null
+++ b/meshmc/launcher/minecraft/auth/steps/XboxUserStep.cpp
@@ -0,0 +1,93 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "XboxUserStep.h"
+
+#include <QNetworkRequest>
+
+#include "minecraft/auth/AuthRequest.h"
+#include "minecraft/auth/Parsers.h"
+
+XboxUserStep::XboxUserStep(AccountData* data) : AuthStep(data) {}
+
+XboxUserStep::~XboxUserStep() noexcept = default;
+
+QString XboxUserStep::describe()
+{
+ return tr("Logging in as an Xbox user.");
+}
+
+void XboxUserStep::rehydrate()
+{
+ // NOOP, for now. We only save bools and there's nothing to check.
+}
+
+void XboxUserStep::perform()
+{
+ QString xbox_auth_template = R"XXX(
+{
+ "Properties": {
+ "AuthMethod": "RPS",
+ "SiteName": "user.auth.xboxlive.com",
+ "RpsTicket": "d=%1"
+ },
+ "RelyingParty": "http://auth.xboxlive.com",
+ "TokenType": "JWT"
+}
+)XXX";
+ auto xbox_auth_data = xbox_auth_template.arg(m_data->msaToken.token);
+
+ QNetworkRequest request = QNetworkRequest(
+ QUrl("https://user.auth.xboxlive.com/user/authenticate"));
+ request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
+ request.setRawHeader("Accept", "application/json");
+ auto* requestor = new AuthRequest(this);
+ connect(requestor, &AuthRequest::finished, this,
+ &XboxUserStep::onRequestDone);
+ requestor->post(request, xbox_auth_data.toUtf8());
+ qDebug() << "First layer of XBox auth ... commencing.";
+}
+
+void XboxUserStep::onRequestDone(QNetworkReply::NetworkError error,
+ QByteArray data,
+ QList<QNetworkReply::RawHeaderPair> headers)
+{
+ auto requestor = qobject_cast<AuthRequest*>(QObject::sender());
+ requestor->deleteLater();
+
+ if (error != QNetworkReply::NoError) {
+ qWarning() << "Reply error:" << error;
+ emit finished(AccountTaskState::STATE_FAILED_SOFT,
+ tr("XBox user authentication failed."));
+ return;
+ }
+
+ Katabasis::Token temp;
+ if (!Parsers::parseXTokenResponse(data, temp, "UToken")) {
+ qWarning() << "Could not parse user authentication response...";
+ emit finished(
+ AccountTaskState::STATE_FAILED_SOFT,
+ tr("XBox user authentication response could not be understood."));
+ return;
+ }
+ m_data->userToken = temp;
+ emit finished(AccountTaskState::STATE_WORKING, tr("Got Xbox user token"));
+}
diff --git a/meshmc/launcher/minecraft/auth/steps/XboxUserStep.h b/meshmc/launcher/minecraft/auth/steps/XboxUserStep.h
new file mode 100644
index 0000000000..d783b534c9
--- /dev/null
+++ b/meshmc/launcher/minecraft/auth/steps/XboxUserStep.h
@@ -0,0 +1,44 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+#include <QObject>
+
+#include "QObjectPtr.h"
+#include "minecraft/auth/AuthStep.h"
+
+class XboxUserStep : public AuthStep
+{
+ Q_OBJECT
+
+ public:
+ explicit XboxUserStep(AccountData* data);
+ virtual ~XboxUserStep() noexcept;
+
+ void perform() override;
+ void rehydrate() override;
+
+ QString describe() override;
+
+ private slots:
+ void onRequestDone(QNetworkReply::NetworkError, QByteArray,
+ QList<QNetworkReply::RawHeaderPair>);
+};
diff --git a/meshmc/launcher/minecraft/gameoptions/GameOptions.cpp b/meshmc/launcher/minecraft/gameoptions/GameOptions.cpp
new file mode 100644
index 0000000000..f799785543
--- /dev/null
+++ b/meshmc/launcher/minecraft/gameoptions/GameOptions.cpp
@@ -0,0 +1,155 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "GameOptions.h"
+#include "FileSystem.h"
+#include <QDebug>
+#include <QSaveFile>
+
+namespace
+{
+ bool load(const QString& path, std::vector<GameOptionItem>& contents,
+ int& version)
+ {
+ contents.clear();
+ QFile file(path);
+ if (!file.open(QFile::ReadOnly)) {
+ qWarning() << "Failed to read options file.";
+ return false;
+ }
+ version = 0;
+ while (!file.atEnd()) {
+ auto line = file.readLine();
+ if (line.endsWith('\n')) {
+ line.chop(1);
+ }
+ auto separatorIndex = line.indexOf(':');
+ if (separatorIndex == -1) {
+ continue;
+ }
+ auto key = QString::fromUtf8(line.data(), separatorIndex);
+ auto value = QString::fromUtf8(line.data() + separatorIndex + 1,
+ line.size() - 1 - separatorIndex);
+ qDebug() << "!!" << key << "!!";
+ if (key == "version") {
+ version = value.toInt();
+ continue;
+ }
+ contents.emplace_back(GameOptionItem{key, value});
+ }
+ qDebug() << "Loaded" << path << "with version:" << version;
+ return true;
+ }
+ bool save(const QString& path, std::vector<GameOptionItem>& mapping,
+ int version)
+ {
+ QSaveFile out(path);
+ if (!out.open(QIODevice::WriteOnly)) {
+ return false;
+ }
+ if (version != 0) {
+ QString versionLine = QString("version:%1\n").arg(version);
+ out.write(versionLine.toUtf8());
+ }
+ auto iter = mapping.begin();
+ while (iter != mapping.end()) {
+ out.write(iter->key.toUtf8());
+ out.write(":");
+ out.write(iter->value.toUtf8());
+ out.write("\n");
+ iter++;
+ }
+ return out.commit();
+ }
+} // namespace
+
+GameOptions::GameOptions(const QString& path) : path(path)
+{
+ reload();
+}
+
+QVariant GameOptions::headerData(int section, Qt::Orientation orientation,
+ int role) const
+{
+ if (role != Qt::DisplayRole) {
+ return QAbstractListModel::headerData(section, orientation, role);
+ }
+ switch (section) {
+ case 0:
+ return tr("Key");
+ case 1:
+ return tr("Value");
+ default:
+ return QVariant();
+ }
+}
+
+QVariant GameOptions::data(const QModelIndex& index, int role) const
+{
+ if (!index.isValid())
+ return QVariant();
+
+ int row = index.row();
+ int column = index.column();
+
+ if (row < 0 || row >= int(contents.size()))
+ return QVariant();
+
+ switch (role) {
+ case Qt::DisplayRole:
+ if (column == 0) {
+ return contents[row].key;
+ } else {
+ return contents[row].value;
+ }
+ default:
+ return QVariant();
+ }
+ return QVariant();
+}
+
+int GameOptions::rowCount(const QModelIndex&) const
+{
+ return contents.size();
+}
+
+int GameOptions::columnCount(const QModelIndex&) const
+{
+ return 2;
+}
+
+bool GameOptions::isLoaded() const
+{
+ return loaded;
+}
+
+bool GameOptions::reload()
+{
+ beginResetModel();
+ loaded = load(path, contents, version);
+ endResetModel();
+ return loaded;
+}
+
+bool GameOptions::save()
+{
+ return ::save(path, contents, version);
+}
diff --git a/meshmc/launcher/minecraft/gameoptions/GameOptions.h b/meshmc/launcher/minecraft/gameoptions/GameOptions.h
new file mode 100644
index 0000000000..69197178a7
--- /dev/null
+++ b/meshmc/launcher/minecraft/gameoptions/GameOptions.h
@@ -0,0 +1,56 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <map>
+#include <QString>
+#include <QAbstractListModel>
+
+struct GameOptionItem {
+ QString key;
+ QString value;
+};
+
+class GameOptions : public QAbstractListModel
+{
+ Q_OBJECT
+ public:
+ explicit GameOptions(const QString& path);
+ virtual ~GameOptions() = default;
+
+ int rowCount(const QModelIndex& parent = QModelIndex()) const override;
+ int columnCount(const QModelIndex& parent) const override;
+ QVariant data(const QModelIndex& index,
+ int role = Qt::DisplayRole) const override;
+ QVariant headerData(int section, Qt::Orientation orientation,
+ int role) const override;
+
+ bool isLoaded() const;
+ bool reload();
+ bool save();
+
+ private:
+ std::vector<GameOptionItem> contents;
+ bool loaded = false;
+ QString path;
+ int version = 0;
+};
diff --git a/meshmc/launcher/minecraft/launch/ClaimAccount.cpp b/meshmc/launcher/minecraft/launch/ClaimAccount.cpp
new file mode 100644
index 0000000000..a1ed6fdb69
--- /dev/null
+++ b/meshmc/launcher/minecraft/launch/ClaimAccount.cpp
@@ -0,0 +1,49 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "ClaimAccount.h"
+#include <launch/LaunchTask.h>
+
+#include "Application.h"
+#include "minecraft/auth/AccountList.h"
+
+ClaimAccount::ClaimAccount(LaunchTask* parent, AuthSessionPtr session)
+ : LaunchStep(parent)
+{
+ if (session->status == AuthSession::Status::PlayableOnline &&
+ !session->demo) {
+ 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/meshmc/launcher/minecraft/launch/ClaimAccount.h b/meshmc/launcher/minecraft/launch/ClaimAccount.h
new file mode 100644
index 0000000000..d3f64bd8ed
--- /dev/null
+++ b/meshmc/launcher/minecraft/launch/ClaimAccount.h
@@ -0,0 +1,61 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * 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 <launch/LaunchStep.h>
+#include <minecraft/auth/MinecraftAccount.h>
+
+class ClaimAccount : public LaunchStep
+{
+ Q_OBJECT
+ public:
+ explicit ClaimAccount(LaunchTask* parent, AuthSessionPtr session);
+ virtual ~ClaimAccount() {};
+
+ void executeTask() override;
+ void finalize() override;
+ bool canAbort() const override
+ {
+ return false;
+ }
+
+ private:
+ std::unique_ptr<UseLock> lock;
+ MinecraftAccountPtr m_account;
+};
diff --git a/meshmc/launcher/minecraft/launch/CreateGameFolders.cpp b/meshmc/launcher/minecraft/launch/CreateGameFolders.cpp
new file mode 100644
index 0000000000..32cd010905
--- /dev/null
+++ b/meshmc/launcher/minecraft/launch/CreateGameFolders.cpp
@@ -0,0 +1,50 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "CreateGameFolders.h"
+#include "minecraft/MinecraftInstance.h"
+#include "launch/LaunchTask.h"
+#include "FileSystem.h"
+
+CreateGameFolders::CreateGameFolders(LaunchTask* parent) : LaunchStep(parent) {}
+
+void CreateGameFolders::executeTask()
+{
+ auto instance = m_parent->instance();
+ std::shared_ptr<MinecraftInstance> minecraftInstance =
+ std::dynamic_pointer_cast<MinecraftInstance>(instance);
+
+ if (!FS::ensureFolderPathExists(minecraftInstance->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(
+ minecraftInstance->gameRoot(), "server-resource-packs"))) {
+ emit logLine("Couldn't create the 'server-resource-packs' folder",
+ MessageLevel::Error);
+ }
+ emitSucceeded();
+}
diff --git a/meshmc/launcher/minecraft/launch/CreateGameFolders.h b/meshmc/launcher/minecraft/launch/CreateGameFolders.h
new file mode 100644
index 0000000000..76a9225c21
--- /dev/null
+++ b/meshmc/launcher/minecraft/launch/CreateGameFolders.h
@@ -0,0 +1,58 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * 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 <launch/LaunchStep.h>
+#include <LoggedProcess.h>
+#include <minecraft/auth/AuthSession.h>
+
+// Create the main .minecraft for the instance and any other necessary folders
+class CreateGameFolders : public LaunchStep
+{
+ Q_OBJECT
+ public:
+ explicit CreateGameFolders(LaunchTask* parent);
+ virtual ~CreateGameFolders() {};
+
+ virtual void executeTask();
+ virtual bool canAbort() const
+ {
+ return false;
+ }
+};
diff --git a/meshmc/launcher/minecraft/launch/DirectJavaLaunch.cpp b/meshmc/launcher/minecraft/launch/DirectJavaLaunch.cpp
new file mode 100644
index 0000000000..c1dc249eac
--- /dev/null
+++ b/meshmc/launcher/minecraft/launch/DirectJavaLaunch.cpp
@@ -0,0 +1,173 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * 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 "DirectJavaLaunch.h"
+#include <launch/LaunchTask.h>
+#include <minecraft/MinecraftInstance.h>
+#include <FileSystem.h>
+#include <Commandline.h>
+#include <QStandardPaths>
+
+DirectJavaLaunch::DirectJavaLaunch(LaunchTask* parent) : LaunchStep(parent)
+{
+ connect(&m_process, &LoggedProcess::log, this, &DirectJavaLaunch::logLines);
+ connect(&m_process, &LoggedProcess::stateChanged, this,
+ &DirectJavaLaunch::on_state);
+}
+
+void DirectJavaLaunch::executeTask()
+{
+ auto instance = m_parent->instance();
+ std::shared_ptr<MinecraftInstance> minecraftInstance =
+ std::dynamic_pointer_cast<MinecraftInstance>(instance);
+ QStringList args = minecraftInstance->javaArguments();
+
+ args.append("-Djava.library.path=" + minecraftInstance->getNativePath());
+
+ auto classPathEntries = minecraftInstance->getClassPath();
+ args.append("-cp");
+ QString classpath;
+#ifdef Q_OS_WIN32
+ classpath = classPathEntries.join(';');
+#else
+ classpath = classPathEntries.join(':');
+#endif
+ args.append(classpath);
+ args.append(minecraftInstance->getMainClass());
+
+ QString allArgs = args.join(", ");
+ emit logLine("Java Arguments:\n[" + m_parent->censorPrivateInfo(allArgs) +
+ "]\n\n",
+ MessageLevel::MeshMC);
+
+ auto javaPath =
+ FS::ResolveExecutable(instance->settings()->get("JavaPath").toString());
+
+ m_process.setProcessEnvironment(instance->createEnvironment());
+
+ // make detachable - this will keep the process running even if the object
+ // is destroyed
+ m_process.setDetachable(true);
+
+ auto mcArgs =
+ minecraftInstance->processMinecraftArgs(m_session, m_serverToJoin);
+ args.append(mcArgs);
+
+ QString wrapperCommandStr = instance->getWrapperCommand().trimmed();
+ if (!wrapperCommandStr.isEmpty()) {
+ 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::MeshMC);
+ args.prepend(javaPath);
+ m_process.start(wrapperCommand, wrapperArgs + args);
+ } else {
+ m_process.start(javaPath, args);
+ }
+}
+
+void DirectJavaLaunch::on_state(LoggedProcess::State state)
+{
+ switch (state) {
+ case LoggedProcess::FailedToStart: {
+ //: Error message displayed if instance 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_parent->setPid(-1);
+ emitFailed(tr("Game crashed."));
+ return;
+ }
+ case LoggedProcess::Finished: {
+ m_parent->setPid(-1);
+ // 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;
+ }
+ // FIXME: make this work again
+ // m_postlaunchprocess.processEnvironment().insert("INST_EXITCODE",
+ // QString(exitCode)); run post-exit
+ emitSucceeded();
+ break;
+ }
+ case LoggedProcess::Running:
+ emit logLine(QString("Minecraft process ID: %1\n\n")
+ .arg(m_process.processId()),
+ MessageLevel::MeshMC);
+ m_parent->setPid(m_process.processId());
+ m_parent->instance()->setLastLaunch();
+ break;
+ default:
+ break;
+ }
+}
+
+void DirectJavaLaunch::setWorkingDirectory(const QString& wd)
+{
+ m_process.setWorkingDirectory(wd);
+}
+
+void DirectJavaLaunch::proceed()
+{
+ // nil
+}
+
+bool DirectJavaLaunch::abort()
+{
+ auto state = m_process.state();
+ if (state == LoggedProcess::Running || state == LoggedProcess::Starting) {
+ m_process.kill();
+ }
+ return true;
+}
diff --git a/meshmc/launcher/minecraft/launch/DirectJavaLaunch.h b/meshmc/launcher/minecraft/launch/DirectJavaLaunch.h
new file mode 100644
index 0000000000..f696e999ea
--- /dev/null
+++ b/meshmc/launcher/minecraft/launch/DirectJavaLaunch.h
@@ -0,0 +1,80 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * 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 <launch/LaunchStep.h>
+#include <LoggedProcess.h>
+#include <minecraft/auth/AuthSession.h>
+
+#include "MinecraftServerTarget.h"
+
+class DirectJavaLaunch : public LaunchStep
+{
+ Q_OBJECT
+ public:
+ explicit DirectJavaLaunch(LaunchTask* parent);
+ virtual ~DirectJavaLaunch() {};
+
+ 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 setServerToJoin(MinecraftServerTargetPtr serverToJoin)
+ {
+ m_serverToJoin = std::move(serverToJoin);
+ }
+
+ private slots:
+ void on_state(LoggedProcess::State state);
+
+ private:
+ LoggedProcess m_process;
+ QString m_command;
+ AuthSessionPtr m_session;
+ MinecraftServerTargetPtr m_serverToJoin;
+};
diff --git a/meshmc/launcher/minecraft/launch/ExtractNatives.cpp b/meshmc/launcher/minecraft/launch/ExtractNatives.cpp
new file mode 100644
index 0000000000..706820468b
--- /dev/null
+++ b/meshmc/launcher/minecraft/launch/ExtractNatives.cpp
@@ -0,0 +1,133 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * 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 "ExtractNatives.h"
+#include <minecraft/MinecraftInstance.h>
+#include <launch/LaunchTask.h>
+
+#include "MMCZip.h"
+#include "FileSystem.h"
+#include <QDir>
+
+#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 unzipNatives(QString source, QString targetFolder,
+ bool applyJnilibHack, bool nativeOpenAL,
+ bool nativeGLFW)
+{
+ QDir directory(targetFolder);
+ QStringList entries = MMCZip::listEntries(source);
+ if (entries.isEmpty()) {
+ return false;
+ }
+ for (const auto& name : entries) {
+ auto lowercase = name.toLower();
+ if (nativeGLFW && name.contains("glfw")) {
+ continue;
+ }
+ if (nativeOpenAL && name.contains("openal")) {
+ continue;
+ }
+ // Skip directories
+ if (name.endsWith('/'))
+ continue;
+
+ QString outName = name;
+ if (applyJnilibHack) {
+ outName = replaceSuffix(outName, ".jnilib", ".dylib");
+ }
+ QString absFilePath = directory.absoluteFilePath(outName);
+ if (!MMCZip::extractRelFile(source, name, absFilePath)) {
+ return false;
+ }
+ }
+ return true;
+}
+
+void ExtractNatives::executeTask()
+{
+ auto instance = m_parent->instance();
+ std::shared_ptr<MinecraftInstance> minecraftInstance =
+ std::dynamic_pointer_cast<MinecraftInstance>(instance);
+ auto toExtract = minecraftInstance->getNativeJars();
+ if (toExtract.isEmpty()) {
+ emitSucceeded();
+ return;
+ }
+ auto settings = minecraftInstance->settings();
+ bool nativeOpenAL = settings->get("UseNativeOpenAL").toBool();
+ bool nativeGLFW = settings->get("UseNativeGLFW").toBool();
+
+ auto outputPath = minecraftInstance->getNativePath();
+ auto javaVersion = minecraftInstance->getJavaVersion();
+ bool jniHackEnabled = javaVersion.major() >= 8;
+ for (const auto& source : toExtract) {
+ if (!unzipNatives(source, outputPath, jniHackEnabled, nativeOpenAL,
+ nativeGLFW)) {
+ 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_parent->instance();
+ QString target_dir = FS::PathCombine(instance->instanceRoot(), "natives/");
+ QDir dir(target_dir);
+ dir.removeRecursively();
+}
diff --git a/meshmc/launcher/minecraft/launch/ExtractNatives.h b/meshmc/launcher/minecraft/launch/ExtractNatives.h
new file mode 100644
index 0000000000..0550603f7e
--- /dev/null
+++ b/meshmc/launcher/minecraft/launch/ExtractNatives.h
@@ -0,0 +1,59 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * 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 <launch/LaunchStep.h>
+#include <memory>
+#include "minecraft/auth/AuthSession.h"
+
+// FIXME: temporary wrapper for existing task.
+class ExtractNatives : public LaunchStep
+{
+ Q_OBJECT
+ public:
+ explicit ExtractNatives(LaunchTask* parent) : LaunchStep(parent) {};
+ virtual ~ExtractNatives() {};
+
+ void executeTask() override;
+ bool canAbort() const override
+ {
+ return false;
+ }
+ void finalize() override;
+};
diff --git a/meshmc/launcher/minecraft/launch/MeshMCPartLaunch.cpp b/meshmc/launcher/minecraft/launch/MeshMCPartLaunch.cpp
new file mode 100644
index 0000000000..83f8c4436d
--- /dev/null
+++ b/meshmc/launcher/minecraft/launch/MeshMCPartLaunch.cpp
@@ -0,0 +1,237 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * 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 "MeshMCPartLaunch.h"
+
+#include <QStandardPaths>
+
+#include "launch/LaunchTask.h"
+#include "minecraft/MinecraftInstance.h"
+#include "FileSystem.h"
+#include "Commandline.h"
+#include "Application.h"
+
+MeshMCPartLaunch::MeshMCPartLaunch(LaunchTask* parent) : LaunchStep(parent)
+{
+ connect(&m_process, &LoggedProcess::log, this, &MeshMCPartLaunch::logLines);
+ connect(&m_process, &LoggedProcess::stateChanged, this,
+ &MeshMCPartLaunch::on_state);
+}
+
+#ifdef Q_OS_WIN
+// returns 8.3 file format from long path
+#include <windows.h>
+QString shortPathName(const QString& file)
+{
+ auto input = file.toStdWString();
+ std::wstring output;
+ long length = GetShortPathNameW(input.c_str(), NULL, 0);
+ // NOTE: this resizing might seem weird...
+ // when GetShortPathNameW fails, it returns length including null character
+ // when it succeeds, it returns length excluding null character
+ // See:
+ // https://msdn.microsoft.com/en-us/library/windows/desktop/aa364989(v=vs.85).aspx
+ output.resize(length);
+ GetShortPathNameW(input.c_str(), (LPWSTR)output.c_str(), length);
+ output.resize(length - 1);
+ QString ret = QString::fromStdWString(output);
+ return ret;
+}
+#endif
+
+// if the string survives roundtrip through local 8bit encoding...
+bool fitsInLocal8bit(const QString& string)
+{
+ return string == QString::fromLocal8Bit(string.toLocal8Bit());
+}
+
+void MeshMCPartLaunch::executeTask()
+{
+ auto instance = m_parent->instance();
+ std::shared_ptr<MinecraftInstance> minecraftInstance =
+ std::dynamic_pointer_cast<MinecraftInstance>(instance);
+
+ m_launchScript =
+ minecraftInstance->createLaunchScript(m_session, m_serverToJoin);
+ QStringList args = minecraftInstance->javaArguments();
+ QString allArgs = args.join(", ");
+ emit logLine("Java Arguments:\n[" + m_parent->censorPrivateInfo(allArgs) +
+ "]\n\n",
+ MessageLevel::MeshMC);
+
+ auto javaPath =
+ FS::ResolveExecutable(instance->settings()->get("JavaPath").toString());
+
+ m_process.setProcessEnvironment(instance->createEnvironment());
+
+ // make detachable - this will keep the process running even if the object
+ // is destroyed
+ m_process.setDetachable(true);
+
+ auto classPath = minecraftInstance->getClassPath();
+ classPath.prepend(
+ FS::PathCombine(APPLICATION->getJarsPath(), "NewLaunch.jar"));
+
+ auto natPath = minecraftInstance->getNativePath();
+#ifdef Q_OS_WIN
+ if (!fitsInLocal8bit(natPath)) {
+ args << "-Djava.library.path=" + shortPathName(natPath);
+ } else {
+ args << "-Djava.library.path=" + natPath;
+ }
+#else
+ args << "-Djava.library.path=" + natPath;
+#endif
+
+ args << "-cp";
+#ifdef Q_OS_WIN
+ QStringList processed;
+ for (auto& item : classPath) {
+ if (!fitsInLocal8bit(item)) {
+ processed << shortPathName(item);
+ } else {
+ processed << item;
+ }
+ }
+ args << processed.join(';');
+#else
+ args << classPath.join(':');
+#endif
+ args << "org.projecttick.EntryPoint";
+
+ qDebug() << args.join(' ');
+
+ QString wrapperCommandStr = instance->getWrapperCommand().trimmed();
+ if (!wrapperCommandStr.isEmpty()) {
+ 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::MeshMC);
+ args.prepend(javaPath);
+ m_process.start(wrapperCommand, wrapperArgs + args);
+ } else {
+ m_process.start(javaPath, args);
+ }
+}
+
+void MeshMCPartLaunch::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_parent->setPid(-1);
+ emitFailed(tr("Game crashed."));
+ return;
+ }
+ case LoggedProcess::Finished: {
+ m_parent->setPid(-1);
+ // 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;
+ }
+ // FIXME: make this work again
+ // m_postlaunchprocess.processEnvironment().insert("INST_EXITCODE",
+ // QString(exitCode)); run post-exit
+ emitSucceeded();
+ break;
+ }
+ case LoggedProcess::Running:
+ emit logLine(QString("Minecraft process ID: %1\n\n")
+ .arg(m_process.processId()),
+ MessageLevel::MeshMC);
+ m_parent->setPid(m_process.processId());
+ m_parent->instance()->setLastLaunch();
+ // send the launch script to MeshMC part
+ m_process.write(m_launchScript.toUtf8());
+
+ mayProceed = true;
+ emit readyForLaunch();
+ break;
+ default:
+ break;
+ }
+}
+
+void MeshMCPartLaunch::setWorkingDirectory(const QString& wd)
+{
+ m_process.setWorkingDirectory(wd);
+}
+
+void MeshMCPartLaunch::proceed()
+{
+ if (mayProceed) {
+ QString launchString("launch\n");
+ m_process.write(launchString.toUtf8());
+ mayProceed = false;
+ }
+}
+
+bool MeshMCPartLaunch::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/meshmc/launcher/minecraft/launch/MeshMCPartLaunch.h b/meshmc/launcher/minecraft/launch/MeshMCPartLaunch.h
new file mode 100644
index 0000000000..ff4285365f
--- /dev/null
+++ b/meshmc/launcher/minecraft/launch/MeshMCPartLaunch.h
@@ -0,0 +1,83 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * 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 <launch/LaunchStep.h>
+#include <LoggedProcess.h>
+#include <minecraft/auth/AuthSession.h>
+
+#include "MinecraftServerTarget.h"
+
+class MeshMCPartLaunch : public LaunchStep
+{
+ Q_OBJECT
+ public:
+ explicit MeshMCPartLaunch(LaunchTask* parent);
+ virtual ~MeshMCPartLaunch() {};
+
+ 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 setServerToJoin(MinecraftServerTargetPtr serverToJoin)
+ {
+ m_serverToJoin = std::move(serverToJoin);
+ }
+
+ private slots:
+ void on_state(LoggedProcess::State state);
+
+ private:
+ LoggedProcess m_process;
+ QString m_command;
+ AuthSessionPtr m_session;
+ QString m_launchScript;
+ MinecraftServerTargetPtr m_serverToJoin;
+
+ bool mayProceed = false;
+};
diff --git a/meshmc/launcher/minecraft/launch/MinecraftServerTarget.cpp b/meshmc/launcher/minecraft/launch/MinecraftServerTarget.cpp
new file mode 100644
index 0000000000..a3443ce6a0
--- /dev/null
+++ b/meshmc/launcher/minecraft/launch/MinecraftServerTarget.cpp
@@ -0,0 +1,85 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * 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 "MinecraftServerTarget.h"
+
+#include <QStringList>
+
+// FIXME: the way this is written, it can't ever do any sort of validation and
+// can accept total junk
+MinecraftServerTarget MinecraftServerTarget::parse(const QString& fullAddress)
+{
+ QStringList split = fullAddress.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 MeshMC when entering
+ // server addresses.
+ if (fullAddress.startsWith("[")) {
+ int bracket = fullAddress.indexOf("]");
+ if (bracket > 0) {
+ QString ipv6 = fullAddress.mid(1, bracket - 1);
+ QString port = fullAddress.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({fullAddress});
+ }
+
+ QString realAddress = split[0];
+
+ quint16 realPort = 25565;
+ if (split.size() > 1) {
+ bool ok;
+ realPort = split[1].toUInt(&ok);
+
+ if (!ok) {
+ realPort = 25565;
+ }
+ }
+
+ return MinecraftServerTarget{realAddress, realPort};
+}
diff --git a/meshmc/launcher/minecraft/launch/MinecraftServerTarget.h b/meshmc/launcher/minecraft/launch/MinecraftServerTarget.h
new file mode 100644
index 0000000000..ae453d637d
--- /dev/null
+++ b/meshmc/launcher/minecraft/launch/MinecraftServerTarget.h
@@ -0,0 +1,52 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * 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>
+
+struct MinecraftServerTarget {
+ QString address;
+ quint16 port;
+
+ static MinecraftServerTarget parse(const QString& fullAddress);
+};
+
+typedef std::shared_ptr<MinecraftServerTarget> MinecraftServerTargetPtr;
diff --git a/meshmc/launcher/minecraft/launch/ModMinecraftJar.cpp b/meshmc/launcher/minecraft/launch/ModMinecraftJar.cpp
new file mode 100644
index 0000000000..72da25ec64
--- /dev/null
+++ b/meshmc/launcher/minecraft/launch/ModMinecraftJar.cpp
@@ -0,0 +1,103 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * 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.h"
+#include "launch/LaunchTask.h"
+#include "MMCZip.h"
+#include "minecraft/OpSys.h"
+#include "FileSystem.h"
+#include "minecraft/MinecraftInstance.h"
+#include "minecraft/PackProfile.h"
+
+void ModMinecraftJar::executeTask()
+{
+ auto m_inst =
+ std::dynamic_pointer_cast<MinecraftInstance>(m_parent->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(currentSystem, 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 =
+ std::dynamic_pointer_cast<MinecraftInstance>(m_parent->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/meshmc/launcher/minecraft/launch/ModMinecraftJar.h b/meshmc/launcher/minecraft/launch/ModMinecraftJar.h
new file mode 100644
index 0000000000..9453dd233e
--- /dev/null
+++ b/meshmc/launcher/minecraft/launch/ModMinecraftJar.h
@@ -0,0 +1,60 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * 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 <launch/LaunchStep.h>
+#include <memory>
+
+class ModMinecraftJar : public LaunchStep
+{
+ Q_OBJECT
+ public:
+ explicit ModMinecraftJar(LaunchTask* parent) : LaunchStep(parent) {};
+ virtual ~ModMinecraftJar() {};
+
+ virtual void executeTask() override;
+ virtual bool canAbort() const override
+ {
+ return false;
+ }
+ void finalize() override;
+
+ private:
+ bool removeJar();
+};
diff --git a/meshmc/launcher/minecraft/launch/PrintInstanceInfo.cpp b/meshmc/launcher/minecraft/launch/PrintInstanceInfo.cpp
new file mode 100644
index 0000000000..b7fabeb606
--- /dev/null
+++ b/meshmc/launcher/minecraft/launch/PrintInstanceInfo.cpp
@@ -0,0 +1,166 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * 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 <fstream>
+#include <string>
+
+#include "PrintInstanceInfo.h"
+#include <launch/LaunchTask.h>
+
+#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD)
+namespace
+{
+#if defined(Q_OS_LINUX)
+ void probeProcCpuinfo(QStringList& log)
+ {
+ std::ifstream cpuin("/proc/cpuinfo");
+ for (std::string line; std::getline(cpuin, line);) {
+ if (strncmp(line.c_str(), "model name", 10) == 0) {
+ log << QString::fromStdString(
+ line.substr(13, std::string::npos));
+ break;
+ }
+ }
+ }
+
+ void runLspci(QStringList& log)
+ {
+ // FIXME: fixed size buffers...
+ char buff[512];
+ int gpuline = -1;
+ int cline = 0;
+ FILE* lspci = popen("lspci -k", "r");
+
+ if (!lspci)
+ return;
+
+ while (fgets(buff, 512, lspci) != NULL) {
+ std::string str(buff);
+ if (str.length() < 9)
+ continue;
+ if (str.substr(8, 3) == "VGA") {
+ gpuline = cline;
+ log << QString::fromStdString(
+ str.substr(35, std::string::npos));
+ }
+ if (gpuline > -1 && gpuline != cline) {
+ if (cline - gpuline < 3) {
+ log << QString::fromStdString(
+ str.substr(1, std::string::npos));
+ }
+ }
+ cline++;
+ }
+ pclose(lspci);
+ }
+#elif defined(Q_OS_FREEBSD)
+ void runSysctlHwModel(QStringList& log)
+ {
+ char buff[512];
+ FILE* hwmodel = popen("sysctl hw.model", "r");
+ while (fgets(buff, 512, hwmodel) != NULL) {
+ 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) != NULL) {
+ 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);
+ }
+#endif
+ void runGlxinfo(QStringList& log)
+ {
+ // FIXME: fixed size buffers...
+ char buff[512];
+ FILE* glxinfo = popen("glxinfo", "r");
+ if (!glxinfo)
+ return;
+
+ while (fgets(buff, 512, glxinfo) != NULL) {
+ if (strncmp(buff, "OpenGL version string:", 22) == 0) {
+ log << QString::fromUtf8(buff);
+ break;
+ }
+ }
+ pclose(glxinfo);
+ }
+
+} // namespace
+#endif
+
+void PrintInstanceInfo::executeTask()
+{
+ auto instance = m_parent->instance();
+ QStringList log;
+
+#if defined(Q_OS_LINUX)
+ ::probeProcCpuinfo(log);
+ ::runLspci(log);
+ ::runGlxinfo(log);
+#elif defined(Q_OS_FREEBSD)
+ ::runSysctlHwModel(log);
+ ::runPciconf(log);
+ ::runGlxinfo(log);
+#endif
+
+ logLines(log, MessageLevel::MeshMC);
+ logLines(instance->verboseDescription(m_session, m_serverToJoin),
+ MessageLevel::MeshMC);
+ emitSucceeded();
+}
diff --git a/meshmc/launcher/minecraft/launch/PrintInstanceInfo.h b/meshmc/launcher/minecraft/launch/PrintInstanceInfo.h
new file mode 100644
index 0000000000..572c5c84c7
--- /dev/null
+++ b/meshmc/launcher/minecraft/launch/PrintInstanceInfo.h
@@ -0,0 +1,66 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * 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 <launch/LaunchStep.h>
+#include <memory>
+#include "minecraft/auth/AuthSession.h"
+#include "minecraft/launch/MinecraftServerTarget.h"
+
+// FIXME: temporary wrapper for existing task.
+class PrintInstanceInfo : public LaunchStep
+{
+ Q_OBJECT
+ public:
+ explicit PrintInstanceInfo(LaunchTask* parent, AuthSessionPtr session,
+ MinecraftServerTargetPtr serverToJoin)
+ : LaunchStep(parent), m_session(session),
+ m_serverToJoin(serverToJoin) {};
+ virtual ~PrintInstanceInfo() {};
+
+ virtual void executeTask();
+ virtual bool canAbort() const
+ {
+ return false;
+ }
+
+ private:
+ AuthSessionPtr m_session;
+ MinecraftServerTargetPtr m_serverToJoin;
+};
diff --git a/meshmc/launcher/minecraft/launch/ReconstructAssets.cpp b/meshmc/launcher/minecraft/launch/ReconstructAssets.cpp
new file mode 100644
index 0000000000..95c9bedda3
--- /dev/null
+++ b/meshmc/launcher/minecraft/launch/ReconstructAssets.cpp
@@ -0,0 +1,61 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * 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 "ReconstructAssets.h"
+#include "minecraft/MinecraftInstance.h"
+#include "minecraft/PackProfile.h"
+#include "minecraft/AssetsUtils.h"
+#include "launch/LaunchTask.h"
+
+void ReconstructAssets::executeTask()
+{
+ auto instance = m_parent->instance();
+ std::shared_ptr<MinecraftInstance> minecraftInstance =
+ std::dynamic_pointer_cast<MinecraftInstance>(instance);
+ auto components = minecraftInstance->getPackProfile();
+ auto profile = components->getProfile();
+ auto assets = profile->getMinecraftAssets();
+
+ if (!AssetsUtils::reconstructAssets(assets->id,
+ minecraftInstance->resourcesDir())) {
+ emit logLine("Failed to reconstruct Minecraft assets.",
+ MessageLevel::Error);
+ }
+
+ emitSucceeded();
+}
diff --git a/meshmc/launcher/minecraft/launch/ReconstructAssets.h b/meshmc/launcher/minecraft/launch/ReconstructAssets.h
new file mode 100644
index 0000000000..1f080485c0
--- /dev/null
+++ b/meshmc/launcher/minecraft/launch/ReconstructAssets.h
@@ -0,0 +1,56 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * 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 <launch/LaunchStep.h>
+#include <memory>
+
+class ReconstructAssets : public LaunchStep
+{
+ Q_OBJECT
+ public:
+ explicit ReconstructAssets(LaunchTask* parent) : LaunchStep(parent) {};
+ virtual ~ReconstructAssets() {};
+
+ void executeTask() override;
+ bool canAbort() const override
+ {
+ return false;
+ }
+};
diff --git a/meshmc/launcher/minecraft/launch/ScanModFolders.cpp b/meshmc/launcher/minecraft/launch/ScanModFolders.cpp
new file mode 100644
index 0000000000..e9829718c7
--- /dev/null
+++ b/meshmc/launcher/minecraft/launch/ScanModFolders.cpp
@@ -0,0 +1,85 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * 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.h"
+#include "launch/LaunchTask.h"
+#include "MMCZip.h"
+#include "minecraft/OpSys.h"
+#include "FileSystem.h"
+#include "minecraft/MinecraftInstance.h"
+#include "minecraft/mod/ModFolderModel.h"
+
+void ScanModFolders::executeTask()
+{
+ auto m_inst =
+ std::dynamic_pointer_cast<MinecraftInstance>(m_parent->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;
+ }
+ checkDone();
+}
+
+void ScanModFolders::modsDone()
+{
+ m_modsDone = true;
+ checkDone();
+}
+
+void ScanModFolders::coreModsDone()
+{
+ m_coreModsDone = true;
+ checkDone();
+}
+
+void ScanModFolders::checkDone()
+{
+ if (m_modsDone && m_coreModsDone) {
+ emitSucceeded();
+ }
+}
diff --git a/meshmc/launcher/minecraft/launch/ScanModFolders.h b/meshmc/launcher/minecraft/launch/ScanModFolders.h
new file mode 100644
index 0000000000..e572022c90
--- /dev/null
+++ b/meshmc/launcher/minecraft/launch/ScanModFolders.h
@@ -0,0 +1,66 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * 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 <launch/LaunchStep.h>
+#include <memory>
+
+class ScanModFolders : public LaunchStep
+{
+ Q_OBJECT
+ public:
+ explicit ScanModFolders(LaunchTask* parent) : LaunchStep(parent) {};
+ virtual ~ScanModFolders() {};
+
+ virtual void executeTask() override;
+ virtual bool canAbort() const override
+ {
+ return false;
+ }
+ private slots:
+ void coreModsDone();
+ void modsDone();
+
+ private:
+ void checkDone();
+
+ private: // DATA
+ bool m_modsDone = false;
+ bool m_coreModsDone = false;
+};
diff --git a/meshmc/launcher/minecraft/launch/VerifyJavaInstall.cpp b/meshmc/launcher/minecraft/launch/VerifyJavaInstall.cpp
new file mode 100644
index 0000000000..0f58b5efa4
--- /dev/null
+++ b/meshmc/launcher/minecraft/launch/VerifyJavaInstall.cpp
@@ -0,0 +1,374 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "VerifyJavaInstall.h"
+
+#include <QDir>
+#include <QDirIterator>
+#include <QFileInfo>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonArray>
+#include <QProcess>
+#include <QRegularExpression>
+
+#include <launch/LaunchTask.h>
+#include <minecraft/MinecraftInstance.h>
+#include <minecraft/PackProfile.h>
+#include <minecraft/VersionFilterData.h>
+
+#include "Application.h"
+#include "FileSystem.h"
+#include "Json.h"
+#include "java/JavaUtils.h"
+
+#ifndef MeshMC_DISABLE_JAVA_DOWNLOADER
+#include "BuildConfig.h"
+#include "net/Download.h"
+#endif
+
+#ifdef major
+#undef major
+#endif
+#ifdef minor
+#undef minor
+#endif
+
+namespace
+{
+ std::optional<JavaVersion> probeJavaVersion(const QString& javaPath)
+ {
+ const auto checkerJar =
+ FS::PathCombine(APPLICATION->getJarsPath(), "JavaCheck.jar");
+ if (!QFileInfo::exists(checkerJar)) {
+ return std::nullopt;
+ }
+
+ QProcess process;
+ process.setProgram(javaPath);
+ process.setArguments({"-jar", checkerJar});
+ process.setProcessEnvironment(CleanEnviroment());
+ process.setProcessChannelMode(QProcess::SeparateChannels);
+ process.start();
+
+ if (!process.waitForFinished(15000) ||
+ process.exitStatus() != QProcess::NormalExit ||
+ process.exitCode() != 0) {
+ return std::nullopt;
+ }
+
+ const auto stdoutData =
+ QString::fromLocal8Bit(process.readAllStandardOutput());
+ const auto lines = stdoutData.split('\n', Qt::SkipEmptyParts);
+ for (const auto& rawLine : lines) {
+ const auto line = rawLine.trimmed();
+ if (!line.startsWith("java.version=")) {
+ continue;
+ }
+ return JavaVersion(line.mid(QString("java.version=").size()));
+ }
+
+ return std::nullopt;
+ }
+} // namespace
+
+int VerifyJavaInstall::determineRequiredJavaMajor() const
+{
+ auto m_inst =
+ std::dynamic_pointer_cast<MinecraftInstance>(m_parent->instance());
+ auto minecraftComponent =
+ m_inst->getPackProfile()->getComponent("net.minecraft");
+
+ if (minecraftComponent->getReleaseDateTime() >=
+ g_VersionFilterData.java25BeginsDate)
+ return 25;
+ if (minecraftComponent->getReleaseDateTime() >=
+ g_VersionFilterData.java21BeginsDate)
+ return 21;
+ if (minecraftComponent->getReleaseDateTime() >=
+ g_VersionFilterData.java17BeginsDate)
+ return 17;
+ if (minecraftComponent->getReleaseDateTime() >=
+ g_VersionFilterData.java16BeginsDate)
+ return 16;
+ if (minecraftComponent->getReleaseDateTime() >=
+ g_VersionFilterData.java8BeginsDate)
+ return 8;
+ return 0;
+}
+
+QString VerifyJavaInstall::javaInstallDir() const
+{
+ return JavaUtils::managedJavaRoot();
+}
+
+QString VerifyJavaInstall::findInstalledJava(int requiredMajor) const
+{
+ JavaUtils javaUtils;
+ QList<QString> systemJavas = javaUtils.FindJavaPaths();
+ QSet<QString> seenPaths;
+ for (const QString& javaPath : systemJavas) {
+ QString resolved = FS::ResolveExecutable(javaPath);
+ if (resolved.isEmpty() || seenPaths.contains(resolved))
+ continue;
+
+ seenPaths.insert(resolved);
+ const auto version = probeJavaVersion(resolved);
+ if (version.has_value() && version->major() >= requiredMajor) {
+ return resolved;
+ }
+ }
+
+ return {};
+}
+
+void VerifyJavaInstall::executeTask()
+{
+ auto m_inst =
+ std::dynamic_pointer_cast<MinecraftInstance>(m_parent->instance());
+
+ auto javaVersion = m_inst->getJavaVersion();
+ int requiredMajor = determineRequiredJavaMajor();
+
+ // No Java requirement or already met
+ if (requiredMajor == 0 || javaVersion.major() >= requiredMajor) {
+ emitSucceeded();
+ return;
+ }
+
+ // Java version insufficient — try to find an already-downloaded one
+ emit logLine(
+ tr("Current Java version %1 does not meet the requirement of Java %2.")
+ .arg(javaVersion.toString())
+ .arg(requiredMajor),
+ MessageLevel::Warning);
+
+ QString existingJava = findInstalledJava(requiredMajor);
+ if (!existingJava.isEmpty()) {
+ emit logLine(tr("Found installed Java %1 at: %2")
+ .arg(requiredMajor)
+ .arg(existingJava),
+ MessageLevel::MeshMC);
+#ifndef MeshMC_DISABLE_JAVA_DOWNLOADER
+ setJavaPathAndSucceed(existingJava);
+#else
+ m_inst->settings()->set("OverrideJavaLocation", true);
+ m_inst->settings()->set("JavaPath", existingJava);
+ emit logLine(tr("Java path set to: %1").arg(existingJava),
+ MessageLevel::MeshMC);
+ emitSucceeded();
+#endif
+ return;
+ }
+
+#ifndef MeshMC_DISABLE_JAVA_DOWNLOADER
+ // Not found — auto-download
+ emit logLine(
+ tr("No installed Java %1 found. Downloading...").arg(requiredMajor),
+ MessageLevel::MeshMC);
+ autoDownloadJava(requiredMajor);
+#else
+ emitFailed(
+ tr("Java %1 is required but not installed. Please install it manually.")
+ .arg(requiredMajor));
+#endif
+}
+
+#ifndef MeshMC_DISABLE_JAVA_DOWNLOADER
+void VerifyJavaInstall::autoDownloadJava(int requiredMajor)
+{
+ // Fetch version list from net.minecraft.java (Mojang)
+ fetchVersionList(requiredMajor);
+}
+
+void VerifyJavaInstall::fetchVersionList(int requiredMajor)
+{
+ m_fetchData.clear();
+ QString uid = "net.minecraft.java";
+ QString url = QString("%1%2/index.json").arg(BuildConfig.META_URL, uid);
+
+ m_fetchJob = new NetJob(tr("Fetch Java versions"), APPLICATION->network());
+ auto dl = Net::Download::makeByteArray(QUrl(url), &m_fetchData);
+ m_fetchJob->addNetAction(dl);
+
+ connect(
+ m_fetchJob.get(), &NetJob::succeeded, this,
+ [this, uid, requiredMajor]() {
+ m_fetchJob.reset();
+
+ QJsonDocument doc;
+ try {
+ doc = Json::requireDocument(m_fetchData);
+ } catch (const Exception& e) {
+ emitFailed(
+ tr("Failed to parse Java version list from meta server: %1")
+ .arg(e.cause()));
+ return;
+ }
+ if (!doc.isObject()) {
+ emitFailed(
+ tr("Failed to parse Java version list from meta server."));
+ return;
+ }
+
+ auto versions = JavaDownload::parseVersionIndex(doc.object(), uid);
+
+ // Find the matching version (e.g., "java25" for requiredMajor=25)
+ QString targetVersionId = QString("java%1").arg(requiredMajor);
+ bool found = false;
+ for (const auto& ver : versions) {
+ if (ver.versionId == targetVersionId) {
+ found = true;
+ fetchRuntimes(ver.versionId, requiredMajor);
+ return;
+ }
+ }
+
+ if (!found) {
+ emitFailed(tr("Java %1 is not available for download from "
+ "Mojang. Please install it manually.")
+ .arg(requiredMajor));
+ }
+ });
+
+ connect(m_fetchJob.get(), &NetJob::failed, this,
+ [this, requiredMajor](QString reason) {
+ emitFailed(tr("Failed to fetch Java version list: %1. Please "
+ "install Java %2 manually.")
+ .arg(reason)
+ .arg(requiredMajor));
+ m_fetchJob.reset();
+ });
+
+ m_fetchJob->start();
+}
+
+void VerifyJavaInstall::fetchRuntimes(const QString& versionId,
+ int requiredMajor)
+{
+ m_fetchData.clear();
+ QString uid = "net.minecraft.java";
+ QString url =
+ QString("%1%2/%3.json").arg(BuildConfig.META_URL, uid, versionId);
+
+ m_fetchJob =
+ new NetJob(tr("Fetch Java runtime details"), APPLICATION->network());
+ auto dl = Net::Download::makeByteArray(QUrl(url), &m_fetchData);
+ m_fetchJob->addNetAction(dl);
+
+ connect(m_fetchJob.get(), &NetJob::succeeded, this,
+ [this, requiredMajor]() {
+ auto fetchJob = std::move(m_fetchJob);
+
+ QJsonDocument doc;
+ try {
+ doc = Json::requireDocument(m_fetchData);
+ } catch (const Exception& e) {
+ emitFailed(tr("Failed to parse Java runtime details: %1")
+ .arg(e.cause()));
+ return;
+ }
+ if (!doc.isObject()) {
+ emitFailed(tr("Failed to parse Java runtime details."));
+ return;
+ }
+
+ auto allRuntimes = JavaDownload::parseRuntimes(doc.object());
+ QString myOS = JavaDownload::currentRuntimeOS();
+
+ // Filter for current platform
+ for (const auto& rt : allRuntimes) {
+ if (rt.runtimeOS == myOS) {
+ emit logLine(tr("Downloading %1 (%2)...")
+ .arg(rt.name, rt.version.toString()),
+ MessageLevel::MeshMC);
+ startDownload(rt, requiredMajor);
+ return;
+ }
+ }
+
+ emitFailed(tr("No Java %1 download available for your platform "
+ "(%2). Please install it manually.")
+ .arg(requiredMajor)
+ .arg(myOS));
+ });
+
+ connect(m_fetchJob.get(), &NetJob::failed, this, [this](QString reason) {
+ emitFailed(tr("Failed to fetch Java runtime details: %1").arg(reason));
+ m_fetchJob.reset();
+ });
+
+ m_fetchJob->start();
+}
+
+void VerifyJavaInstall::startDownload(const JavaDownload::RuntimeEntry& runtime,
+ int requiredMajor)
+{
+ QString dirName =
+ QString("%1-%2").arg(runtime.name, runtime.version.toString());
+ QString targetDir =
+ FS::PathCombine(javaInstallDir(), runtime.vendor, dirName);
+
+ m_downloadTask =
+ std::make_unique<JavaDownloadTask>(runtime, targetDir, this);
+
+ connect(m_downloadTask.get(), &Task::succeeded, this, [this]() {
+ QString javaPath = m_downloadTask->installedJavaPath();
+
+ if (javaPath.isEmpty()) {
+ emitFailed(
+ tr("Java was downloaded but the binary could not be found."));
+ return;
+ }
+
+ emit logLine(tr("Java downloaded and installed at: %1").arg(javaPath),
+ MessageLevel::MeshMC);
+ setJavaPathAndSucceed(javaPath);
+ });
+
+ connect(m_downloadTask.get(), &Task::failed, this,
+ [this, requiredMajor](const QString& reason) {
+ emitFailed(tr("Failed to download Java %1: %2")
+ .arg(requiredMajor)
+ .arg(reason));
+ m_downloadTask.reset();
+ });
+
+ connect(m_downloadTask.get(), &Task::status, this,
+ [this](const QString& status) {
+ emit logLine(status, MessageLevel::MeshMC);
+ });
+
+ m_downloadTask->start();
+}
+
+void VerifyJavaInstall::setJavaPathAndSucceed(const QString& javaPath)
+{
+ auto m_inst =
+ std::dynamic_pointer_cast<MinecraftInstance>(m_parent->instance());
+ // Set Java path override on the instance only, not globally
+ m_inst->settings()->set("OverrideJavaLocation", true);
+ m_inst->settings()->set("JavaPath", javaPath);
+ emit logLine(tr("Java path set to: %1").arg(javaPath),
+ MessageLevel::MeshMC);
+ emitSucceeded();
+}
+#endif
diff --git a/meshmc/launcher/minecraft/launch/VerifyJavaInstall.h b/meshmc/launcher/minecraft/launch/VerifyJavaInstall.h
new file mode 100644
index 0000000000..f54717e403
--- /dev/null
+++ b/meshmc/launcher/minecraft/launch/VerifyJavaInstall.h
@@ -0,0 +1,61 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <launch/LaunchStep.h>
+#ifndef MeshMC_DISABLE_JAVA_DOWNLOADER
+#include "java/download/JavaRuntime.h"
+#include "java/download/JavaDownloadTask.h"
+#endif
+#include "net/NetJob.h"
+
+class VerifyJavaInstall : public LaunchStep
+{
+ Q_OBJECT
+
+ public:
+ explicit VerifyJavaInstall(LaunchTask* parent) : LaunchStep(parent) {};
+ ~VerifyJavaInstall() override = default;
+
+ void executeTask() override;
+ bool canAbort() const override
+ {
+ return false;
+ }
+
+ private:
+ int determineRequiredJavaMajor() const;
+ QString findInstalledJava(int requiredMajor) const;
+ QString javaInstallDir() const;
+#ifndef MeshMC_DISABLE_JAVA_DOWNLOADER
+ void autoDownloadJava(int requiredMajor);
+ void fetchVersionList(int requiredMajor);
+ void fetchRuntimes(const QString& versionId, int requiredMajor);
+ void startDownload(const JavaDownload::RuntimeEntry& runtime,
+ int requiredMajor);
+ void setJavaPathAndSucceed(const QString& javaPath);
+
+ NetJob::Ptr m_fetchJob;
+ QByteArray m_fetchData;
+ std::unique_ptr<JavaDownloadTask> m_downloadTask;
+#endif
+};
diff --git a/meshmc/launcher/minecraft/legacy/LegacyInstance.cpp b/meshmc/launcher/minecraft/legacy/LegacyInstance.cpp
new file mode 100644
index 0000000000..c010974481
--- /dev/null
+++ b/meshmc/launcher/minecraft/legacy/LegacyInstance.cpp
@@ -0,0 +1,270 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * 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 <QFileInfo>
+#include <minecraft/launch/MeshMCPartLaunch.h>
+#include <QDir>
+#include <settings/Setting.h>
+
+#include "LegacyInstance.h"
+
+#include "minecraft/legacy/LegacyModList.h"
+#include "minecraft/WorldList.h"
+#include <MMCZip.h>
+#include <FileSystem.h>
+
+LegacyInstance::LegacyInstance(SettingsObjectPtr globalSettings,
+ SettingsObjectPtr settings,
+ const QString& rootDir)
+ : BaseInstance(globalSettings, settings, rootDir)
+{
+ settings->registerSetting("NeedsRebuild", true);
+ settings->registerSetting("ShouldUpdate", false);
+ settings->registerSetting("JarVersion", QString());
+ settings->registerSetting("IntendedJarVersion", QString());
+ /*
+ * custom base jar has no default. it is determined in code... see the
+ * accessor methods for it
+ *
+ * for instances that DO NOT have the CustomBaseJar setting (legacy
+ * instances),
+ * [.]minecraft/bin/mcbackup.jar is the default base jar
+ */
+ settings->registerSetting("UseCustomBaseJar", true);
+ settings->registerSetting("CustomBaseJar", "");
+}
+
+QString LegacyInstance::mainJarToPreserve() const
+{
+ bool customJar = m_settings->get("UseCustomBaseJar").toBool();
+ if (customJar) {
+ auto base = baseJar();
+ if (QFile::exists(base)) {
+ return base;
+ }
+ }
+ auto runnable = runnableJar();
+ if (QFile::exists(runnable)) {
+ return runnable;
+ }
+ return QString();
+}
+
+QString LegacyInstance::baseJar() const
+{
+ bool customJar = m_settings->get("UseCustomBaseJar").toBool();
+ if (customJar) {
+ return customBaseJar();
+ } else
+ return defaultBaseJar();
+}
+
+QString LegacyInstance::customBaseJar() const
+{
+ QString value = m_settings->get("CustomBaseJar").toString();
+ if (value.isNull() || value.isEmpty()) {
+ return defaultCustomBaseJar();
+ }
+ return value;
+}
+
+bool LegacyInstance::shouldUseCustomBaseJar() const
+{
+ return m_settings->get("UseCustomBaseJar").toBool();
+}
+
+Task::Ptr LegacyInstance::createUpdateTask(Net::Mode)
+{
+ return nullptr;
+}
+
+std::shared_ptr<LegacyModList> LegacyInstance::jarModList() const
+{
+ if (!jar_mod_list) {
+ auto list = new LegacyModList(jarModsDir(), modListFile());
+ jar_mod_list.reset(list);
+ }
+ jar_mod_list->update();
+ return jar_mod_list;
+}
+
+QString LegacyInstance::gameRoot() const
+{
+ QFileInfo mcDir(FS::PathCombine(instanceRoot(), "minecraft"));
+ QFileInfo dotMCDir(FS::PathCombine(instanceRoot(), ".minecraft"));
+
+ if (mcDir.exists() && !dotMCDir.exists())
+ return mcDir.filePath();
+ else
+ return dotMCDir.filePath();
+}
+
+QString LegacyInstance::binRoot() const
+{
+ return FS::PathCombine(gameRoot(), "bin");
+}
+
+QString LegacyInstance::modsRoot() const
+{
+ return FS::PathCombine(gameRoot(), "mods");
+}
+
+QString LegacyInstance::jarModsDir() const
+{
+ return FS::PathCombine(instanceRoot(), "instMods");
+}
+
+QString LegacyInstance::libDir() const
+{
+ return FS::PathCombine(gameRoot(), "lib");
+}
+
+QString LegacyInstance::savesDir() const
+{
+ return FS::PathCombine(gameRoot(), "saves");
+}
+
+QString LegacyInstance::coreModsDir() const
+{
+ return FS::PathCombine(gameRoot(), "coremods");
+}
+
+QString LegacyInstance::resourceDir() const
+{
+ return FS::PathCombine(gameRoot(), "resources");
+}
+QString LegacyInstance::texturePacksDir() const
+{
+ return FS::PathCombine(gameRoot(), "texturepacks");
+}
+
+QString LegacyInstance::runnableJar() const
+{
+ return FS::PathCombine(binRoot(), "minecraft.jar");
+}
+
+QString LegacyInstance::modListFile() const
+{
+ return FS::PathCombine(instanceRoot(), "modlist");
+}
+
+QString LegacyInstance::instanceConfigFolder() const
+{
+ return FS::PathCombine(gameRoot(), "config");
+}
+
+bool LegacyInstance::shouldRebuild() const
+{
+ return m_settings->get("NeedsRebuild").toBool();
+}
+
+QString LegacyInstance::currentVersionId() const
+{
+ return m_settings->get("JarVersion").toString();
+}
+
+QString LegacyInstance::intendedVersionId() const
+{
+ return m_settings->get("IntendedJarVersion").toString();
+}
+
+bool LegacyInstance::shouldUpdate() const
+{
+ QVariant var = settings()->get("ShouldUpdate");
+ if (!var.isValid() || var.toBool() == false) {
+ return intendedVersionId() != currentVersionId();
+ }
+ return true;
+}
+
+QString LegacyInstance::defaultBaseJar() const
+{
+ return "versions/" + intendedVersionId() + "/" + intendedVersionId() +
+ ".jar";
+}
+
+QString LegacyInstance::defaultCustomBaseJar() const
+{
+ return FS::PathCombine(binRoot(), "mcbackup.jar");
+}
+
+std::shared_ptr<WorldList> LegacyInstance::worldList() const
+{
+ if (!m_world_list) {
+ m_world_list.reset(new WorldList(savesDir()));
+ }
+ return m_world_list;
+}
+
+QString LegacyInstance::typeName() const
+{
+ return tr("Legacy");
+}
+
+QString LegacyInstance::getStatusbarDescription()
+{
+ return tr("Instance from previous versions.");
+}
+
+QStringList
+LegacyInstance::verboseDescription(AuthSessionPtr session,
+ MinecraftServerTargetPtr serverToJoin)
+{
+ QStringList out;
+
+ auto alltraits = traits();
+ if (alltraits.size()) {
+ out << "Traits:";
+ for (auto trait : alltraits) {
+ out << " " + trait;
+ }
+ out << "";
+ }
+
+ 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 << "";
+ return out;
+}
diff --git a/meshmc/launcher/minecraft/legacy/LegacyInstance.h b/meshmc/launcher/minecraft/legacy/LegacyInstance.h
new file mode 100644
index 0000000000..6edcee96db
--- /dev/null
+++ b/meshmc/launcher/minecraft/legacy/LegacyInstance.h
@@ -0,0 +1,172 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * 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 "BaseInstance.h"
+#include "launch/LaunchTask.h"
+
+class ModFolderModel;
+class LegacyModList;
+class WorldList;
+class Task;
+/*
+ * WHY: Legacy instances - from MultiMC 3 and 4 - are here only to provide a way
+ * to upgrade them to the current format.
+ */
+class LegacyInstance : public BaseInstance
+{
+ Q_OBJECT
+ public:
+ explicit LegacyInstance(SettingsObjectPtr globalSettings,
+ SettingsObjectPtr settings, const QString& rootDir);
+
+ virtual void saveNow() override {}
+
+ /// Path to the instance's minecraft.jar
+ QString runnableJar() const;
+
+ //! Path to the instance's modlist file.
+ QString modListFile() const;
+
+ ////// Directories //////
+ QString libDir() const;
+ QString savesDir() const;
+ QString texturePacksDir() const;
+ QString jarModsDir() const;
+ QString coreModsDir() const;
+ QString resourceDir() const;
+
+ QString instanceConfigFolder() const override;
+
+ QString
+ gameRoot() const override; // Path to the instance's minecraft directory.
+ QString
+ modsRoot() const override; // Path to the instance's minecraft directory.
+ QString binRoot() const; // Path to the instance's minecraft bin directory.
+
+ /// Get the curent base jar of this instance. By default, it's the
+ /// versions/$version/$version.jar
+ QString baseJar() const;
+
+ /// the default base jar of this instance
+ QString defaultBaseJar() const;
+ /// the default custom base jar of this instance
+ QString defaultCustomBaseJar() const;
+
+ // the main jar that we actually want to keep when migrating the instance
+ QString mainJarToPreserve() const;
+
+ /*!
+ * Whether or not custom base jar is used
+ */
+ bool shouldUseCustomBaseJar() const;
+
+ /*!
+ * The value of the custom base jar
+ */
+ QString customBaseJar() const;
+
+ std::shared_ptr<LegacyModList> jarModList() const;
+ std::shared_ptr<WorldList> worldList() const;
+
+ /*!
+ * Whether or not the instance's minecraft.jar needs to be rebuilt.
+ * If this is true, when the instance launches, its jar mods will be
+ * re-added to a fresh minecraft.jar file.
+ */
+ bool shouldRebuild() const;
+
+ QString currentVersionId() const;
+ QString intendedVersionId() const;
+
+ QSet<QString> traits() const override
+ {
+ return {"legacy-instance", "texturepacks"};
+ };
+
+ virtual bool shouldUpdate() const;
+ virtual Task::Ptr createUpdateTask(Net::Mode mode) override;
+
+ virtual QString typeName() const override;
+
+ bool canLaunch() const override
+ {
+ return false;
+ }
+ bool canEdit() const override
+ {
+ return true;
+ }
+ bool canExport() const override
+ {
+ return false;
+ }
+ shared_qobject_ptr<LaunchTask>
+ createLaunchTask(AuthSessionPtr account,
+ MinecraftServerTargetPtr serverToJoin) override
+ {
+ return nullptr;
+ }
+ IPathMatcher::Ptr getLogFileMatcher() override
+ {
+ return nullptr;
+ }
+ QString getLogFileRoot() override
+ {
+ return gameRoot();
+ }
+
+ QString getStatusbarDescription() override;
+ QStringList
+ verboseDescription(AuthSessionPtr session,
+ MinecraftServerTargetPtr serverToJoin) override;
+
+ QProcessEnvironment createEnvironment() override
+ {
+ return QProcessEnvironment();
+ }
+ QMap<QString, QString> getVariables() const override
+ {
+ return {};
+ }
+
+ protected:
+ mutable std::shared_ptr<LegacyModList> jar_mod_list;
+ mutable std::shared_ptr<WorldList> m_world_list;
+};
diff --git a/meshmc/launcher/minecraft/legacy/LegacyModList.cpp b/meshmc/launcher/minecraft/legacy/LegacyModList.cpp
new file mode 100644
index 0000000000..32158bf59e
--- /dev/null
+++ b/meshmc/launcher/minecraft/legacy/LegacyModList.cpp
@@ -0,0 +1,150 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * 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 "LegacyModList.h"
+#include <FileSystem.h>
+#include <QString>
+#include <QDebug>
+
+LegacyModList::LegacyModList(const QString& dir, const QString& list_file)
+ : m_dir(dir), m_list_file(list_file)
+{
+ 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);
+}
+
+struct OrderItem {
+ QString id;
+ bool enabled = false;
+};
+typedef QList<OrderItem> OrderList;
+
+static void internalSort(QList<LegacyModList::Mod>& what)
+{
+ auto predicate = [](const LegacyModList::Mod& left,
+ const LegacyModList::Mod& right) {
+ return left.fileName().localeAwareCompare(right.fileName()) < 0;
+ };
+ std::sort(what.begin(), what.end(), predicate);
+}
+
+static OrderList readListFile(const QString& m_list_file)
+{
+ OrderList itemList;
+ if (m_list_file.isNull() || m_list_file.isEmpty())
+ return itemList;
+
+ QFile textFile(m_list_file);
+ if (!textFile.open(QIODevice::ReadOnly | QIODevice::Text))
+ return OrderList();
+
+ QTextStream textStream;
+ textStream.setAutoDetectUnicode(true);
+ textStream.setDevice(&textFile);
+ while (true) {
+ QString line = textStream.readLine();
+ if (line.isNull() || line.isEmpty())
+ break;
+ else {
+ OrderItem it;
+ it.enabled = !line.endsWith(".disabled");
+ if (!it.enabled) {
+ line.chop(9);
+ }
+ it.id = line;
+ itemList.append(it);
+ }
+ }
+ textFile.close();
+ return itemList;
+}
+
+bool LegacyModList::update()
+{
+ if (!m_dir.exists() || !m_dir.isReadable())
+ return false;
+
+ QList<Mod> orderedMods;
+ QList<Mod> newMods;
+ m_dir.refresh();
+ auto folderContents = m_dir.entryInfoList();
+
+ // first, process the ordered items (if any)
+ OrderList listOrder = readListFile(m_list_file);
+ for (auto item : listOrder) {
+ QFileInfo infoEnabled(m_dir.filePath(item.id));
+ QFileInfo infoDisabled(m_dir.filePath(item.id + ".disabled"));
+ int idxEnabled = folderContents.indexOf(infoEnabled);
+ int idxDisabled = folderContents.indexOf(infoDisabled);
+ bool isEnabled;
+ // if both enabled and disabled versions are present, it's a special
+ // case...
+ if (idxEnabled >= 0 && idxDisabled >= 0) {
+ // we only process the one we actually have in the order file.
+ // and exactly as we have it.
+ // THIS IS A CORNER CASE
+ isEnabled = item.enabled;
+ } else {
+ // only one is present.
+ // we pick the one that we found.
+ // we assume the mod was enabled/disabled by external means
+ isEnabled = idxEnabled >= 0;
+ }
+ int idx = isEnabled ? idxEnabled : idxDisabled;
+ QFileInfo& info = isEnabled ? infoEnabled : infoDisabled;
+ // if the file from the index file exists
+ if (idx != -1) {
+ // remove from the actual folder contents list
+ folderContents.takeAt(idx);
+ // append the new mod
+ orderedMods.append(info);
+ }
+ }
+ // if there are any untracked files... append them sorted at the end
+ if (folderContents.size()) {
+ for (auto entry : folderContents) {
+ newMods.append(entry);
+ }
+ internalSort(newMods);
+ orderedMods.append(newMods);
+ }
+ mods.swap(orderedMods);
+ return true;
+}
diff --git a/meshmc/launcher/minecraft/legacy/LegacyModList.h b/meshmc/launcher/minecraft/legacy/LegacyModList.h
new file mode 100644
index 0000000000..4a5627e7c8
--- /dev/null
+++ b/meshmc/launcher/minecraft/legacy/LegacyModList.h
@@ -0,0 +1,69 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * 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 <QList>
+#include <QString>
+#include <QDir>
+
+class LegacyModList
+{
+ public:
+ using Mod = QFileInfo;
+
+ LegacyModList(const QString& dir, const QString& list_file = QString());
+
+ /// Reloads the mod list and returns true if the list changed.
+ bool update();
+
+ QDir dir()
+ {
+ return m_dir;
+ }
+
+ const QList<Mod>& allMods()
+ {
+ return mods;
+ }
+
+ protected:
+ QDir m_dir;
+ QString m_list_file;
+ QList<Mod> mods;
+};
diff --git a/meshmc/launcher/minecraft/legacy/LegacyUpgradeTask.cpp b/meshmc/launcher/minecraft/legacy/LegacyUpgradeTask.cpp
new file mode 100644
index 0000000000..89b3506153
--- /dev/null
+++ b/meshmc/launcher/minecraft/legacy/LegacyUpgradeTask.cpp
@@ -0,0 +1,151 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "LegacyUpgradeTask.h"
+#include "settings/INISettingsObject.h"
+#include "FileSystem.h"
+#include "NullInstance.h"
+#include "pathmatcher/RegexpMatcher.h"
+#include <QtConcurrentRun>
+#include "LegacyInstance.h"
+#include "minecraft/MinecraftInstance.h"
+#include "minecraft/PackProfile.h"
+#include "LegacyModList.h"
+#include "classparser.h"
+
+LegacyUpgradeTask::LegacyUpgradeTask(InstancePtr origInstance)
+{
+ m_origInstance = origInstance;
+}
+
+void LegacyUpgradeTask::executeTask()
+{
+ setStatus(tr("Copying instance %1").arg(m_origInstance->name()));
+
+ FS::copy folderCopy(m_origInstance->instanceRoot(), m_stagingPath);
+ folderCopy.followSymlinks(true);
+
+ m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), folderCopy);
+ connect(&m_copyFutureWatcher, &QFutureWatcher<bool>::finished, this,
+ &LegacyUpgradeTask::copyFinished);
+ connect(&m_copyFutureWatcher, &QFutureWatcher<bool>::canceled, this,
+ &LegacyUpgradeTask::copyAborted);
+ m_copyFutureWatcher.setFuture(m_copyFuture);
+}
+
+static QString decideVersion(const QString& currentVersion,
+ const QString& intendedVersion)
+{
+ if (intendedVersion != currentVersion) {
+ if (!intendedVersion.isEmpty()) {
+ return intendedVersion;
+ } else if (!currentVersion.isEmpty()) {
+ return currentVersion;
+ }
+ } else {
+ if (!intendedVersion.isEmpty()) {
+ return intendedVersion;
+ }
+ }
+ return QString();
+}
+
+void LegacyUpgradeTask::copyFinished()
+{
+ auto successful = m_copyFuture.result();
+ if (!successful) {
+ emitFailed(tr("Instance folder copy failed."));
+ return;
+ }
+ auto legacyInst = std::dynamic_pointer_cast<LegacyInstance>(m_origInstance);
+
+ auto instanceSettings = std::make_shared<INISettingsObject>(
+ FS::PathCombine(m_stagingPath, "instance.cfg"));
+ instanceSettings->registerSetting("InstanceType", "Legacy");
+ instanceSettings->set("InstanceType", "OneSix");
+ // NOTE: this scope ensures the instance is fully saved before we
+ // emitSucceeded
+ {
+ MinecraftInstance inst(m_globalSettings, instanceSettings,
+ m_stagingPath);
+ inst.setName(m_instName);
+
+ QString preferredVersionNumber = decideVersion(
+ legacyInst->currentVersionId(), legacyInst->intendedVersionId());
+ if (preferredVersionNumber.isNull()) {
+ // try to decide version based on the jar(s?)
+ preferredVersionNumber =
+ classparser::GetMinecraftJarVersion(legacyInst->baseJar());
+ if (preferredVersionNumber.isNull()) {
+ preferredVersionNumber = classparser::GetMinecraftJarVersion(
+ legacyInst->runnableJar());
+ if (preferredVersionNumber.isNull()) {
+ emitFailed(tr("Could not decide Minecraft version."));
+ return;
+ }
+ }
+ }
+ auto components = inst.getPackProfile();
+ components->buildingFromScratch();
+ components->setComponentVersion("net.minecraft", preferredVersionNumber,
+ true);
+
+ QString jarPath = legacyInst->mainJarToPreserve();
+ if (!jarPath.isNull()) {
+ qDebug() << "Preserving base jar! : " << jarPath;
+ // FIXME: handle case when the jar is unreadable?
+ // TODO: check the hash, if it's the same as the upstream jar, do
+ // not do this
+ components->installCustomJar(jarPath);
+ }
+
+ auto jarMods = legacyInst->jarModList()->allMods();
+ for (auto& jarMod : jarMods) {
+ QString modPath = jarMod.absoluteFilePath();
+ qDebug() << "jarMod: " << modPath;
+ components->installJarMods({modPath});
+ }
+
+ // remove all the extra garbage we no longer need
+ auto removeAll = [&](const QString& root, const QStringList& things) {
+ for (auto& thing : things) {
+ auto removePath = FS::PathCombine(root, thing);
+ QFileInfo stat(removePath);
+ if (stat.isDir()) {
+ FS::deletePath(removePath);
+ } else {
+ QFile::remove(removePath);
+ }
+ }
+ };
+ QStringList rootRemovables = {"modlist", "version", "instMods"};
+ QStringList mcRemovables = {"bin", "MeshMCLauncher.jar", "icon.png"};
+ removeAll(inst.instanceRoot(), rootRemovables);
+ removeAll(inst.gameRoot(), mcRemovables);
+ }
+ emitSucceeded();
+}
+
+void LegacyUpgradeTask::copyAborted()
+{
+ emitFailed(tr("Instance folder copy has been aborted."));
+ return;
+}
diff --git a/meshmc/launcher/minecraft/legacy/LegacyUpgradeTask.h b/meshmc/launcher/minecraft/legacy/LegacyUpgradeTask.h
new file mode 100644
index 0000000000..a407cb8df8
--- /dev/null
+++ b/meshmc/launcher/minecraft/legacy/LegacyUpgradeTask.h
@@ -0,0 +1,49 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "InstanceTask.h"
+#include "net/NetJob.h"
+#include <QUrl>
+#include <QFuture>
+#include <QFutureWatcher>
+#include "settings/SettingsObject.h"
+#include "BaseVersion.h"
+#include "BaseInstance.h"
+
+class LegacyUpgradeTask : public InstanceTask
+{
+ Q_OBJECT
+ public:
+ explicit LegacyUpgradeTask(InstancePtr origInstance);
+
+ protected:
+ //! Entry point for tasks.
+ virtual void executeTask() override;
+ void copyFinished();
+ void copyAborted();
+
+ private: /* data */
+ InstancePtr m_origInstance;
+ QFuture<bool> m_copyFuture;
+ QFutureWatcher<bool> m_copyFutureWatcher;
+};
diff --git a/meshmc/launcher/minecraft/mod/LocalModParseTask.cpp b/meshmc/launcher/minecraft/mod/LocalModParseTask.cpp
new file mode 100644
index 0000000000..09ff5f20ae
--- /dev/null
+++ b/meshmc/launcher/minecraft/mod/LocalModParseTask.cpp
@@ -0,0 +1,423 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "LocalModParseTask.h"
+
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonArray>
+#include <QJsonValue>
+#include <toml.h>
+
+#include "MMCZip.h"
+
+#include "settings/INIFile.h"
+#include "FileSystem.h"
+
+namespace
+{
+
+ // NEW format
+ // https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/6f62b37cea040daf350dc253eae6326dd9c822c3
+
+ // OLD format:
+ // https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/5bf6a2d05145ec79387acc0d45c958642fb049fc
+ std::shared_ptr<ModDetails> ReadMCModInfo(QByteArray contents)
+ {
+ auto getInfoFromArray =
+ [&](QJsonArray arr) -> std::shared_ptr<ModDetails> {
+ if (!arr.at(0).isObject()) {
+ return nullptr;
+ }
+ std::shared_ptr<ModDetails> details =
+ std::make_shared<ModDetails>();
+ 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();
+ details->updateurl = firstObj.value("updateUrl").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) {
+ // FIXME: what is the format of this? is there any?
+ authors = firstObj.value("authors").toArray();
+ }
+
+ for (auto author : authors) {
+ details->authors.append(author.toString());
+ }
+ details->credits = firstObj.value("credits").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 = val.toDouble();
+ if (version != 2) {
+ qCritical() << "BAD stuff happened to mod json:";
+ qCritical() << contents;
+ return nullptr;
+ }
+ auto arrVal = jsonDoc.object().value("modlist");
+ if (arrVal.isUndefined()) {
+ arrVal = jsonDoc.object().value("modList");
+ }
+ if (arrVal.isArray()) {
+ return getInfoFromArray(arrVal.toArray());
+ }
+ }
+ return nullptr;
+ }
+
+ // https://github.com/MinecraftForge/Documentation/blob/5ab4ba6cf9abc0ac4c0abd96ad187461aefd72af/docs/gettingstarted/structuring.md
+ std::shared_ptr<ModDetails> ReadMCModTOML(QByteArray contents)
+ {
+ std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>();
+
+ char errbuf[200];
+ // top-level table
+ toml_table_t* tomlData =
+ toml_parse(contents.data(), errbuf, sizeof(errbuf));
+
+ if (!tomlData) {
+ return nullptr;
+ }
+
+ // array defined by [[mods]]
+ toml_array_t* tomlModsArr = toml_array_in(tomlData, "mods");
+ if (!tomlModsArr) {
+ qWarning() << "Corrupted mods.toml? Couldn't find [[mods]] array!";
+ return nullptr;
+ }
+
+ // we only really care about the first element, since multiple mods in
+ // one file is not supported by us at the moment
+ toml_table_t* tomlModsTable0 = toml_table_at(tomlModsArr, 0);
+ if (!tomlModsTable0) {
+ qWarning() << "Corrupted mods.toml? [[mods]] didn't have an "
+ "element at index 0!";
+ return nullptr;
+ }
+
+ // mandatory properties - always in [[mods]]
+ toml_datum_t modIdDatum = toml_string_in(tomlModsTable0, "modId");
+ if (modIdDatum.ok) {
+ details->mod_id = modIdDatum.u.s;
+ // library says this is required for strings
+ free(modIdDatum.u.s);
+ }
+ toml_datum_t versionDatum = toml_string_in(tomlModsTable0, "version");
+ if (versionDatum.ok) {
+ details->version = versionDatum.u.s;
+ free(versionDatum.u.s);
+ }
+ toml_datum_t displayNameDatum =
+ toml_string_in(tomlModsTable0, "displayName");
+ if (displayNameDatum.ok) {
+ details->name = displayNameDatum.u.s;
+ free(displayNameDatum.u.s);
+ }
+ toml_datum_t descriptionDatum =
+ toml_string_in(tomlModsTable0, "description");
+ if (descriptionDatum.ok) {
+ details->description = descriptionDatum.u.s;
+ free(descriptionDatum.u.s);
+ }
+
+ // optional properties - can be in the root table or [[mods]]
+ toml_datum_t authorsDatum = toml_string_in(tomlData, "authors");
+ QString authors = "";
+ if (authorsDatum.ok) {
+ authors = authorsDatum.u.s;
+ free(authorsDatum.u.s);
+ } else {
+ authorsDatum = toml_string_in(tomlModsTable0, "authors");
+ if (authorsDatum.ok) {
+ authors = authorsDatum.u.s;
+ free(authorsDatum.u.s);
+ }
+ }
+ if (!authors.isEmpty()) {
+ // author information is stored as a string now, not a list
+ details->authors.append(authors);
+ }
+ // is credits even used anywhere? including this for completion/parity
+ // with old data version
+ toml_datum_t creditsDatum = toml_string_in(tomlData, "credits");
+ QString credits = "";
+ if (creditsDatum.ok) {
+ authors = creditsDatum.u.s;
+ free(creditsDatum.u.s);
+ } else {
+ creditsDatum = toml_string_in(tomlModsTable0, "credits");
+ if (creditsDatum.ok) {
+ credits = creditsDatum.u.s;
+ free(creditsDatum.u.s);
+ }
+ }
+ details->credits = credits;
+ toml_datum_t homeurlDatum = toml_string_in(tomlData, "displayURL");
+ QString homeurl = "";
+ if (homeurlDatum.ok) {
+ homeurl = homeurlDatum.u.s;
+ free(homeurlDatum.u.s);
+ } else {
+ homeurlDatum = toml_string_in(tomlModsTable0, "displayURL");
+ if (homeurlDatum.ok) {
+ homeurl = homeurlDatum.u.s;
+ free(homeurlDatum.u.s);
+ }
+ }
+ if (!homeurl.isEmpty()) {
+ // fix up url.
+ if (!homeurl.startsWith("http://") &&
+ !homeurl.startsWith("https://") &&
+ !homeurl.startsWith("ftp://")) {
+ homeurl.prepend("http://");
+ }
+ }
+ details->homeurl = homeurl;
+
+ // this seems to be recursive, so it should free everything
+ toml_free(tomlData);
+
+ return details;
+ }
+
+ // https://fabricmc.net/wiki/documentation:fabric_mod_json
+ std::shared_ptr<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;
+
+ std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>();
+
+ 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();
+ }
+ }
+ }
+ return details;
+ }
+
+ std::shared_ptr<ModDetails> ReadForgeInfo(QByteArray contents)
+ {
+ std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>();
+ // 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;
+ }
+
+ std::shared_ptr<ModDetails> ReadLiteModInfo(QByteArray contents)
+ {
+ std::shared_ptr<ModDetails> details = std::make_shared<ModDetails>();
+ 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;
+ }
+
+} // namespace
+
+LocalModParseTask::LocalModParseTask(int token, Mod::ModType type,
+ const QFileInfo& modFile)
+ : m_token(token), m_type(type), m_modFile(modFile), m_result(new Result())
+{
+}
+
+void LocalModParseTask::processAsZip()
+{
+ QString zipPath = m_modFile.filePath();
+
+ QByteArray modsToml =
+ MMCZip::readFileFromZip(zipPath, "META-INF/mods.toml");
+ if (!modsToml.isEmpty()) {
+ m_result->details = ReadMCModTOML(modsToml);
+
+ // to replace ${file.jarVersion} with the actual version, as needed
+ if (m_result->details &&
+ m_result->details->version == "${file.jarVersion}") {
+ QByteArray manifestData =
+ MMCZip::readFileFromZip(zipPath, "META-INF/MANIFEST.MF");
+ if (!manifestData.isEmpty()) {
+ // quick and dirty line-by-line parser
+ auto manifestLines = manifestData.split('\n');
+ QString manifestVersion = "";
+ for (auto& line : manifestLines) {
+ if (QString(line).startsWith("Implementation-Version: ")) {
+ manifestVersion =
+ QString(line).remove("Implementation-Version: ");
+ 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";
+ }
+
+ m_result->details->version = manifestVersion;
+ }
+ }
+ return;
+ }
+
+ QByteArray mcmodInfo = MMCZip::readFileFromZip(zipPath, "mcmod.info");
+ if (!mcmodInfo.isEmpty()) {
+ m_result->details = ReadMCModInfo(mcmodInfo);
+ return;
+ }
+
+ QByteArray fabricModJson =
+ MMCZip::readFileFromZip(zipPath, "fabric.mod.json");
+ if (!fabricModJson.isEmpty()) {
+ m_result->details = ReadFabricModInfo(fabricModJson);
+ return;
+ }
+
+ QByteArray forgeVersionProps =
+ MMCZip::readFileFromZip(zipPath, "forgeversion.properties");
+ if (!forgeVersionProps.isEmpty()) {
+ m_result->details = ReadForgeInfo(forgeVersionProps);
+ return;
+ }
+}
+
+void LocalModParseTask::processAsFolder()
+{
+ QFileInfo mcmod_info(FS::PathCombine(m_modFile.filePath(), "mcmod.info"));
+ if (mcmod_info.isFile()) {
+ QFile mcmod(mcmod_info.filePath());
+ if (!mcmod.open(QIODevice::ReadOnly))
+ return;
+ auto data = mcmod.readAll();
+ if (data.isEmpty() || data.isNull())
+ return;
+ m_result->details = ReadMCModInfo(data);
+ }
+}
+
+void LocalModParseTask::processAsLitemod()
+{
+ QByteArray litemodJson =
+ MMCZip::readFileFromZip(m_modFile.filePath(), "litemod.json");
+ if (!litemodJson.isEmpty()) {
+ m_result->details = ReadLiteModInfo(litemodJson);
+ }
+}
+
+void LocalModParseTask::run()
+{
+ switch (m_type) {
+ case Mod::MOD_ZIPFILE:
+ processAsZip();
+ break;
+ case Mod::MOD_FOLDER:
+ processAsFolder();
+ break;
+ case Mod::MOD_LITEMOD:
+ processAsLitemod();
+ break;
+ default:
+ break;
+ }
+ emit finished(m_token);
+}
diff --git a/meshmc/launcher/minecraft/mod/LocalModParseTask.h b/meshmc/launcher/minecraft/mod/LocalModParseTask.h
new file mode 100644
index 0000000000..bd85bbc27b
--- /dev/null
+++ b/meshmc/launcher/minecraft/mod/LocalModParseTask.h
@@ -0,0 +1,59 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+#include <QRunnable>
+#include <QDebug>
+#include <QObject>
+#include "Mod.h"
+#include "ModDetails.h"
+
+class LocalModParseTask : public QObject, public QRunnable
+{
+ Q_OBJECT
+ public:
+ struct Result {
+ QString id;
+ std::shared_ptr<ModDetails> details;
+ };
+ using ResultPtr = std::shared_ptr<Result>;
+ ResultPtr result() const
+ {
+ return m_result;
+ }
+
+ LocalModParseTask(int token, Mod::ModType type, const QFileInfo& modFile);
+ void run();
+
+ signals:
+ void finished(int token);
+
+ private:
+ void processAsZip();
+ void processAsFolder();
+ void processAsLitemod();
+
+ private:
+ int m_token;
+ Mod::ModType m_type;
+ QFileInfo m_modFile;
+ ResultPtr m_result;
+};
diff --git a/meshmc/launcher/minecraft/mod/Mod.cpp b/meshmc/launcher/minecraft/mod/Mod.cpp
new file mode 100644
index 0000000000..dae36f968e
--- /dev/null
+++ b/meshmc/launcher/minecraft/mod/Mod.cpp
@@ -0,0 +1,158 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * 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 <QDir>
+#include <QString>
+
+#include "Mod.h"
+#include <QDebug>
+#include <FileSystem.h>
+
+namespace
+{
+
+ ModDetails invalidDetails;
+
+}
+
+Mod::Mod(const QFileInfo& file)
+{
+ repath(file);
+ m_changedDateTime = file.lastModified();
+}
+
+void Mod::repath(const QFileInfo& file)
+{
+ m_file = file;
+ QString name_base = file.fileName();
+
+ m_type = Mod::MOD_UNKNOWN;
+
+ m_mmc_id = name_base;
+
+ if (m_file.isDir()) {
+ m_type = MOD_FOLDER;
+ m_name = name_base;
+ } else if (m_file.isFile()) {
+ if (name_base.endsWith(".disabled")) {
+ m_enabled = false;
+ name_base.chop(9);
+ } else {
+ m_enabled = true;
+ }
+ if (name_base.endsWith(".zip") || name_base.endsWith(".jar")) {
+ m_type = MOD_ZIPFILE;
+ name_base.chop(4);
+ } else if (name_base.endsWith(".litemod")) {
+ m_type = MOD_LITEMOD;
+ name_base.chop(8);
+ } else {
+ m_type = MOD_SINGLEFILE;
+ }
+ m_name = name_base;
+ }
+}
+
+bool Mod::enable(bool value)
+{
+ if (m_type == Mod::MOD_UNKNOWN || m_type == Mod::MOD_FOLDER)
+ return false;
+
+ if (m_enabled == value)
+ return false;
+
+ QString path = m_file.absoluteFilePath();
+ if (value) {
+ QFile foo(path);
+ if (!path.endsWith(".disabled"))
+ return false;
+ path.chop(9);
+ if (!foo.rename(path))
+ return false;
+ } else {
+ QFile foo(path);
+ path += ".disabled";
+ if (!foo.rename(path))
+ return false;
+ }
+ repath(QFileInfo(path));
+ m_enabled = value;
+ return true;
+}
+
+bool Mod::destroy()
+{
+ m_type = MOD_UNKNOWN;
+ return FS::deletePath(m_file.filePath());
+}
+
+const ModDetails& Mod::details() const
+{
+ if (!m_localDetails)
+ return invalidDetails;
+ return *m_localDetails;
+}
+
+QString Mod::version() const
+{
+ return details().version;
+}
+
+QString Mod::name() const
+{
+ auto& d = details();
+ if (!d.name.isEmpty()) {
+ return d.name;
+ }
+ return m_name;
+}
+
+QString Mod::homeurl() const
+{
+ return details().homeurl;
+}
+
+QString Mod::description() const
+{
+ return details().description;
+}
+
+QStringList Mod::authors() const
+{
+ return details().authors;
+}
diff --git a/meshmc/launcher/minecraft/mod/Mod.h b/meshmc/launcher/minecraft/mod/Mod.h
new file mode 100644
index 0000000000..430ae9519d
--- /dev/null
+++ b/meshmc/launcher/minecraft/mod/Mod.h
@@ -0,0 +1,140 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * 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 <QDateTime>
+#include <QList>
+#include <memory>
+
+#include "ModDetails.h"
+
+class Mod
+{
+ public:
+ enum ModType {
+ MOD_UNKNOWN, //!< Indicates an unspecified mod type.
+ MOD_ZIPFILE, //!< The mod is a zip file containing the mod's class
+ //!< files.
+ MOD_SINGLEFILE, //!< The mod is a single file (not a zip file).
+ MOD_FOLDER, //!< The mod is in a folder on the filesystem.
+ MOD_LITEMOD, //!< The mod is a litemod
+ };
+
+ Mod() = default;
+ Mod(const QFileInfo& file);
+
+ QFileInfo filename() const
+ {
+ return m_file;
+ }
+ QString mmc_id() const
+ {
+ return m_mmc_id;
+ }
+ ModType type() const
+ {
+ return m_type;
+ }
+ bool valid()
+ {
+ return m_type != MOD_UNKNOWN;
+ }
+
+ QDateTime dateTimeChanged() const
+ {
+ return m_changedDateTime;
+ }
+
+ bool enabled() const
+ {
+ return m_enabled;
+ }
+
+ const ModDetails& details() const;
+
+ QString name() const;
+ QString version() const;
+ QString homeurl() const;
+ QString description() const;
+ QStringList authors() const;
+
+ bool enable(bool value);
+
+ // delete all the files of this mod
+ bool destroy();
+
+ // change the mod's filesystem path (used by mod lists for *MAGIC* purposes)
+ void repath(const QFileInfo& file);
+
+ bool shouldResolve()
+ {
+ return !m_resolving && !m_resolved;
+ }
+ bool isResolving()
+ {
+ return m_resolving;
+ }
+ int resolutionTicket()
+ {
+ return m_resolutionTicket;
+ }
+ void setResolving(bool resolving, int resolutionTicket)
+ {
+ m_resolving = resolving;
+ m_resolutionTicket = resolutionTicket;
+ }
+ void finishResolvingWithDetails(std::shared_ptr<ModDetails> details)
+ {
+ m_resolving = false;
+ m_resolved = true;
+ m_localDetails = details;
+ }
+
+ protected:
+ QFileInfo m_file;
+ QDateTime m_changedDateTime;
+ QString m_mmc_id;
+ QString m_name;
+ bool m_enabled = true;
+ bool m_resolving = false;
+ bool m_resolved = false;
+ int m_resolutionTicket = 0;
+ ModType m_type = MOD_UNKNOWN;
+ std::shared_ptr<ModDetails> m_localDetails;
+};
diff --git a/meshmc/launcher/minecraft/mod/ModDetails.h b/meshmc/launcher/minecraft/mod/ModDetails.h
new file mode 100644
index 0000000000..37aa78b7cf
--- /dev/null
+++ b/meshmc/launcher/minecraft/mod/ModDetails.h
@@ -0,0 +1,37 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <QString>
+#include <QStringList>
+
+struct ModDetails {
+ QString mod_id;
+ QString name;
+ QString version;
+ QString mcversion;
+ QString homeurl;
+ QString updateurl;
+ QString description;
+ QStringList authors;
+ QString credits;
+};
diff --git a/meshmc/launcher/minecraft/mod/ModFolderLoadTask.cpp b/meshmc/launcher/minecraft/mod/ModFolderLoadTask.cpp
new file mode 100644
index 0000000000..9272009539
--- /dev/null
+++ b/meshmc/launcher/minecraft/mod/ModFolderLoadTask.cpp
@@ -0,0 +1,38 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "ModFolderLoadTask.h"
+#include <QDebug>
+
+ModFolderLoadTask::ModFolderLoadTask(QDir dir)
+ : m_dir(dir), m_result(new Result())
+{
+}
+
+void ModFolderLoadTask::run()
+{
+ m_dir.refresh();
+ for (auto entry : m_dir.entryInfoList()) {
+ Mod m(entry);
+ m_result->mods[m.mmc_id()] = m;
+ }
+ emit succeeded();
+}
diff --git a/meshmc/launcher/minecraft/mod/ModFolderLoadTask.h b/meshmc/launcher/minecraft/mod/ModFolderLoadTask.h
new file mode 100644
index 0000000000..1aaafa6bbb
--- /dev/null
+++ b/meshmc/launcher/minecraft/mod/ModFolderLoadTask.h
@@ -0,0 +1,52 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+#include <QRunnable>
+#include <QObject>
+#include <QDir>
+#include <QMap>
+#include "Mod.h"
+#include <memory>
+
+class ModFolderLoadTask : public QObject, public QRunnable
+{
+ Q_OBJECT
+ public:
+ struct Result {
+ QMap<QString, Mod> mods;
+ };
+ using ResultPtr = std::shared_ptr<Result>;
+ ResultPtr result() const
+ {
+ return m_result;
+ }
+
+ public:
+ ModFolderLoadTask(QDir dir);
+ void run();
+ signals:
+ void succeeded();
+
+ private:
+ QDir m_dir;
+ ResultPtr m_result;
+};
diff --git a/meshmc/launcher/minecraft/mod/ModFolderModel.cpp b/meshmc/launcher/minecraft/mod/ModFolderModel.cpp
new file mode 100644
index 0000000000..faaa814590
--- /dev/null
+++ b/meshmc/launcher/minecraft/mod/ModFolderModel.cpp
@@ -0,0 +1,573 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * 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.h"
+#include <FileSystem.h>
+#include <QMimeData>
+#include <QUrl>
+#include <QUuid>
+#include <QString>
+#include <QFileSystemWatcher>
+#include <QDebug>
+#include "ModFolderLoadTask.h"
+#include <QThreadPool>
+#include <algorithm>
+#include "LocalModParseTask.h"
+
+ModFolderModel::ModFolderModel(const QString& dir)
+ : QAbstractListModel(), 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);
+ connect(m_watcher, SIGNAL(directoryChanged(QString)), this,
+ SLOT(directoryChanged(QString)));
+}
+
+void ModFolderModel::startWatching()
+{
+ if (is_watching)
+ return;
+
+ update();
+
+ is_watching = m_watcher->addPath(m_dir.absolutePath());
+ if (is_watching) {
+ qDebug() << "Started watching " << m_dir.absolutePath();
+ } else {
+ qDebug() << "Failed to start watching " << m_dir.absolutePath();
+ }
+}
+
+void ModFolderModel::stopWatching()
+{
+ if (!is_watching)
+ return;
+
+ is_watching = !m_watcher->removePath(m_dir.absolutePath());
+ if (!is_watching) {
+ qDebug() << "Stopped watching " << m_dir.absolutePath();
+ } else {
+ qDebug() << "Failed to stop watching " << m_dir.absolutePath();
+ }
+}
+
+bool ModFolderModel::update()
+{
+ if (!isValid()) {
+ return false;
+ }
+ if (m_update) {
+ scheduled_update = true;
+ return true;
+ }
+
+ auto task = new ModFolderLoadTask(m_dir);
+ m_update = task->result();
+ QThreadPool* threadPool = QThreadPool::globalInstance();
+ connect(task, &ModFolderLoadTask::succeeded, this,
+ &ModFolderModel::finishUpdate);
+ threadPool->start(task);
+ return true;
+}
+
+void ModFolderModel::finishUpdate()
+{
+ auto keys1 = modsIndex.keys();
+ QSet<QString> currentSet(keys1.begin(), keys1.end());
+ auto& newMods = m_update->mods;
+ auto keys2 = newMods.keys();
+ QSet<QString> newSet(keys2.begin(), keys2.end());
+
+ // see if the kept mods changed in some way
+ {
+ QSet<QString> kept = currentSet;
+ kept.intersect(newSet);
+ for (auto& keptMod : kept) {
+ auto& newMod = newMods[keptMod];
+ auto row = modsIndex[keptMod];
+ auto& currentMod = mods[row];
+ if (newMod.dateTimeChanged() == currentMod.dateTimeChanged()) {
+ // no significant change, ignore...
+ continue;
+ }
+ auto& oldMod = mods[row];
+ if (oldMod.isResolving()) {
+ activeTickets.remove(oldMod.resolutionTicket());
+ }
+ oldMod = newMod;
+ resolveMod(mods[row]);
+ emit dataChanged(index(row, 0),
+ index(row, columnCount(QModelIndex()) - 1));
+ }
+ }
+
+ // remove mods no longer present
+ {
+ QSet<QString> removed = currentSet;
+ QList<int> removedRows;
+ removed.subtract(newSet);
+ for (auto& removedMod : removed) {
+ removedRows.append(modsIndex[removedMod]);
+ }
+ std::sort(removedRows.begin(), removedRows.end(), std::greater<int>());
+ for (auto iter = removedRows.begin(); iter != removedRows.end();
+ iter++) {
+ int removedIndex = *iter;
+ beginRemoveRows(QModelIndex(), removedIndex, removedIndex);
+ auto removedIter = mods.begin() + removedIndex;
+ if (removedIter->isResolving()) {
+ activeTickets.remove(removedIter->resolutionTicket());
+ }
+ mods.erase(removedIter);
+ endRemoveRows();
+ }
+ }
+
+ // add new mods to the end
+ {
+ QSet<QString> added = newSet;
+ added.subtract(currentSet);
+ beginInsertRows(QModelIndex(), mods.size(),
+ mods.size() + added.size() - 1);
+ for (auto& addedMod : added) {
+ mods.append(newMods[addedMod]);
+ resolveMod(mods.last());
+ }
+ endInsertRows();
+ }
+
+ // update index
+ {
+ modsIndex.clear();
+ int idx = 0;
+ for (auto& mod : mods) {
+ modsIndex[mod.mmc_id()] = idx;
+ idx++;
+ }
+ }
+
+ m_update.reset();
+
+ emit updateFinished();
+
+ if (scheduled_update) {
+ scheduled_update = false;
+ update();
+ }
+}
+
+void ModFolderModel::resolveMod(Mod& m)
+{
+ if (!m.shouldResolve()) {
+ return;
+ }
+
+ auto task =
+ new LocalModParseTask(nextResolutionTicket, m.type(), m.filename());
+ auto result = task->result();
+ result->id = m.mmc_id();
+ activeTickets.insert(nextResolutionTicket, result);
+ m.setResolving(true, nextResolutionTicket);
+ nextResolutionTicket++;
+ QThreadPool* threadPool = QThreadPool::globalInstance();
+ connect(task, &LocalModParseTask::finished, this,
+ &ModFolderModel::finishModParse);
+ threadPool->start(task);
+}
+
+void ModFolderModel::finishModParse(int token)
+{
+ auto iter = activeTickets.find(token);
+ if (iter == activeTickets.end()) {
+ return;
+ }
+ auto result = *iter;
+ activeTickets.remove(token);
+ int row = modsIndex[result->id];
+ auto& mod = mods[row];
+ mod.finishResolvingWithDetails(result->details);
+ emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1));
+}
+
+void ModFolderModel::disableInteraction(bool disabled)
+{
+ if (interaction_disabled == disabled) {
+ return;
+ }
+ interaction_disabled = disabled;
+ if (size()) {
+ emit dataChanged(index(0), index(size() - 1));
+ }
+}
+
+void ModFolderModel::directoryChanged(QString path)
+{
+ update();
+}
+
+bool ModFolderModel::isValid()
+{
+ return m_dir.exists() && m_dir.isReadable();
+}
+
+// FIXME: this does not take disabled mod (with extra .disable extension) into
+// account...
+bool ModFolderModel::installMod(const QString& filename)
+{
+ if (interaction_disabled) {
+ return false;
+ }
+
+ // NOTE: fix for GH-1178: remove trailing slash to avoid issues with using
+ // the empty result of QFileInfo::fileName
+ auto originalPath = FS::NormalizePath(filename);
+ QFileInfo fileinfo(originalPath);
+
+ if (!fileinfo.exists() || !fileinfo.isReadable()) {
+ qWarning() << "Caught attempt to install non-existing file or "
+ "file-like object:"
+ << originalPath;
+ return false;
+ }
+ qDebug() << "installing: " << fileinfo.absoluteFilePath();
+
+ Mod installedMod(fileinfo);
+ if (!installedMod.valid()) {
+ qDebug() << originalPath << "is not a valid mod. Ignoring it.";
+ return false;
+ }
+
+ auto type = installedMod.type();
+ if (type == Mod::MOD_UNKNOWN) {
+ qDebug() << "Cannot recognize mod type of" << originalPath
+ << ", ignoring it.";
+ return false;
+ }
+
+ auto newpath =
+ FS::NormalizePath(FS::PathCombine(m_dir.path(), fileinfo.fileName()));
+ if (originalPath == newpath) {
+ qDebug() << "Overwriting the mod (" << originalPath
+ << ") with itself makes no sense...";
+ return false;
+ }
+
+ if (type == Mod::MOD_SINGLEFILE || type == Mod::MOD_ZIPFILE ||
+ type == Mod::MOD_LITEMOD) {
+ if (QFile::exists(newpath) ||
+ QFile::exists(newpath + QString(".disabled"))) {
+ if (!QFile::remove(newpath)) {
+ // FIXME: report error in a user-visible way
+ qWarning() << "Copy from" << originalPath << "to" << newpath
+ << "has failed.";
+ return false;
+ }
+ qDebug() << newpath << "has been deleted.";
+ }
+ if (!QFile::copy(fileinfo.filePath(), newpath)) {
+ qWarning() << "Copy from" << originalPath << "to" << newpath
+ << "has failed.";
+ // FIXME: report error in a user-visible way
+ return false;
+ }
+ FS::updateTimestamp(newpath);
+ installedMod.repath(QFileInfo(newpath));
+ update();
+ return true;
+ } else if (type == Mod::MOD_FOLDER) {
+ QString from = fileinfo.filePath();
+ if (QFile::exists(newpath)) {
+ qDebug() << "Ignoring folder " << from << ", it would merge with "
+ << newpath;
+ return false;
+ }
+
+ if (!FS::copy(from, newpath)()) {
+ qWarning() << "Copy of folder from" << originalPath << "to"
+ << newpath << "has (potentially partially) failed.";
+ return false;
+ }
+ installedMod.repath(QFileInfo(newpath));
+ update();
+ return true;
+ }
+ return false;
+}
+
+bool ModFolderModel::setModStatus(const QModelIndexList& indexes,
+ ModStatusAction enable)
+{
+ if (interaction_disabled) {
+ return false;
+ }
+
+ if (indexes.isEmpty())
+ return true;
+
+ for (auto index : indexes) {
+ if (index.column() != 0) {
+ continue;
+ }
+ setModStatus(index.row(), enable);
+ }
+ return true;
+}
+
+bool ModFolderModel::deleteMods(const QModelIndexList& indexes)
+{
+ if (interaction_disabled) {
+ return false;
+ }
+
+ if (indexes.isEmpty())
+ return true;
+
+ for (auto i : indexes) {
+ Mod& m = mods[i.row()];
+ m.destroy();
+ }
+ return true;
+}
+
+int ModFolderModel::columnCount(const QModelIndex& parent) const
+{
+ return NUM_COLUMNS;
+}
+
+QVariant ModFolderModel::data(const QModelIndex& index, int role) const
+{
+ if (!index.isValid())
+ return QVariant();
+
+ int row = index.row();
+ int column = index.column();
+
+ if (row < 0 || row >= mods.size())
+ return QVariant();
+
+ switch (role) {
+ case Qt::DisplayRole:
+ switch (column) {
+ case NameColumn:
+ return mods[row].name();
+ case VersionColumn: {
+ switch (mods[row].type()) {
+ case Mod::MOD_FOLDER:
+ return tr("Folder");
+ case Mod::MOD_SINGLEFILE:
+ return tr("File");
+ default:
+ break;
+ }
+ return mods[row].version();
+ }
+ case DateColumn:
+ return mods[row].dateTimeChanged();
+
+ default:
+ return QVariant();
+ }
+
+ case Qt::ToolTipRole:
+ return mods[row].mmc_id();
+
+ case Qt::CheckStateRole:
+ switch (column) {
+ case ActiveColumn:
+ return mods[row].enabled() ? Qt::Checked : Qt::Unchecked;
+ default:
+ return QVariant();
+ }
+ default:
+ return QVariant();
+ }
+}
+
+bool ModFolderModel::setData(const QModelIndex& index, const QVariant& value,
+ int role)
+{
+ if (index.row() < 0 || index.row() >= rowCount(index) || !index.isValid()) {
+ return false;
+ }
+
+ if (role == Qt::CheckStateRole) {
+ return setModStatus(index.row(), Toggle);
+ }
+ return false;
+}
+
+bool ModFolderModel::setModStatus(int row,
+ ModFolderModel::ModStatusAction action)
+{
+ if (row < 0 || row >= mods.size()) {
+ return false;
+ }
+
+ auto& mod = mods[row];
+ bool desiredStatus;
+ switch (action) {
+ case Enable:
+ desiredStatus = true;
+ break;
+ case Disable:
+ desiredStatus = false;
+ break;
+ case Toggle:
+ default:
+ desiredStatus = !mod.enabled();
+ break;
+ }
+
+ if (desiredStatus == mod.enabled()) {
+ return true;
+ }
+
+ // preserve the row, but change its ID
+ auto oldId = mod.mmc_id();
+ if (!mod.enable(!mod.enabled())) {
+ return false;
+ }
+ auto newId = mod.mmc_id();
+ if (modsIndex.contains(newId)) {
+ // NOTE: this could handle a corner case, where we are overwriting a
+ // file, because the same 'mod' exists both enabled and disabled But is
+ // it necessary?
+ }
+ modsIndex.remove(oldId);
+ modsIndex[newId] = row;
+ emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1));
+ return true;
+}
+
+QVariant ModFolderModel::headerData(int section, Qt::Orientation orientation,
+ int role) const
+{
+ switch (role) {
+ case Qt::DisplayRole:
+ switch (section) {
+ case ActiveColumn:
+ return QString();
+ case NameColumn:
+ return tr("Name");
+ case VersionColumn:
+ return tr("Version");
+ case DateColumn:
+ return tr("Last changed");
+ 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).");
+ default:
+ return QVariant();
+ }
+ default:
+ return QVariant();
+ }
+ return QVariant();
+}
+
+Qt::ItemFlags ModFolderModel::flags(const QModelIndex& index) const
+{
+ Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index);
+ auto flags = defaultFlags;
+ if (interaction_disabled) {
+ flags &= ~Qt::ItemIsDropEnabled;
+ } else {
+ flags |= Qt::ItemIsDropEnabled;
+ if (index.isValid()) {
+ flags |= Qt::ItemIsUserCheckable;
+ }
+ }
+ return flags;
+}
+
+Qt::DropActions ModFolderModel::supportedDropActions() const
+{
+ // copy from outside, move from within and other mod lists
+ return Qt::CopyAction | Qt::MoveAction;
+}
+
+QStringList ModFolderModel::mimeTypes() const
+{
+ QStringList types;
+ types << "text/uri-list";
+ return types;
+}
+
+bool ModFolderModel::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;
+ }
+ // TODO: implement not only copy, but also move
+ // FIXME: handle errors here
+ installMod(url.toLocalFile());
+ }
+ return true;
+ }
+ return false;
+}
diff --git a/meshmc/launcher/minecraft/mod/ModFolderModel.h b/meshmc/launcher/minecraft/mod/ModFolderModel.h
new file mode 100644
index 0000000000..845c6d6e4a
--- /dev/null
+++ b/meshmc/launcher/minecraft/mod/ModFolderModel.h
@@ -0,0 +1,169 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * 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 <QList>
+#include <QMap>
+#include <QSet>
+#include <QString>
+#include <QDir>
+#include <QAbstractListModel>
+
+#include "Mod.h"
+
+#include "ModFolderLoadTask.h"
+#include "LocalModParseTask.h"
+
+class LegacyInstance;
+class BaseInstance;
+class QFileSystemWatcher;
+
+/**
+ * A legacy mod list.
+ * Backed by a folder.
+ */
+class ModFolderModel : public QAbstractListModel
+{
+ Q_OBJECT
+ public:
+ enum Columns {
+ ActiveColumn = 0,
+ NameColumn,
+ VersionColumn,
+ DateColumn,
+ NUM_COLUMNS
+ };
+ enum ModStatusAction { Disable, Enable, Toggle };
+ ModFolderModel(const QString& dir);
+
+ 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;
+ Qt::DropActions supportedDropActions() const override;
+
+ /// flags, mostly to support drag&drop
+ virtual Qt::ItemFlags flags(const QModelIndex& index) const override;
+ QStringList mimeTypes() const override;
+ bool dropMimeData(const QMimeData* data, Qt::DropAction action, int row,
+ int column, const QModelIndex& parent) override;
+
+ virtual int rowCount(const QModelIndex&) const override
+ {
+ return size();
+ }
+
+ virtual QVariant headerData(int section, Qt::Orientation orientation,
+ int role = Qt::DisplayRole) const override;
+ virtual int columnCount(const QModelIndex& parent) const override;
+
+ size_t size() const
+ {
+ return mods.size();
+ };
+ bool empty() const
+ {
+ return size() == 0;
+ }
+ Mod& operator[](size_t index)
+ {
+ return mods[index];
+ }
+ const Mod& at(size_t index) const
+ {
+ return mods.at(index);
+ }
+
+ /// Reloads the mod list and returns true if the list changed.
+ bool update();
+
+ /**
+ * Adds the given mod to the list at the given index - if the list supports
+ * custom ordering
+ */
+ bool installMod(const QString& filename);
+
+ /// Deletes all the selected mods
+ bool deleteMods(const QModelIndexList& indexes);
+
+ /// Enable or disable listed mods
+ bool setModStatus(const QModelIndexList& indexes, ModStatusAction action);
+
+ void startWatching();
+ void stopWatching();
+
+ bool isValid();
+
+ QDir dir()
+ {
+ return m_dir;
+ }
+
+ const QList<Mod>& allMods()
+ {
+ return mods;
+ }
+
+ public slots:
+ void disableInteraction(bool disabled);
+
+ private slots:
+ void directoryChanged(QString path);
+ void finishUpdate();
+ void finishModParse(int token);
+
+ signals:
+ void updateFinished();
+
+ private:
+ void resolveMod(Mod& m);
+ bool setModStatus(int index, ModStatusAction action);
+
+ protected:
+ QFileSystemWatcher* m_watcher;
+ bool is_watching = false;
+ ModFolderLoadTask::ResultPtr m_update;
+ bool scheduled_update = false;
+ bool interaction_disabled = false;
+ QDir m_dir;
+ QMap<QString, int> modsIndex;
+ QMap<int, LocalModParseTask::ResultPtr> activeTickets;
+ int nextResolutionTicket = 0;
+ QList<Mod> mods;
+};
diff --git a/meshmc/launcher/minecraft/mod/ModFolderModel_test.cpp b/meshmc/launcher/minecraft/mod/ModFolderModel_test.cpp
new file mode 100644
index 0000000000..12b0d44478
--- /dev/null
+++ b/meshmc/launcher/minecraft/mod/ModFolderModel_test.cpp
@@ -0,0 +1,71 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <QTest>
+#include <QTemporaryDir>
+#include "TestUtil.h"
+
+#include "FileSystem.h"
+#include "minecraft/mod/ModFolderModel.h"
+
+class ModFolderModelTest : public QObject
+{
+ Q_OBJECT
+
+ private slots:
+ // test for GH-1178 - install a folder with files to a mod list
+ void test_1178()
+ {
+ // source
+ QString source = QFINDTESTDATA("data/test_folder");
+
+ // sanity check
+ QVERIFY(!source.endsWith('/'));
+
+ auto verify = [](QString path) {
+ QDir target_dir(FS::PathCombine(path, "test_folder"));
+ QVERIFY(target_dir.entryList().contains("pack.mcmeta"));
+ QVERIFY(target_dir.entryList().contains("assets"));
+ };
+
+ // 1. test with no trailing /
+ {
+ QString folder = source;
+ QTemporaryDir tempDir;
+ ModFolderModel m(tempDir.path());
+ m.installMod(folder);
+ verify(tempDir.path());
+ }
+
+ // 2. test with trailing /
+ {
+ QString folder = source + '/';
+ QTemporaryDir tempDir;
+ ModFolderModel m(tempDir.path());
+ m.installMod(folder);
+ verify(tempDir.path());
+ }
+ }
+};
+
+QTEST_GUILESS_MAIN(ModFolderModelTest)
+
+#include "ModFolderModel_test.moc"
diff --git a/meshmc/launcher/minecraft/mod/ResourcePackFolderModel.cpp b/meshmc/launcher/minecraft/mod/ResourcePackFolderModel.cpp
new file mode 100644
index 0000000000..2f43cb2ff1
--- /dev/null
+++ b/meshmc/launcher/minecraft/mod/ResourcePackFolderModel.cpp
@@ -0,0 +1,50 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "ResourcePackFolderModel.h"
+
+ResourcePackFolderModel::ResourcePackFolderModel(const QString& dir)
+ : ModFolderModel(dir)
+{
+}
+
+QVariant ResourcePackFolderModel::headerData(int section,
+ Qt::Orientation orientation,
+ int role) const
+{
+ if (role == Qt::ToolTipRole) {
+ switch (section) {
+ case ActiveColumn:
+ return tr("Is the resource pack enabled?");
+ case NameColumn:
+ return tr("The name of the resource pack.");
+ case VersionColumn:
+ return tr("The version of the resource pack.");
+ case DateColumn:
+ return tr("The date and time this resource pack was last "
+ "changed (or added).");
+ default:
+ return QVariant();
+ }
+ }
+
+ return ModFolderModel::headerData(section, orientation, role);
+}
diff --git a/meshmc/launcher/minecraft/mod/ResourcePackFolderModel.h b/meshmc/launcher/minecraft/mod/ResourcePackFolderModel.h
new file mode 100644
index 0000000000..7c3008a432
--- /dev/null
+++ b/meshmc/launcher/minecraft/mod/ResourcePackFolderModel.h
@@ -0,0 +1,35 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "ModFolderModel.h"
+
+class ResourcePackFolderModel : public ModFolderModel
+{
+ Q_OBJECT
+
+ public:
+ explicit ResourcePackFolderModel(const QString& dir);
+
+ QVariant headerData(int section, Qt::Orientation orientation,
+ int role) const override;
+};
diff --git a/meshmc/launcher/minecraft/mod/TexturePackFolderModel.cpp b/meshmc/launcher/minecraft/mod/TexturePackFolderModel.cpp
new file mode 100644
index 0000000000..af8510d643
--- /dev/null
+++ b/meshmc/launcher/minecraft/mod/TexturePackFolderModel.cpp
@@ -0,0 +1,50 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "TexturePackFolderModel.h"
+
+TexturePackFolderModel::TexturePackFolderModel(const QString& dir)
+ : ModFolderModel(dir)
+{
+}
+
+QVariant TexturePackFolderModel::headerData(int section,
+ Qt::Orientation orientation,
+ int role) const
+{
+ if (role == Qt::ToolTipRole) {
+ switch (section) {
+ case ActiveColumn:
+ return tr("Is the texture pack enabled?");
+ case NameColumn:
+ return tr("The name of the texture pack.");
+ case VersionColumn:
+ return tr("The version of the texture pack.");
+ case DateColumn:
+ return tr("The date and time this texture pack was last "
+ "changed (or added).");
+ default:
+ return QVariant();
+ }
+ }
+
+ return ModFolderModel::headerData(section, orientation, role);
+}
diff --git a/meshmc/launcher/minecraft/mod/TexturePackFolderModel.h b/meshmc/launcher/minecraft/mod/TexturePackFolderModel.h
new file mode 100644
index 0000000000..d7a49d0ffe
--- /dev/null
+++ b/meshmc/launcher/minecraft/mod/TexturePackFolderModel.h
@@ -0,0 +1,35 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "ModFolderModel.h"
+
+class TexturePackFolderModel : public ModFolderModel
+{
+ Q_OBJECT
+
+ public:
+ explicit TexturePackFolderModel(const QString& dir);
+
+ QVariant headerData(int section, Qt::Orientation orientation,
+ int role) const override;
+};
diff --git a/meshmc/launcher/minecraft/services/CapeChange.cpp b/meshmc/launcher/minecraft/services/CapeChange.cpp
new file mode 100644
index 0000000000..801082e7d0
--- /dev/null
+++ b/meshmc/launcher/minecraft/services/CapeChange.cpp
@@ -0,0 +1,96 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "CapeChange.h"
+
+#include <QNetworkRequest>
+#include <QHttpMultiPart>
+
+#include "Application.h"
+
+CapeChange::CapeChange(QObject* parent, QString token, QString cape)
+ : Task(parent), m_capeId(cape), m_token(token)
+{
+}
+
+void CapeChange::setCape(QString& cape)
+{
+ QNetworkRequest request(QUrl(
+ "https://api.minecraftservices.com/minecraft/profile/capes/active"));
+ auto requestString = QString("{\"capeId\":\"%1\"}").arg(m_capeId);
+ request.setRawHeader("Authorization",
+ QString("Bearer %1").arg(m_token).toLocal8Bit());
+ QNetworkReply* rep =
+ APPLICATION->network()->put(request, requestString.toUtf8());
+
+ setStatus(tr("Equipping cape"));
+
+ m_reply = shared_qobject_ptr<QNetworkReply>(rep);
+ connect(rep, &QNetworkReply::uploadProgress, this, &Task::setProgress);
+ connect(rep, SIGNAL(errorOccurred(QNetworkReply::NetworkError)), this,
+ SLOT(downloadError(QNetworkReply::NetworkError)));
+ connect(rep, SIGNAL(finished()), this, SLOT(downloadFinished()));
+}
+
+void CapeChange::clearCape()
+{
+ QNetworkRequest request(QUrl(
+ "https://api.minecraftservices.com/minecraft/profile/capes/active"));
+ auto requestString = QString("{\"capeId\":\"%1\"}").arg(m_capeId);
+ request.setRawHeader("Authorization",
+ QString("Bearer %1").arg(m_token).toLocal8Bit());
+ QNetworkReply* rep = APPLICATION->network()->deleteResource(request);
+
+ setStatus(tr("Removing cape"));
+
+ m_reply = shared_qobject_ptr<QNetworkReply>(rep);
+ connect(rep, &QNetworkReply::uploadProgress, this, &Task::setProgress);
+ connect(rep, SIGNAL(errorOccurred(QNetworkReply::NetworkError)), this,
+ SLOT(downloadError(QNetworkReply::NetworkError)));
+ connect(rep, SIGNAL(finished()), this, SLOT(downloadFinished()));
+}
+
+void CapeChange::executeTask()
+{
+ if (m_capeId.isEmpty()) {
+ clearCape();
+ } else {
+ setCape(m_capeId);
+ }
+}
+
+void CapeChange::downloadError(QNetworkReply::NetworkError error)
+{
+ // error happened during download.
+ qCritical() << "Network error: " << error;
+ emitFailed(m_reply->errorString());
+}
+
+void CapeChange::downloadFinished()
+{
+ // if the download failed
+ if (m_reply->error() != QNetworkReply::NetworkError::NoError) {
+ emitFailed(QString("Network error: %1").arg(m_reply->errorString()));
+ m_reply.reset();
+ return;
+ }
+ emitSucceeded();
+}
diff --git a/meshmc/launcher/minecraft/services/CapeChange.h b/meshmc/launcher/minecraft/services/CapeChange.h
new file mode 100644
index 0000000000..9b2e0e8258
--- /dev/null
+++ b/meshmc/launcher/minecraft/services/CapeChange.h
@@ -0,0 +1,52 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <QFile>
+#include <QtNetwork/QtNetwork>
+#include <memory>
+#include "tasks/Task.h"
+#include "QObjectPtr.h"
+
+class CapeChange : public Task
+{
+ Q_OBJECT
+ public:
+ CapeChange(QObject* parent, QString token, QString capeId);
+ virtual ~CapeChange() {}
+
+ private:
+ void setCape(QString& cape);
+ void clearCape();
+
+ private:
+ QString m_capeId;
+ QString m_token;
+ shared_qobject_ptr<QNetworkReply> m_reply;
+
+ protected:
+ virtual void executeTask();
+
+ public slots:
+ void downloadError(QNetworkReply::NetworkError);
+ void downloadFinished();
+};
diff --git a/meshmc/launcher/minecraft/services/SkinDelete.cpp b/meshmc/launcher/minecraft/services/SkinDelete.cpp
new file mode 100644
index 0000000000..77f4e16937
--- /dev/null
+++ b/meshmc/launcher/minecraft/services/SkinDelete.cpp
@@ -0,0 +1,66 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "SkinDelete.h"
+
+#include <QNetworkRequest>
+#include <QHttpMultiPart>
+
+#include "Application.h"
+
+SkinDelete::SkinDelete(QObject* parent, QString token)
+ : Task(parent), m_token(token)
+{
+}
+
+void SkinDelete::executeTask()
+{
+ QNetworkRequest request(QUrl(
+ "https://api.minecraftservices.com/minecraft/profile/skins/active"));
+ request.setRawHeader("Authorization",
+ QString("Bearer %1").arg(m_token).toLocal8Bit());
+ QNetworkReply* rep = APPLICATION->network()->deleteResource(request);
+ m_reply = shared_qobject_ptr<QNetworkReply>(rep);
+
+ setStatus(tr("Deleting skin"));
+ connect(rep, &QNetworkReply::uploadProgress, this, &Task::setProgress);
+ connect(rep, SIGNAL(errorOccurred(QNetworkReply::NetworkError)), this,
+ SLOT(downloadError(QNetworkReply::NetworkError)));
+ connect(rep, SIGNAL(finished()), this, SLOT(downloadFinished()));
+}
+
+void SkinDelete::downloadError(QNetworkReply::NetworkError error)
+{
+ // error happened during download.
+ qCritical() << "Network error: " << error;
+ emitFailed(m_reply->errorString());
+}
+
+void SkinDelete::downloadFinished()
+{
+ // if the download failed
+ if (m_reply->error() != QNetworkReply::NetworkError::NoError) {
+ emitFailed(QString("Network error: %1").arg(m_reply->errorString()));
+ m_reply.reset();
+ return;
+ }
+ emitSucceeded();
+}
diff --git a/meshmc/launcher/minecraft/services/SkinDelete.h b/meshmc/launcher/minecraft/services/SkinDelete.h
new file mode 100644
index 0000000000..b2c48aa3d8
--- /dev/null
+++ b/meshmc/launcher/minecraft/services/SkinDelete.h
@@ -0,0 +1,47 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <QFile>
+#include <QtNetwork/QtNetwork>
+#include "tasks/Task.h"
+
+typedef shared_qobject_ptr<class SkinDelete> SkinDeletePtr;
+
+class SkinDelete : public Task
+{
+ Q_OBJECT
+ public:
+ SkinDelete(QObject* parent, QString token);
+ virtual ~SkinDelete() = default;
+
+ private:
+ QString m_token;
+ shared_qobject_ptr<QNetworkReply> m_reply;
+
+ protected:
+ virtual void executeTask();
+
+ public slots:
+ void downloadError(QNetworkReply::NetworkError);
+ void downloadFinished();
+};
diff --git a/meshmc/launcher/minecraft/services/SkinUpload.cpp b/meshmc/launcher/minecraft/services/SkinUpload.cpp
new file mode 100644
index 0000000000..243bd6a5fb
--- /dev/null
+++ b/meshmc/launcher/minecraft/services/SkinUpload.cpp
@@ -0,0 +1,96 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "SkinUpload.h"
+
+#include <QNetworkRequest>
+#include <QHttpMultiPart>
+
+#include "Application.h"
+
+QByteArray getVariant(SkinUpload::Model model)
+{
+ switch (model) {
+ default:
+ qDebug() << "Unknown skin type!";
+ case SkinUpload::STEVE:
+ return "CLASSIC";
+ case SkinUpload::ALEX:
+ return "SLIM";
+ }
+}
+
+SkinUpload::SkinUpload(QObject* parent, QString token, QByteArray skin,
+ SkinUpload::Model model)
+ : Task(parent), m_model(model), m_skin(skin), m_token(token)
+{
+}
+
+void SkinUpload::executeTask()
+{
+ QNetworkRequest request(
+ QUrl("https://api.minecraftservices.com/minecraft/profile/skins"));
+ request.setRawHeader("Authorization",
+ QString("Bearer %1").arg(m_token).toLocal8Bit());
+ QHttpMultiPart* multiPart =
+ new QHttpMultiPart(QHttpMultiPart::FormDataType);
+
+ QHttpPart skin;
+ skin.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("image/png"));
+ skin.setHeader(QNetworkRequest::ContentDispositionHeader,
+ QVariant("form-data; name=\"file\"; filename=\"skin.png\""));
+ skin.setBody(m_skin);
+
+ QHttpPart model;
+ model.setHeader(QNetworkRequest::ContentDispositionHeader,
+ QVariant("form-data; name=\"variant\""));
+ model.setBody(getVariant(m_model));
+
+ multiPart->append(skin);
+ multiPart->append(model);
+
+ QNetworkReply* rep = APPLICATION->network()->post(request, multiPart);
+ m_reply = shared_qobject_ptr<QNetworkReply>(rep);
+
+ setStatus(tr("Uploading skin"));
+ connect(rep, &QNetworkReply::uploadProgress, this, &Task::setProgress);
+ connect(rep, SIGNAL(errorOccurred(QNetworkReply::NetworkError)), this,
+ SLOT(downloadError(QNetworkReply::NetworkError)));
+ connect(rep, SIGNAL(finished()), this, SLOT(downloadFinished()));
+}
+
+void SkinUpload::downloadError(QNetworkReply::NetworkError error)
+{
+ // error happened during download.
+ qCritical() << "Network error: " << error;
+ emitFailed(m_reply->errorString());
+}
+
+void SkinUpload::downloadFinished()
+{
+ // if the download failed
+ if (m_reply->error() != QNetworkReply::NetworkError::NoError) {
+ emitFailed(QString("Network error: %1").arg(m_reply->errorString()));
+ m_reply.reset();
+ return;
+ }
+ emitSucceeded();
+}
diff --git a/meshmc/launcher/minecraft/services/SkinUpload.h b/meshmc/launcher/minecraft/services/SkinUpload.h
new file mode 100644
index 0000000000..39609b5cfe
--- /dev/null
+++ b/meshmc/launcher/minecraft/services/SkinUpload.h
@@ -0,0 +1,56 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <QFile>
+#include <QtNetwork/QtNetwork>
+#include <memory>
+#include "tasks/Task.h"
+
+typedef shared_qobject_ptr<class SkinUpload> SkinUploadPtr;
+
+class SkinUpload : public Task
+{
+ Q_OBJECT
+ public:
+ enum Model { STEVE, ALEX };
+
+ // Note this class takes ownership of the file.
+ SkinUpload(QObject* parent, QString token, QByteArray skin,
+ Model model = STEVE);
+ virtual ~SkinUpload() {}
+
+ private:
+ Model m_model;
+ QByteArray m_skin;
+ QString m_token;
+ shared_qobject_ptr<QNetworkReply> m_reply;
+
+ protected:
+ virtual void executeTask();
+
+ public slots:
+
+ void downloadError(QNetworkReply::NetworkError);
+
+ void downloadFinished();
+};
diff --git a/meshmc/launcher/minecraft/testdata/1.9-simple.json b/meshmc/launcher/minecraft/testdata/1.9-simple.json
new file mode 100644
index 0000000000..574c5b065b
--- /dev/null
+++ b/meshmc/launcher/minecraft/testdata/1.9-simple.json
@@ -0,0 +1,198 @@
+{
+ "assets": "1.9",
+ "id": "1.9",
+ "libraries": [
+ {
+ "name": "oshi-project:oshi-core:1.1"
+ },
+ {
+ "name": "net.java.dev.jna:jna:3.4.0"
+ },
+ {
+ "name": "net.java.dev.jna:platform:3.4.0"
+ },
+ {
+ "name": "com.ibm.icu:icu4j-core-mojang:51.2"
+ },
+ {
+ "name": "net.sf.jopt-simple:jopt-simple:4.6"
+ },
+ {
+ "name": "com.paulscode:codecjorbis:20101023"
+ },
+ {
+ "name": "com.paulscode:codecwav:20101023"
+ },
+ {
+ "name": "com.paulscode:libraryjavasound:20101123"
+ },
+ {
+ "name": "com.paulscode:librarylwjglopenal:20100824"
+ },
+ {
+ "name": "com.paulscode:soundsystem:20120107"
+ },
+ {
+ "name": "io.netty:netty-all:4.0.23.Final"
+ },
+ {
+ "name": "com.google.guava:guava:17.0"
+ },
+ {
+ "name": "org.apache.commons:commons-lang3:3.3.2"
+ },
+ {
+ "name": "commons-io:commons-io:2.4"
+ },
+ {
+ "name": "commons-codec:commons-codec:1.9"
+ },
+ {
+ "name": "net.java.jinput:jinput:2.0.5"
+ },
+ {
+ "name": "net.java.jutils:jutils:1.0.0"
+ },
+ {
+ "name": "com.google.code.gson:gson:2.2.4"
+ },
+ {
+ "name": "com.mojang:authlib:1.5.22"
+ },
+ {
+ "name": "com.mojang:realms:1.8.4"
+ },
+ {
+ "name": "org.apache.commons:commons-compress:1.8.1"
+ },
+ {
+ "name": "org.apache.httpcomponents:httpclient:4.3.3"
+ },
+ {
+ "name": "commons-logging:commons-logging:1.1.3"
+ },
+ {
+ "name": "org.apache.httpcomponents:httpcore:4.3.2"
+ },
+ {
+ "name": "org.apache.logging.log4j:log4j-api:2.0-beta9"
+ },
+ {
+ "name": "org.apache.logging.log4j:log4j-core:2.0-beta9"
+ },
+ {
+ "name": "org.lwjgl.lwjgl:lwjgl:2.9.4-nightly-20150209",
+ "rules": [
+ {
+ "action": "allow"
+ },
+ {
+ "action": "disallow",
+ "os": {
+ "name": "osx"
+ }
+ }
+ ]
+ },
+ {
+ "name": "org.lwjgl.lwjgl:lwjgl_util:2.9.4-nightly-20150209",
+ "rules": [
+ {
+ "action": "allow"
+ },
+ {
+ "action": "disallow",
+ "os": {
+ "name": "osx"
+ }
+ }
+ ]
+ },
+ {
+ "extract": {
+ "exclude": [
+ "META-INF/"
+ ]
+ },
+ "name": "org.lwjgl.lwjgl:lwjgl-platform:2.9.4-nightly-20150209",
+ "natives": {
+ "linux": "natives-linux",
+ "osx": "natives-osx",
+ "windows": "natives-windows"
+ },
+ "rules": [
+ {
+ "action": "allow"
+ },
+ {
+ "action": "disallow",
+ "os": {
+ "name": "osx"
+ }
+ }
+ ]
+ },
+ {
+ "name": "org.lwjgl.lwjgl:lwjgl:2.9.2-nightly-20140822",
+ "rules": [
+ {
+ "action": "allow",
+ "os": {
+ "name": "osx"
+ }
+ }
+ ]
+ },
+ {
+ "name": "org.lwjgl.lwjgl:lwjgl_util:2.9.2-nightly-20140822",
+ "rules": [
+ {
+ "action": "allow",
+ "os": {
+ "name": "osx"
+ }
+ }
+ ]
+ },
+ {
+ "extract": {
+ "exclude": [
+ "META-INF/"
+ ]
+ },
+ "name": "org.lwjgl.lwjgl:lwjgl-platform:2.9.2-nightly-20140822",
+ "natives": {
+ "linux": "natives-linux",
+ "osx": "natives-osx",
+ "windows": "natives-windows"
+ },
+ "rules": [
+ {
+ "action": "allow",
+ "os": {
+ "name": "osx"
+ }
+ }
+ ]
+ },
+ {
+ "extract": {
+ "exclude": [
+ "META-INF/"
+ ]
+ },
+ "name": "net.java.jinput:jinput-platform:2.0.5",
+ "natives": {
+ "linux": "natives-linux",
+ "osx": "natives-osx",
+ "windows": "natives-windows"
+ }
+ }
+ ],
+ "mainClass": "net.minecraft.client.main.Main",
+ "minecraftArguments": "--username ${auth_player_name} --version ${version_name} --gameDir ${game_directory} --assetsDir ${assets_root} --assetIndex ${assets_index_name} --uuid ${auth_uuid} --accessToken ${auth_access_token} --userType ${user_type} --versionType ${version_type}",
+ "minimumLauncherVersion": 18,
+ "releaseTime": "2016-02-29T13:49:54+00:00",
+ "time": "2016-03-01T13:14:53+00:00",
+ "type": "release"
+}
diff --git a/meshmc/launcher/minecraft/testdata/1.9.json b/meshmc/launcher/minecraft/testdata/1.9.json
new file mode 100644
index 0000000000..697c605909
--- /dev/null
+++ b/meshmc/launcher/minecraft/testdata/1.9.json
@@ -0,0 +1,529 @@
+{
+ "assetIndex": {
+ "id": "1.9",
+ "sha1": "cde65b47a43f638653ab1da3848b53f8a7477b16",
+ "size": 136916,
+ "totalSize": 119917473,
+ "url": "https://launchermeta.mojang.com/mc-staging/assets/1.9/cde65b47a43f638653ab1da3848b53f8a7477b16/1.9.json"
+ },
+ "assets": "1.9",
+ "downloads": {
+ "client": {
+ "sha1": "2f67dfe8953299440d1902f9124f0f2c3a2c940f",
+ "size": 8697592,
+ "url": "https://launcher.mojang.com/mc/game/1.9/client/2f67dfe8953299440d1902f9124f0f2c3a2c940f/client.jar"
+ },
+ "server": {
+ "sha1": "b4d449cf2918e0f3bd8aa18954b916a4d1880f0d",
+ "size": 8848015,
+ "url": "https://launcher.mojang.com/mc/game/1.9/server/b4d449cf2918e0f3bd8aa18954b916a4d1880f0d/server.jar"
+ }
+ },
+ "id": "1.9",
+ "libraries": [
+ {
+ "downloads": {
+ "artifact": {
+ "path": "oshi-project/oshi-core/1.1/oshi-core-1.1.jar",
+ "sha1": "9ddf7b048a8d701be231c0f4f95fd986198fd2d8",
+ "size": 30973,
+ "url": "https://libraries.minecraft.net/oshi-project/oshi-core/1.1/oshi-core-1.1.jar"
+ }
+ },
+ "name": "oshi-project:oshi-core:1.1"
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "net/java/dev/jna/jna/3.4.0/jna-3.4.0.jar",
+ "sha1": "803ff252fedbd395baffd43b37341dc4a150a554",
+ "size": 1008730,
+ "url": "https://libraries.minecraft.net/net/java/dev/jna/jna/3.4.0/jna-3.4.0.jar"
+ }
+ },
+ "name": "net.java.dev.jna:jna:3.4.0"
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "net/java/dev/jna/platform/3.4.0/platform-3.4.0.jar",
+ "sha1": "e3f70017be8100d3d6923f50b3d2ee17714e9c13",
+ "size": 913436,
+ "url": "https://libraries.minecraft.net/net/java/dev/jna/platform/3.4.0/platform-3.4.0.jar"
+ }
+ },
+ "name": "net.java.dev.jna:platform:3.4.0"
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "com/ibm/icu/icu4j-core-mojang/51.2/icu4j-core-mojang-51.2.jar",
+ "sha1": "63d216a9311cca6be337c1e458e587f99d382b84",
+ "size": 1634692,
+ "url": "https://libraries.minecraft.net/com/ibm/icu/icu4j-core-mojang/51.2/icu4j-core-mojang-51.2.jar"
+ }
+ },
+ "name": "com.ibm.icu:icu4j-core-mojang:51.2"
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "net/sf/jopt-simple/jopt-simple/4.6/jopt-simple-4.6.jar",
+ "sha1": "306816fb57cf94f108a43c95731b08934dcae15c",
+ "size": 62477,
+ "url": "https://libraries.minecraft.net/net/sf/jopt-simple/jopt-simple/4.6/jopt-simple-4.6.jar"
+ }
+ },
+ "name": "net.sf.jopt-simple:jopt-simple:4.6"
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "com/paulscode/codecjorbis/20101023/codecjorbis-20101023.jar",
+ "sha1": "c73b5636faf089d9f00e8732a829577de25237ee",
+ "size": 103871,
+ "url": "https://libraries.minecraft.net/com/paulscode/codecjorbis/20101023/codecjorbis-20101023.jar"
+ }
+ },
+ "name": "com.paulscode:codecjorbis:20101023"
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "com/paulscode/codecwav/20101023/codecwav-20101023.jar",
+ "sha1": "12f031cfe88fef5c1dd36c563c0a3a69bd7261da",
+ "size": 5618,
+ "url": "https://libraries.minecraft.net/com/paulscode/codecwav/20101023/codecwav-20101023.jar"
+ }
+ },
+ "name": "com.paulscode:codecwav:20101023"
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "com/paulscode/libraryjavasound/20101123/libraryjavasound-20101123.jar",
+ "sha1": "5c5e304366f75f9eaa2e8cca546a1fb6109348b3",
+ "size": 21679,
+ "url": "https://libraries.minecraft.net/com/paulscode/libraryjavasound/20101123/libraryjavasound-20101123.jar"
+ }
+ },
+ "name": "com.paulscode:libraryjavasound:20101123"
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "com/paulscode/librarylwjglopenal/20100824/librarylwjglopenal-20100824.jar",
+ "sha1": "73e80d0794c39665aec3f62eee88ca91676674ef",
+ "size": 18981,
+ "url": "https://libraries.minecraft.net/com/paulscode/librarylwjglopenal/20100824/librarylwjglopenal-20100824.jar"
+ }
+ },
+ "name": "com.paulscode:librarylwjglopenal:20100824"
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "com/paulscode/soundsystem/20120107/soundsystem-20120107.jar",
+ "sha1": "419c05fe9be71f792b2d76cfc9b67f1ed0fec7f6",
+ "size": 65020,
+ "url": "https://libraries.minecraft.net/com/paulscode/soundsystem/20120107/soundsystem-20120107.jar"
+ }
+ },
+ "name": "com.paulscode:soundsystem:20120107"
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "io/netty/netty-all/4.0.23.Final/netty-all-4.0.23.Final.jar",
+ "sha1": "0294104aaf1781d6a56a07d561e792c5d0c95f45",
+ "size": 1779991,
+ "url": "https://libraries.minecraft.net/io/netty/netty-all/4.0.23.Final/netty-all-4.0.23.Final.jar"
+ }
+ },
+ "name": "io.netty:netty-all:4.0.23.Final"
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "com/google/guava/guava/17.0/guava-17.0.jar",
+ "sha1": "9c6ef172e8de35fd8d4d8783e4821e57cdef7445",
+ "size": 2243036,
+ "url": "https://libraries.minecraft.net/com/google/guava/guava/17.0/guava-17.0.jar"
+ }
+ },
+ "name": "com.google.guava:guava:17.0"
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "org/apache/commons/commons-lang3/3.3.2/commons-lang3-3.3.2.jar",
+ "sha1": "90a3822c38ec8c996e84c16a3477ef632cbc87a3",
+ "size": 412739,
+ "url": "https://libraries.minecraft.net/org/apache/commons/commons-lang3/3.3.2/commons-lang3-3.3.2.jar"
+ }
+ },
+ "name": "org.apache.commons:commons-lang3:3.3.2"
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "commons-io/commons-io/2.4/commons-io-2.4.jar",
+ "sha1": "b1b6ea3b7e4aa4f492509a4952029cd8e48019ad",
+ "size": 185140,
+ "url": "https://libraries.minecraft.net/commons-io/commons-io/2.4/commons-io-2.4.jar"
+ }
+ },
+ "name": "commons-io:commons-io:2.4"
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "commons-codec/commons-codec/1.9/commons-codec-1.9.jar",
+ "sha1": "9ce04e34240f674bc72680f8b843b1457383161a",
+ "size": 263965,
+ "url": "https://libraries.minecraft.net/commons-codec/commons-codec/1.9/commons-codec-1.9.jar"
+ }
+ },
+ "name": "commons-codec:commons-codec:1.9"
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "net/java/jinput/jinput/2.0.5/jinput-2.0.5.jar",
+ "sha1": "39c7796b469a600f72380316f6b1f11db6c2c7c4",
+ "size": 208338,
+ "url": "https://libraries.minecraft.net/net/java/jinput/jinput/2.0.5/jinput-2.0.5.jar"
+ }
+ },
+ "name": "net.java.jinput:jinput:2.0.5"
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "net/java/jutils/jutils/1.0.0/jutils-1.0.0.jar",
+ "sha1": "e12fe1fda814bd348c1579329c86943d2cd3c6a6",
+ "size": 7508,
+ "url": "https://libraries.minecraft.net/net/java/jutils/jutils/1.0.0/jutils-1.0.0.jar"
+ }
+ },
+ "name": "net.java.jutils:jutils:1.0.0"
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "com/google/code/gson/gson/2.2.4/gson-2.2.4.jar",
+ "sha1": "a60a5e993c98c864010053cb901b7eab25306568",
+ "size": 190432,
+ "url": "https://libraries.minecraft.net/com/google/code/gson/gson/2.2.4/gson-2.2.4.jar"
+ }
+ },
+ "name": "com.google.code.gson:gson:2.2.4"
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "com/mojang/authlib/1.5.22/authlib-1.5.22.jar",
+ "sha1": "afaa8f6df976fcb5520e76ef1d5798c9e6b5c0b2",
+ "size": 64539,
+ "url": "https://libraries.minecraft.net/com/mojang/authlib/1.5.22/authlib-1.5.22.jar"
+ }
+ },
+ "name": "com.mojang:authlib:1.5.22"
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "com/mojang/realms/1.8.4/realms-1.8.4.jar",
+ "sha1": "15f8dc326c97a96dee6e65392e145ad6d1cb46cb",
+ "size": 1131574,
+ "url": "https://libraries.minecraft.net/com/mojang/realms/1.8.4/realms-1.8.4.jar"
+ }
+ },
+ "name": "com.mojang:realms:1.8.4"
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "org/apache/commons/commons-compress/1.8.1/commons-compress-1.8.1.jar",
+ "sha1": "a698750c16740fd5b3871425f4cb3bbaa87f529d",
+ "size": 365552,
+ "url": "https://libraries.minecraft.net/org/apache/commons/commons-compress/1.8.1/commons-compress-1.8.1.jar"
+ }
+ },
+ "name": "org.apache.commons:commons-compress:1.8.1"
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "org/apache/httpcomponents/httpclient/4.3.3/httpclient-4.3.3.jar",
+ "sha1": "18f4247ff4572a074444572cee34647c43e7c9c7",
+ "size": 589512,
+ "url": "https://libraries.minecraft.net/org/apache/httpcomponents/httpclient/4.3.3/httpclient-4.3.3.jar"
+ }
+ },
+ "name": "org.apache.httpcomponents:httpclient:4.3.3"
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "commons-logging/commons-logging/1.1.3/commons-logging-1.1.3.jar",
+ "sha1": "f6f66e966c70a83ffbdb6f17a0919eaf7c8aca7f",
+ "size": 62050,
+ "url": "https://libraries.minecraft.net/commons-logging/commons-logging/1.1.3/commons-logging-1.1.3.jar"
+ }
+ },
+ "name": "commons-logging:commons-logging:1.1.3"
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "org/apache/httpcomponents/httpcore/4.3.2/httpcore-4.3.2.jar",
+ "sha1": "31fbbff1ddbf98f3aa7377c94d33b0447c646b6e",
+ "size": 282269,
+ "url": "https://libraries.minecraft.net/org/apache/httpcomponents/httpcore/4.3.2/httpcore-4.3.2.jar"
+ }
+ },
+ "name": "org.apache.httpcomponents:httpcore:4.3.2"
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "org/apache/logging/log4j/log4j-api/2.0-beta9/log4j-api-2.0-beta9.jar",
+ "sha1": "1dd66e68cccd907880229f9e2de1314bd13ff785",
+ "size": 108161,
+ "url": "https://libraries.minecraft.net/org/apache/logging/log4j/log4j-api/2.0-beta9/log4j-api-2.0-beta9.jar"
+ }
+ },
+ "name": "org.apache.logging.log4j:log4j-api:2.0-beta9"
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "org/apache/logging/log4j/log4j-core/2.0-beta9/log4j-core-2.0-beta9.jar",
+ "sha1": "678861ba1b2e1fccb594bb0ca03114bb05da9695",
+ "size": 681134,
+ "url": "https://libraries.minecraft.net/org/apache/logging/log4j/log4j-core/2.0-beta9/log4j-core-2.0-beta9.jar"
+ }
+ },
+ "name": "org.apache.logging.log4j:log4j-core:2.0-beta9"
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "org/lwjgl/lwjgl/lwjgl/2.9.4-nightly-20150209/lwjgl-2.9.4-nightly-20150209.jar",
+ "sha1": "697517568c68e78ae0b4544145af031c81082dfe",
+ "size": 1047168,
+ "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl/2.9.4-nightly-20150209/lwjgl-2.9.4-nightly-20150209.jar"
+ }
+ },
+ "name": "org.lwjgl.lwjgl:lwjgl:2.9.4-nightly-20150209",
+ "rules": [
+ {
+ "action": "allow"
+ },
+ {
+ "action": "disallow",
+ "os": {
+ "name": "osx"
+ }
+ }
+ ]
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "org/lwjgl/lwjgl/lwjgl_util/2.9.4-nightly-20150209/lwjgl_util-2.9.4-nightly-20150209.jar",
+ "sha1": "d51a7c040a721d13efdfbd34f8b257b2df882ad0",
+ "size": 173887,
+ "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl_util/2.9.4-nightly-20150209/lwjgl_util-2.9.4-nightly-20150209.jar"
+ }
+ },
+ "name": "org.lwjgl.lwjgl:lwjgl_util:2.9.4-nightly-20150209",
+ "rules": [
+ {
+ "action": "allow"
+ },
+ {
+ "action": "disallow",
+ "os": {
+ "name": "osx"
+ }
+ }
+ ]
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209.jar",
+ "sha1": "b04f3ee8f5e43fa3b162981b50bb72fe1acabb33",
+ "size": 22,
+ "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209.jar"
+ },
+ "classifiers": {
+ "natives-linux": {
+ "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-linux.jar",
+ "sha1": "931074f46c795d2f7b30ed6395df5715cfd7675b",
+ "size": 578680,
+ "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-linux.jar"
+ },
+ "natives-osx": {
+ "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-osx.jar",
+ "sha1": "bcab850f8f487c3f4c4dbabde778bb82bd1a40ed",
+ "size": 426822,
+ "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-osx.jar"
+ },
+ "natives-windows": {
+ "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-windows.jar",
+ "sha1": "b84d5102b9dbfabfeb5e43c7e2828d98a7fc80e0",
+ "size": 613748,
+ "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-windows.jar"
+ }
+ }
+ },
+ "extract": {
+ "exclude": [
+ "META-INF/"
+ ]
+ },
+ "name": "org.lwjgl.lwjgl:lwjgl-platform:2.9.4-nightly-20150209",
+ "natives": {
+ "linux": "natives-linux",
+ "osx": "natives-osx",
+ "windows": "natives-windows"
+ },
+ "rules": [
+ {
+ "action": "allow"
+ },
+ {
+ "action": "disallow",
+ "os": {
+ "name": "osx"
+ }
+ }
+ ]
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "org/lwjgl/lwjgl/lwjgl/2.9.2-nightly-20140822/lwjgl-2.9.2-nightly-20140822.jar",
+ "sha1": "7707204c9ffa5d91662de95f0a224e2f721b22af",
+ "size": 1045632,
+ "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl/2.9.2-nightly-20140822/lwjgl-2.9.2-nightly-20140822.jar"
+ }
+ },
+ "name": "org.lwjgl.lwjgl:lwjgl:2.9.2-nightly-20140822",
+ "rules": [
+ {
+ "action": "allow",
+ "os": {
+ "name": "osx"
+ }
+ }
+ ]
+ },
+ {
+ "downloads": {
+ "artifact": {
+ "path": "org/lwjgl/lwjgl/lwjgl_util/2.9.2-nightly-20140822/lwjgl_util-2.9.2-nightly-20140822.jar",
+ "sha1": "f0e612c840a7639c1f77f68d72a28dae2f0c8490",
+ "size": 173887,
+ "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl_util/2.9.2-nightly-20140822/lwjgl_util-2.9.2-nightly-20140822.jar"
+ }
+ },
+ "name": "org.lwjgl.lwjgl:lwjgl_util:2.9.2-nightly-20140822",
+ "rules": [
+ {
+ "action": "allow",
+ "os": {
+ "name": "osx"
+ }
+ }
+ ]
+ },
+ {
+ "downloads": {
+ "classifiers": {
+ "natives-linux": {
+ "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.2-nightly-20140822/lwjgl-platform-2.9.2-nightly-20140822-natives-linux.jar",
+ "sha1": "d898a33b5d0a6ef3fed3a4ead506566dce6720a5",
+ "size": 578539,
+ "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.2-nightly-20140822/lwjgl-platform-2.9.2-nightly-20140822-natives-linux.jar"
+ },
+ "natives-osx": {
+ "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.2-nightly-20140822/lwjgl-platform-2.9.2-nightly-20140822-natives-osx.jar",
+ "sha1": "79f5ce2fea02e77fe47a3c745219167a542121d7",
+ "size": 468116,
+ "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.2-nightly-20140822/lwjgl-platform-2.9.2-nightly-20140822-natives-osx.jar"
+ },
+ "natives-windows": {
+ "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.2-nightly-20140822/lwjgl-platform-2.9.2-nightly-20140822-natives-windows.jar",
+ "sha1": "78b2a55ce4dc29c6b3ec4df8ca165eba05f9b341",
+ "size": 613680,
+ "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.2-nightly-20140822/lwjgl-platform-2.9.2-nightly-20140822-natives-windows.jar"
+ }
+ }
+ },
+ "extract": {
+ "exclude": [
+ "META-INF/"
+ ]
+ },
+ "name": "org.lwjgl.lwjgl:lwjgl-platform:2.9.2-nightly-20140822",
+ "natives": {
+ "linux": "natives-linux",
+ "osx": "natives-osx",
+ "windows": "natives-windows"
+ },
+ "rules": [
+ {
+ "action": "allow",
+ "os": {
+ "name": "osx"
+ }
+ }
+ ]
+ },
+ {
+ "downloads": {
+ "classifiers": {
+ "natives-linux": {
+ "path": "net/java/jinput/jinput-platform/2.0.5/jinput-platform-2.0.5-natives-linux.jar",
+ "sha1": "7ff832a6eb9ab6a767f1ade2b548092d0fa64795",
+ "size": 10362,
+ "url": "https://libraries.minecraft.net/net/java/jinput/jinput-platform/2.0.5/jinput-platform-2.0.5-natives-linux.jar"
+ },
+ "natives-osx": {
+ "path": "net/java/jinput/jinput-platform/2.0.5/jinput-platform-2.0.5-natives-osx.jar",
+ "sha1": "53f9c919f34d2ca9de8c51fc4e1e8282029a9232",
+ "size": 12186,
+ "url": "https://libraries.minecraft.net/net/java/jinput/jinput-platform/2.0.5/jinput-platform-2.0.5-natives-osx.jar"
+ },
+ "natives-windows": {
+ "path": "net/java/jinput/jinput-platform/2.0.5/jinput-platform-2.0.5-natives-windows.jar",
+ "sha1": "385ee093e01f587f30ee1c8a2ee7d408fd732e16",
+ "size": 155179,
+ "url": "https://libraries.minecraft.net/net/java/jinput/jinput-platform/2.0.5/jinput-platform-2.0.5-natives-windows.jar"
+ }
+ }
+ },
+ "extract": {
+ "exclude": [
+ "META-INF/"
+ ]
+ },
+ "name": "net.java.jinput:jinput-platform:2.0.5",
+ "natives": {
+ "linux": "natives-linux",
+ "osx": "natives-osx",
+ "windows": "natives-windows"
+ }
+ }
+ ],
+ "mainClass": "net.minecraft.client.main.Main",
+ "minecraftArguments": "--username ${auth_player_name} --version ${version_name} --gameDir ${game_directory} --assetsDir ${assets_root} --assetIndex ${assets_index_name} --uuid ${auth_uuid} --accessToken ${auth_access_token} --userType ${user_type} --versionType ${version_type}",
+ "minimumLauncherVersion": 18,
+ "releaseTime": "2016-02-29T13:49:54+00:00",
+ "time": "2016-03-01T13:14:53+00:00",
+ "type": "release"
+}
diff --git a/meshmc/launcher/minecraft/testdata/codecwav-20101023.jar b/meshmc/launcher/minecraft/testdata/codecwav-20101023.jar
new file mode 100644
index 0000000000..f5236083c6
--- /dev/null
+++ b/meshmc/launcher/minecraft/testdata/codecwav-20101023.jar
@@ -0,0 +1 @@
+dummy test file.
diff --git a/meshmc/launcher/minecraft/testdata/lib-native-arch.json b/meshmc/launcher/minecraft/testdata/lib-native-arch.json
new file mode 100644
index 0000000000..501826ae10
--- /dev/null
+++ b/meshmc/launcher/minecraft/testdata/lib-native-arch.json
@@ -0,0 +1,46 @@
+{
+ "downloads": {
+ "classifiers": {
+ "natives-osx": {
+ "path": "tv/twitch/twitch-platform/5.16/twitch-platform-5.16-natives-osx.jar",
+ "sha1": "62503ee712766cf77f97252e5902786fd834b8c5",
+ "size": 418331,
+ "url": "https://libraries.minecraft.net/tv/twitch/twitch-platform/5.16/twitch-platform-5.16-natives-osx.jar"
+ },
+ "natives-windows-32": {
+ "path": "tv/twitch/twitch-platform/5.16/twitch-platform-5.16-natives-windows-32.jar",
+ "sha1": "7c6affe439099806a4f552da14c42f9d643d8b23",
+ "size": 386792,
+ "url": "https://libraries.minecraft.net/tv/twitch/twitch-platform/5.16/twitch-platform-5.16-natives-windows-32.jar"
+ },
+ "natives-windows-64": {
+ "path": "tv/twitch/twitch-platform/5.16/twitch-platform-5.16-natives-windows-64.jar",
+ "sha1": "39d0c3d363735b4785598e0e7fbf8297c706a9f9",
+ "size": 463390,
+ "url": "https://libraries.minecraft.net/tv/twitch/twitch-platform/5.16/twitch-platform-5.16-natives-windows-64.jar"
+ }
+ }
+ },
+ "extract": {
+ "exclude": [
+ "META-INF/"
+ ]
+ },
+ "name": "tv.twitch:twitch-platform:5.16",
+ "natives": {
+ "linux": "natives-linux",
+ "osx": "natives-osx",
+ "windows": "natives-windows-${arch}"
+ },
+ "rules": [
+ {
+ "action": "allow"
+ },
+ {
+ "action": "disallow",
+ "os": {
+ "name": "linux"
+ }
+ }
+ ]
+}
diff --git a/meshmc/launcher/minecraft/testdata/lib-native.json b/meshmc/launcher/minecraft/testdata/lib-native.json
new file mode 100644
index 0000000000..5b9f3b5562
--- /dev/null
+++ b/meshmc/launcher/minecraft/testdata/lib-native.json
@@ -0,0 +1,52 @@
+{
+ "downloads": {
+ "artifact": {
+ "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209.jar",
+ "sha1": "b04f3ee8f5e43fa3b162981b50bb72fe1acabb33",
+ "size": 22,
+ "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209.jar"
+ },
+ "classifiers": {
+ "natives-linux": {
+ "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-linux.jar",
+ "sha1": "931074f46c795d2f7b30ed6395df5715cfd7675b",
+ "size": 578680,
+ "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-linux.jar"
+ },
+ "natives-osx": {
+ "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-osx.jar",
+ "sha1": "bcab850f8f487c3f4c4dbabde778bb82bd1a40ed",
+ "size": 426822,
+ "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-osx.jar"
+ },
+ "natives-windows": {
+ "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-windows.jar",
+ "sha1": "b84d5102b9dbfabfeb5e43c7e2828d98a7fc80e0",
+ "size": 613748,
+ "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-windows.jar"
+ }
+ }
+ },
+ "extract": {
+ "exclude": [
+ "META-INF/"
+ ]
+ },
+ "name": "org.lwjgl.lwjgl:lwjgl-platform:2.9.4-nightly-20150209",
+ "natives": {
+ "linux": "natives-linux",
+ "osx": "natives-osx",
+ "windows": "natives-windows"
+ },
+ "rules": [
+ {
+ "action": "allow"
+ },
+ {
+ "action": "disallow",
+ "os": {
+ "name": "osx"
+ }
+ }
+ ]
+}
diff --git a/meshmc/launcher/minecraft/testdata/lib-simple.json b/meshmc/launcher/minecraft/testdata/lib-simple.json
new file mode 100644
index 0000000000..90bbff074d
--- /dev/null
+++ b/meshmc/launcher/minecraft/testdata/lib-simple.json
@@ -0,0 +1,11 @@
+{
+ "downloads": {
+ "artifact": {
+ "path": "com/paulscode/codecwav/20101023/codecwav-20101023.jar",
+ "sha1": "12f031cfe88fef5c1dd36c563c0a3a69bd7261da",
+ "size": 5618,
+ "url": "https://libraries.minecraft.net/com/paulscode/codecwav/20101023/codecwav-20101023.jar"
+ }
+ },
+ "name": "com.paulscode:codecwav:20101023"
+}
diff --git a/meshmc/launcher/minecraft/testdata/testname-testversion-linux-32.jar b/meshmc/launcher/minecraft/testdata/testname-testversion-linux-32.jar
new file mode 100644
index 0000000000..f5236083c6
--- /dev/null
+++ b/meshmc/launcher/minecraft/testdata/testname-testversion-linux-32.jar
@@ -0,0 +1 @@
+dummy test file.
diff --git a/meshmc/launcher/minecraft/update/AssetUpdateTask.cpp b/meshmc/launcher/minecraft/update/AssetUpdateTask.cpp
new file mode 100644
index 0000000000..e6b2574df8
--- /dev/null
+++ b/meshmc/launcher/minecraft/update/AssetUpdateTask.cpp
@@ -0,0 +1,133 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "AssetUpdateTask.h"
+
+#include "minecraft/MinecraftInstance.h"
+#include "minecraft/PackProfile.h"
+#include "net/ChecksumValidator.h"
+#include "minecraft/AssetsUtils.h"
+
+#include "Application.h"
+
+AssetUpdateTask::AssetUpdateTask(MinecraftInstance* inst)
+{
+ m_inst = inst;
+}
+
+AssetUpdateTask::~AssetUpdateTask() {}
+
+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 = new 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::Download::makeCached(indexUrl, entry);
+ auto rawSha1 = QByteArray::fromHex(assets->sha1.toLatin1());
+ dl->addValidator(
+ new Net::ChecksumValidator(QCryptographicHash::Sha1, rawSha1));
+ 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::progress, this,
+ &AssetUpdateTask::progress);
+
+ 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";
+ // FIXME: this looks like a job for a generic validator based on json
+ // schema?
+ 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) {
+ setStatus(tr("Getting the assets files from Mojang..."));
+ downloadJob = job;
+ connect(downloadJob.get(), &NetJob::succeeded, this,
+ &AssetUpdateTask::emitSucceeded);
+ connect(downloadJob.get(), &NetJob::failed, this,
+ &AssetUpdateTask::assetsFailed);
+ connect(downloadJob.get(), &NetJob::progress, this,
+ &AssetUpdateTask::progress);
+ 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/meshmc/launcher/minecraft/update/AssetUpdateTask.h b/meshmc/launcher/minecraft/update/AssetUpdateTask.h
new file mode 100644
index 0000000000..02b49837b6
--- /dev/null
+++ b/meshmc/launcher/minecraft/update/AssetUpdateTask.h
@@ -0,0 +1,49 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+#include "tasks/Task.h"
+#include "net/NetJob.h"
+class MinecraftInstance;
+
+class AssetUpdateTask : public Task
+{
+ Q_OBJECT
+ public:
+ AssetUpdateTask(MinecraftInstance* inst);
+ virtual ~AssetUpdateTask();
+
+ 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/meshmc/launcher/minecraft/update/FMLLibrariesTask.cpp b/meshmc/launcher/minecraft/update/FMLLibrariesTask.cpp
new file mode 100644
index 0000000000..635af84ef1
--- /dev/null
+++ b/meshmc/launcher/minecraft/update/FMLLibrariesTask.cpp
@@ -0,0 +1,147 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "FMLLibrariesTask.h"
+
+#include "FileSystem.h"
+#include "minecraft/VersionFilterData.h"
+#include "minecraft/MinecraftInstance.h"
+#include "minecraft/PackProfile.h"
+
+#include "BuildConfig.h"
+#include "Application.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..."));
+ auto dljob = new NetJob("FML libraries", APPLICATION->network());
+ auto metacache = APPLICATION->metacache();
+ for (auto& lib : fmlLibsToProcess) {
+ auto entry = metacache->resolveEntry("fmllibs", lib.filename);
+ QString urlString = BuildConfig.FMLLIBS_BASE_URL + lib.filename;
+ dljob->addNetAction(Net::Download::makeCached(QUrl(urlString), entry));
+ }
+
+ connect(dljob, &NetJob::succeeded, this,
+ &FMLLibrariesTask::fmllibsFinished);
+ connect(dljob, &NetJob::failed, this, &FMLLibrariesTask::fmllibsFailed);
+ connect(dljob, &NetJob::progress, this, &FMLLibrariesTask::progress);
+ 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/meshmc/launcher/minecraft/update/FMLLibrariesTask.h b/meshmc/launcher/minecraft/update/FMLLibrariesTask.h
new file mode 100644
index 0000000000..938e287aa3
--- /dev/null
+++ b/meshmc/launcher/minecraft/update/FMLLibrariesTask.h
@@ -0,0 +1,51 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+#include "tasks/Task.h"
+#include "net/NetJob.h"
+#include "minecraft/VersionFilterData.h"
+
+class MinecraftInstance;
+
+class FMLLibrariesTask : public Task
+{
+ Q_OBJECT
+ public:
+ FMLLibrariesTask(MinecraftInstance* inst);
+ virtual ~FMLLibrariesTask() {};
+
+ 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/meshmc/launcher/minecraft/update/FoldersTask.cpp b/meshmc/launcher/minecraft/update/FoldersTask.cpp
new file mode 100644
index 0000000000..9d48baf3c1
--- /dev/null
+++ b/meshmc/launcher/minecraft/update/FoldersTask.cpp
@@ -0,0 +1,40 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "FoldersTask.h"
+#include "minecraft/MinecraftInstance.h"
+#include <QDir>
+
+FoldersTask::FoldersTask(MinecraftInstance* inst) : Task()
+{
+ 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/meshmc/launcher/minecraft/update/FoldersTask.h b/meshmc/launcher/minecraft/update/FoldersTask.h
new file mode 100644
index 0000000000..be31135647
--- /dev/null
+++ b/meshmc/launcher/minecraft/update/FoldersTask.h
@@ -0,0 +1,38 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "tasks/Task.h"
+
+class MinecraftInstance;
+class FoldersTask : public Task
+{
+ Q_OBJECT
+ public:
+ FoldersTask(MinecraftInstance* inst);
+ virtual ~FoldersTask() {};
+
+ void executeTask() override;
+
+ private:
+ MinecraftInstance* m_inst;
+};
diff --git a/meshmc/launcher/minecraft/update/LibrariesTask.cpp b/meshmc/launcher/minecraft/update/LibrariesTask.cpp
new file mode 100644
index 0000000000..0b0524f4e3
--- /dev/null
+++ b/meshmc/launcher/minecraft/update/LibrariesTask.cpp
@@ -0,0 +1,122 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "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("Getting the library files from Mojang..."));
+ 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();
+
+ auto job = new NetJob(tr("Libraries for instance %1").arg(inst->name()),
+ APPLICATION->network());
+ downloadJob.reset(job);
+
+ auto metacache = APPLICATION->metacache();
+
+ auto processArtifactPool = [&](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(currentSystem, 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());
+ 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::progress, this,
+ &LibrariesTask::progress);
+ 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/meshmc/launcher/minecraft/update/LibrariesTask.h b/meshmc/launcher/minecraft/update/LibrariesTask.h
new file mode 100644
index 0000000000..7ac7ec538c
--- /dev/null
+++ b/meshmc/launcher/minecraft/update/LibrariesTask.h
@@ -0,0 +1,47 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+#include "tasks/Task.h"
+#include "net/NetJob.h"
+class MinecraftInstance;
+
+class LibrariesTask : public Task
+{
+ Q_OBJECT
+ public:
+ LibrariesTask(MinecraftInstance* inst);
+ virtual ~LibrariesTask() {};
+
+ 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;
+};