diff options
Diffstat (limited to 'libs/fnistool/src')
| -rw-r--r-- | libs/fnistool/src/CMakeLists.txt | 6 | ||||
| -rw-r--r-- | libs/fnistool/src/FNISPatches.py | 245 | ||||
| -rw-r--r-- | libs/fnistool/src/FNISTool.py | 342 | ||||
| -rw-r--r-- | libs/fnistool/src/FNISToolReset.py | 76 | ||||
| -rw-r--r-- | libs/fnistool/src/FNISTool_en.ts | 295 |
5 files changed, 964 insertions, 0 deletions
diff --git a/libs/fnistool/src/CMakeLists.txt b/libs/fnistool/src/CMakeLists.txt new file mode 100644 index 0000000..84c2fc2 --- /dev/null +++ b/libs/fnistool/src/CMakeLists.txt @@ -0,0 +1,6 @@ +cmake_minimum_required(VERSION 3.16) + +find_package(mo2-cmake CONFIG REQUIRED) + +add_custom_target(FNISTool ALL) +mo2_configure_python(FNISTool SIMPLE) diff --git a/libs/fnistool/src/FNISPatches.py b/libs/fnistool/src/FNISPatches.py new file mode 100644 index 0000000..87fdbf0 --- /dev/null +++ b/libs/fnistool/src/FNISPatches.py @@ -0,0 +1,245 @@ +# This Mod Organizer plugin is released to the pubic under the terms of the GNU GPL version 3, which is accessible from the Free Software Foundation here: https://www.gnu.org/licenses/gpl-3.0-standalone.html + +# To use this plugin, place it in the plugins directory of your Mod Organizer install. You will then find a 'ENTER BUTTON NAME HERE BEFORE RELEASE!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!' option under the tools menu. + +# Intended behaviour: +# * Adds button to tools menu. +# * If FNIS' location isn't known (or isn't valid, e.g. FNIS isn't actually there) when the button is pressed, nag the user to run the main FNIS Tool at least once, and exit. +# * Reads FNIS' patches file for the current game and the MyPatches.txt file to determine which are active. +# * Displays a popup where the user can enable or disable patches, and saves the results to MyPatches.txt when Save is pressed. + +# Future behaviour: +# * Maybe allow the user to show hidden patches +# * Maybe move the saved list to something stored per-profile + +import os +import pathlib +import sys + +from FNISTool import FNISTool +from PyQt6.QtCore import QCoreApplication, qCritical, QFileInfo, Qt +from PyQt6.QtGui import QIcon, QFileSystemModel +from PyQt6.QtWidgets import QDialogButtonBox, QLabel, QListWidget, QListWidgetItem, QMessageBox, QVBoxLayout, QDialog + +import mobase + +patchListNames = { + "Skyrim": "PatchList.txt", + "Skyrim Special Edition": "PatchListSE.txt", + "Skyrim VR": "PatchListVR.txt" +} + +class Patch: + + def __init__(self, fullString): + fields = fullString.split('#') + # field names taken from FNIS PatchListSE.txt + self.patchid = fields[0] + self.hidden = fields[1] == "1" + self.num_bones = fields[2] + self.required_behaviors_pattern = fields[3] + self.text_for_patch = fields[4] + if len(fields) > 5: + self.optional_file_path_for_mod_install_check = fields[5] + else: + self.optional_file_path_for_mod_install_check = None + + def asQListWidgetItem(self): + listItem = QListWidgetItem(self.text_for_patch, None, 0) + listItem.setFlags(listItem.flags() | Qt.ItemFlag.ItemIsUserCheckable) + # We can't set the hidden status until the item is actually in a list + listItem.setData(Qt.ItemDataRole.UserRole, self.patchid) + + return listItem + +class ExpandingQListWidget(QListWidget): + + def __init__(self, parent=None): + super().__init__(parent) + + def sizeHint(self): + return self.childrenRect().size() + +class FNISPatches(mobase.IPluginTool): + + def __init__(self): + super(FNISPatches, self).__init__() + self.__organizer = None + self.__parentWidget = None + + def init(self, organizer): + self.__organizer = organizer + if sys.version_info < (3, 0): + qCritical(self.tr("FNISPatches plugin requires a Python 3 interpreter, but is running on a Python 2 interpreter.")) + QMessageBox.critical(self.__parentWidget, self.tr("Incompatible Python version."), self.tr("This version of the FNIS Patches plugin requires a Python 3 interpreter, but Mod Organizer has provided a Python 2 interpreter. You should check for an updated version, including in the Mod Organizer 2 Development Discord Server.")) + return False + return True + + def name(self): + return "FNIS Patches Tool" + + def localizedName(self): + return self.tr("FNIS Patches Tool") + + def author(self): + return "AnyOldName3" + + def description(self): + return self.tr("Configures the patches which FNIS applies to the game.") + + def version(self): + return mobase.VersionInfo(1, 0, 1, mobase.ReleaseType.final) + + def master(self): + return "FNIS Integration Tool" + + def settings(self): + return [] + + def displayName(self): + return self.tr("FNIS/Configure FNIS Patches") + + def tooltip(self): + return self.tr("Configures the patches which FNIS applies to the game.") + + def icon(self): + fnisPath = self.__organizer.pluginSetting(self.__mainToolName(), "fnis-path") + if os.path.exists(fnisPath): + # We can't directly grab the icon from an executable, but this seems like the simplest alternative. + fin = QFileInfo(fnisPath) + model = QFileSystemModel() + model.setRootPath(fin.path()) + return model.fileIcon(model.index(fin.filePath())) + else: + # Fall back to where the user might have put an icon manually. + return QIcon("plugins/FNIS.ico") + + def setParentWidget(self, widget): + self.__parentWidget = widget + + def display(self): + fnisPath = self.__getFNISPath() + if fnisPath == None: + return + + enabledPatches = self.__loadEnabledPatches() + + availablePatches = self.__loadAvailablePatches() + + dialog = QDialog(self.__parentWidget) + dialog.setWindowTitle(self.tr("Select Patches")) + + label = QLabel(self.tr("Note: Some patches may be automatically enabled or disabled by Fore's New Idles in Skyrim, so don't be surprised if its list differs from this one.")) + label.setWordWrap(True) + + listWidget = ExpandingQListWidget() + for patch in availablePatches.values(): + listItem = patch.asQListWidgetItem() + if patch.patchid in enabledPatches: + listItem.setCheckState(Qt.CheckState.Checked) + else: + listItem.setCheckState(Qt.CheckState.Unchecked) + listWidget.addItem(listItem) + listItem.setHidden(patch.hidden) + + buttonBox = QDialogButtonBox(QDialogButtonBox.StandardButton.Save | QDialogButtonBox.StandardButton.Cancel) + buttonBox.accepted.connect(dialog.accept) + buttonBox.rejected.connect(dialog.reject) + + layout = QVBoxLayout() + layout.addWidget(label) + layout.addWidget(listWidget) + layout.addWidget(buttonBox) + dialog.setLayout(layout) + + result = dialog.exec() + if result == QDialog.DialogCode.Rejected: + # Cancel was pressed + return + + enabledPatches = set() + for i in range(0, listWidget.count()): + listItem = listWidget.item(i) + if listItem.checkState() == Qt.CheckState.Checked: + enabledPatches.add(listItem.data(Qt.ItemDataRole.UserRole)) + self.__saveEnabledPatches(enabledPatches) + + def tr(self, str): + return QCoreApplication.translate("FNISPatches", str) + + @staticmethod + def __mainToolName(): + return FNISTool().name() + + def __getFNISPath(self): + savedPath = self.__organizer.pluginSetting(self.__mainToolName(), "fnis-path") + # FNIS must be installed within the game's data directory, so needs to either be within that or a mod folder + modDirectory = self.__getModDirectory() + gameDataDirectory = pathlib.Path(self.__organizer.managedGame().dataDirectory().absolutePath()) + pathlibPath = pathlib.Path(savedPath) + inGoodLocation = self.__withinDirectory(pathlibPath, modDirectory) + inGoodLocation |= self.__withinDirectory(pathlibPath, gameDataDirectory) + if not pathlibPath.is_file() or not inGoodLocation: + QMessageBox.information(self.__parentWidget, self.tr("Unable to find FNIS"), self.tr("Fore's New Idles in Skyrim can't be found using the location saved in Mod Organizer's settings. Please run the main FNIS integration tool before using this one.")) + return None + return savedPath + + def __getModDirectory(self): + modDirectory = None + modList = self.__organizer.modList().allModsByProfilePriority() + # Get the first managed mod so we can access the mods directory. + for mod in modList: + if (self.__organizer.modList().state(mod) & 0x2) != 0: + modDirectory = pathlib.Path(self.__organizer.modList().getMod(mod).absolutePath()).parent + break + return modDirectory + + @staticmethod + def __withinDirectory(innerPath, outerDir): + for path in innerPath.parents: + if path.samefile(outerDir): + return True + return False + + def __loadEnabledPatches(self, ): + path = pathlib.Path(self.__getFNISPath()).parent + path /= "MyPatches.txt" + if not path.is_file(): + return set() + + enabledPatches = set() + with path.open() as f: + for line in f: + if line != "": + enabledPatches.add(line.strip()) + return enabledPatches + + def __saveEnabledPatches(self, enabledPatches): + path = pathlib.Path(self.__getFNISPath()).parent + path /= "MyPatches.txt" + with path.open('w') as f: + for patchName in enabledPatches: + f.write(patchName) + f.write("\n") + + def __loadAvailablePatches(self): + patchListName = patchListNames[self.__organizer.managedGame().gameName()] + path = pathlib.Path(self.__getFNISPath()).parent + path /= patchListName + availablePatches = {} + # FNIS uses UTF-8 with a BOM, Python assumes everything's using the platform's default encoding + with path.open(encoding="utf-8-sig") as f: + # The first line is a header thing + firstLine = True + for line in f: + if firstLine: + firstLine = False + continue + if line[0] == "'": + continue + patch = Patch(line.strip()) + availablePatches[patch.patchid] = patch + return availablePatches + +def createPlugin(): + return FNISPatches() diff --git a/libs/fnistool/src/FNISTool.py b/libs/fnistool/src/FNISTool.py new file mode 100644 index 0000000..e219bed --- /dev/null +++ b/libs/fnistool/src/FNISTool.py @@ -0,0 +1,342 @@ +# This Mod Organizer plugin is released to the pubic under the terms of the GNU GPL version 3, which is accessible from the Free Software Foundation here: https://www.gnu.org/licenses/gpl-3.0-standalone.html + +# To use this plugin, place it in the plugins directory of your Mod Organizer install. You will then find a 'Run FNIS' option under the tools menu. + +# Intended behaviour: +# * Adds button to tools menu. +# * If FNIS' location isn't known (or isn't valid, e.g. FNIS isn't actually there) when the button is pressed, a file chooser is displayed to find FNIS. +# * `GenerateFNISforUsers.exe RedirectFiles="<some mod path>" InstantExecute=1` is then run within the VFS, with the RedirectFiles option being controlled by other settings (which the user is prompted to fill in if they have not yet been specified). +# * When it exits, if necessary, its return code is used to generate a helpful popup saying whether or not it worked. + +# Future behaviour: +# * As in Vortex's FNIS integration, keeps track of mod files which affect FNIS. +# * If they don't match what was there when the last FNIS output was created, uses IPluginDiagnose interface to display a warning +# * This may already be handled by MO's built-in (but disabled) FNIS checker plugin +import os +import pathlib +import sys + +from PyQt6.QtCore import QCoreApplication, qCritical, QFileInfo +from PyQt6.QtGui import QIcon, QFileSystemModel +from PyQt6.QtWidgets import QFileDialog, QMessageBox + +import mobase + +class FNISMissingException(Exception): + """Thrown if GenerateFNISforUsers.exe path can't be found""" + pass + +class FNISInactiveException(Exception): + """Thrown if GenerateFNISforUsers.exe is installed to an inactive mod""" + pass + +class UnknownOutputPreferenceException(Exception): + """Thrown if the user hasn't specified whether to output to a separate mod""" + pass + +class FNISTool(mobase.IPluginTool): + + def __init__(self): + super(FNISTool, self).__init__() + self.__organizer = None + self.__parentWidget = None + + def init(self, organizer): + self.__organizer = organizer + if sys.version_info < (3, 0): + qCritical(self.tr("FNISTool plugin requires a Python 3 interpreter, but is running on a Python 2 interpreter.")) + QMessageBox.critical(self.__parentWidget, self.tr("Incompatible Python version."), self.tr("This version of the FNIS Integration plugin requires a Python 3 interpreter, but Mod Organizer has provided a Python 2 interpreter. You should check for an updated version, including in the Mod Organizer 2 Development Discord Server.")) + return False + return True + + def name(self): + return "FNIS Integration Tool" + + def localizedName(self): + return self.tr("FNIS Integration Tool") + + def author(self): + return "AnyOldName3" + + def description(self): + return self.tr("Runs GenerateFNISforUsers.exe so the game can load custom animations.") + + def version(self): + return mobase.VersionInfo(1, 2, 0, 0) + + def requirements(self): + return [ + mobase.PluginRequirementFactory.gameDependency({ + "Skyrim", + "Skyrim Special Edition", + "Skyrim VR" + }) + ] + + def settings(self): + return [ + mobase.PluginSetting("fnis-path", self.tr("Path to GenerateFNISforUsers.exe"), ""), + mobase.PluginSetting("output-to-mod", self.tr("Whether or not to direct the FNIS output to a mod folder."), True), + mobase.PluginSetting("output-path", self.tr("When output-to-mod is enabled, the path to the mod to use."), ""), + mobase.PluginSetting("initialised", self.tr("Settings have been initialised. Set to False to reinitialise them."), False), + mobase.PluginSetting("output-logs-to-mod", self.tr("Whether or not to direct any new FNIS logs to a mod folder."), True), + mobase.PluginSetting("output-logs-path", self.tr("When output-logs-to-mod is enabled, the path to the mod to use."), ""), + ] + + def displayName(self): + return self.tr("FNIS/Run FNIS") + + def tooltip(self): + return self.tr("Runs GenerateFNISforUsers.exe so the game can load custom animations.") + + def icon(self): + fnisPath = self.__organizer.pluginSetting(self.name(), "fnis-path") + if os.path.exists(fnisPath): + # We can't directly grab the icon from an executable, but this seems like the simplest alternative. + fin = QFileInfo(fnisPath) + model = QFileSystemModel() + model.setRootPath(fin.path()) + return model.fileIcon(model.index(fin.filePath())) + else: + # Fall back to where the user might have put an icon manually. + return QIcon("plugins/FNIS.ico") + + def setParentWidget(self, widget): + self.__parentWidget = widget + + def display(self): + args = [] + redirectOutput = True + outputModName = None + logOutputModName = "" + + if not bool(self.__organizer.pluginSetting(self.name(), "initialised")): + self.__organizer.setPluginSetting(self.name(), "fnis-path", "") + self.__organizer.setPluginSetting(self.name(), "output-path", "") + self.__organizer.setPluginSetting(self.name(), "output-to-mod", True) + self.__organizer.setPluginSetting(self.name(), "output-logs-path", "") + self.__organizer.setPluginSetting(self.name(), "output-logs-to-mod", True) + + try: + redirectOutput = self.__getRedirectOutput() + except UnknownOutputPreferenceException: + QMessageBox.critical(self.__parentWidget, self.tr("Output preference not set"), self.tr("Whether or not to output to a mod was not specified. The tool will now exit.")) + return + if redirectOutput: + try: + outputPath = self.__getOutputPath() + args.append('RedirectFiles="' + outputPath + '"') + outputModName = pathlib.Path(outputPath).name + except UnknownOutputPreferenceException: + QMessageBox.critical(self.__parentWidget, self.tr("Output mod not set"), self.tr("The mod to output to was not specified. The tool will now exit.")) + return + args.append('InstantExecute=1') + + if redirectOutput: + try: + redirectLogs = self.__getRedirectLogs() + except UnknownOutputPreferenceException: + QMessageBox.critical(self.__parentWidget, self.tr("Output preference not set"), self.tr("Whether or not to output to a mod was not specified. The tool will now exit.")) + return + if redirectLogs: + try: + outputPath = self.__getLogOutputPath() + logOutputModName = pathlib.Path(outputPath).name + except UnknownOutputPreferenceException: + QMessageBox.critical(self.__parentWidget, self.tr("Output mod not set"), self.tr("The mod to output to was not specified. The tool will now exit.")) + return + + try: + executable = self.__getFNISPath() + except FNISMissingException: + QMessageBox.critical(self.__parentWidget, self.tr("FNIS path not specified"), self.tr("The path to GenerateFNISforUsers.exe wasn't specified. The tool will now exit.")) + return + except FNISInactiveException: + # Error has already been displayed, just quit + return + + self.__organizer.setPluginSetting(self.name(), "initialised", True) + + if redirectOutput: + # Disable the output mod as USVFS isn't designed to cope with its input directories being modified + self.__organizer.modList().setActive(outputModName, False) + + if redirectLogs: + # Enable the log output mod + self.__organizer.modList().setActive(logOutputModName, True) + + + handle = self.__organizer.startApplication(executable, args, forcedCustomOverwrite=logOutputModName, ignoreCustomOverwrite=not bool(logOutputModName)) + result, exitCode = self.__organizer.waitForApplication(handle) + + if redirectOutput: + # Enable the output mod + self.__organizer.modList().setActive(outputModName, True) + # Ensure the 'No valid game data' message goes away + self.__organizer.modDataChanged(self.__organizer.getMod(outputModName)) + + if redirectLogs: + self.__organizer.modDataChanged(self.__organizer.getMod(logOutputModName)) + + def tr(self, str): + return QCoreApplication.translate("FNISTool", str) + + def __getRedirectOutput(self): + redirectOutput = bool(self.__organizer.pluginSetting(self.name(), "output-to-mod")) + initialised = bool(self.__organizer.pluginSetting(self.name(), "initialised")) + if not initialised: + result = QMessageBox.question(self.__parentWidget, self.tr("Output to a mod?"), self.tr("Fore's New Idles in Skyrim can output either to Mod Organizer's VFS (potentially overwriting files from multiple mods) or to a separate mod. Would you like FNIS to output to a separate mod? This setting can be updated in the Plugins tab of the Mod Organizer Settings menu."), QMessageBox.StandardButton(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No | QMessageBox.StandardButton.Cancel)) + if result == QMessageBox.StandardButton.Yes: + redirectOutput = True + elif result == QMessageBox.StandardButton.No: + redirectOutput = False + else: + # the user pressed cancel + raise UnknownOutputPreferenceException + + self.__organizer.setPluginSetting(self.name(), "output-to-mod", redirectOutput) + return redirectOutput + + def __getRedirectLogs(self): + redirectLogs = bool(self.__organizer.pluginSetting(self.name(), "output-logs-to-mod")) + initialised = bool(self.__organizer.pluginSetting(self.name(), "initialised")) + if not initialised: + result = QMessageBox.question(self.__parentWidget, self.tr("Output logs to a mod?"), self.tr("Any new logs generated when running FNIS will end up in Mod Organizer's overwrite folder. Would you like these logs to be output to a separate mod? This setting can be updated in the Plugins tab of the Mod Organizer Settings menu."), QMessageBox.StandardButton(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No | QMessageBox.StandardButton.Cancel)) + if result == QMessageBox.StandardButton.Yes: + redirectLogs = True + elif result == QMessageBox.StandardButton.No: + redirectLogs = False + else: + # the user pressed cancel + raise UnknownOutputPreferenceException + + self.__organizer.setPluginSetting(self.name(), "output-logs-to-mod", redirectLogs) + return redirectLogs + + def __getOutputPath(self): + path = self.__organizer.pluginSetting(self.name(), "output-path") + pathlibPath = pathlib.Path(path) + modDirectory = self.__getModDirectory() + isAMod = pathlibPath.parent.samefile(modDirectory) + if not pathlibPath.is_dir() or not isAMod: + QMessageBox.information(self.__parentWidget, self.tr("Choose an output mod"), self.tr("Please choose an output mod for Fore's New Idles in Skyrim. This must be a directory in Mod Organizer's mods directory, and you can create one if you do not have one already. This mod will not be available to the VFS when FNIS is run, so do not choose a mod you use for anything else. This setting can be updated in the Plugins tab of the Mod Organizer Settings menu.")) + while not pathlibPath.is_dir() or not isAMod: + path = QFileDialog.getExistingDirectory(self.__parentWidget, self.tr("Choose an output mod"), str(modDirectory), QFileDialog.Option.ShowDirsOnly) + if not path: + # cancel was pressed + raise UnknownOutputPreferenceException + pathlibPath = pathlib.Path(path) + isAMod = pathlibPath.parent.samefile(modDirectory) + if not isAMod: + QMessageBox.information(self.__parentWidget, self.tr("Not a mod..."), self.tr("The selected directory is not a Mod Organizer managed mod. Please choose a directory within the mods directory.")) + continue + empty = True + for item in pathlibPath.iterdir(): + if item.name != "meta.ini": + empty = False + break + if not empty: + if QMessageBox.question(self.__parentWidget, self.tr("Mod not empty"), self.tr("The selected mod already contains files. Are you sure want to use it as the output mod?"), QMessageBox.StandardButton(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)) == QMessageBox.StandardButton.Yes: + # Proceed normally - the user is happy + pass + else: + # Restart outer loop - the user wants to pick again + isAMod = False + # The user may have created a new mod in the MO mods directory, so we must trigger a refresh + self.__organizer.refreshModList() + self.__organizer.setPluginSetting(self.name(), "output-path", path) + return path + + def __getLogOutputPath(self): + path = self.__organizer.pluginSetting(self.name(), "output-logs-path") + pathlibPath = pathlib.Path(path) + modDirectory = self.__getModDirectory() + fnisOutputPath = pathlib.Path(self.__getOutputPath()) + isAMod = pathlibPath.parent.samefile(modDirectory) + isSameAsFnisOutput = pathlibPath.samefile(fnisOutputPath) + if not pathlibPath.is_dir() or not isAMod or isSameAsFnisOutput: + QMessageBox.information(self.__parentWidget, self.tr("Choose an output mod"), self.tr("Please choose an output mod for logs for Fore's New Idles in Skyrim. This must be a directory in Mod Organizer's mods directory, must not be the same as the FNIS output mod, and you can create one if you do not have one already. This setting can be updated in the Plugins tab of the Mod Organizer Settings menu.")) + while not pathlibPath.is_dir() or not isAMod or isSameAsFnisOutput: + path = QFileDialog.getExistingDirectory(self.__parentWidget, self.tr("Choose a log output mod"), str(modDirectory), QFileDialog.Option.ShowDirsOnly) + if not path: + # cancel was pressed + raise UnknownOutputPreferenceException + pathlibPath = pathlib.Path(path) + isAMod = pathlibPath.parent.samefile(modDirectory) + if not isAMod: + QMessageBox.information(self.__parentWidget, self.tr("Not a mod..."), self.tr("The selected directory is not a Mod Organizer managed mod. Please choose a directory within the mods directory.")) + continue + isSameAsFnisOutput = pathlibPath.samefile(fnisOutputPath) + if isSameAsFnisOutput: + QMessageBox.information(self.__parentWidget, self.tr("Same as FNIS output"), self.tr("The selected mod is the same as the FNIS output mod. Please choose a different mod.")) + continue + empty = True + for item in pathlibPath.iterdir(): + if item.name != "meta.ini": + empty = False + break + if not empty: + if QMessageBox.question(self.__parentWidget, self.tr("Mod not empty"), self.tr("The selected mod already contains files. Are you sure want to use it as the output mod?"), QMessageBox.StandardButton(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)) == QMessageBox.StandardButton.Yes: + # Proceed normally - the user is happy + pass + else: + # Restart outer loop - the user wants to pick again + isAMod = False + + # The user may have created a new mod in the MO mods directory, so we must trigger a refresh + self.__organizer.refreshModList() + self.__organizer.setPluginSetting(self.name(), "output-logs-path", path) + return path + + def __getFNISPath(self): + savedPath = self.__organizer.pluginSetting(self.name(), "fnis-path") + # FNIS must be installed within the game's data directory, so needs to either be within that or a mod folder + modDirectory = self.__getModDirectory() + gameDataDirectory = pathlib.Path(self.__organizer.managedGame().dataDirectory().absolutePath()) + pathlibPath = pathlib.Path(savedPath) + inGoodLocation = self.__withinDirectory(pathlibPath, modDirectory) + inGoodLocation |= self.__withinDirectory(pathlibPath, gameDataDirectory) + if not pathlibPath.is_file() or not inGoodLocation: + QMessageBox.information(self.__parentWidget, self.tr("Find FNIS"), self.tr("Fore's New Idles in Skyrim can't be found using the location saved in Mod Organizer's settings. Please find GenerateFNISforUsers.exe in the file picker. FNIS must be visible within the VFS, so choose an installation either within the game's data directory or within a mod folder. This setting can be updated in the Plugins tab of the Mod Organizer Settings menu.")) + while True: + path = QFileDialog.getOpenFileName(self.__parentWidget, self.tr("Locate GenerateFNISforUsers.exe"), str(modDirectory), "FNIS (GenerateFNISforUsers.exe)")[0] + if path == "": + # Cancel was pressed + raise FNISMissingException + pathlibPath = pathlib.Path(path) + inGoodLocation = self.__withinDirectory(pathlibPath, modDirectory) + inGoodLocation |= self.__withinDirectory(pathlibPath, gameDataDirectory) + if pathlibPath.is_file() and inGoodLocation: + self.__organizer.setPluginSetting(self.name(), "fnis-path", path) + savedPath = path + break + else: + QMessageBox.information(self.__parentWidget, self.tr("Not a compatible location..."), self.tr("Fore's New Idles in Skyrim only works when within the VFS, so must be installed to the game's data directory or within a mod folder. Please select a different FNIS installation.")) + # Check the mod is actually enabled + if self.__withinDirectory(pathlibPath, modDirectory): + fnisModName = None + for path in pathlibPath.parents: + if path.parent.samefile(modDirectory): + fnisModName = path.name + break + if (self.__organizer.modList().state(fnisModName) & mobase.ModState.active) == 0: + # FNIS is installed to an inactive mod + result = QMessageBox.question(self.__parentWidget, self.tr("FNIS mod deactivated"), self.tr("Fore's New Idles in Skyrim is installed to an inactive mod. Press OK to activate it or Cancel to quit the tool"), QMessageBox.StandardButton(QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Cancel)) + if result == QMessageBox.StandardButton.Ok: + self.__organizer.modList().setActive(fnisModName, True) + else: + raise FNISInactiveException + return savedPath + + def __getModDirectory(self): + return self.__organizer.modsPath() + + @staticmethod + def __withinDirectory(innerPath, outerDir): + for path in innerPath.parents: + if path.samefile(outerDir): + return True + return False + +def createPlugin(): + return FNISTool() diff --git a/libs/fnistool/src/FNISToolReset.py b/libs/fnistool/src/FNISToolReset.py new file mode 100644 index 0000000..d1f4bc9 --- /dev/null +++ b/libs/fnistool/src/FNISToolReset.py @@ -0,0 +1,76 @@ +import os +import sys + +from FNISTool import FNISTool +from PyQt6.QtCore import QFileInfo, QCoreApplication +from PyQt6.QtGui import QFileSystemModel, QIcon +from PyQt6.QtWidgets import QMessageBox + +import mobase + +class FNISToolReset(mobase.IPluginTool): + def __init__(self): + super(FNISToolReset, self).__init__() + self.__organizer = None + self.__parentWidget = None + + def init(self, organizer): + self.__organizer = organizer + return True + + def name(self): + return "FNIS Integration Tool Reset" + + def localizedName(self): + return self.tr("FNIS Integration Tool Reset") + + def author(self): + return "LostDragonist" + + def description(self): + return self.tr("Provides an easier way to reset the FNIS integration tool settings when needed.") + + def version(self): + return mobase.VersionInfo(1, 0, 0, 0) + + def master(self): + return "FNIS Integration Tool" + + def settings(self): + return [] + + def displayName(self): + return self.tr("FNIS/Reset FNIS Settings") + + def tooltip(self): + return self.description() + + def icon(self): + fnisPath = self.__organizer.pluginSetting(self.__mainToolName(), "fnis-path") + if os.path.exists(fnisPath): + # We can't directly grab the icon from an executable, but this seems like the simplest alternative. + fin = QFileInfo(fnisPath) + model = QFileSystemModel() + model.setRootPath(fin.path()) + return model.fileIcon(model.index(fin.filePath())) + else: + # Fall back to where the user might have put an icon manually. + return QIcon("plugins/FNIS.ico") + + def setParentWidget(self, widget): + self.__parentWidget = widget + + def display(self): + result = QMessageBox.question(self.__parentWidget, self.tr("Reset settings?"), self.tr("Would you like to reset the options that pop up when you first ran \"{}\"?").format(self.__mainToolName())) + if result == QMessageBox.StandardButton.Yes: + self.__organizer.setPluginSetting(self.__mainToolName(), "initialised", False) + + def tr(self, str): + return QCoreApplication.translate("FNISToolReset", str) + + @staticmethod + def __mainToolName(): + return FNISTool().name() + +def createPlugin(): + return FNISToolReset() diff --git a/libs/fnistool/src/FNISTool_en.ts b/libs/fnistool/src/FNISTool_en.ts new file mode 100644 index 0000000..9ccdbee --- /dev/null +++ b/libs/fnistool/src/FNISTool_en.ts @@ -0,0 +1,295 @@ +<?xml version="1.0" encoding="utf-8"?> +<!DOCTYPE TS> +<TS version="2.0"> + <context> + <name>FNISPatches</name> + <message> + <location filename="FNISPatches.py" line="73" /> + <source>FNISPatches plugin requires a Python 3 interpreter, but is running on a Python 2 interpreter.</source> + <translation type="unfinished" /> + </message> + <message> + <location filename="FNISPatches.py" line="74" /> + <source>Incompatible Python version.</source> + <translation type="unfinished" /> + </message> + <message> + <location filename="FNISPatches.py" line="74" /> + <source>This version of the FNIS Patches plugin requires a Python 3 interpreter, but Mod Organizer has provided a Python 2 interpreter. You should check for an updated version, including in the Mod Organizer 2 Development Discord Server.</source> + <translation type="unfinished" /> + </message> + <message> + <location filename="FNISPatches.py" line="82" /> + <source>FNIS Patches Tool</source> + <translation type="unfinished" /> + </message> + <message> + <location filename="FNISPatches.py" line="103" /> + <location filename="FNISPatches.py" line="88" /> + <source>Configures the patches which FNIS applies to the game.</source> + <translation type="unfinished" /> + </message> + <message> + <location filename="FNISPatches.py" line="100" /> + <source>FNIS/Configure FNIS Patches</source> + <translation type="unfinished" /> + </message> + <message> + <location filename="FNISPatches.py" line="130" /> + <source>Select Patches</source> + <translation type="unfinished" /> + </message> + <message> + <location filename="FNISPatches.py" line="132" /> + <source>Note: Some patches may be automatically enabled or disabled by Fore's New Idles in Skyrim, so don't be surprised if its list differs from this one.</source> + <translation type="unfinished" /> + </message> + <message> + <location filename="FNISPatches.py" line="183" /> + <source>Unable to find FNIS</source> + <translation type="unfinished" /> + </message> + <message> + <location filename="FNISPatches.py" line="183" /> + <source>Fore's New Idles in Skyrim can't be found using the location saved in Mod Organizer's settings. Please run the main FNIS integration tool before using this one.</source> + <translation type="unfinished" /> + </message> + </context> + <context> + <name>FNISTool</name> + <message> + <location filename="FNISTool.py" line="47" /> + <source>FNISTool plugin requires a Python 3 interpreter, but is running on a Python 2 interpreter.</source> + <translation type="unfinished" /> + </message> + <message> + <location filename="FNISTool.py" line="48" /> + <source>Incompatible Python version.</source> + <translation type="unfinished" /> + </message> + <message> + <location filename="FNISTool.py" line="48" /> + <source>This version of the FNIS Integration plugin requires a Python 3 interpreter, but Mod Organizer has provided a Python 2 interpreter. You should check for an updated version, including in the Mod Organizer 2 Development Discord Server.</source> + <translation type="unfinished" /> + </message> + <message> + <location filename="FNISTool.py" line="56" /> + <source>FNIS Integration Tool</source> + <translation type="unfinished" /> + </message> + <message> + <location filename="FNISTool.py" line="90" /> + <location filename="FNISTool.py" line="62" /> + <source>Runs GenerateFNISforUsers.exe so the game can load custom animations.</source> + <translation type="unfinished" /> + </message> + <message> + <location filename="FNISTool.py" line="78" /> + <source>Path to GenerateFNISforUsers.exe</source> + <translation type="unfinished" /> + </message> + <message> + <location filename="FNISTool.py" line="79" /> + <source>Whether or not to direct the FNIS output to a mod folder.</source> + <translation type="unfinished" /> + </message> + <message> + <location filename="FNISTool.py" line="80" /> + <source>When output-to-mod is enabled, the path to the mod to use.</source> + <translation type="unfinished" /> + </message> + <message> + <location filename="FNISTool.py" line="81" /> + <source>Settings have been initialised. Set to False to reinitialise them.</source> + <translation type="unfinished" /> + </message> + <message> + <location filename="FNISTool.py" line="82" /> + <source>Whether or not to direct any new FNIS logs to a mod folder.</source> + <translation type="unfinished" /> + </message> + <message> + <location filename="FNISTool.py" line="83" /> + <source>When output-logs-to-mod is enabled, the path to the mod to use.</source> + <translation type="unfinished" /> + </message> + <message> + <location filename="FNISTool.py" line="87" /> + <source>FNIS/Run FNIS</source> + <translation type="unfinished" /> + </message> + <message> + <location filename="FNISTool.py" line="139" /> + <location filename="FNISTool.py" line="123" /> + <source>Output preference not set</source> + <translation type="unfinished" /> + </message> + <message> + <location filename="FNISTool.py" line="139" /> + <location filename="FNISTool.py" line="123" /> + <source>Whether or not to output to a mod was not specified. The tool will now exit.</source> + <translation type="unfinished" /> + </message> + <message> + <location filename="FNISTool.py" line="146" /> + <location filename="FNISTool.py" line="131" /> + <source>Output mod not set</source> + <translation type="unfinished" /> + </message> + <message> + <location filename="FNISTool.py" line="146" /> + <location filename="FNISTool.py" line="131" /> + <source>The mod to output to was not specified. The tool will now exit.</source> + <translation type="unfinished" /> + </message> + <message> + <location filename="FNISTool.py" line="152" /> + <source>FNIS path not specified</source> + <translation type="unfinished" /> + </message> + <message> + <location filename="FNISTool.py" line="152" /> + <source>The path to GenerateFNISforUsers.exe wasn't specified. The tool will now exit.</source> + <translation type="unfinished" /> + </message> + <message> + <location filename="FNISTool.py" line="188" /> + <source>Output to a mod?</source> + <translation type="unfinished" /> + </message> + <message> + <location filename="FNISTool.py" line="188" /> + <source>Fore's New Idles in Skyrim can output either to Mod Organizer's VFS (potentially overwriting files from multiple mods) or to a separate mod. Would you like FNIS to output to a separate mod? This setting can be updated in the Plugins tab of the Mod Organizer Settings menu.</source> + <translation type="unfinished" /> + </message> + <message> + <location filename="FNISTool.py" line="204" /> + <source>Output logs to a mod?</source> + <translation type="unfinished" /> + </message> + <message> + <location filename="FNISTool.py" line="204" /> + <source>Any new logs generated when running FNIS will end up in Mod Organizer's overwrite folder. Would you like these logs to be output to a separate mod? This setting can be updated in the Plugins tab of the Mod Organizer Settings menu.</source> + <translation type="unfinished" /> + </message> + <message> + <location filename="FNISTool.py" line="258" /> + <location filename="FNISTool.py" line="224" /> + <location filename="FNISTool.py" line="222" /> + <source>Choose an output mod</source> + <translation type="unfinished" /> + </message> + <message> + <location filename="FNISTool.py" line="222" /> + <source>Please choose an output mod for Fore's New Idles in Skyrim. This must be a directory in Mod Organizer's mods directory, and you can create one if you do not have one already. This mod will not be available to the VFS when FNIS is run, so do not choose a mod you use for anything else. This setting can be updated in the Plugins tab of the Mod Organizer Settings menu.</source> + <translation type="unfinished" /> + </message> + <message> + <location filename="FNISTool.py" line="267" /> + <location filename="FNISTool.py" line="231" /> + <source>Not a mod...</source> + <translation type="unfinished" /> + </message> + <message> + <location filename="FNISTool.py" line="267" /> + <location filename="FNISTool.py" line="231" /> + <source>The selected directory is not a Mod Organizer managed mod. Please choose a directory within the mods directory.</source> + <translation type="unfinished" /> + </message> + <message> + <location filename="FNISTool.py" line="279" /> + <location filename="FNISTool.py" line="239" /> + <source>Mod not empty</source> + <translation type="unfinished" /> + </message> + <message> + <location filename="FNISTool.py" line="279" /> + <location filename="FNISTool.py" line="239" /> + <source>The selected mod already contains files. Are you sure want to use it as the output mod?</source> + <translation type="unfinished" /> + </message> + <message> + <location filename="FNISTool.py" line="258" /> + <source>Please choose an output mod for logs for Fore's New Idles in Skyrim. This must be a directory in Mod Organizer's mods directory, must not be the same as the FNIS output mod, and you can create one if you do not have one already. This setting can be updated in the Plugins tab of the Mod Organizer Settings menu.</source> + <translation type="unfinished" /> + </message> + <message> + <location filename="FNISTool.py" line="260" /> + <source>Choose a log output mod</source> + <translation type="unfinished" /> + </message> + <message> + <location filename="FNISTool.py" line="271" /> + <source>Same as FNIS output</source> + <translation type="unfinished" /> + </message> + <message> + <location filename="FNISTool.py" line="271" /> + <source>The selected mod is the same as the FNIS output mod. Please choose a different mod.</source> + <translation type="unfinished" /> + </message> + <message> + <location filename="FNISTool.py" line="300" /> + <source>Find FNIS</source> + <translation type="unfinished" /> + </message> + <message> + <location filename="FNISTool.py" line="300" /> + <source>Fore's New Idles in Skyrim can't be found using the location saved in Mod Organizer's settings. Please find GenerateFNISforUsers.exe in the file picker. FNIS must be visible within the VFS, so choose an installation either within the game's data directory or within a mod folder. This setting can be updated in the Plugins tab of the Mod Organizer Settings menu.</source> + <translation type="unfinished" /> + </message> + <message> + <location filename="FNISTool.py" line="302" /> + <source>Locate GenerateFNISforUsers.exe</source> + <translation type="unfinished" /> + </message> + <message> + <location filename="FNISTool.py" line="314" /> + <source>Not a compatible location...</source> + <translation type="unfinished" /> + </message> + <message> + <location filename="FNISTool.py" line="314" /> + <source>Fore's New Idles in Skyrim only works when within the VFS, so must be installed to the game's data directory or within a mod folder. Please select a different FNIS installation.</source> + <translation type="unfinished" /> + </message> + <message> + <location filename="FNISTool.py" line="324" /> + <source>FNIS mod deactivated</source> + <translation type="unfinished" /> + </message> + <message> + <location filename="FNISTool.py" line="324" /> + <source>Fore's New Idles in Skyrim is installed to an inactive mod. Press OK to activate it or Cancel to quit the tool</source> + <translation type="unfinished" /> + </message> + </context> + <context> + <name>FNISToolReset</name> + <message> + <location filename="FNISToolReset.py" line="25" /> + <source>FNIS Integration Tool Reset</source> + <translation type="unfinished" /> + </message> + <message> + <location filename="FNISToolReset.py" line="31" /> + <source>Provides an easier way to reset the FNIS integration tool settings when needed.</source> + <translation type="unfinished" /> + </message> + <message> + <location filename="FNISToolReset.py" line="43" /> + <source>FNIS/Reset FNIS Settings</source> + <translation type="unfinished" /> + </message> + <message> + <location filename="FNISToolReset.py" line="64" /> + <source>Reset settings?</source> + <translation type="unfinished" /> + </message> + <message> + <location filename="FNISToolReset.py" line="64" /> + <source>Would you like to reset the options that pop up when you first ran "{}"?</source> + <translation type="unfinished" /> + </message> + </context> +</TS> |
