aboutsummaryrefslogtreecommitdiff
path: root/libs/check_fnis/src/checkfnis.cpp
diff options
context:
space:
mode:
authorSulfurNitride <SulfurNitride@users.noreply.github.com>2026-02-11 02:37:39 -0600
committerSulfurNitride <SulfurNitride@users.noreply.github.com>2026-02-11 02:37:39 -0600
commit7ee008e150bc5bcf76082d726f719ee0fdfda982 (patch)
tree27fb39be241fdb5ac2734c574de678977d1856d0 /libs/check_fnis/src/checkfnis.cpp
Fluorine Manager: full Linux port of Mod Organizer 2
Complete native Linux port with FUSE-based virtual filesystem, Proton/umu-run integration, and Flatpak packaging. Key features: - FUSE VFS replacing Windows USVFS (in-process + standalone helper for Flatpak) - Proton/GE-Proton/umu-run launcher with env var forwarding - Flatpak support (sandbox-aware VFS, NXM handler, umu-run) - Wine prefix management UI - Case-insensitive path resolution for Linux filesystems - QSettings-safe INI handling (avoids Bethesda INI corruption) - Portable instance support with auto-generated launcher scripts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'libs/check_fnis/src/checkfnis.cpp')
-rw-r--r--libs/check_fnis/src/checkfnis.cpp293
1 files changed, 293 insertions, 0 deletions
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