From 755698f1adceef55f52c8e44c9746537885c9bf7 Mon Sep 17 00:00:00 2001 From: SulfurNitride Date: Thu, 12 Mar 2026 07:08:09 -0500 Subject: 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 --- libs/basic_games_native/src/basicgameplugin.cpp | 380 ++++++++++++++++++++++++ 1 file changed, 380 insertions(+) create mode 100644 libs/basic_games_native/src/basicgameplugin.cpp (limited to 'libs/basic_games_native/src/basicgameplugin.cpp') diff --git a/libs/basic_games_native/src/basicgameplugin.cpp b/libs/basic_games_native/src/basicgameplugin.cpp new file mode 100644 index 0000000..8c73e4c --- /dev/null +++ b/libs/basic_games_native/src/basicgameplugin.cpp @@ -0,0 +1,380 @@ +#include "basicgameplugin.h" +#include "steamutils.h" + +#include + +#include +#include +#include + +// Simple ISaveGame implementation for basic games +class BasicSaveGame : public MOBase::ISaveGame +{ +public: + BasicSaveGame(const QString& filePath) + : m_filePath(filePath), m_fileInfo(filePath) + { + } + + QString getFilepath() const override { return m_filePath; } + + QDateTime getCreationTime() const override + { + return m_fileInfo.lastModified(); + } + + QString getName() const override + { + return m_fileInfo.completeBaseName(); + } + + QString getSaveGroupIdentifier() const override { return ""; } + + QStringList allFiles() const override { return {m_filePath}; } + +private: + QString m_filePath; + QFileInfo m_fileInfo; +}; + +BasicGamePlugin::BasicGamePlugin(const GameDefinition& def) : m_def(def) {} + +bool BasicGamePlugin::init(MOBase::IOrganizer* organizer) +{ + m_organizer = organizer; + return true; +} + +QString BasicGamePlugin::name() const +{ + return m_def.pluginName; +} + +QString BasicGamePlugin::localizedName() const +{ + return m_def.pluginName + " (Native)"; +} + +QString BasicGamePlugin::author() const +{ + return m_def.author; +} + +QString BasicGamePlugin::description() const +{ + return "Adds support for " + m_def.gameName; +} + +MOBase::VersionInfo BasicGamePlugin::version() const +{ + return MOBase::VersionInfo(m_def.version); +} + +QList BasicGamePlugin::settings() const +{ + return {}; +} + +QString BasicGamePlugin::gameName() const +{ + return m_def.gameName; +} + +void BasicGamePlugin::detectGame() +{ + // Try Steam first + for (int steamId : m_def.steamAppIds) { + QString path = findSteamGamePath(steamId); + if (!path.isEmpty()) { + setGamePath(path); + return; + } + } + + // GOG via Heroic launcher + if (!m_def.gogAppIds.isEmpty()) { + // Check Heroic GOG installed games + QStringList heroicPaths = { + QDir::homePath() + "/.config/heroic/gog_store/installed.json", + QDir::homePath() + + "/.var/app/com.heroicgameslauncher.hgl/config/heroic/" + "gog_store/installed.json", + }; + for (const auto& heroicPath : heroicPaths) { + QFile file(heroicPath); + if (!file.open(QIODevice::ReadOnly)) + continue; + QByteArray data = file.readAll(); + // Simple JSON parsing for install_path + for (int gogId : m_def.gogAppIds) { + QString idStr = QString::number(gogId); + if (data.contains(idStr.toUtf8())) { + // Find the install_path for this entry + int idx = data.indexOf(idStr.toUtf8()); + int pathIdx = data.indexOf("install_path", idx); + if (pathIdx >= 0) { + // Find the value after "install_path" + int colonIdx = data.indexOf(':', pathIdx); + int quoteStart = data.indexOf('"', colonIdx + 1); + int quoteEnd = data.indexOf('"', quoteStart + 1); + if (quoteStart >= 0 && quoteEnd > quoteStart) { + QString path = + QString::fromUtf8(data.mid(quoteStart + 1, quoteEnd - quoteStart - 1)); + if (QDir(path).exists()) { + setGamePath(path); + return; + } + } + } + } + } + } + } +} + +void BasicGamePlugin::initializeProfile(const QDir& directory, + ProfileSettings settings) const +{ + // Create the profile directory if needed + if (!directory.exists()) { + directory.mkpath("."); + } +} + +std::vector> +BasicGamePlugin::listSaves(QDir folder) const +{ + std::vector> saves; + + if (!m_def.saveExtension.isEmpty() && folder.exists()) { + QStringList filters; + filters << "*." + m_def.saveExtension; + QStringList entries = folder.entryList(filters, QDir::Files, QDir::Time); + for (const auto& entry : entries) { + saves.push_back( + std::make_shared(folder.filePath(entry))); + } + } + + return saves; +} + +bool BasicGamePlugin::isInstalled() const +{ + return m_installed; +} + +QIcon BasicGamePlugin::gameIcon() const +{ + return QIcon(); +} + +QDir BasicGamePlugin::gameDirectory() const +{ + return QDir(m_gameDir); +} + +QDir BasicGamePlugin::dataDirectory() const +{ + QString dataDir = resolveVariables(m_def.dataDirectory); + if (dataDir.isEmpty()) { + return gameDirectory(); + } + QDir dir(dataDir); + if (dir.isAbsolute()) { + return dir; + } + return QDir(m_gameDir + "/" + dataDir); +} + +void BasicGamePlugin::setGamePath(const QString& path) +{ + m_gameDir = path; + m_installed = !path.isEmpty() && QDir(path).exists(); +} + +QDir BasicGamePlugin::documentsDirectory() const +{ + if (m_def.documentsDirectory.isEmpty()) { + // Default: try My Games/ then under Documents + QString docs = + QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); + QDir myGames(docs + "/My Games/" + m_def.gameName); + if (myGames.exists()) + return myGames; + QDir plain(docs + "/" + m_def.gameName); + if (plain.exists()) + return plain; + return QDir(); + } + return QDir(resolveVariables(m_def.documentsDirectory)); +} + +QDir BasicGamePlugin::savesDirectory() const +{ + if (m_def.savesDirectory.isEmpty()) { + return documentsDirectory(); + } + return QDir(resolveVariables(m_def.savesDirectory)); +} + +QList BasicGamePlugin::executables() const +{ + QList list; + QDir dir = gameDirectory(); + QFileInfo binary(dir.filePath(m_def.binaryName)); + if (binary.exists()) { + list.append(MOBase::ExecutableInfo(m_def.gameName, binary)); + } + if (!m_def.launcherName.isEmpty()) { + QFileInfo launcher(dir.filePath(m_def.launcherName)); + if (launcher.exists()) { + list.append(MOBase::ExecutableInfo(m_def.gameName + " Launcher", launcher)); + } + } + return list; +} + +QList +BasicGamePlugin::executableForcedLoads() const +{ + return {}; +} + +QString BasicGamePlugin::steamAPPId() const +{ + if (!m_def.steamAppIds.isEmpty()) { + return QString::number(m_def.steamAppIds.first()); + } + return ""; +} + +QStringList BasicGamePlugin::primaryPlugins() const +{ + return m_def.primaryPlugins; +} + +QStringList BasicGamePlugin::gameVariants() const +{ + return {}; +} + +void BasicGamePlugin::setGameVariant(const QString&) {} + +QString BasicGamePlugin::binaryName() const +{ + return m_def.binaryName; +} + +QString BasicGamePlugin::gameShortName() const +{ + return m_def.gameShortName; +} + +QStringList BasicGamePlugin::validShortNames() const +{ + return m_def.validShortNames; +} + +QString BasicGamePlugin::gameNexusName() const +{ + return m_def.gameNexusName; +} + +QStringList BasicGamePlugin::iniFiles() const +{ + QStringList resolved; + for (const auto& ini : m_def.iniFiles) { + resolved.append(resolveVariables(ini)); + } + return resolved; +} + +QStringList BasicGamePlugin::DLCPlugins() const +{ + return m_def.dlcPlugins; +} + +MOBase::IPluginGame::LoadOrderMechanism +BasicGamePlugin::loadOrderMechanism() const +{ + return m_def.loadOrderMechanism; +} + +MOBase::IPluginGame::SortMechanism BasicGamePlugin::sortMechanism() const +{ + return m_def.sortMechanism; +} + +int BasicGamePlugin::nexusGameID() const +{ + return m_def.nexusGameId; +} + +bool BasicGamePlugin::looksValid(QDir const& dir) const +{ + return dir.exists(m_def.binaryName); +} + +QString BasicGamePlugin::gameVersion() const +{ + // On Linux we can't easily read PE version resources + // Return empty; MO2 will show "N/A" + return ""; +} + +QString BasicGamePlugin::getLauncherName() const +{ + return m_def.launcherName; +} + +QString BasicGamePlugin::getSupportURL() const +{ + return m_def.supportURL; +} + +QString BasicGamePlugin::resolveVariables(const QString& input) const +{ + if (input.isEmpty()) + return input; + + QString result = input; + + // Normalize backslashes to forward slashes first + result.replace('\\', '/'); + + // %DOCUMENTS% + if (result.contains("%DOCUMENTS%")) { + QString docs = + QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); + result.replace("%DOCUMENTS%", docs); + } + + // %USERPROFILE% - on Linux, resolve to the Wine prefix userprofile + if (result.contains("%USERPROFILE%")) { + QString userProfile; + // Try the global Fluorine prefix + QString prefixPath = + QDir::homePath() + + "/.local/share/fluorine/Prefix/pfx/drive_c/users/steamuser"; + if (QDir(prefixPath).exists()) { + userProfile = prefixPath; + } else { + // Fallback to home directory + userProfile = QDir::homePath(); + } + result.replace("%USERPROFILE%", userProfile); + } + + // %GAME_PATH% + if (result.contains("%GAME_PATH%")) { + result.replace("%GAME_PATH%", m_gameDir); + } + + // %GAME_DOCUMENTS% + if (result.contains("%GAME_DOCUMENTS%")) { + result.replace("%GAME_DOCUMENTS%", documentsDirectory().absolutePath()); + } + + return result; +} -- cgit v1.3.1