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 --- .../src/CMakeLists.txt | 6 + .../src/ScriptExtenderPluginChecker.py | 318 +++++++++++++++++++++ .../src/ScriptExtenderPluginChecker_en.ts | 138 +++++++++ 3 files changed, 462 insertions(+) create mode 100644 libs/script_extender_plugin_checker/src/CMakeLists.txt create mode 100644 libs/script_extender_plugin_checker/src/ScriptExtenderPluginChecker.py create mode 100644 libs/script_extender_plugin_checker/src/ScriptExtenderPluginChecker_en.ts (limited to 'libs/script_extender_plugin_checker/src') diff --git a/libs/script_extender_plugin_checker/src/CMakeLists.txt b/libs/script_extender_plugin_checker/src/CMakeLists.txt new file mode 100644 index 0000000..51da5ba --- /dev/null +++ b/libs/script_extender_plugin_checker/src/CMakeLists.txt @@ -0,0 +1,6 @@ +cmake_minimum_required(VERSION 3.16) + +find_package(mo2-cmake CONFIG REQUIRED) + +add_custom_target(ScriptExtenderPluginChecker ALL) +mo2_configure_python(ScriptExtenderPluginChecker SIMPLE) diff --git a/libs/script_extender_plugin_checker/src/ScriptExtenderPluginChecker.py b/libs/script_extender_plugin_checker/src/ScriptExtenderPluginChecker.py new file mode 100644 index 0000000..35498ea --- /dev/null +++ b/libs/script_extender_plugin_checker/src/ScriptExtenderPluginChecker.py @@ -0,0 +1,318 @@ +from __future__ import annotations + +import re +from enum import Enum, auto +from pathlib import Path +from typing import Callable, NamedTuple + +import mobase +from PyQt6.QtCore import QCoreApplication, qDebug + + +class PluginMessage: + kUnknownOrigin = "" + + def __init__(self, pluginPath: str | Path, organizer: mobase.IOrganizer): + self._pluginPath = Path(pluginPath) + try: + self._pluginOrigin = organizer.getFileOrigins( + str(self._pluginPath.relative_to(organizer.managedGame().dataDirectory().absolutePath())))[0] + except: + self._pluginOrigin = PluginMessage.kUnknownOrigin + + def successful(self): + return not self.valid() + + def valid(self): + return self._pluginOrigin != PluginMessage.kUnknownOrigin + + def asMessage(self): + return self.tr("{0} ({1}) existed.").format(self._pluginPath.name, self._pluginOrigin) + + def tr(self, value: str): + return QCoreApplication.translate("PluginMessage", value) + + def pluginPath(self): + return self._pluginPath + + messageTypes: list[tuple[re.Pattern[str], Callable[[re.Match[str], mobase.IOrganizer], PluginMessage]]] = [] + + @staticmethod + def registerMessageType( + messageType: tuple[ + re.Pattern[str], Callable[[re.Match[str], mobase.IOrganizer], PluginMessage] + ], + ): + PluginMessage.messageTypes.append(messageType) + + @staticmethod + def PluginMessageFactory(line: str, organizer: mobase.IOrganizer): + for messageType in PluginMessage.messageTypes: + match = messageType[0].fullmatch(line) + if match: + return messageType[1](match, organizer) + return None + + +class NormalPluginMessage(PluginMessage): + def __init__(self, match: re.Match[str], organizer: mobase.IOrganizer): + super().__init__(match.group("pluginPath"), organizer) + self.__infoVersion = int(match.group("infoVersion"), 16) + self.__name = match.group("name") + self.__version = int(match.group("version"), 16) + self.__loadStatus = match.group("loadStatus") + + def successful(self): + return not self.valid() or self.__loadStatus == "loaded correctly" or self.__loadStatus == "no version data" + + def asMessage(self): + return self.tr("{0} version {1} ({2}, {4}) {3}.").format(self.__name, self.__version, self._pluginPath.name, + self.__trLoadStatus(), self._pluginOrigin) + + def __trLoadStatus(self): + # We need to list the possible options so they get detected as translatable strings. + loadStatusTranslations = { + "loaded correctly": self.tr("loaded correctly"), + # Messages taken from SKSE64 2.1.3 + "disabled, address library needs to be updated": self.tr("disabled, address library needs to be updated"), + "disabled, fatal error occurred while loading plugin": self.tr( + "disabled, fatal error occurred while loading plugin"), + "disabled, bad version data": self.tr("disabled, bad version data"), + "disabled, no name specified": self.tr("disabled, no name specified"), + "disabled, unsupported version independence method": self.tr( + "disabled, unsupported version independence method"), + "disabled, incompatible with current runtime version": self.tr( + "disabled, incompatible with current runtime version"), + "disabled, requires newer script extender": self.tr("disabled, requires newer script extender"), + # Legacy messages + "reported as incompatible during query": self.tr("reported as incompatible during query"), + "reported as incompatible during load": self.tr("reported as incompatible during load"), + "disabled, fatal error occurred while checking plugin compatibility": self.tr( + "disabled, fatal error occurred while checking plugin compatibility"), + "disabled, fatal error occurred while querying plugin": self.tr( + "disabled, fatal error occurred while querying plugin"), + } + if self.__loadStatus in loadStatusTranslations: + return loadStatusTranslations[self.__loadStatus] + else: + # There's a blacklist of known broken plugins that get excluded, each with their own reason. + # We aren't translating that. + return self.__loadStatus + + def tr(self, value: str): + return QCoreApplication.translate("NormalPluginMessage", value) + + +PluginMessage.registerMessageType((re.compile( + r"plugin (?P.+) \((?P[\dA-Fa-f]{8}) (?P.*) (?P[\dA-Fa-f]{8})\) (?P.+?)(?P \d+)?( \(handle \d+\))?\s$"), + NormalPluginMessage)) + + +class CouldntLoadPluginMessage(PluginMessage): + def __init__(self, match: re.Match[str], organizer: mobase.IOrganizer): + super().__init__(match.group("pluginPath"), organizer) + self.__lastError = int(match.group("lastError")) + self.__scriptExtenderDetails = match.group("seDetails") + if not self.__scriptExtenderDetails or self.__scriptExtenderDetails.isspace(): + self.__scriptExtenderDetails = "" + + def successful(self): + return not self.valid() + + def asMessage(self): + if self.__lastError == 126: + message = self.tr("Couldn't load {0} ({2}). A dependency DLL could not be found (code {1}). {3}") + elif self.__lastError == 193: + message = self.tr("Couldn't load {0} ({2}). A DLL is invalid (code {1}).") + else: + message = self.tr("Couldn't load {0} ({2}). The last error code was {1}.") + + return message.format(self._pluginPath.name, self.__lastError, self._pluginOrigin, self.__scriptExtenderDetails) + + def tr(self, value: str): + return QCoreApplication.translate("CouldntLoadPluginMessage", value) + + +PluginMessage.registerMessageType((re.compile( + r"couldn't load plugin (?P.+) \(Error (code )?(?P[-+]?\d+)(:\s*(?P.*))?\)\s"), + CouldntLoadPluginMessage)) + + +class NotAPluginMessage(PluginMessage): + def __init__(self, match: re.Match[str], organizer: mobase.IOrganizer): + super().__init__(match.group("pluginPath"), organizer) + + def successful(self): + return not self.valid() + + def asMessage(self): + return self.tr("{0} ({1}) does not appear to be a script extender plugin.").format(self._pluginPath.name, + self._pluginOrigin) + + def tr(self, value: str): + return QCoreApplication.translate("NotAPluginMessage", value) + + +PluginMessage.registerMessageType((re.compile( + r"plugin (?P.+) does not appear to be an (?:SK)|(?:F4)|(?:NV)|(?:FO)|(?:OB)SE plugin\s"), + NotAPluginMessage)) + + +class LogLocation(Enum): + DOCS = auto() + INSTALL = auto() + + +class ScriptExtenderPluginChecker(mobase.IPluginDiagnose): + class GameType(NamedTuple): + base: LogLocation + gameSuffix: Path | None + editorSuffix: Path | None + + supportedGames = { + "Skyrim": GameType(LogLocation.DOCS, Path("SKSE") / "skse.log", Path("SKSE") / "skse_editor.log"), + "Skyrim Special Edition": GameType(LogLocation.DOCS, Path("SKSE") / "skse64.log", None), + # No editor log defined + "Skyrim VR": GameType(LogLocation.DOCS, Path("SKSE") / "sksevr.log", None), # No editor log defined + # "Enderal" : GameType(LogLocation.DOCS, Path("")), + "Fallout 4": GameType(LogLocation.DOCS, Path("F4SE") / "f4se.log", None), # No editor log defined + "Oblivion": GameType(LogLocation.INSTALL, Path("obse.log"), Path("obse_editor.log")), + "New Vegas": GameType(LogLocation.INSTALL, Path("nvse.log"), Path("nvse_editor.log")), + "TTW": GameType(LogLocation.INSTALL, Path("nvse.log"), Path("nvse_editor.log")), + "Fallout 3": GameType(LogLocation.INSTALL, Path("fose.log"), Path("fose_editor.log")) + } + + __organizer: mobase.IOrganizer + + def __init__(self): + super(ScriptExtenderPluginChecker, self).__init__() + + def init(self, organizer: mobase.IOrganizer): + self.__organizer = organizer + + organizer.onFinishedRun(lambda a, b: self._invalidate()) + + return True + + def name(self): + return "Script Extender Plugin Load Checker" + + def localizedName(self): + return self.tr("Script Extender Plugin Load Checker") + + def author(self): + return "AnyOldName3" + + def description(self): + return self.tr("Checks script extender log to see if any plugins failed to load.") + + def version(self): + return mobase.VersionInfo(1, 2, 0, 0) + + def requirements(self) -> list[mobase.IPluginRequirement]: + return [ + mobase.PluginRequirementFactory.gameDependency(list(self.supportedGames)) + ] + + def settings(self) -> list[mobase.PluginSetting]: + return [] + + def activeProblems(self) -> list[int]: + if self.__scanLog(): + return [0] + else: + return [] + + def shortDescription(self, key: int): + return self.tr("Script extender log reports incompatible plugins.") + + def fullDescription(self, key: int): + pluginList = self.__listBadPluginMessagess() + pluginListString = "\n • " + ("\n • ".join(pluginList)) + return self.tr("You have one or more script extender plugins which failed to load!\n\n " + "If you want this notification to go away, here are some steps you can take:\n" + " • Look for updates to the mod or the specific plugin included in the mod.\n" + " • Disable the mod containing the plugin.\n" + " • Hide or delete the plugin from the mod.\n\n" + "To refresh the script extender logs, you will need to run the game and/or editor again!\n\n" + "The failed plugins are:{0}").format(pluginListString) + + def hasGuidedFix(self, key: int): + return False + + def startGuidedFix(self, key: int): + pass + + def tr(self, value: str): + return QCoreApplication.translate("ScriptExtenderPluginChecker", value) + + def __scanLog(self): + return len(self.__listBadPluginMessagess()) > 0 + + def __listBadPluginMessagess(self) -> list[str]: + base, gameSuffix, editorSuffix = self.supportedGames[self.__organizer.managedGame().gameName()] + + if base == LogLocation.DOCS: + base = Path(self.__organizer.managedGame().documentsDirectory().absolutePath()) + elif base == LogLocation.INSTALL: + base = Path(self.__organizer.managedGame().gameDirectory().absolutePath()) + + editorMessageList: list[PluginMessage] = [] + gameMessageList: list[PluginMessage] = [] + + if gameSuffix is not None: + gameLogPath = base / gameSuffix + try: + if gameLogPath.exists(): + with gameLogPath.open('r', encoding='cp1252') as logFile: + for line in logFile: + message = PluginMessage.PluginMessageFactory(line, self.__organizer) + if message: + gameMessageList.append(message) + except Exception as e: + qDebug(str(e)) + # There's almost certainly just no log yet + pass + + if editorSuffix is not None: + editorLogPath = base / editorSuffix + try: + if editorLogPath.exists(): + with editorLogPath.open('r', encoding='cp1252') as logFile: + for line in logFile: + message = PluginMessage.PluginMessageFactory(line, self.__organizer) + if message: + editorMessageList.append(message) + except Exception as e: + qDebug(str(e)) + # There's almost certainly just no log yet + pass + + messageList: list[str] = [] + + # Search each list for plugins that are not successful in either list + for gameMessage in gameMessageList: + if not gameMessage.successful(): + for editorMessage in editorMessageList: + if gameMessage.pluginPath() == editorMessage.pluginPath() and editorMessage.successful(): + break + else: + message = gameMessage.asMessage() + if message not in messageList: + messageList.append(message) + + for editorMessage in editorMessageList: + if not editorMessage.successful(): + for gameMessage in gameMessageList: + if editorMessage.pluginPath() == gameMessage.pluginPath() and gameMessage.successful(): + break + else: + message = editorMessage.asMessage() + if message not in messageList: + messageList.append(message) + + return messageList + + +def createPlugin(): + return ScriptExtenderPluginChecker() diff --git a/libs/script_extender_plugin_checker/src/ScriptExtenderPluginChecker_en.ts b/libs/script_extender_plugin_checker/src/ScriptExtenderPluginChecker_en.ts new file mode 100644 index 0000000..bbc6872 --- /dev/null +++ b/libs/script_extender_plugin_checker/src/ScriptExtenderPluginChecker_en.ts @@ -0,0 +1,138 @@ + + + + + CouldntLoadPluginMessage + + + Couldn't load {0} ({2}). A dependency DLL could not be found (code {1}). {3} + + + + + Couldn't load {0} ({2}). A DLL is invalid (code {1}). + + + + + Couldn't load {0} ({2}). The last error code was {1}. + + + + + NormalPluginMessage + + + {0} version {1} ({2}, {4}) {3}. + + + + + loaded correctly + + + + + disabled, address library needs to be updated + + + + + disabled, fatal error occurred while loading plugin + + + + + disabled, bad version data + + + + + disabled, no name specified + + + + + disabled, unsupported version independence method + + + + + disabled, incompatible with current runtime version + + + + + disabled, requires newer script extender + + + + + reported as incompatible during query + + + + + reported as incompatible during load + + + + + disabled, fatal error occurred while checking plugin compatibility + + + + + disabled, fatal error occurred while querying plugin + + + + + NotAPluginMessage + + + {0} ({1}) does not appear to be a script extender plugin. + + + + + PluginMessage + + + {0} ({1}) existed. + + + + + ScriptExtenderPluginChecker + + + Script Extender Plugin Load Checker + + + + + Checks script extender log to see if any plugins failed to load. + + + + + Script extender log reports incompatible plugins. + + + + + You have one or more script extender plugins which failed to load! + + If you want this notification to go away, here are some steps you can take: + • Look for updates to the mod or the specific plugin included in the mod. + • Disable the mod containing the plugin. + • Hide or delete the plugin from the mod. + +To refresh the script extender logs, you will need to run the game and/or editor again! + +The failed plugins are:{0} + + + + -- cgit v1.3.1