from functools import cmp_to_key from typing import Sequence from PyQt6.QtCore import ( QByteArray, QCoreApplication, QDateTime, QFile, QFileInfo, QStringConverter, QStringEncoder, qCritical, qWarning, ) import mobase class OblivionRemasteredGamePlugins(mobase.GamePlugins): """ Reimplementation of GameGamebryo "GamePlugins" code, in the Skyrim style. Should properly account for disabled plugins and the loadorder.txt profile file. """ def __init__(self, organizer: mobase.IOrganizer): super().__init__() self._last_read = QDateTime().currentDateTime() self._organizer = organizer # Not currently used. These plugins exist in the base game but are not enabled by default. self._plugin_blacklist = ["TamrielLevelledRegion.esp", "AltarGymNavigation.esp"] def writePluginLists(self, plugin_list: mobase.IPluginList) -> None: if not self._last_read.isValid(): return self.writePluginList( plugin_list, self._organizer.profile().absolutePath() + "/plugins.txt" ) self.writeLoadOrderList( plugin_list, self._organizer.profile().absolutePath() + "/loadorder.txt" ) self._last_read = QDateTime.currentDateTime() def readPluginLists(self, plugin_list: mobase.IPluginList) -> None: load_order_path = self._organizer.profile().absolutePath() + "/loadorder.txt" load_order = self.readLoadOrderList(plugin_list, load_order_path) plugin_list.setLoadOrder(load_order) self.readPluginList(plugin_list) self._last_read = QDateTime.currentDateTime() def getLoadOrder(self) -> Sequence[str]: load_order_path = self._organizer.profile().absolutePath() + "/loadorder.txt" plugins_path = self._organizer.profile().absolutePath() + "/plugins.txt" load_order_is_new = ( not self._last_read.isValid() or not QFileInfo(load_order_path).exists() or QFileInfo(load_order_path).lastModified() > self._last_read ) plugins_is_new = ( not self._last_read.isValid() or QFileInfo(plugins_path).lastModified() > self._last_read ) if load_order_is_new or not plugins_is_new: return self.readLoadOrderList(self._organizer.pluginList(), load_order_path) else: return self.readPluginList(self._organizer.pluginList()) def writePluginList(self, plugin_list: mobase.IPluginList, filePath: str): self.writeList(plugin_list, filePath, False) def writeLoadOrderList(self, plugin_list: mobase.IPluginList, filePath: str): self.writeList(plugin_list, filePath, True) def writeList( self, plugin_list: mobase.IPluginList, filePath: str, load_order: bool ): plugins_file = open(filePath, "w") encoder = ( QStringEncoder(QStringConverter.Encoding.Utf8) if load_order else QStringEncoder(QStringConverter.Encoding.System) ) plugins_text = "# This file was automatically generated by Mod Organizer.\n" invalid_filenames = False written_count = 0 plugins = plugin_list.pluginNames() plugins_sorted = sorted( plugins, key=cmp_to_key( lambda lhs, rhs: plugin_list.priority(lhs) - plugin_list.priority(rhs) ), ) for plugin_name in plugins_sorted: if ( load_order or plugin_list.state(plugin_name) == mobase.PluginState.ACTIVE ): result = encoder.encode(plugin_name) if encoder.hasError(): invalid_filenames = True qCritical("invalid plugin name %s" % plugin_name) plugins_text += result.data().decode() + "\n" written_count += 1 if invalid_filenames: qCritical( QCoreApplication.translate( "MainWindow", "Some of your plugins have invalid names! These " + "plugins can not be loaded by the game. Please see " + "mo_interface.log for a list of affected plugins " + "and rename them.", ) ) if written_count == 0: qWarning( "plugin list would be empty, this is almost certainly wrong. Not saving." ) else: plugins_file.write(plugins_text) plugins_file.close() def readLoadOrderList( self, plugin_list: mobase.IPluginList, file_path: str ) -> list[str]: plugin_names = [ plugin for plugin in self._organizer.managedGame().primaryPlugins() ] plugin_lookup: set[str] = set() for name in plugin_names: if name.lower() not in plugin_lookup: plugin_lookup.add(name.lower()) try: with open(file_path) as file: for line in file: if line.startswith("#"): continue plugin_file = line.rstrip("\n") if plugin_file.lower() not in plugin_lookup: plugin_lookup.add(plugin_file.lower()) plugin_names.append(plugin_file) except FileNotFoundError: return self.readPluginList(plugin_list) return plugin_names def readPluginList(self, plugin_list: mobase.IPluginList) -> list[str]: plugins = [plugin for plugin in plugin_list.pluginNames()] sorted_plugins: list[str] = [] primary = [plugin for plugin in self._organizer.managedGame().primaryPlugins()] primary_lower = [plugin.lower() for plugin in primary] for plugin_name in primary: if plugin_list.state(plugin_name) != mobase.PluginState.MISSING: plugin_list.setState(plugin_name, mobase.PluginState.ACTIVE) sorted_plugins.append(plugin_name) plugin_remove = [ plugin for plugin in plugins if plugin.lower() in primary_lower ] for plugin in plugin_remove: plugins.remove(plugin) plugins_txt_exists = True file_path = self._organizer.profile().absolutePath() + "/plugins.txt" file = QFile(file_path) if not file.open(QFile.OpenModeFlag.ReadOnly): plugins_txt_exists = False if file.size() == 0: plugins_txt_exists = False if plugins_txt_exists: while not file.atEnd(): line = file.readLine() file_plugin_name = QByteArray() if line.size() > 0 and line.at(0).decode() != "#": encoder = QStringEncoder(QStringEncoder.Encoding.System) file_plugin_name = encoder.encode(line.trimmed().data().decode()) if file_plugin_name.size() > 0: if file_plugin_name.data().decode().lower() in [ plugin.lower() for plugin in plugins ]: plugin_list.setState( file_plugin_name.data().decode(), mobase.PluginState.ACTIVE ) sorted_plugins.append(file_plugin_name.data().decode()) plugins.remove(file_plugin_name.data().decode()) file.close() for plugin_name in plugins: plugin_list.setState(plugin_name, mobase.PluginState.INACTIVE) else: for plugin_name in plugins: plugin_list.setState(plugin_name, mobase.PluginState.INACTIVE) return sorted_plugins + plugins def lightPluginsAreSupported(self) -> bool: return False def mediumPluginsAreSupported(self) -> bool: return False def blueprintPluginsAreSupported(self) -> bool: return False