diff options
| author | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-02-11 02:37:39 -0600 |
|---|---|---|
| committer | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-02-11 02:37:39 -0600 |
| commit | 7ee008e150bc5bcf76082d726f719ee0fdfda982 (patch) | |
| tree | 27fb39be241fdb5ac2734c574de678977d1856d0 /libs/check_fnis | |
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')
| -rw-r--r-- | libs/check_fnis/.clang-format | 41 | ||||
| -rw-r--r-- | libs/check_fnis/.gitattributes | 7 | ||||
| -rw-r--r-- | libs/check_fnis/.gitignore | 5 | ||||
| -rw-r--r-- | libs/check_fnis/.pre-commit-config.yaml | 20 | ||||
| -rw-r--r-- | libs/check_fnis/CMakeLists.txt | 5 | ||||
| -rw-r--r-- | libs/check_fnis/CMakePresets.json | 57 | ||||
| -rw-r--r-- | libs/check_fnis/src/CMakeLists.txt | 14 | ||||
| -rw-r--r-- | libs/check_fnis/src/check_fnis_en.ts | 66 | ||||
| -rw-r--r-- | libs/check_fnis/src/checkfnis.cpp | 293 | ||||
| -rw-r--r-- | libs/check_fnis/src/checkfnis.h | 67 | ||||
| -rw-r--r-- | libs/check_fnis/vcpkg.json | 15 |
11 files changed, 590 insertions, 0 deletions
diff --git a/libs/check_fnis/.clang-format b/libs/check_fnis/.clang-format new file mode 100644 index 0000000..6098e1f --- /dev/null +++ b/libs/check_fnis/.clang-format @@ -0,0 +1,41 @@ +--- +# We'll use defaults from the LLVM style, but with 4 columns indentation. +BasedOnStyle: LLVM +IndentWidth: 2 +--- +Language: Cpp +DeriveLineEnding: false +UseCRLF: true +DerivePointerAlignment: false +PointerAlignment: Left +AlignConsecutiveAssignments: true +AllowShortFunctionsOnASingleLine: Inline +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: Empty +AlwaysBreakTemplateDeclarations: Yes +AccessModifierOffset: -2 +AlignTrailingComments: true +SpacesBeforeTrailingComments: 2 +NamespaceIndentation: Inner +MaxEmptyLinesToKeep: 1 +BreakBeforeBraces: Custom +BraceWrapping: + AfterCaseLabel: false + AfterClass: true + AfterControlStatement: false + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterStruct: true + AfterUnion: true + AfterExternBlock: true + BeforeCatch: false + BeforeElse: false + BeforeLambdaBody: false + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: false + SplitEmptyNamespace: true +ColumnLimit: 88 +ForEachMacros: ['Q_FOREACH', 'foreach'] diff --git a/libs/check_fnis/.gitattributes b/libs/check_fnis/.gitattributes new file mode 100644 index 0000000..f869712 --- /dev/null +++ b/libs/check_fnis/.gitattributes @@ -0,0 +1,7 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +* text=auto + +# Explicitly declare text files you want to always be normalized and converted +# to native line endings on checkout. +*.cpp text eol=crlf +*.h text eol=crlf diff --git a/libs/check_fnis/.gitignore b/libs/check_fnis/.gitignore new file mode 100644 index 0000000..cf71be7 --- /dev/null +++ b/libs/check_fnis/.gitignore @@ -0,0 +1,5 @@ +edit +CMakeLists.txt.user +/msbuild.log +/*std*.log +/*build diff --git a/libs/check_fnis/.pre-commit-config.yaml b/libs/check_fnis/.pre-commit-config.yaml new file mode 100644 index 0000000..eb8969c --- /dev/null +++ b/libs/check_fnis/.pre-commit-config.yaml @@ -0,0 +1,20 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-merge-conflict + - id: check-case-conflict + - repo: https://github.com/pre-commit/mirrors-clang-format + rev: v21.1.8 + hooks: + - id: clang-format + 'types_or': [c++, c] + +ci: + autofix_commit_msg: "[pre-commit.ci] Auto fixes from pre-commit.com hooks." + autofix_prs: true + autoupdate_commit_msg: "[pre-commit.ci] Pre-commit autoupdate." + autoupdate_schedule: quarterly + submodules: false diff --git a/libs/check_fnis/CMakeLists.txt b/libs/check_fnis/CMakeLists.txt new file mode 100644 index 0000000..f7eb6af --- /dev/null +++ b/libs/check_fnis/CMakeLists.txt @@ -0,0 +1,5 @@ +cmake_minimum_required(VERSION 3.16) + +project(check_fnis) + +add_subdirectory(src) diff --git a/libs/check_fnis/CMakePresets.json b/libs/check_fnis/CMakePresets.json new file mode 100644 index 0000000..cc009b3 --- /dev/null +++ b/libs/check_fnis/CMakePresets.json @@ -0,0 +1,57 @@ +{ + "configurePresets": [ + { + "errors": { + "deprecated": true + }, + "hidden": true, + "name": "cmake-dev", + "warnings": { + "deprecated": true, + "dev": true + } + }, + { + "cacheVariables": { + "VCPKG_MANIFEST_NO_DEFAULT_FEATURES": { + "type": "BOOL", + "value": "ON" + } + }, + "toolchainFile": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", + "hidden": true, + "name": "vcpkg" + }, + { + "hidden": true, + "inherits": ["vcpkg"], + "name": "vcpkg-dev" + }, + { + "binaryDir": "${sourceDir}/vsbuild", + "architecture": { + "strategy": "set", + "value": "x64" + }, + "cacheVariables": { + "CMAKE_CXX_FLAGS": "/EHsc /MP /W4", + "VCPKG_TARGET_TRIPLET": { + "type": "STRING", + "value": "x64-windows-static-md" + } + }, + "generator": "Visual Studio 17 2022", + "inherits": ["cmake-dev", "vcpkg-dev"], + "name": "vs2022-windows", + "toolset": "v143" + } + ], + "buildPresets": [ + { + "name": "vs2022-windows", + "resolvePackageReferences": "on", + "configurePreset": "vs2022-windows" + } + ], + "version": 4 +} 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.<br></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 diff --git a/libs/check_fnis/vcpkg.json b/libs/check_fnis/vcpkg.json new file mode 100644 index 0000000..a639ce6 --- /dev/null +++ b/libs/check_fnis/vcpkg.json @@ -0,0 +1,15 @@ +{ + "features": { + "standalone": { + "description": "Build Standalone.", + "dependencies": ["mo2-cmake"] + } + }, + "vcpkg-configuration": { + "default-registry": { + "kind": "git", + "repository": "https://github.com/ModOrganizer2/vcpkg-registry", + "baseline": "8beb2e0efa9c17dd6d17bb05288dd1e40727f673" + } + } +} |
