From 0af956560c6a4940b16ec8a2b57263b12f6dc451 Mon Sep 17 00:00:00 2001 From: AL <26797547+Al12rs@users.noreply.github.com> Date: Fri, 1 May 2026 12:25:12 +0200 Subject: [PATCH 01/11] Index API response into lookup maps --- src/mainwindow.cpp | 59 ++++++++++++++++++++++++---------------------- 1 file changed, 31 insertions(+), 28 deletions(-) diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index a651c2d7f..b93f9c716 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -3214,6 +3214,20 @@ void MainWindow::nxmUpdatesAvailable(QString gameName, int modID, QVariant userD } } + QHash filesById; + filesById.reserve(files.size()); + for (const auto& file : files) { + const QVariantMap fileData = file.toMap(); + filesById.insert(fileData["file_id"].toInt(), fileData); + } + + QHash updatesByOldId; + updatesByOldId.reserve(fileUpdates.size()); + for (const auto& updateEntry : fileUpdates) { + const QVariantMap updateData = updateEntry.toMap(); + updatesByOldId.insert(updateData["old_file_id"].toInt(), updateData); + } + std::vector modsList = ModInfo::getByModID(gameNameReal, modID); bool requiresInfo = false; @@ -3272,39 +3286,28 @@ void MainWindow::nxmUpdatesAvailable(QString gameName, int modID, QVariant userD // there is at least one update if (currentUpdateId > 0) { - bool lookForMoreUpdates = true; - // follow the update chain until there are no more updates - while (lookForMoreUpdates) { - lookForMoreUpdates = false; - - for (auto& updateEntry : fileUpdates) { - const QVariantMap& updateData = updateEntry.toMap(); - - if (currentUpdateId == updateData["old_file_id"].toInt()) { - currentUpdateId = updateData["new_file_id"].toInt(); - - // check if the new file is still active - for (auto& file : files) { - const QVariantMap& fileData = file.toMap(); + while (true) { + auto updateIt = updatesByOldId.constFind(currentUpdateId); + if (updateIt == updatesByOldId.constEnd()) { + break; + } - if (currentUpdateId == fileData["file_id"].toInt()) { - int updateStatus = fileData["category_id"].toInt(); + currentUpdateId = updateIt.value()["new_file_id"].toInt(); - if (updateStatus != NexusInterface::FileStatus::OLD_VERSION && - updateStatus != NexusInterface::FileStatus::REMOVED && - updateStatus != NexusInterface::FileStatus::ARCHIVED) { + // check if the new file is still active + auto fileIt = filesById.constFind(currentUpdateId); + if (fileIt != filesById.constEnd()) { + const QVariantMap& fileData = fileIt.value(); + int updateStatus = fileData["category_id"].toInt(); - // new version is active, so record it - validNewVersion = fileData["version"].toString(); - foundActiveUpdate = true; - } - break; - } - } + if (updateStatus != NexusInterface::FileStatus::OLD_VERSION && + updateStatus != NexusInterface::FileStatus::REMOVED && + updateStatus != NexusInterface::FileStatus::ARCHIVED) { - lookForMoreUpdates = true; - break; + // new version is active, so record it + validNewVersion = fileData["version"].toString(); + foundActiveUpdate = true; } } } From 57a39b18ef52192407179a50f6fc0574610ccda9 Mon Sep 17 00:00:00 2001 From: AL <26797547+Al12rs@users.noreply.github.com> Date: Fri, 1 May 2026 12:36:31 +0200 Subject: [PATCH 02/11] extract new findLatestActiveSuccessor method from update check function --- src/mainwindow.cpp | 85 +++++++++++++++++++++++++++++++--------------- 1 file changed, 57 insertions(+), 28 deletions(-) diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index b93f9c716..f691e85b4 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -3199,6 +3199,57 @@ void MainWindow::finishUpdateInfo(const NxmUpdateInfoData& data) } } +/** + * @brief Walks the Nexus file update chain forward to find the most recent + * active successor of an installed file. + * + * "Active" means the successor's Nexus category is not OLD_VERSION, REMOVED, + * or ARCHIVED. Successors referenced by the chain but missing from filesById + * are not considered active, but the walk still follows them in case a later + * step is active. Cycles in the chain are guarded against. + * + * @param installedFileId The Nexus file_id to start walking from. If the chain + * has no entry keyed on this id, returns nullopt. + * @param updatesByOldId Index of file_updates entries keyed by old_file_id. + * @param filesById Index of files entries keyed by file_id. + * @return The file_id of the most recent active successor in the chain, or + * nullopt if no active successor exists. + */ +static std::optional +findLatestActiveSuccessor(int installedFileId, + const QHash& updatesByOldId, + const QHash& filesById) +{ + std::optional latestActiveSuccessor; + QSet visited; + int currentId = installedFileId; + + while (true) { + const auto updateIt = updatesByOldId.constFind(currentId); + if (updateIt == updatesByOldId.constEnd()) { + break; + } + + currentId = updateIt.value()["new_file_id"].toInt(); + if (visited.contains(currentId)) { + break; + } + visited.insert(currentId); + + const auto fileIt = filesById.constFind(currentId); + if (fileIt != filesById.constEnd()) { + const int categoryId = fileIt.value()["category_id"].toInt(); + if (categoryId != NexusInterface::FileStatus::OLD_VERSION && + categoryId != NexusInterface::FileStatus::REMOVED && + categoryId != NexusInterface::FileStatus::ARCHIVED) { + latestActiveSuccessor = currentId; + } + } + } + + return latestActiveSuccessor; +} + void MainWindow::nxmUpdatesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID) { @@ -3282,35 +3333,13 @@ void MainWindow::nxmUpdatesAvailable(QString gameName, int modID, QVariant userD } } - bool foundActiveUpdate = false; - - // there is at least one update - if (currentUpdateId > 0) { - // follow the update chain until there are no more updates - while (true) { - auto updateIt = updatesByOldId.constFind(currentUpdateId); - if (updateIt == updatesByOldId.constEnd()) { - break; - } - - currentUpdateId = updateIt.value()["new_file_id"].toInt(); + const std::optional latestActiveSuccessor = + findLatestActiveSuccessor(currentUpdateId, updatesByOldId, filesById); - // check if the new file is still active - auto fileIt = filesById.constFind(currentUpdateId); - if (fileIt != filesById.constEnd()) { - const QVariantMap& fileData = fileIt.value(); - int updateStatus = fileData["category_id"].toInt(); - - if (updateStatus != NexusInterface::FileStatus::OLD_VERSION && - updateStatus != NexusInterface::FileStatus::REMOVED && - updateStatus != NexusInterface::FileStatus::ARCHIVED) { - - // new version is active, so record it - validNewVersion = fileData["version"].toString(); - foundActiveUpdate = true; - } - } - } + const bool foundActiveUpdate = latestActiveSuccessor.has_value(); + if (foundActiveUpdate) { + validNewVersion = + filesById.value(*latestActiveSuccessor)["version"].toString(); } // if there were no active direct file updates for the installedFile From d4594b3e1643fa55def552626c39ed16e0072b06 Mon Sep 17 00:00:00 2001 From: AL <26797547+Al12rs@users.noreply.github.com> Date: Fri, 1 May 2026 13:01:51 +0200 Subject: [PATCH 03/11] extract method to check if a file is active --- src/mainwindow.cpp | 9 ++------- src/nexusinterface.h | 24 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index f691e85b4..e88f2c44c 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -3239,9 +3239,7 @@ findLatestActiveSuccessor(int installedFileId, const auto fileIt = filesById.constFind(currentId); if (fileIt != filesById.constEnd()) { const int categoryId = fileIt.value()["category_id"].toInt(); - if (categoryId != NexusInterface::FileStatus::OLD_VERSION && - categoryId != NexusInterface::FileStatus::REMOVED && - categoryId != NexusInterface::FileStatus::ARCHIVED) { + if (NexusInterface::isActiveFileStatus(categoryId)) { latestActiveSuccessor = currentId; } } @@ -3300,10 +3298,7 @@ void MainWindow::nxmUpdatesAvailable(QString gameName, int modID, QVariant userD foundFileData = fileData; newModStatus = foundFileData["category_id"].toInt(); - if (newModStatus != NexusInterface::FileStatus::OLD_VERSION && - newModStatus != NexusInterface::FileStatus::REMOVED && - newModStatus != NexusInterface::FileStatus::ARCHIVED) { - + if (NexusInterface::isActiveFileStatus(newModStatus)) { // since the file is still active if there are no updates for it, use this // as current version validNewVersion = foundFileData["version"].toString(); diff --git a/src/nexusinterface.h b/src/nexusinterface.h index f6efef1d9..4dadaa5aa 100644 --- a/src/nexusinterface.h +++ b/src/nexusinterface.h @@ -183,6 +183,30 @@ class NexusInterface : public QObject // listed we can assume they were hidden. }; + /** + * @brief Whether a Nexus file category represents a file that is still + * listed and considered current on the mod page. + * + * Inactive (false) refers to files that have been marked as obsolete or have been removed. + */ + static bool isActiveFileStatus(int status) + { + switch (status) { + case FileStatus::MAIN: + case FileStatus::UPDATE: + case FileStatus::OPTIONAL_FILE: + case FileStatus::MISCELLANEOUS: + return true; + case FileStatus::OLD_VERSION: + case FileStatus::REMOVED: + case FileStatus::ARCHIVED: + case FileStatus::ARCHIVED_HIDDEN: + return false; + default: + return false; + } + } + public: static APILimits defaultAPILimits(); static APILimits parseLimits(const QNetworkReply* reply); From 61d47757d3bbad7214cf7194b2129af49b77b1fc Mon Sep 17 00:00:00 2001 From: AL <26797547+Al12rs@users.noreply.github.com> Date: Fri, 1 May 2026 13:04:56 +0200 Subject: [PATCH 04/11] use new isActiveFileStatus in modInfoRegular --- src/modinforegular.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modinforegular.cpp b/src/modinforegular.cpp index 0e73b70cd..44563c04f 100644 --- a/src/modinforegular.cpp +++ b/src/modinforegular.cpp @@ -332,7 +332,7 @@ bool ModInfoRegular::updateAvailable() const if (m_IgnoredVersion.isValid() && (m_IgnoredVersion == m_NewestVersion)) { return false; } - if (m_NexusFileStatus == 4 || m_NexusFileStatus == 6) { + if (!NexusInterface::isActiveFileStatus(m_NexusFileStatus)) { return true; } return m_NewestVersion.isValid() && (m_Version < m_NewestVersion); From ac09e1395007564f4472277f65191e3fb6ba9547 Mon Sep 17 00:00:00 2001 From: AL <26797547+Al12rs@users.noreply.github.com> Date: Fri, 1 May 2026 13:37:26 +0200 Subject: [PATCH 05/11] fix bug when merging during mod install --- src/installationmanager.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/installationmanager.cpp b/src/installationmanager.cpp index 37c86c7e4..87c4681f4 100644 --- a/src/installationmanager.cpp +++ b/src/installationmanager.cpp @@ -491,13 +491,14 @@ InstallationResult InstallationManager::doInstall(GuessedValue& modName return {IPluginInstaller::RESULT_FAILED}; } - bool merge = false; // determine target directory InstallationResult result = testOverwrite(modName); if (!result) { return result; } + const bool merge = result.merged(); + result.m_name = modName; QString targetDirectory = QDir(m_ModsDirectory + "/" + modName).canonicalPath(); From a372cba5d43f5cb10dc5d770a0de205bbfbce279 Mon Sep 17 00:00:00 2001 From: AL <26797547+Al12rs@users.noreply.github.com> Date: Fri, 1 May 2026 14:20:17 +0200 Subject: [PATCH 06/11] remember ordering of installed nexus file ids --- src/modinforegular.cpp | 12 +++++++++--- src/modinforegular.h | 20 ++++++++++++++++++-- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/modinforegular.cpp b/src/modinforegular.cpp index 44563c04f..2042f7725 100644 --- a/src/modinforegular.cpp +++ b/src/modinforegular.cpp @@ -227,8 +227,8 @@ void ModInfoRegular::readMeta() int numFiles = metaFile.beginReadArray("installedFiles"); for (int i = 0; i < numFiles; ++i) { metaFile.setArrayIndex(i); - m_InstalledFileIDs.insert(std::make_pair(metaFile.value("modid").toInt(), - metaFile.value("fileid").toInt())); + m_InstalledFileIDs.emplace_back(metaFile.value("modid").toInt(), + metaFile.value("fileid").toInt()); } metaFile.endArray(); @@ -942,7 +942,13 @@ QStringList ModInfoRegular::archives(bool checkOnDisk) void ModInfoRegular::addInstalledFile(int modId, int fileId) { - m_InstalledFileIDs.insert(std::make_pair(modId, fileId)); + const auto entry = std::make_pair(modId, fileId); + const auto existing = + std::find(m_InstalledFileIDs.begin(), m_InstalledFileIDs.end(), entry); + if (existing != m_InstalledFileIDs.end()) { + m_InstalledFileIDs.erase(existing); + } + m_InstalledFileIDs.push_back(entry); m_MetaInfoChanged = true; } diff --git a/src/modinforegular.h b/src/modinforegular.h index 062f3dd8f..6f5daba87 100644 --- a/src/modinforegular.h +++ b/src/modinforegular.h @@ -2,6 +2,7 @@ #define MODINFOREGULAR_H #include +#include #include "modinfowithconflictinfo.h" #include "nexusinterface.h" @@ -445,7 +446,21 @@ class ModInfoRegular : public ModInfoWithConflictInfo virtual bool validated() const override { return m_Validated; } virtual std::set> installedFiles() const override { - return m_InstalledFileIDs; + return {m_InstalledFileIDs.begin(), m_InstalledFileIDs.end()}; + } + + /** + * @brief Nexus file id of this mod's install. When multiple installs have + * been merged, returns the most recent. + * + * @return The file id, or nullopt if no nexusId has been recorded. + */ + std::optional nexusFileId() const + { + if (m_InstalledFileIDs.empty()) { + return std::nullopt; + } + return m_InstalledFileIDs.back().second; } public: // Plugin operations: @@ -505,7 +520,8 @@ private slots: QColor m_Color; int m_NexusID; - std::set> m_InstalledFileIDs; + // Ordered by install time, oldest first; back is the most recent install. + std::vector> m_InstalledFileIDs; // List of plugin settings: std::map> m_PluginSettings; From 3b6fa94b237427bdc7e28afdf53b561ca47a1f5e Mon Sep 17 00:00:00 2001 From: AL <26797547+Al12rs@users.noreply.github.com> Date: Fri, 1 May 2026 15:08:05 +0200 Subject: [PATCH 07/11] refactor update check logic to prioritize Nexus file IDs over filenames --- src/mainwindow.cpp | 100 ++++++++++++++++++++++----------------------- 1 file changed, 50 insertions(+), 50 deletions(-) diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index e88f2c44c..1be03bf22 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -46,6 +46,7 @@ along with Mod Organizer. If not, see . #include "listdialog.h" #include "localsavegames.h" #include "messagedialog.h" +#include "modinforegular.h" #include "modlist.h" #include "modlistcontextmenu.h" #include "modlistviewactions.h" @@ -3283,71 +3284,70 @@ void MainWindow::nxmUpdatesAvailable(QString gameName, int modID, QVariant userD for (auto mod : modsList) { QString validNewVersion; - int newModStatus = -1; - QString installedFile = QFileInfo(mod->installationFile()).fileName(); - - if (!installedFile.isEmpty()) { - QVariantMap foundFileData; - - // update the file status - for (auto& file : files) { - QVariantMap fileData = file.toMap(); - - if (fileData["file_name"].toString().compare(installedFile, - Qt::CaseInsensitive) == 0) { - foundFileData = fileData; - newModStatus = foundFileData["category_id"].toInt(); - - if (NexusInterface::isActiveFileStatus(newModStatus)) { - // since the file is still active if there are no updates for it, use this - // as current version - validNewVersion = foundFileData["version"].toString(); + int newModStatus = -1; + + // The recorded Nexus file id is the modern source of truth; legacy mods + // fall through to filename matching against the response. + std::optional installedFileId; + if (auto modRegular = dynamic_cast(mod.get())) { + installedFileId = modRegular->nexusFileId(); + } + + if (!installedFileId) { + const QString installedFile = QFileInfo(mod->installationFile()).fileName(); + if (!installedFile.isEmpty()) { + for (const auto& file : files) { + const QVariantMap fileData = file.toMap(); + if (fileData["file_name"].toString().compare(installedFile, + Qt::CaseInsensitive) == 0) { + installedFileId = fileData["file_id"].toInt(); + break; + } + } + if (!installedFileId) { + // Last chance for archived/hidden files which aren't in the files + // list but may still appear in the updates history. + for (const auto& updateEntry : fileUpdates) { + const QVariantMap updateData = updateEntry.toMap(); + if (installedFile.compare(updateData["old_file_name"].toString(), + Qt::CaseInsensitive) == 0) { + installedFileId = updateData["old_file_id"].toInt(); + break; + } } - break; } } + } - if (foundFileData.isEmpty()) { - // The file was not listed, the file is likely archived and archived files are - // being hidden on the mod - newModStatus = NexusInterface::FileStatus::ARCHIVED_HIDDEN; - } - - // look for updates of the file - int currentUpdateId = -1; - - // find installed file ID from the updates list since old filenames are not - // guaranteed to be unique - for (auto& updateEntry : fileUpdates) { - const QVariantMap& updateData = updateEntry.toMap(); - - if (installedFile.compare(updateData["old_file_name"].toString(), - Qt::CaseInsensitive) == 0) { - currentUpdateId = updateData["old_file_id"].toInt(); - break; + if (installedFileId) { + const auto fileIt = filesById.constFind(*installedFileId); + if (fileIt != filesById.constEnd()) { + newModStatus = fileIt.value()["category_id"].toInt(); + if (NexusInterface::isActiveFileStatus(newModStatus)) { + // file is still active; use its version as the current version + validNewVersion = fileIt.value()["version"].toString(); } + } else { + // not listed; likely archived with hidden archives on the mod + newModStatus = NexusInterface::FileStatus::ARCHIVED_HIDDEN; } - const std::optional latestActiveSuccessor = - findLatestActiveSuccessor(currentUpdateId, updatesByOldId, filesById); - + const auto latestActiveSuccessor = + findLatestActiveSuccessor(*installedFileId, updatesByOldId, filesById); const bool foundActiveUpdate = latestActiveSuccessor.has_value(); if (foundActiveUpdate) { validNewVersion = filesById.value(*latestActiveSuccessor)["version"].toString(); } - // if there were no active direct file updates for the installedFile - if (!foundActiveUpdate) { - // get the global mod version in case the file isn't an optional - if (newModStatus != NexusInterface::FileStatus::OPTIONAL_FILE && - newModStatus != NexusInterface::FileStatus::MISCELLANEOUS) { - requiresInfo = true; - } + if (!foundActiveUpdate && + newModStatus != NexusInterface::FileStatus::OPTIONAL_FILE && + newModStatus != NexusInterface::FileStatus::MISCELLANEOUS) { + // No active direct update; fall back to the mod's global version. + requiresInfo = true; } } else { - // No installedFile means we don't know what to look at for a version so - // just get the global mod version + // No file id available; fall back to the mod's global version. requiresInfo = true; } From c480c1888167ee028604315f197bef6bce3b53b7 Mon Sep 17 00:00:00 2001 From: AL <26797547+Al12rs@users.noreply.github.com> Date: Fri, 1 May 2026 15:17:40 +0200 Subject: [PATCH 08/11] refactor nxmUpdatesAvailable to simplify update checking logic --- src/mainwindow.cpp | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 1be03bf22..46c6cc9d7 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -3280,8 +3280,6 @@ void MainWindow::nxmUpdatesAvailable(QString gameName, int modID, QVariant userD std::vector modsList = ModInfo::getByModID(gameNameReal, modID); - bool requiresInfo = false; - for (auto mod : modsList) { QString validNewVersion; int newModStatus = -1; @@ -3334,21 +3332,10 @@ void MainWindow::nxmUpdatesAvailable(QString gameName, int modID, QVariant userD const auto latestActiveSuccessor = findLatestActiveSuccessor(*installedFileId, updatesByOldId, filesById); - const bool foundActiveUpdate = latestActiveSuccessor.has_value(); - if (foundActiveUpdate) { + if (latestActiveSuccessor) { validNewVersion = filesById.value(*latestActiveSuccessor)["version"].toString(); } - - if (!foundActiveUpdate && - newModStatus != NexusInterface::FileStatus::OPTIONAL_FILE && - newModStatus != NexusInterface::FileStatus::MISCELLANEOUS) { - // No active direct update; fall back to the mod's global version. - requiresInfo = true; - } - } else { - // No file id available; fall back to the mod's global version. - requiresInfo = true; } if (newModStatus > 0) { @@ -3357,17 +3344,12 @@ void MainWindow::nxmUpdatesAvailable(QString gameName, int modID, QVariant userD if (!validNewVersion.isEmpty()) { mod->setNewestVersion(validNewVersion); - mod->setLastNexusUpdate(QDateTime::currentDateTimeUtc()); } + mod->setLastNexusUpdate(QDateTime::currentDateTimeUtc()); } // invalidate the filter to display mods with an update ui->modList->invalidateFilter(); - - if (requiresInfo) { - NexusInterface::instance().requestModInfo(gameNameReal, modID, this, QVariant(), - QString()); - } } void MainWindow::nxmModInfoAvailable(QString gameName, int modID, QVariant userData, From 8980bf7279f2e67d1c17219d3789d9c280543734 Mon Sep 17 00:00:00 2001 From: AL <26797547+Al12rs@users.noreply.github.com> Date: Fri, 1 May 2026 16:50:35 +0200 Subject: [PATCH 09/11] refactor update check logic to find Nexus file IDs by filename and streamline successor retrieval --- src/mainwindow.cpp | 174 ++++++++++++++++++++++++++++----------------- 1 file changed, 108 insertions(+), 66 deletions(-) diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 46c6cc9d7..d7d607718 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -3201,27 +3201,55 @@ void MainWindow::finishUpdateInfo(const NxmUpdateInfoData& data) } /** - * @brief Walks the Nexus file update chain forward to find the most recent - * active successor of an installed file. + * @brief Find a Nexus file_id by archive filename (case-insensitive) in the + * update-check response. * - * "Active" means the successor's Nexus category is not OLD_VERSION, REMOVED, - * or ARCHIVED. Successors referenced by the chain but missing from filesById - * are not considered active, but the walk still follows them in case a later - * step is active. Cycles in the chain are guarded against. - * - * @param installedFileId The Nexus file_id to start walking from. If the chain - * has no entry keyed on this id, returns nullopt. - * @param updatesByOldId Index of file_updates entries keyed by old_file_id. - * @param filesById Index of files entries keyed by file_id. - * @return The file_id of the most recent active successor in the chain, or - * nullopt if no active successor exists. + * @return The matched file_id, or nullopt if no entry matches. */ static std::optional -findLatestActiveSuccessor(int installedFileId, - const QHash& updatesByOldId, - const QHash& filesById) +findFileIdByName(const QString& filename, + const QList& files, + const QList& fileUpdates) +{ + // Check the files list + for (const auto& file : files) { + const auto fileData = file.toMap(); + if (fileData["file_name"].toString().compare(filename, + Qt::CaseInsensitive) == 0) { + return fileData["file_id"].toInt(); + } + } + + // Check the update chain entries, archived files can be hidden from + // the files list but still appear in the update chain + for (const auto& updateEntry : fileUpdates) { + const auto updateData = updateEntry.toMap(); + if (filename.compare(updateData["old_file_name"].toString(), + Qt::CaseInsensitive) == 0) { + return updateData["old_file_id"].toInt(); + } + if (filename.compare(updateData["new_file_name"].toString(), + Qt::CaseInsensitive) == 0) { + return updateData["new_file_id"].toInt(); + } + } + return std::nullopt; +} + +/** + * @brief Walks the Nexus file update chain forward and collects every + * successor of an installed file, in chain order. + * + * @param installedFileId The Nexus file_id to start walking from. Returns an + * empty list when the chain has no entry keyed on this id. + * @param updatesByOldId hash map of file_updates entries keyed by old_file_id. + * @return File ids of every chain successor in order (oldest first). + */ +static std::vector +findUpdateChainSuccessors(int installedFileId, + const QHash& updatesByOldId) { - std::optional latestActiveSuccessor; + std::vector successors; QSet visited; int currentId = installedFileId; @@ -3236,17 +3264,10 @@ findLatestActiveSuccessor(int installedFileId, break; } visited.insert(currentId); - - const auto fileIt = filesById.constFind(currentId); - if (fileIt != filesById.constEnd()) { - const int categoryId = fileIt.value()["category_id"].toInt(); - if (NexusInterface::isActiveFileStatus(categoryId)) { - latestActiveSuccessor = currentId; - } - } + successors.push_back(currentId); } - return latestActiveSuccessor; + return successors; } void MainWindow::nxmUpdatesAvailable(QString gameName, int modID, QVariant userData, @@ -3255,11 +3276,11 @@ void MainWindow::nxmUpdatesAvailable(QString gameName, int modID, QVariant userD QVariantMap resultInfo = resultData.toMap(); QList files = resultInfo["files"].toList(); QList fileUpdates = resultInfo["file_updates"].toList(); - QString gameNameReal; + QString gameShortName; for (IPluginGame* game : m_PluginContainer.plugins()) { if (game->gameNexusName() == gameName) { - gameNameReal = game->gameShortName(); + gameShortName = game->gameShortName(); break; } } @@ -3278,78 +3299,99 @@ void MainWindow::nxmUpdatesAvailable(QString gameName, int modID, QVariant userD updatesByOldId.insert(updateData["old_file_id"].toInt(), updateData); } - std::vector modsList = ModInfo::getByModID(gameNameReal, modID); + std::vector modsList = ModInfo::getByModID(gameShortName, modID); - for (auto mod : modsList) { - QString validNewVersion; + bool needsModPageVersion = false; + + for (const auto& mod : modsList) { + QString newestVersionValue; int newModStatus = -1; - // The recorded Nexus file id is the modern source of truth; legacy mods - // fall through to filename matching against the response. + // Use nexus file_id, or fall back to installation file name matching if not available. std::optional installedFileId; if (auto modRegular = dynamic_cast(mod.get())) { installedFileId = modRegular->nexusFileId(); } if (!installedFileId) { - const QString installedFile = QFileInfo(mod->installationFile()).fileName(); - if (!installedFile.isEmpty()) { - for (const auto& file : files) { - const QVariantMap fileData = file.toMap(); - if (fileData["file_name"].toString().compare(installedFile, - Qt::CaseInsensitive) == 0) { - installedFileId = fileData["file_id"].toInt(); - break; - } - } - if (!installedFileId) { - // Last chance for archived/hidden files which aren't in the files - // list but may still appear in the updates history. - for (const auto& updateEntry : fileUpdates) { - const QVariantMap updateData = updateEntry.toMap(); - if (installedFile.compare(updateData["old_file_name"].toString(), - Qt::CaseInsensitive) == 0) { - installedFileId = updateData["old_file_id"].toInt(); - break; - } - } - } + const QString installedFileName = + QFileInfo(mod->installationFile()).fileName(); + if (!installedFileName.isEmpty()) { + installedFileId = findFileIdByName(installedFileName, files, fileUpdates); } } - if (installedFileId) { + if (!installedFileId) { + // No installed file we can identify — typically a manually created mod + // with a mod id assigned via the edit interface. Fall back to the + // mod-page global version since we have nothing to anchor on. + needsModPageVersion = true; + } else { const auto fileIt = filesById.constFind(*installedFileId); if (fileIt != filesById.constEnd()) { newModStatus = fileIt.value()["category_id"].toInt(); - if (NexusInterface::isActiveFileStatus(newModStatus)) { - // file is still active; use its version as the current version - validNewVersion = fileIt.value()["version"].toString(); - } } else { // not listed; likely archived with hidden archives on the mod newModStatus = NexusInterface::FileStatus::ARCHIVED_HIDDEN; } - const auto latestActiveSuccessor = - findLatestActiveSuccessor(*installedFileId, updatesByOldId, filesById); + // Walk the chain once; pick latestActive and latestKnown in a single + // reverse scan (newest entries are at the back). + const auto chainSuccessors = + findUpdateChainSuccessors(*installedFileId, updatesByOldId); + std::optional latestActiveSuccessor; + std::optional latestKnownSuccessor; + for (auto it = chainSuccessors.rbegin(); it != chainSuccessors.rend(); + ++it) { + const auto succIt = filesById.constFind(*it); + if (succIt == filesById.constEnd()) { + continue; + } + if (!latestKnownSuccessor) { + latestKnownSuccessor = *it; + } + if (NexusInterface::isActiveFileStatus( + succIt.value()["category_id"].toInt())) { + latestActiveSuccessor = *it; + break; + } + } + + // Pick the version source per status rules: + // active file → active successor, else our own version + // obsolete → active successor, else latest known successor, else our own version + const bool fileIsActive = + NexusInterface::isActiveFileStatus(newModStatus); + std::optional versionSourceId; if (latestActiveSuccessor) { - validNewVersion = - filesById.value(*latestActiveSuccessor)["version"].toString(); + versionSourceId = latestActiveSuccessor; + } else if (!fileIsActive && latestKnownSuccessor) { + versionSourceId = latestKnownSuccessor; + } else { + versionSourceId = installedFileId; } + + newestVersionValue = + filesById.value(*versionSourceId)["version"].toString(); } if (newModStatus > 0) { mod->setNexusFileStatus(newModStatus); } - if (!validNewVersion.isEmpty()) { - mod->setNewestVersion(validNewVersion); + if (!newestVersionValue.isEmpty()) { + mod->setNewestVersion(newestVersionValue); } mod->setLastNexusUpdate(QDateTime::currentDateTimeUtc()); } // invalidate the filter to display mods with an update ui->modList->invalidateFilter(); + + if (needsModPageVersion) { + NexusInterface::instance().requestModInfo(gameShortName, modID, this, QVariant(), + QString()); + } } void MainWindow::nxmModInfoAvailable(QString gameName, int modID, QVariant userData, From a96a2dc34386855b020f31a4cfdaac1613ea35cf Mon Sep 17 00:00:00 2001 From: AL <26797547+Al12rs@users.noreply.github.com> Date: Fri, 1 May 2026 17:54:57 +0200 Subject: [PATCH 10/11] refactor update checking to streamline version resolution and improve successor retrieval --- src/mainwindow.cpp | 240 +++++++++++++++++++++++++-------------------- 1 file changed, 131 insertions(+), 109 deletions(-) diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index d7d607718..5e78a0b6f 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -69,6 +69,8 @@ along with Mod Organizer. If not, see . #include "spawn.h" #include "statusbar.h" #include "systemtraymanager.h" +#include + #include #include #include @@ -3201,35 +3203,81 @@ void MainWindow::finishUpdateInfo(const NxmUpdateInfoData& data) } /** - * @brief Find a Nexus file_id by archive filename (case-insensitive) in the - * update-check response. + * @brief Walks the Nexus file update chain forward and collects every + * successor of an installed file, in chain order. + * + * @param installedFileId The Nexus file_id to start walking from. Returns an + * empty list when the chain has no entry keyed on this id. + * @param updatesByOldId hash map of file_updates entries keyed by old_file_id. + * @return File ids of every chain successor in order (oldest first). + */ +static std::vector +findUpdateChainSuccessors(int installedFileId, + const QHash& updatesByOldId) +{ + std::vector successors; + QSet visited; + int currentId = installedFileId; + + while (true) { + const auto updateIt = updatesByOldId.constFind(currentId); + if (updateIt == updatesByOldId.constEnd()) { + break; + } + + currentId = updateIt.value()["new_file_id"].toInt(); + if (visited.contains(currentId)) { + break; + } + visited.insert(currentId); + successors.push_back(currentId); + } + + return successors; +} + +/** + * @brief Resolve the Nexus file_id of a mod's installed file (recorded id, + * else filename match). * - * @return The matched file_id, or nullopt if no entry matches. + * @return The file_id, or nullopt if neither path yields one. */ static std::optional -findFileIdByName(const QString& filename, - const QList& files, - const QList& fileUpdates) +resolveInstalledFileId(const ModInfo::Ptr& mod, + const QList& files, + const QList& fileUpdates) { - // Check the files list + if (auto modRegular = dynamic_cast(mod.get())) { + if (auto fileId = modRegular->nexusFileId()) { + return fileId; + } + } + + const QString installedFileName = + QFileInfo(mod->installationFile()).fileName(); + if (installedFileName.isEmpty()) { + return std::nullopt; + } + + // Match the installed filename against the files list. for (const auto& file : files) { const auto fileData = file.toMap(); - if (fileData["file_name"].toString().compare(filename, + if (fileData["file_name"].toString().compare(installedFileName, Qt::CaseInsensitive) == 0) { return fileData["file_id"].toInt(); } } - // Check the update chain entries, archived files can be hidden from - // the files list but still appear in the update chain + // Match against the update chain, archived files can be hidden from the + // files list but still appear here. for (const auto& updateEntry : fileUpdates) { const auto updateData = updateEntry.toMap(); - if (filename.compare(updateData["old_file_name"].toString(), - Qt::CaseInsensitive) == 0) { + if (installedFileName.compare(updateData["old_file_name"].toString(), + Qt::CaseInsensitive) == 0) { return updateData["old_file_id"].toInt(); } - if (filename.compare(updateData["new_file_name"].toString(), - Qt::CaseInsensitive) == 0) { + if (installedFileName.compare(updateData["new_file_name"].toString(), + Qt::CaseInsensitive) == 0) { return updateData["new_file_id"].toInt(); } } @@ -3237,37 +3285,59 @@ findFileIdByName(const QString& filename, } /** - * @brief Walks the Nexus file update chain forward and collects every - * successor of an installed file, in chain order. + * @brief Pick the newest available version for an installed file by walking + * the update chain. Active files prefer an active successor; obsolete + * files also accept the latest downloadable (listed, not removed) + * successor; both fall back to the file's own version. * - * @param installedFileId The Nexus file_id to start walking from. Returns an - * empty list when the chain has no entry keyed on this id. - * @param updatesByOldId hash map of file_updates entries keyed by old_file_id. - * @return File ids of every chain successor in order (oldest first). + * @return Version string, or empty if none can be derived. */ -static std::vector -findUpdateChainSuccessors(int installedFileId, - const QHash& updatesByOldId) +static QString +pickNewestVersion(int installedFileId, + bool fileIsActive, + const QHash& filesById, + const QHash& updatesByOldId) { - std::vector successors; - QSet visited; - int currentId = installedFileId; + const auto chainSuccessors = + findUpdateChainSuccessors(installedFileId, updatesByOldId); - while (true) { - const auto updateIt = updatesByOldId.constFind(currentId); - if (updateIt == updatesByOldId.constEnd()) { - break; + std::optional latestActiveSuccessor; + std::optional latestDownloadableSuccessor; + + // Find the latest active and latest downloadable successors + for (const int successorId : std::views::reverse(chainSuccessors)) { + const auto succIt = filesById.constFind(successorId); + + if (succIt == filesById.constEnd()) { + // Not in files list — effectively archived and hidden by the author. + continue; } - currentId = updateIt.value()["new_file_id"].toInt(); - if (visited.contains(currentId)) { + const int succStatus = succIt.value()["category_id"].toInt(); + + if (NexusInterface::isActiveFileStatus(succStatus)) { + latestActiveSuccessor = successorId; break; } - visited.insert(currentId); - successors.push_back(currentId); + + if (!latestDownloadableSuccessor && + succStatus != NexusInterface::FileStatus::REMOVED) { + latestDownloadableSuccessor = successorId; + } } - return successors; + std::optional versionSourceId; + if (latestActiveSuccessor) { + versionSourceId = latestActiveSuccessor; + + } else if (!fileIsActive && latestDownloadableSuccessor) { + versionSourceId = latestDownloadableSuccessor; + + } else { + versionSourceId = installedFileId; + } + + return filesById.value(*versionSourceId)["version"].toString(); } void MainWindow::nxmUpdatesAvailable(QString gameName, int modID, QVariant userData, @@ -3276,8 +3346,8 @@ void MainWindow::nxmUpdatesAvailable(QString gameName, int modID, QVariant userD QVariantMap resultInfo = resultData.toMap(); QList files = resultInfo["files"].toList(); QList fileUpdates = resultInfo["file_updates"].toList(); - QString gameShortName; + QString gameShortName; for (IPluginGame* game : m_PluginContainer.plugins()) { if (game->gameNexusName() == gameName) { gameShortName = game->gameShortName(); @@ -3299,90 +3369,42 @@ void MainWindow::nxmUpdatesAvailable(QString gameName, int modID, QVariant userD updatesByOldId.insert(updateData["old_file_id"].toInt(), updateData); } - std::vector modsList = ModInfo::getByModID(gameShortName, modID); - bool needsModPageVersion = false; + std::vector installedMods = ModInfo::getByModID(gameShortName, modID); - for (const auto& mod : modsList) { - QString newestVersionValue; - int newModStatus = -1; - - // Use nexus file_id, or fall back to installation file name matching if not available. - std::optional installedFileId; - if (auto modRegular = dynamic_cast(mod.get())) { - installedFileId = modRegular->nexusFileId(); - } + // Check each installed mod matching nexus modID against the update info + for (const auto& installedMod : installedMods) { + installedMod->setLastNexusUpdate(QDateTime::currentDateTimeUtc()); - if (!installedFileId) { - const QString installedFileName = - QFileInfo(mod->installationFile()).fileName(); - if (!installedFileName.isEmpty()) { - installedFileId = findFileIdByName(installedFileName, files, fileUpdates); - } - } + // Identify the installed file. + const auto installedFileId = + resolveInstalledFileId(installedMod, files, fileUpdates); if (!installedFileId) { - // No installed file we can identify — typically a manually created mod - // with a mod id assigned via the edit interface. Fall back to the - // mod-page global version since we have nothing to anchor on. + // Manually created mod with mod_id assigned via the edit interface; + // fall back to mod page version. needsModPageVersion = true; - } else { - const auto fileIt = filesById.constFind(*installedFileId); - if (fileIt != filesById.constEnd()) { - newModStatus = fileIt.value()["category_id"].toInt(); - } else { - // not listed; likely archived with hidden archives on the mod - newModStatus = NexusInterface::FileStatus::ARCHIVED_HIDDEN; - } - - // Walk the chain once; pick latestActive and latestKnown in a single - // reverse scan (newest entries are at the back). - const auto chainSuccessors = - findUpdateChainSuccessors(*installedFileId, updatesByOldId); - std::optional latestActiveSuccessor; - std::optional latestKnownSuccessor; - for (auto it = chainSuccessors.rbegin(); it != chainSuccessors.rend(); - ++it) { - const auto succIt = filesById.constFind(*it); - if (succIt == filesById.constEnd()) { - continue; - } - if (!latestKnownSuccessor) { - latestKnownSuccessor = *it; - } - if (NexusInterface::isActiveFileStatus( - succIt.value()["category_id"].toInt())) { - latestActiveSuccessor = *it; - break; - } - } - - // Pick the version source per status rules: - // active file → active successor, else our own version - // obsolete → active successor, else latest known successor, else our own version - const bool fileIsActive = - NexusInterface::isActiveFileStatus(newModStatus); - std::optional versionSourceId; - if (latestActiveSuccessor) { - versionSourceId = latestActiveSuccessor; - } else if (!fileIsActive && latestKnownSuccessor) { - versionSourceId = latestKnownSuccessor; - } else { - versionSourceId = installedFileId; - } - - newestVersionValue = - filesById.value(*versionSourceId)["version"].toString(); + continue; } - if (newModStatus > 0) { - mod->setNexusFileStatus(newModStatus); - } + // Determine the installed file's current Nexus status. + int nexusFileStatus = NexusInterface::FileStatus::ARCHIVED_HIDDEN; + if (const auto fileIt = filesById.constFind(*installedFileId); + fileIt != filesById.constEnd()) { + nexusFileStatus = fileIt.value()["category_id"].toInt(); + } + installedMod->setNexusFileStatus(nexusFileStatus); + + // Find the newest available version per the update chain. + const QString newestVersionValue = pickNewestVersion( + *installedFileId, + NexusInterface::isActiveFileStatus(nexusFileStatus), + filesById, updatesByOldId); + if (!newestVersionValue.isEmpty()) { - mod->setNewestVersion(newestVersionValue); + installedMod->setNewestVersion(newestVersionValue); } - mod->setLastNexusUpdate(QDateTime::currentDateTimeUtc()); } // invalidate the filter to display mods with an update From 5e3351037456ea57d80340bba4c6626dd55daf3a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 16:07:02 +0000 Subject: [PATCH 11/11] [pre-commit.ci] Auto fixes from pre-commit.com hooks. --- src/mainwindow.cpp | 27 +++++++++++---------------- src/nexusinterface.h | 27 ++++++++++++++------------- 2 files changed, 25 insertions(+), 29 deletions(-) diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 5e78a0b6f..1abe6c7c7 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -3242,10 +3242,9 @@ findUpdateChainSuccessors(int installedFileId, * * @return The file_id, or nullopt if neither path yields one. */ -static std::optional -resolveInstalledFileId(const ModInfo::Ptr& mod, - const QList& files, - const QList& fileUpdates) +static std::optional resolveInstalledFileId(const ModInfo::Ptr& mod, + const QList& files, + const QList& fileUpdates) { if (auto modRegular = dynamic_cast(mod.get())) { if (auto fileId = modRegular->nexusFileId()) { @@ -3253,8 +3252,7 @@ resolveInstalledFileId(const ModInfo::Ptr& mod, } } - const QString installedFileName = - QFileInfo(mod->installationFile()).fileName(); + const QString installedFileName = QFileInfo(mod->installationFile()).fileName(); if (installedFileName.isEmpty()) { return std::nullopt; } @@ -3292,11 +3290,9 @@ resolveInstalledFileId(const ModInfo::Ptr& mod, * * @return Version string, or empty if none can be derived. */ -static QString -pickNewestVersion(int installedFileId, - bool fileIsActive, - const QHash& filesById, - const QHash& updatesByOldId) +static QString pickNewestVersion(int installedFileId, bool fileIsActive, + const QHash& filesById, + const QHash& updatesByOldId) { const auto chainSuccessors = findUpdateChainSuccessors(installedFileId, updatesByOldId); @@ -3336,7 +3332,7 @@ pickNewestVersion(int installedFileId, } else { versionSourceId = installedFileId; } - + return filesById.value(*versionSourceId)["version"].toString(); } @@ -3369,7 +3365,7 @@ void MainWindow::nxmUpdatesAvailable(QString gameName, int modID, QVariant userD updatesByOldId.insert(updateData["old_file_id"].toInt(), updateData); } - bool needsModPageVersion = false; + bool needsModPageVersion = false; std::vector installedMods = ModInfo::getByModID(gameShortName, modID); // Check each installed mod matching nexus modID against the update info @@ -3398,10 +3394,9 @@ void MainWindow::nxmUpdatesAvailable(QString gameName, int modID, QVariant userD // Find the newest available version per the update chain. const QString newestVersionValue = pickNewestVersion( - *installedFileId, - NexusInterface::isActiveFileStatus(nexusFileStatus), + *installedFileId, NexusInterface::isActiveFileStatus(nexusFileStatus), filesById, updatesByOldId); - + if (!newestVersionValue.isEmpty()) { installedMod->setNewestVersion(newestVersionValue); } diff --git a/src/nexusinterface.h b/src/nexusinterface.h index 4dadaa5aa..dd9650b11 100644 --- a/src/nexusinterface.h +++ b/src/nexusinterface.h @@ -187,23 +187,24 @@ class NexusInterface : public QObject * @brief Whether a Nexus file category represents a file that is still * listed and considered current on the mod page. * - * Inactive (false) refers to files that have been marked as obsolete or have been removed. + * Inactive (false) refers to files that have been marked as obsolete or have been + * removed. */ static bool isActiveFileStatus(int status) { switch (status) { - case FileStatus::MAIN: - case FileStatus::UPDATE: - case FileStatus::OPTIONAL_FILE: - case FileStatus::MISCELLANEOUS: - return true; - case FileStatus::OLD_VERSION: - case FileStatus::REMOVED: - case FileStatus::ARCHIVED: - case FileStatus::ARCHIVED_HIDDEN: - return false; - default: - return false; + case FileStatus::MAIN: + case FileStatus::UPDATE: + case FileStatus::OPTIONAL_FILE: + case FileStatus::MISCELLANEOUS: + return true; + case FileStatus::OLD_VERSION: + case FileStatus::REMOVED: + case FileStatus::ARCHIVED: + case FileStatus::ARCHIVED_HIDDEN: + return false; + default: + return false; } }