diff options
| author | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-04-02 18:45:07 +0300 |
|---|---|---|
| committer | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-04-02 18:45:07 +0300 |
| commit | 31b9a8949ed0a288143e23bf739f2eb64fdc63be (patch) | |
| tree | 8a984fa143c38fccad461a77792d6864f3e82cd3 /meshmc/launcher/minecraft/ComponentUpdateTask.cpp | |
| parent | 934382c8a1ce738589dee9ee0f14e1cec812770e (diff) | |
| parent | fad6a1066616b69d7f5fef01178efdf014c59537 (diff) | |
| download | Project-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.cpp | 667 |
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(); + } +} |
