summaryrefslogtreecommitdiff
path: root/meshmc/launcher/minecraft/ComponentUpdateTask.cpp
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/ComponentUpdateTask.cpp
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/ComponentUpdateTask.cpp')
-rw-r--r--meshmc/launcher/minecraft/ComponentUpdateTask.cpp667
1 files changed, 667 insertions, 0 deletions
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();
+ }
+}