aboutsummaryrefslogtreecommitdiff
path: root/libs/check_fnis/src
diff options
context:
space:
mode:
Diffstat (limited to 'libs/check_fnis/src')
-rw-r--r--libs/check_fnis/src/CMakeLists.txt14
-rw-r--r--libs/check_fnis/src/check_fnis_en.ts66
-rw-r--r--libs/check_fnis/src/checkfnis.cpp293
-rw-r--r--libs/check_fnis/src/checkfnis.h67
4 files changed, 440 insertions, 0 deletions
diff --git a/libs/check_fnis/src/CMakeLists.txt b/libs/check_fnis/src/CMakeLists.txt
new file mode 100644
index 0000000..38451b1
--- /dev/null
+++ b/libs/check_fnis/src/CMakeLists.txt
@@ -0,0 +1,14 @@
+cmake_minimum_required(VERSION 3.16)
+
+file(GLOB check_fnis_SOURCES CONFIGURE_DEPENDS
+ ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/*.h
+ ${CMAKE_CURRENT_SOURCE_DIR}/*.ui
+ ${CMAKE_CURRENT_SOURCE_DIR}/*.qrc
+)
+
+add_library(check_fnis SHARED ${check_fnis_SOURCES})
+mo2_configure_plugin(check_fnis NO_SOURCES WARNINGS OFF)
+target_link_libraries(check_fnis PRIVATE mo2::uibase)
+
+mo2_install_plugin(check_fnis)
diff --git a/libs/check_fnis/src/check_fnis_en.ts b/libs/check_fnis/src/check_fnis_en.ts
new file mode 100644
index 0000000..408428e
--- /dev/null
+++ b/libs/check_fnis/src/check_fnis_en.ts
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="en_US">
+<context>
+ <name>CheckFNIS</name>
+ <message>
+ <location filename="checkfnis.cpp" line="74"/>
+ <source>FNIS Checker</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="checkfnis.cpp" line="84"/>
+ <source>Checks if FNIS behaviours need to be updated whenever you start the game. This is only relevant for Skyrim and if FNIS is installed.&lt;br&gt;</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="checkfnis.cpp" line="218"/>
+ <source>Run FNIS before %1?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="checkfnis.cpp" line="219"/>
+ <source>FNIS source data has been changed. You should run GenerateFNIS.exe now.</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="checkfnis.cpp" line="226"/>
+ <source>Failed to start %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="checkfnis.cpp" line="232"/>
+ <location filename="checkfnis.cpp" line="239"/>
+ <location filename="checkfnis.cpp" line="263"/>
+ <source>Start %1?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="checkfnis.cpp" line="233"/>
+ <source>FNIS reported a %1, do you want to run the application anyway?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="checkfnis.cpp" line="234"/>
+ <location filename="checkfnis.cpp" line="265"/>
+ <source>warning</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="checkfnis.cpp" line="235"/>
+ <location filename="checkfnis.cpp" line="266"/>
+ <source>critical error</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="checkfnis.cpp" line="240"/>
+ <source>Failed to determine FNIS exit code, do you want to run the application anyway?</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="checkfnis.cpp" line="264"/>
+ <source>FNIS reported a %1. Do you want to assume it worked?</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+</TS>
diff --git a/libs/check_fnis/src/checkfnis.cpp b/libs/check_fnis/src/checkfnis.cpp
new file mode 100644
index 0000000..73f2ac0
--- /dev/null
+++ b/libs/check_fnis/src/checkfnis.cpp
@@ -0,0 +1,293 @@
+#include "checkfnis.h"
+
+#include <uibase/iplugingame.h>
+#include <uibase/pluginsetting.h>
+#include <uibase/questionboxmemory.h>
+#include <uibase/report.h>
+#include <uibase/scopeguard.h>
+
+#include <QCryptographicHash>
+#include <QDialogButtonBox>
+#include <QDir>
+#include <QFile>
+#include <QFileInfo>
+#include <QMessageBox>
+#include <QStringList>
+#include <QVariant>
+
+#include <Qt> // for Qt::CaseInsensitive
+#include <QtDebug> // for qCritical, qDebug
+
+#include <functional>
+
+using namespace MOBase;
+
+CheckFNIS::CheckFNIS()
+ : m_MOInfo(nullptr), m_Active(false),
+ m_MatchExpressions(std::vector<QRegularExpression>{
+ // MSVC2013 bug. The (std::vector<QRegExp> shouldn't be necessary
+ QRegularExpression("\\\\FNIS_.*_List\\.txt$",
+ QRegularExpression::PatternOption::CaseInsensitiveOption),
+ QRegularExpression("\\\\FNIS.*Behavior\\.txt$",
+ QRegularExpression::PatternOption::CaseInsensitiveOption),
+ QRegularExpression("\\\\PatchList\\.txt$",
+ QRegularExpression::PatternOption::CaseInsensitiveOption),
+ QRegularExpression(
+ "\\\\skeleton.*\\.hkx$",
+ QRegularExpression::PatternOption::CaseInsensitiveOption)}),
+ m_SensitiveMatchExpressions(std::vector<QRegularExpression>{
+ QRegularExpression("\\\\animations\\\\.*\\.hkx$",
+ QRegularExpression::PatternOption::CaseInsensitiveOption)})
+{}
+
+CheckFNIS::~CheckFNIS() {}
+
+bool CheckFNIS::init(IOrganizer* moInfo)
+{
+ m_MOInfo = moInfo;
+
+ if (!moInfo->onAboutToRun([this](const auto& binary) {
+ return fnisCheck(binary);
+ })) {
+ qCritical("failed to connect to about to run event");
+ return false;
+ }
+
+ if (!moInfo->onFinishedRun(std::bind(&CheckFNIS::fnisEndCheck, this,
+ std::placeholders::_1, std::placeholders::_2))) {
+ qCritical("failed to connect to finished run event");
+ return false;
+ }
+
+ return true;
+}
+
+QString CheckFNIS::name() const
+{
+ return "FNIS Checker";
+}
+
+QString CheckFNIS::localizedName() const
+{
+ return tr("FNIS Checker");
+}
+
+QString CheckFNIS::author() const
+{
+ return "Tannin";
+}
+
+QString CheckFNIS::description() const
+{
+ return tr("Checks if FNIS behaviours need to be updated whenever you start the game."
+ " This is only relevant for Skyrim and if FNIS is installed.<br>");
+}
+
+VersionInfo CheckFNIS::version() const
+{
+ return VersionInfo(1, 0, 1, VersionInfo::RELEASE_FINAL);
+}
+
+std::vector<std::shared_ptr<const MOBase::IPluginRequirement>>
+CheckFNIS::requirements() const
+{
+ return {Requirements::gameDependency("Skyrim")};
+}
+
+QList<PluginSetting> CheckFNIS::settings() const
+{
+ QList<PluginSetting> result;
+ result.push_back(PluginSetting("sensitive",
+ "check changes on non-fnis animations. Makes this "
+ "more reliable but will cause FNIS to be called more "
+ "often than necessary.",
+ QVariant(false)));
+ return result;
+}
+
+bool CheckFNIS::testFileRelevant(const IOrganizer::FileInfo& fileName) const
+{
+ if (!fileName.archive.isEmpty()) {
+ return false;
+ }
+
+ for (auto& expr : m_MatchExpressions) {
+ auto match = expr.match(fileName.filePath);
+ if (match.hasMatch()) {
+ return true;
+ }
+ }
+
+ if (m_MOInfo->pluginSetting(name(), "sensitive").toBool()) {
+ for (auto& expr : m_SensitiveMatchExpressions) {
+ auto match = expr.match(fileName.filePath);
+ if (match.hasMatch()) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+void CheckFNIS::findRelevantFilesRecursive(const QString& path,
+ QMap<QString, QString>& fileList) const
+{
+ // find all relevant files
+ QList<IOrganizer::FileInfo> files = m_MOInfo->findFileInfos(
+ path, std::bind(&CheckFNIS::testFileRelevant, this, std::placeholders::_1));
+ foreach (const IOrganizer::FileInfo& fileInfo, files) {
+ QFile file(fileInfo.filePath);
+ if (file.open(QIODevice::ReadOnly)) {
+ QString hash(
+ QCryptographicHash::hash(file.readAll(), QCryptographicHash::Md5).toHex());
+ fileList.insert(fileInfo.filePath, hash);
+ } else {
+ qCritical("failed to open %s", qUtf8Printable(fileInfo.filePath));
+ }
+ }
+
+ QStringList subDirectories = m_MOInfo->listDirectories(path);
+ foreach (const QString& directory, subDirectories) {
+ findRelevantFilesRecursive(QDir(path).filePath(directory), fileList);
+ }
+}
+
+QString CheckFNIS::generateIdentifier() const
+{
+ QMap<QString, QString> fileList;
+
+ findRelevantFilesRecursive("meshes\\actors", fileList);
+
+ QStringList flattenedList;
+ for (auto iter = fileList.begin(); iter != fileList.end(); ++iter) {
+ flattenedList.append(iter.key() + "=" + iter.value());
+ }
+
+ return QCryptographicHash::hash(flattenedList.join(",").toUtf8(),
+ QCryptographicHash::Md5)
+ .toHex();
+}
+
+bool CheckFNIS::appIsFNIS(QString const& application, QString const& fnisApp)
+{
+ return QString::compare(QDir::fromNativeSeparators(application),
+ QDir::fromNativeSeparators(fnisApp),
+ Qt::CaseInsensitive) == 0;
+}
+
+QString CheckFNIS::getFnisPath() const
+{
+ if (!m_MOInfo->pluginSetting(name(), "enabled").toBool()) {
+ return "";
+ }
+
+ // Check if it's actually installed (...)
+ QStringList fnisBinaryList = m_MOInfo->findFiles(
+ "tools/GenerateFNIS_for_Users", [](const QString& fileName) -> bool {
+ return fileName.endsWith("GenerateFNISforUsers.exe", Qt::CaseInsensitive);
+ });
+
+ if (fnisBinaryList.count() == 0) {
+ // fnis seems not to be installed even though this is enabled
+ qDebug("fnis not installed");
+ return "";
+ }
+
+ // As this is looking in the vfs, there can be precisely 0 or 1 instances of FNIS
+ return fnisBinaryList.at(0);
+}
+
+bool CheckFNIS::fnisCheck(const QString& application)
+{
+
+ QString fnisApp(getFnisPath());
+ if (fnisApp.isEmpty() || appIsFNIS(application, fnisApp)) {
+ return true;
+ }
+
+ // prevent this check from being called recursively
+ if (m_Active) {
+ return true;
+ }
+
+ m_Active = true;
+ ON_BLOCK_EXIT([&] {
+ m_Active = false;
+ });
+
+ QString const newHash = generateIdentifier();
+
+ if (newHash == m_MOInfo->persistent(name(), m_MOInfo->profileName(), "").toString()) {
+ // Don't need to run fnis as nothing relevant has changed.
+ return true;
+ }
+
+ QDialogButtonBox::StandardButton res = QuestionBoxMemory::query(
+ nullptr, "fnisCheck", QFileInfo(application).fileName(),
+ tr("Run FNIS before %1?").arg(application),
+ tr("FNIS source data has been changed. You should run GenerateFNIS.exe now."),
+ QDialogButtonBox::Yes | QDialogButtonBox::No | QDialogButtonBox::Cancel,
+ QDialogButtonBox::Yes);
+
+ if (res == QDialogButtonBox::Yes) {
+ HANDLE process = m_MOInfo->startApplication(fnisApp);
+ bool cont = true;
+ if (process == INVALID_HANDLE_VALUE) {
+ reportError(tr("Failed to start %1").arg(fnisApp));
+ } else {
+ DWORD exitCodeU;
+ if (m_MOInfo->waitForApplication(process, &exitCodeU)) {
+ int exitCode = static_cast<int>(exitCodeU);
+ if (exitCode != 0) {
+ cont = QMessageBox::question(
+ nullptr, tr("Start %1?").arg(application),
+ tr("FNIS reported a %1, do you want to run the application "
+ "anyway?")
+ .arg(exitCode < 0 ? tr("warning") : tr("critical error")),
+ QMessageBox::Yes | QMessageBox::No,
+ QMessageBox::No) == QMessageBox::Yes;
+ }
+ } else {
+ cont = QMessageBox::question(nullptr, tr("Start %1?").arg(application),
+ tr("Failed to determine FNIS exit code, do you "
+ "want to run the application "
+ "anyway?"),
+ QMessageBox::Yes | QMessageBox::No,
+ QMessageBox::No) == QMessageBox::Yes;
+ }
+ if (cont) {
+ m_MOInfo->setPersistent(name(), m_MOInfo->profileName(), newHash);
+ }
+ }
+ return cont;
+ } else if (res == QDialogButtonBox::No) {
+ return true;
+ } else {
+ // Don't run the app if they pressed cancel
+ return false;
+ }
+}
+
+void CheckFNIS::fnisEndCheck(const QString& application, unsigned int code)
+{
+ if (appIsFNIS(application, getFnisPath())) {
+ bool update = true;
+ int exitCode = static_cast<int>(code);
+ if (exitCode != 0) {
+ update =
+ QMessageBox::question(
+ nullptr, tr("Start %1?").arg(application),
+ tr("FNIS reported a %1. Do you want to assume it worked?")
+ .arg(exitCode < 0 ? tr("warning") : tr("critical error")),
+ QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::Yes;
+ }
+ if (update) {
+ m_MOInfo->setPersistent(name(), m_MOInfo->profileName(), generateIdentifier());
+ }
+ }
+}
+
+#if QT_VERSION < QT_VERSION_CHECK(5, 0, 0)
+Q_EXPORT_PLUGIN2(checkfnis, CheckFNIS)
+#endif
diff --git a/libs/check_fnis/src/checkfnis.h b/libs/check_fnis/src/checkfnis.h
new file mode 100644
index 0000000..8971ab2
--- /dev/null
+++ b/libs/check_fnis/src/checkfnis.h
@@ -0,0 +1,67 @@
+#ifndef CHECKFNIS_H
+#define CHECKFNIS_H
+
+#include <uibase/imoinfo.h>
+#include <uibase/iplugin.h>
+#include <uibase/versioninfo.h>
+
+#include <QList>
+#include <QMap>
+#include <QObject>
+#include <QString>
+
+#include <QtGlobal> // for QT_VERSION, QT_VERSION_CHECK
+
+#include <vector>
+
+namespace MOBase
+{
+struct PluginSetting;
+}
+
+class CheckFNIS : public QObject, public MOBase::IPlugin
+{
+
+ Q_OBJECT
+ Q_INTERFACES(MOBase::IPlugin)
+#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)
+ Q_PLUGIN_METADATA(IID "org.tannin.CheckFNIS")
+#endif
+
+public:
+ CheckFNIS();
+ ~CheckFNIS();
+
+public: // IPlugin
+ virtual bool init(MOBase::IOrganizer* moInfo) override;
+ virtual QString name() const override;
+ virtual QString localizedName() const override;
+ virtual std::vector<std::shared_ptr<const MOBase::IPluginRequirement>>
+ requirements() const override;
+ virtual QString author() const override;
+ virtual QString description() const override;
+ virtual MOBase::VersionInfo version() const override;
+ virtual QList<MOBase::PluginSetting> settings() const override;
+
+private:
+ bool fnisCheck(const QString& application);
+ void fnisEndCheck(const QString& application, unsigned int code);
+
+ QString getFnisPath() const;
+ static bool appIsFNIS(QString const& application, const QString& fnisApp);
+
+ bool testFileRelevant(const MOBase::IOrganizer::FileInfo& fileName) const;
+ void findRelevantFilesRecursive(const QString& path,
+ QMap<QString, QString>& fileList) const;
+ // generates a map of file-names and hashes. Any change prompts a new fnis run
+ QString generateIdentifier() const;
+
+private:
+ MOBase::IOrganizer* m_MOInfo;
+ bool m_Active;
+
+ std::vector<QRegularExpression> const m_MatchExpressions;
+ std::vector<QRegularExpression> const m_SensitiveMatchExpressions;
+};
+
+#endif // CHECKFNIS_H