/* Copyright (C) 2012 Sebastian Herbord. All rights reserved. This file is part of Mod Organizer. Mod Organizer is free software: 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. Mod Organizer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Mod Organizer. If not, see . */ #include "directoryrefresher.h" #include "shared/fileentry.h" #include "shared/filesorigin.h" #include "envfs.h" #include "game_features.h" #include "iplugingame.h" #include "modinfo.h" #include "modinfodialogfwd.h" #include "organizercore.h" #include "report.h" #include "settings.h" #include "shared/util.h" #include "utility.h" #include #include #include #include #include using namespace MOBase; using namespace MOShared; DirectoryStats::DirectoryStats() { std::memset(this, 0, sizeof(DirectoryStats)); } DirectoryStats& DirectoryStats::operator+=(const DirectoryStats& o) { dirTimes += o.dirTimes; fileTimes += o.fileTimes; sortTimes += o.sortTimes; subdirLookupTimes += o.subdirLookupTimes; addDirectoryTimes += o.addDirectoryTimes; filesLookupTimes += o.filesLookupTimes; addFileTimes += o.addFileTimes; addOriginToFileTimes += o.addOriginToFileTimes; addFileToOriginTimes += o.addFileToOriginTimes; addFileToRegisterTimes += o.addFileToRegisterTimes; originExists += o.originExists; originCreate += o.originCreate; originsNeededEnabled += o.originsNeededEnabled; subdirExists += o.subdirExists; subdirCreate += o.subdirCreate; fileExists += o.fileExists; fileCreate += o.fileCreate; filesInsertedInRegister += o.filesInsertedInRegister; filesAssignedInRegister += o.filesAssignedInRegister; return *this; } std::string DirectoryStats::csvHeader() { QStringList sl = {"dirTimes", "fileTimes", "sortTimes", "subdirLookupTimes", "addDirectoryTimes", "filesLookupTimes", "addFileTimes", "addOriginToFileTimes", "addFileToOriginTimes", "addFileToRegisterTimes", "originExists", "originCreate", "originsNeededEnabled", "subdirExists", "subdirCreate", "fileExists", "fileCreate", "filesInsertedInRegister", "filesAssignedInRegister"}; return sl.join(",").toStdString(); } std::string DirectoryStats::toCsv() const { QStringList oss; auto s = [](auto ns) { return ns.count() / 1000.0 / 1000.0 / 1000.0; }; oss << QString::number(s(dirTimes)) << QString::number(s(fileTimes)) << QString::number(s(sortTimes)) << QString::number(s(subdirLookupTimes)) << QString::number(s(addDirectoryTimes)) << QString::number(s(filesLookupTimes)) << QString::number(s(addFileTimes)) << QString::number(s(addOriginToFileTimes)) << QString::number(s(addFileToOriginTimes)) << QString::number(s(addFileToRegisterTimes)) << QString::number(originExists) << QString::number(originCreate) << QString::number(originsNeededEnabled) << QString::number(subdirExists) << QString::number(subdirCreate) << QString::number(fileExists) << QString::number(fileCreate) << QString::number(filesInsertedInRegister) << QString::number(filesAssignedInRegister); return oss.join(",").toStdString(); } void dumpStats(std::vector& stats) { static int run = 0; static const std::string file("c:\\tmp\\data.csv"); if (run == 0) { std::ofstream out(file, std::ios::out | std::ios::trunc); out << std::format("what,run,{}", DirectoryStats::csvHeader()) << "\n"; } std::sort(stats.begin(), stats.end(), [](auto&& a, auto&& b) { return (naturalCompare(QString::fromStdString(a.mod), QString::fromStdString(b.mod)) < 0); }); std::ofstream out(file, std::ios::app); DirectoryStats total; for (const auto& s : stats) { out << std::format("{},{},{}", s.mod, run, s.toCsv()) << "\n"; total += s; } out << std::format("total,{},{}", run, total.toCsv()) << "\n"; ++run; } DirectoryRefresher::DirectoryRefresher(OrganizerCore* core, std::size_t threadCount) : m_Core(*core), m_threadCount(threadCount), m_lastFileCount(0) {} DirectoryEntry* DirectoryRefresher::stealDirectoryStructure() { QMutexLocker locker(&m_RefreshLock); return m_Root.release(); } void DirectoryRefresher::setMods( const std::vector>& mods, const std::set& managedArchives) { QMutexLocker locker(&m_RefreshLock); m_Mods.clear(); for (auto mod = mods.begin(); mod != mods.end(); ++mod) { QString name = std::get<0>(*mod); ModInfo::Ptr info = ModInfo::getByIndex(ModInfo::getIndex(name)); QString path = std::get<1>(*mod); QString modDataDir = m_Core.managedGame()->modDataDirectory(); path = modDataDir.isEmpty() ? path : path + "/" + modDataDir; m_Mods.push_back( EntryInfo(name, path, info->stealFiles(), info->archives(), std::get<2>(*mod))); } m_EnabledArchives = managedArchives; } void DirectoryRefresher::cleanStructure(DirectoryEntry* structure) { static const wchar_t* files[] = {L"meta.ini", L"readme.txt"}; for (int i = 0; i < sizeof(files) / sizeof(wchar_t*); ++i) { structure->removeFile(files[i]); } static const wchar_t* dirs[] = {L"fomod"}; for (int i = 0; i < sizeof(dirs) / sizeof(wchar_t*); ++i) { structure->removeDir(std::wstring(dirs[i])); } } void DirectoryRefresher::addModBSAToStructure(DirectoryEntry* root, const QString& modName, int priority, const QString& directory, const QStringList& archives) { QStringList loadOrder; auto gamePlugins = m_Core.gameFeatures().gameFeature(); if (gamePlugins) { loadOrder = gamePlugins->getLoadOrder(); } std::vector lo; for (auto&& s : loadOrder) { lo.push_back(s.toStdWString()); } std::vector archivesW; for (auto&& a : archives) { archivesW.push_back(a.toStdWString()); } std::set enabledArchives; for (auto&& a : m_EnabledArchives) { enabledArchives.insert(a.toStdWString()); } DirectoryStats dummy; root->addFromAllBSAs(modName.toStdWString(), QDir::toNativeSeparators(directory).toStdWString(), priority, archivesW, enabledArchives, lo, dummy); } void DirectoryRefresher::stealModFilesIntoStructure(DirectoryEntry* directoryStructure, const QString& modName, int priority, const QString& directory, const QStringList& stealFiles) { std::wstring directoryW = ToWString(QDir::toNativeSeparators(directory)); // instead of adding all the files of the target directory, we just change the root of // the specified files to this mod DirectoryStats dummy; FilesOrigin& origin = directoryStructure->createOrigin(ToWString(modName), directoryW, priority, dummy); for (const QString& filename : stealFiles) { if (filename.isEmpty()) { log::warn("Trying to find file with no name"); log::warn(" . modName: {}", modName); log::warn(" . directory: {}", directory); log::warn(" . priority: {}", priority); for (int i = 0; i < stealFiles.length(); ++i) log::warn(" . stealFiles[{}]: {}", i, stealFiles[i]); continue; } QFileInfo fileInfo(filename); FileEntryPtr file = directoryStructure->findFile(ToWString(fileInfo.fileName())); if (file.get() != nullptr) { if (file->getOrigin() == 0) { // replace data as the origin on this bsa file->removeOrigin(0); } origin.addFile(file->getIndex()); file->addOrigin(origin.getID(), file->getFileTime(), L"", -1); } else { QString warnStr = fileInfo.absolutePath(); if (warnStr.isEmpty()) warnStr = filename; log::warn("file not found: {}", warnStr); } } } void DirectoryRefresher::addModFilesToStructure(DirectoryEntry* directoryStructure, const QString& modName, int priority, const QString& directory, const QStringList& stealFiles) { TimeThis tt("DirectoryRefresher::addModFilesToStructure()"); std::wstring directoryW = ToWString(QDir::toNativeSeparators(directory)); DirectoryStats dummy; if (stealFiles.length() > 0) { stealModFilesIntoStructure(directoryStructure, modName, priority, directory, stealFiles); } else { directoryStructure->addFromOrigin(ToWString(modName), directoryW, priority, dummy); } } void DirectoryRefresher::addModToStructure(DirectoryEntry* directoryStructure, const QString& modName, int priority, const QString& directory, const QStringList& stealFiles, const QStringList& archives) { TimeThis tt("DirectoryRefresher::addModToStructure()"); DirectoryStats dummy; if (stealFiles.length() > 0) { stealModFilesIntoStructure(directoryStructure, modName, priority, directory, stealFiles); } else { std::wstring directoryW = ToWString(QDir::toNativeSeparators(directory)); directoryStructure->addFromOrigin(ToWString(modName), directoryW, priority, dummy); } if (Settings::instance().archiveParsing()) { addModBSAToStructure(directoryStructure, modName, priority, directory, archives); } } struct ModThread { GameFeatures* gameFeatures; DirectoryRefreshProgress* progress = nullptr; DirectoryEntry* ds = nullptr; std::wstring modName; std::wstring path; int prio = -1; std::vector archives; std::set enabledArchives; std::vector* loadOrder = nullptr; DirectoryStats* stats = nullptr; env::DirectoryWalker walker; std::condition_variable cv; std::mutex mutex; bool ready = false; void wakeup() { { std::scoped_lock lock(mutex); ready = true; } cv.notify_one(); } void run() { std::unique_lock lock(mutex); cv.wait(lock, [&] { return ready; }); SetThisThreadName(QString::fromStdWString(modName + L" refresher")); ds->addFromOrigin(walker, modName, path, prio, *stats); if (Settings::instance().archiveParsing()) { ds->addFromAllBSAs(modName, path, prio, archives, enabledArchives, *loadOrder, *stats); } if (progress) { progress->addDone(); } SetThisThreadName(QString::fromStdWString(L"idle refresher")); ready = false; } }; env::ThreadPool g_threads; void DirectoryRefresher::updateProgress(const DirectoryRefreshProgress* p) { // careful: called from multiple threads emit progress(p); } void DirectoryRefresher::addMultipleModsFilesToStructure( MOShared::DirectoryEntry* directoryStructure, const std::vector& entries, DirectoryRefreshProgress* progress) { std::vector stats(entries.size()); if (progress) { progress->start(entries.size()); } log::debug("refresher: using {} threads", m_threadCount); g_threads.setMax(m_threadCount); std::vector loadOrder; if (Settings::instance().archiveParsing()) { auto gamePlugins = m_Core.gameFeatures().gameFeature(); if (gamePlugins) { QStringList lo = gamePlugins->getLoadOrder(); loadOrder.reserve(lo.size()); for (auto&& s : lo) { loadOrder.push_back(s.toStdWString()); } } } for (std::size_t i = 0; i < entries.size(); ++i) { const auto& e = entries[i]; const int prio = e.priority + 1; if constexpr (DirectoryStats::EnableInstrumentation) { stats[i].mod = entries[i].modName.toStdString(); } try { if (e.stealFiles.length() > 0) { stealModFilesIntoStructure(directoryStructure, e.modName, prio, e.absolutePath, e.stealFiles); if (progress) { progress->addDone(); } } else { auto& mt = g_threads.request(); mt.gameFeatures = &m_Core.gameFeatures(); mt.progress = progress; mt.ds = directoryStructure; mt.modName = e.modName.toStdWString(); mt.path = QDir::toNativeSeparators(e.absolutePath).toStdWString(); mt.prio = prio; mt.archives.clear(); for (auto&& a : e.archives) { mt.archives.push_back(a.toStdWString()); } mt.enabledArchives.clear(); for (auto&& a : m_EnabledArchives) { mt.enabledArchives.insert(a.toStdWString()); } mt.loadOrder = &loadOrder; mt.stats = &stats[i]; mt.wakeup(); } } catch (const std::exception& ex) { emit error(tr("failed to read mod (%1): %2").arg(e.modName, ex.what())); } } g_threads.waitForAll(); if constexpr (DirectoryStats::EnableInstrumentation) { dumpStats(stats); } } void DirectoryRefresher::refresh() { SetThisThreadName("DirectoryRefresher"); TimeThis tt("DirectoryRefresher::refresh()"); auto* p = new DirectoryRefreshProgress(this); { QMutexLocker locker(&m_RefreshLock); m_Root.reset(new DirectoryEntry(L"data", nullptr, 0)); IPluginGame* game = qApp->property("managed_game").value(); std::wstring dataDirectory = QDir::toNativeSeparators(game->dataDirectory().absolutePath()).toStdWString(); { DirectoryStats dummy; m_Root->addFromOrigin(L"data", dataDirectory, 0, dummy); } for (auto directory : game->secondaryDataDirectories().toStdMap()) { DirectoryStats dummy; m_Root->addFromOrigin( directory.first.toStdWString(), QDir::toNativeSeparators(directory.second.absolutePath()).toStdWString(), 0, dummy); } std::sort(m_Mods.begin(), m_Mods.end(), [](auto lhs, auto rhs) { return lhs.priority < rhs.priority; }); addMultipleModsFilesToStructure(m_Root.get(), m_Mods, p); m_Root->getFileRegister()->sortOrigins(); cleanStructure(m_Root.get()); m_lastFileCount = m_Root->getFileRegister()->highestCount(); log::debug("refresher saw {} files", m_lastFileCount); } p->finish(); emit progress(p); emit refreshed(); }