From 7ee008e150bc5bcf76082d726f719ee0fdfda982 Mon Sep 17 00:00:00 2001 From: SulfurNitride Date: Wed, 11 Feb 2026 02:37:39 -0600 Subject: 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 --- libs/check_fnis/.clang-format | 41 +++++ libs/check_fnis/.gitattributes | 7 + libs/check_fnis/.gitignore | 5 + libs/check_fnis/.pre-commit-config.yaml | 20 +++ libs/check_fnis/CMakeLists.txt | 5 + libs/check_fnis/CMakePresets.json | 57 +++++++ libs/check_fnis/src/CMakeLists.txt | 14 ++ libs/check_fnis/src/check_fnis_en.ts | 66 +++++++ libs/check_fnis/src/checkfnis.cpp | 293 ++++++++++++++++++++++++++++++++ libs/check_fnis/src/checkfnis.h | 67 ++++++++ libs/check_fnis/vcpkg.json | 15 ++ 11 files changed, 590 insertions(+) create mode 100644 libs/check_fnis/.clang-format create mode 100644 libs/check_fnis/.gitattributes create mode 100644 libs/check_fnis/.gitignore create mode 100644 libs/check_fnis/.pre-commit-config.yaml create mode 100644 libs/check_fnis/CMakeLists.txt create mode 100644 libs/check_fnis/CMakePresets.json create mode 100644 libs/check_fnis/src/CMakeLists.txt create mode 100644 libs/check_fnis/src/check_fnis_en.ts create mode 100644 libs/check_fnis/src/checkfnis.cpp create mode 100644 libs/check_fnis/src/checkfnis.h create mode 100644 libs/check_fnis/vcpkg.json (limited to 'libs/check_fnis') 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 @@ + + + + + CheckFNIS + + + FNIS Checker + + + + + 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> + + + + + Run FNIS before %1? + + + + + FNIS source data has been changed. You should run GenerateFNIS.exe now. + + + + + Failed to start %1 + + + + + + + Start %1? + + + + + FNIS reported a %1, do you want to run the application anyway? + + + + + + warning + + + + + + critical error + + + + + Failed to determine FNIS exit code, do you want to run the application anyway? + + + + + FNIS reported a %1. Do you want to assume it worked? + + + + 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 +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include // for Qt::CaseInsensitive +#include // for qCritical, qDebug + +#include + +using namespace MOBase; + +CheckFNIS::CheckFNIS() + : m_MOInfo(nullptr), m_Active(false), + m_MatchExpressions(std::vector{ + // MSVC2013 bug. The (std::vector 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("\\\\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.
"); +} + +VersionInfo CheckFNIS::version() const +{ + return VersionInfo(1, 0, 1, VersionInfo::RELEASE_FINAL); +} + +std::vector> +CheckFNIS::requirements() const +{ + return {Requirements::gameDependency("Skyrim")}; +} + +QList CheckFNIS::settings() const +{ + QList 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& fileList) const +{ + // find all relevant files + QList 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 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(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(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 +#include +#include + +#include +#include +#include +#include + +#include // for QT_VERSION, QT_VERSION_CHECK + +#include + +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> + requirements() const override; + virtual QString author() const override; + virtual QString description() const override; + virtual MOBase::VersionInfo version() const override; + virtual QList 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& 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 const m_MatchExpressions; + std::vector 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" + } + } +} -- cgit v1.3.1