aboutsummaryrefslogtreecommitdiff
path: root/libs/script_extender_checker_native/src
diff options
context:
space:
mode:
authorSulfurNitride <SulfurNitride@users.noreply.github.com>2026-03-12 07:08:09 -0500
committerSulfurNitride <SulfurNitride@users.noreply.github.com>2026-03-12 07:08:09 -0500
commit755698f1adceef55f52c8e44c9746537885c9bf7 (patch)
tree23d1a9fea936805708eeccf2ac2684620565d328 /libs/script_extender_checker_native/src
parent019870ffd8308d94289d72d45abeb3db39ccde92 (diff)
Replace Python plugins with native C++ .so, remove bundled Python (~95MB)
- Add native plugins: form43_checker, script_extender_checker, preview_dds, basic_games (75 game defs via IPluginProxy pattern with Steam/GOG detection) - Remove portable Python from Dockerfile and build-inner.sh - Add Python settings tab with venv support (system python3 + PyQt6) - Skip loading plugin_python.so when Python disabled (default: off) - Update pythonrunner.cpp to use venv at ~/.local/share/fluorine/python-venv/ - Preserve real file permissions in FUSE VFS (fixes native executable +x bits) - Add FUSE_SET_ATTR_MODE handler so chmod works through VFS - Fix desktop shortcut Exec= to use fluorine-manager launcher, not bare binary Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'libs/script_extender_checker_native/src')
-rw-r--r--libs/script_extender_checker_native/src/CMakeLists.txt11
-rw-r--r--libs/script_extender_checker_native/src/scriptextenderchecker.cpp363
-rw-r--r--libs/script_extender_checker_native/src/scriptextenderchecker.h72
3 files changed, 446 insertions, 0 deletions
diff --git a/libs/script_extender_checker_native/src/CMakeLists.txt b/libs/script_extender_checker_native/src/CMakeLists.txt
new file mode 100644
index 0000000..7d63643
--- /dev/null
+++ b/libs/script_extender_checker_native/src/CMakeLists.txt
@@ -0,0 +1,11 @@
+cmake_minimum_required(VERSION 3.16)
+
+file(GLOB script_extender_checker_native_SOURCES CONFIGURE_DEPENDS
+ ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/*.h
+)
+
+add_library(script_extender_checker_native SHARED ${script_extender_checker_native_SOURCES})
+mo2_configure_plugin(script_extender_checker_native NO_SOURCES WARNINGS OFF)
+target_link_libraries(script_extender_checker_native PRIVATE mo2::uibase)
+mo2_install_plugin(script_extender_checker_native)
diff --git a/libs/script_extender_checker_native/src/scriptextenderchecker.cpp b/libs/script_extender_checker_native/src/scriptextenderchecker.cpp
new file mode 100644
index 0000000..65276ad
--- /dev/null
+++ b/libs/script_extender_checker_native/src/scriptextenderchecker.cpp
@@ -0,0 +1,363 @@
+#include "scriptextenderchecker.h"
+
+#include <uibase/iplugingame.h>
+#include <uibase/pluginrequirements.h>
+
+#include <QCoreApplication>
+#include <QDir>
+#include <QFile>
+#include <QFileInfo>
+#include <QMap>
+#include <QRegularExpression>
+#include <QStringConverter>
+#include <QTextStream>
+
+using namespace MOBase;
+
+// Regex patterns matching SKSE/F4SE/etc log formats
+static const QRegularExpression RE_NORMAL(
+ R"(plugin (?P<pluginPath>.+) \((?P<infoVersion>[\dA-Fa-f]{8}) (?P<name>.*) (?P<version>[\dA-Fa-f]{8})\) (?P<loadStatus>.+?)(?P<errorCode> \d+)?( \(handle \d+\))?\s*$)");
+
+static const QRegularExpression RE_COULDNT_LOAD(
+ R"(couldn't load plugin (?P<pluginPath>.+) \(Error (?:code )?(?P<lastError>[-+]?\d+)(?::\s*(?P<seDetails>.*))?\)\s*)");
+
+static const QRegularExpression RE_NOT_PLUGIN(
+ R"(plugin (?P<pluginPath>.+) does not appear to be an (?:SK|F4|NV|FO|OB)SE plugin\s*)");
+
+ScriptExtenderChecker::ScriptExtenderChecker() : m_organizer(nullptr) {}
+
+const QMap<QString, ScriptExtenderChecker::GameType>&
+ScriptExtenderChecker::supportedGames()
+{
+ static const QMap<QString, GameType> games = {
+ {"Skyrim",
+ {LogLocation::Docs, "SKSE/skse.log", "SKSE/skse_editor.log"}},
+ {"Skyrim Special Edition",
+ {LogLocation::Docs, "SKSE/skse64.log", ""}},
+ {"Skyrim VR",
+ {LogLocation::Docs, "SKSE/sksevr.log", ""}},
+ {"Fallout 4",
+ {LogLocation::Docs, "F4SE/f4se.log", ""}},
+ {"Oblivion",
+ {LogLocation::Install, "obse.log", "obse_editor.log"}},
+ {"New Vegas",
+ {LogLocation::Install, "nvse.log", "nvse_editor.log"}},
+ {"TTW",
+ {LogLocation::Install, "nvse.log", "nvse_editor.log"}},
+ {"Fallout 3",
+ {LogLocation::Install, "fose.log", "fose_editor.log"}},
+ };
+ return games;
+}
+
+bool ScriptExtenderChecker::init(IOrganizer* moInfo)
+{
+ m_organizer = moInfo;
+ m_organizer->onFinishedRun(
+ [this](const QString&, unsigned int) { invalidate(); });
+ return true;
+}
+
+QString ScriptExtenderChecker::name() const
+{
+ return "Script Extender Plugin Load Checker (Native)";
+}
+
+QString ScriptExtenderChecker::localizedName() const
+{
+ return tr("Script Extender Plugin Load Checker (Native)");
+}
+
+QString ScriptExtenderChecker::author() const
+{
+ return "AnyOldName3";
+}
+
+QString ScriptExtenderChecker::description() const
+{
+ return tr("Checks script extender log to see if any plugins failed to load.");
+}
+
+VersionInfo ScriptExtenderChecker::version() const
+{
+ return VersionInfo(1, 2, 0, VersionInfo::RELEASE_FINAL);
+}
+
+std::vector<std::shared_ptr<const IPluginRequirement>>
+ScriptExtenderChecker::requirements() const
+{
+ const auto& games = supportedGames();
+ return {PluginRequirementFactory::gameDependency(QStringList(games.keys()))};
+}
+
+QList<PluginSetting> ScriptExtenderChecker::settings() const
+{
+ return {};
+}
+
+std::vector<unsigned int> ScriptExtenderChecker::activeProblems() const
+{
+ if (!listBadPluginMessages().isEmpty()) {
+ return {PROBLEM_PLUGIN_LOAD};
+ }
+ return {};
+}
+
+QString ScriptExtenderChecker::shortDescription(unsigned int key) const
+{
+ return tr("Script extender log reports incompatible plugins.");
+}
+
+QString ScriptExtenderChecker::fullDescription(unsigned int key) const
+{
+ QStringList plugins = listBadPluginMessages();
+ QString pluginListString = "\n \u2022 " + plugins.join("\n \u2022 ");
+ return 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"
+ " \u2022 Look for updates to the mod or the specific plugin "
+ "included in the mod.\n"
+ " \u2022 Disable the mod containing the plugin.\n"
+ " \u2022 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:%1")
+ .arg(pluginListString);
+}
+
+bool ScriptExtenderChecker::hasGuidedFix(unsigned int key) const
+{
+ return false;
+}
+
+void ScriptExtenderChecker::startGuidedFix(unsigned int key) const {}
+
+QString ScriptExtenderChecker::resolveOrigin(const QString& pluginPath) const
+{
+ try {
+ QString dataDir =
+ m_organizer->managedGame()->dataDirectory().absolutePath();
+ QString relativePath = QDir(dataDir).relativeFilePath(pluginPath);
+ QStringList origins = m_organizer->getFileOrigins(relativePath);
+ if (!origins.isEmpty()) {
+ return origins.first();
+ }
+ } catch (...) {
+ }
+ return QString();
+}
+
+ScriptExtenderChecker::PluginMessage
+ScriptExtenderChecker::parseNormalLine(
+ const QRegularExpressionMatch& match) const
+{
+ PluginMessage msg;
+ msg.pluginPath = match.captured("pluginPath");
+ QString name = match.captured("name");
+ QString version = match.captured("version");
+ QString status = match.captured("loadStatus");
+ msg.origin = resolveOrigin(msg.pluginPath);
+ msg.success =
+ msg.origin.isEmpty() || status == "loaded correctly" || status == "no version data";
+
+ if (!msg.success) {
+ QString trStatus = status;
+ // Translate known statuses
+ if (status == "disabled, address library needs to be updated")
+ trStatus = tr("disabled, address library needs to be updated");
+ else if (status == "disabled, fatal error occurred while loading plugin")
+ trStatus = tr("disabled, fatal error occurred while loading plugin");
+ else if (status == "disabled, bad version data")
+ trStatus = tr("disabled, bad version data");
+ else if (status == "disabled, no name specified")
+ trStatus = tr("disabled, no name specified");
+ else if (status == "disabled, unsupported version independence method")
+ trStatus = tr("disabled, unsupported version independence method");
+ else if (status == "disabled, incompatible with current runtime version")
+ trStatus = tr("disabled, incompatible with current runtime version");
+ else if (status == "disabled, requires newer script extender")
+ trStatus = tr("disabled, requires newer script extender");
+ else if (status == "reported as incompatible during query")
+ trStatus = tr("reported as incompatible during query");
+ else if (status == "reported as incompatible during load")
+ trStatus = tr("reported as incompatible during load");
+ else if (status ==
+ "disabled, fatal error occurred while checking plugin compatibility")
+ trStatus = tr("disabled, fatal error occurred while checking plugin "
+ "compatibility");
+ else if (status == "disabled, fatal error occurred while querying plugin")
+ trStatus = tr("disabled, fatal error occurred while querying plugin");
+
+ msg.message = tr("%1 version %2 (%3, %4) %5.")
+ .arg(name)
+ .arg(version)
+ .arg(QFileInfo(msg.pluginPath).fileName())
+ .arg(msg.origin)
+ .arg(trStatus);
+ }
+ return msg;
+}
+
+ScriptExtenderChecker::PluginMessage
+ScriptExtenderChecker::parseCouldntLoadLine(
+ const QRegularExpressionMatch& match) const
+{
+ PluginMessage msg;
+ msg.pluginPath = match.captured("pluginPath");
+ int lastError = match.captured("lastError").toInt();
+ QString details = match.captured("seDetails").trimmed();
+ msg.origin = resolveOrigin(msg.pluginPath);
+ msg.success = msg.origin.isEmpty();
+
+ if (!msg.success) {
+ QString fileName = QFileInfo(msg.pluginPath).fileName();
+ if (lastError == 126) {
+ msg.message =
+ tr("Couldn't load %1 (%2). A dependency DLL could not be found "
+ "(code %3). %4")
+ .arg(fileName)
+ .arg(msg.origin)
+ .arg(lastError)
+ .arg(details);
+ } else if (lastError == 193) {
+ msg.message = tr("Couldn't load %1 (%2). A DLL is invalid (code %3).")
+ .arg(fileName)
+ .arg(msg.origin)
+ .arg(lastError);
+ } else {
+ msg.message =
+ tr("Couldn't load %1 (%2). The last error code was %3.")
+ .arg(fileName)
+ .arg(msg.origin)
+ .arg(lastError);
+ }
+ }
+ return msg;
+}
+
+ScriptExtenderChecker::PluginMessage
+ScriptExtenderChecker::parseNotAPluginLine(
+ const QRegularExpressionMatch& match) const
+{
+ PluginMessage msg;
+ msg.pluginPath = match.captured("pluginPath");
+ msg.origin = resolveOrigin(msg.pluginPath);
+ msg.success = msg.origin.isEmpty();
+
+ if (!msg.success) {
+ msg.message =
+ tr("%1 (%2) does not appear to be a script extender plugin.")
+ .arg(QFileInfo(msg.pluginPath).fileName())
+ .arg(msg.origin);
+ }
+ return msg;
+}
+
+QList<ScriptExtenderChecker::PluginMessage>
+ScriptExtenderChecker::parseLog(const QString& logPath) const
+{
+ QList<PluginMessage> messages;
+
+ QFile file(logPath);
+ if (!file.exists() || !file.open(QIODevice::ReadOnly | QIODevice::Text)) {
+ return messages;
+ }
+
+ // Script extender logs use cp1252 encoding
+ QTextStream stream(&file);
+#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
+ stream.setCodec("Windows-1252");
+#else
+ stream.setEncoding(QStringConverter::Latin1);
+#endif
+
+ while (!stream.atEnd()) {
+ QString line = stream.readLine() + "\n";
+
+ QRegularExpressionMatch match = RE_NORMAL.match(line);
+ if (match.hasMatch()) {
+ messages.append(parseNormalLine(match));
+ continue;
+ }
+
+ match = RE_COULDNT_LOAD.match(line);
+ if (match.hasMatch()) {
+ messages.append(parseCouldntLoadLine(match));
+ continue;
+ }
+
+ match = RE_NOT_PLUGIN.match(line);
+ if (match.hasMatch()) {
+ messages.append(parseNotAPluginLine(match));
+ continue;
+ }
+ }
+
+ return messages;
+}
+
+QStringList ScriptExtenderChecker::listBadPluginMessages() const
+{
+ const auto& games = supportedGames();
+ QString gameName = m_organizer->managedGame()->gameName();
+
+ if (!games.contains(gameName)) {
+ return {};
+ }
+
+ const GameType& gameType = games[gameName];
+
+ QString baseDir;
+ if (gameType.base == LogLocation::Docs) {
+ baseDir = m_organizer->managedGame()->documentsDirectory().absolutePath();
+ } else {
+ baseDir = m_organizer->managedGame()->gameDirectory().absolutePath();
+ }
+
+ QList<PluginMessage> gameMessages;
+ QList<PluginMessage> editorMessages;
+
+ if (!gameType.gameSuffix.isEmpty()) {
+ gameMessages = parseLog(QDir(baseDir).filePath(gameType.gameSuffix));
+ }
+ if (!gameType.editorSuffix.isEmpty()) {
+ editorMessages = parseLog(QDir(baseDir).filePath(gameType.editorSuffix));
+ }
+
+ QStringList result;
+
+ // Report game log failures that aren't successful in editor log
+ for (const auto& gameMsg : gameMessages) {
+ if (!gameMsg.success) {
+ bool editorOk = false;
+ for (const auto& editorMsg : editorMessages) {
+ if (gameMsg.pluginPath == editorMsg.pluginPath && editorMsg.success) {
+ editorOk = true;
+ break;
+ }
+ }
+ if (!editorOk && !result.contains(gameMsg.message)) {
+ result.append(gameMsg.message);
+ }
+ }
+ }
+
+ // Report editor log failures that aren't successful in game log
+ for (const auto& editorMsg : editorMessages) {
+ if (!editorMsg.success) {
+ bool gameOk = false;
+ for (const auto& gameMsg : gameMessages) {
+ if (editorMsg.pluginPath == gameMsg.pluginPath && gameMsg.success) {
+ gameOk = true;
+ break;
+ }
+ }
+ if (!gameOk && !result.contains(editorMsg.message)) {
+ result.append(editorMsg.message);
+ }
+ }
+ }
+
+ return result;
+}
diff --git a/libs/script_extender_checker_native/src/scriptextenderchecker.h b/libs/script_extender_checker_native/src/scriptextenderchecker.h
new file mode 100644
index 0000000..8dbadcb
--- /dev/null
+++ b/libs/script_extender_checker_native/src/scriptextenderchecker.h
@@ -0,0 +1,72 @@
+#ifndef SCRIPTEXTENDERCHECKER_H
+#define SCRIPTEXTENDERCHECKER_H
+
+#include <memory>
+
+#include <QString>
+#include <QStringList>
+
+#include <uibase/iplugin.h>
+#include <uibase/iplugindiagnose.h>
+#include <uibase/pluginrequirements.h>
+
+class ScriptExtenderChecker : public QObject,
+ public MOBase::IPlugin,
+ public MOBase::IPluginDiagnose
+{
+ Q_OBJECT
+ Q_INTERFACES(MOBase::IPlugin MOBase::IPluginDiagnose)
+ Q_PLUGIN_METADATA(IID "org.tannin.ScriptExtenderCheckerNative")
+
+public:
+ ScriptExtenderChecker();
+
+public: // IPlugin
+ bool init(MOBase::IOrganizer* moInfo) override;
+ QString name() const override;
+ QString localizedName() const override;
+ QString author() const override;
+ QString description() const override;
+ MOBase::VersionInfo version() const override;
+ std::vector<std::shared_ptr<const MOBase::IPluginRequirement>>
+ requirements() const override;
+ QList<MOBase::PluginSetting> settings() const override;
+
+public: // IPluginDiagnose
+ std::vector<unsigned int> activeProblems() const override;
+ QString shortDescription(unsigned int key) const override;
+ QString fullDescription(unsigned int key) const override;
+ bool hasGuidedFix(unsigned int key) const override;
+ void startGuidedFix(unsigned int key) const override;
+
+private:
+ static const unsigned int PROBLEM_PLUGIN_LOAD = 0;
+
+ enum class LogLocation { Docs, Install };
+
+ struct GameType {
+ LogLocation base;
+ QString gameSuffix;
+ QString editorSuffix; // empty if no editor log
+ };
+
+ struct PluginMessage {
+ QString pluginPath;
+ QString origin;
+ QString message;
+ bool success;
+ };
+
+ QStringList listBadPluginMessages() const;
+ QList<PluginMessage> parseLog(const QString& logPath) const;
+ PluginMessage parseNormalLine(const QRegularExpressionMatch& match) const;
+ PluginMessage parseCouldntLoadLine(const QRegularExpressionMatch& match) const;
+ PluginMessage parseNotAPluginLine(const QRegularExpressionMatch& match) const;
+ QString resolveOrigin(const QString& pluginPath) const;
+
+ static const QMap<QString, GameType>& supportedGames();
+
+ MOBase::IOrganizer* m_organizer;
+};
+
+#endif // SCRIPTEXTENDERCHECKER_H