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 <http://www.gnu.org/licenses/>.
*/
#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 <gameplugins.h>
#include <QApplication>
#include <QDir>
#include <QString>
#include <fstream>
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<DirectoryStats>& 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<std::tuple<QString, QString, int>>& mods,
const std::set<QString>& 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<GamePlugins>();
if (gamePlugins) {
loadOrder = gamePlugins->getLoadOrder();
}
std::vector<std::wstring> lo;
for (auto&& s : loadOrder) {
lo.push_back(s.toStdWString());
}
std::vector<std::wstring> archivesW;
for (auto&& a : archives) {
archivesW.push_back(a.toStdWString());
}
std::set<std::wstring> 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));
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) {
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<std::wstring> archives;
std::set<std::wstring> enabledArchives;
std::vector<std::wstring>* 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<ModThread> g_threads;
void DirectoryRefresher::updateProgress(const DirectoryRefreshProgress* p)
{
emit progress(p);
}
void DirectoryRefresher::addMultipleModsFilesToStructure(
MOShared::DirectoryEntry* directoryStructure, const std::vector<EntryInfo>& entries,
DirectoryRefreshProgress* progress)
{
std::vector<DirectoryStats> stats(entries.size());
if (progress) {
progress->start(entries.size());
}
log::debug("refresher: using {} threads", m_threadCount);
g_threads.setMax(m_threadCount);
std::vector<std::wstring> loadOrder;
if (Settings::instance().archiveParsing()) {
auto gamePlugins = m_Core.gameFeatures().gameFeature<GamePlugins>();
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<IPluginGame*>();
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();
}