-
Notifications
You must be signed in to change notification settings - Fork 7
/
OpenMWExport.py
239 lines (199 loc) · 13.3 KB
/
OpenMWExport.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
# 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 (ideally along with the OpenMW icon). You will then find an 'Export to OpenMW' option under the tools menu.
# If someone wanted to, they could expand upon this by registering callbacks for mod and plugin state changes so that you didn't manually have to click the button any more to make this work.
# Also, if Mod Organizer ever re-implements archive handling, then it wouldn't be too hard to make this also copy the list of these to fallback-archive= lines.
from pathlib import Path
import sys
from PyQt6.QtCore import QCoreApplication, QStandardPaths, QUrl, qCritical
from PyQt6.QtGui import QDesktopServices, QIcon
from PyQt6.QtWidgets import QCheckBox, QFileDialog, QMessageBox
if "mobase" not in sys.modules:
import mobase
class OpenMWExportPlugin(mobase.IPluginTool, mobase.IPluginDiagnose):
__NAME = "OpenMW Exporter"
__CONFIG_PATH = "config path"
__ALWAYS_USE_THIS_CONFIG_PATH = "always use this config path"
__SHOW_FOR_EXPERIMENTAL_GAMES = "show for experimental games"
__PARTIALLY_SUPPORTED_GAMES = [
"Morrowind",
"Oblivion",
"Fallout 3",
"Fallout New Vegas",
"Skyrim"
]
def __init__(self):
super(OpenMWExportPlugin, self).__init__()
# I think this shouldn't be necessary and a base class should be calling super
mobase.IPluginDiagnose.__init__(self)
self.__organizer : mobase.IOrganizer
self.__nexusBridge : mobase.IModRepositoryBridge
def init(self, organizer):
self.__organizer = organizer
self.__nexusBridge = self.__organizer.createNexusBridge()
self.__nexusBridge.descriptionAvailable.connect(self.__onDescriptionReceived)
self.__organizer.onUserInterfaceInitialized(self.__checkForUpdate)
return True
def name(self):
return OpenMWExportPlugin.__NAME
def author(self):
return "AnyOldName3"
def description(self):
return OpenMWExportPlugin.tr("Transfers mod list (left pane) to data fields in OpenMW.cfg and plugin list (right pane, plugins tab) to content fields in OpenMW.cfg. This allows you to run OpenMW with the current profile's setup from outside of Mod Organizer")
def version(self):
return mobase.VersionInfo(4, 1, 0, mobase.ReleaseType.FINAL)
def requirements(self):
return [
mobase.PluginRequirementFactory.gameDependency(
OpenMWExportPlugin.__PARTIALLY_SUPPORTED_GAMES
if self.__organizer.pluginSetting(OpenMWExportPlugin.__NAME, OpenMWExportPlugin.__SHOW_FOR_EXPERIMENTAL_GAMES)
else "Morrowind"
)
]
def isActive(self):
if self.__organizer.pluginSetting(OpenMWExportPlugin.__NAME, OpenMWExportPlugin.__SHOW_FOR_EXPERIMENTAL_GAMES):
return self.__organizer.managedGame().gameName() in OpenMWExportPlugin.__PARTIALLY_SUPPORTED_GAMES
else:
return self.__organizer.managedGame().gameName() == "Morrowind"
def settings(self):
return [
mobase.PluginSetting(OpenMWExportPlugin.__CONFIG_PATH, OpenMWExportPlugin.tr("The most-recently-used openmw.cfg path."), ""),
mobase.PluginSetting(OpenMWExportPlugin.__ALWAYS_USE_THIS_CONFIG_PATH, OpenMWExportPlugin.tr("Whether to always use the saved openmw.cfg without asking each time."), False),
mobase.PluginSetting(OpenMWExportPlugin.__SHOW_FOR_EXPERIMENTAL_GAMES, OpenMWExportPlugin.tr("Whether to show the Export to OpenMW option for games with only limited experimental support."), False)
]
def displayName(self):
return OpenMWExportPlugin.tr("Export to OpenMW")
def tooltip(self):
return OpenMWExportPlugin.tr("Exports the current mod list and plugin load order to OpenMW.cfg")
def icon(self):
return QIcon("plugins/openmw.ico")
def display(self):
game = self.__organizer.managedGame()
if self.__organizer.pluginSetting(OpenMWExportPlugin.__NAME, OpenMWExportPlugin.__SHOW_FOR_EXPERIMENTAL_GAMES):
if game.gameName() != "Morrowind":
QMessageBox.warning(self._parentWidget(), OpenMWExportPlugin.tr("Experimental game"), OpenMWExportPlugin.tr("(At least when this plugin is being written) OpenMW only fully supports game data designed for the Morrowind engine. The game being managed is not Morrowind, so do not expect the game to be fully playable. If you think you know better than this message, update this plugin."))
# Give the user the opportunity to abort
confirmationButton = QMessageBox.question(self._parentWidget(), OpenMWExportPlugin.tr("Before starting export..."), OpenMWExportPlugin.tr("Before starting the export to OpenMW, please ensure you've backed up anything in OpenMW.cfg which you do not want to risk losing forever."), QMessageBox.StandardButton(QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Cancel))
if confirmationButton != QMessageBox.StandardButton.Ok:
return
# Get the path to the OpenMW.cfg file
configPath = self.__getOpenMWConfigPath()
if not (configPath.exists() and configPath.is_file()):
QMessageBox.critical(self._parentWidget(), OpenMWExportPlugin.tr("Config file not specified"), OpenMWExportPlugin.tr("No config file was specified"))
return
# Clear out the existing data= and content= lines from openmw.cfg
self.__clearOpenMWConfig(configPath)
with configPath.open("a", encoding="utf-8") as openmwcfg:
# write out data directories
openmwcfg.write(self.__processDataPath(game.dataDirectory().absolutePath()))
for mod in self.__organizer.modList().allModsByProfilePriority():
self.__processMod(openmwcfg, mod)
self.__processMod(openmwcfg, "Overwrite")
# write out content (plugin) files
# order content files by load order
loadOrder = {}
for plugin in self.__organizer.pluginList().pluginNames():
loadIndex = self.__organizer.pluginList().loadOrder(plugin)
if loadIndex >= 0:
loadOrder[loadIndex] = plugin
# actually write out the list
for pluginIndex in range(len(loadOrder)):
openmwcfg.write("content=" + loadOrder[pluginIndex] + "\n")
QMessageBox.information(self._parentWidget(), OpenMWExportPlugin.tr("OpenMW Export Complete"), OpenMWExportPlugin.tr("The export to OpenMW completed successfully. The current setup was saved to {0}").format(configPath))
def activeProblems(self):
return [0] if self.__organizer.profile().invalidationActive()[0] else []
def shortDescription(self, key):
return OpenMWExportPlugin.tr("Automatic Archive Invalidation is enabled.")
def fullDescription(self, key):
return OpenMWExportPlugin.tr("Automatic Archive Invalidation is enabled in the current profile. Automatic Archive Invalidation attempts to work around a problem in the later Bethesda Games Studios games. It is unnecessary for Morrowind or OpenMW, and the archive used is known to crash the game. You should disable it in the Manage Profiles window.")
def hasGuidedFix(self, key):
return False
def startGuidedFix(self, key):
pass
@staticmethod
def tr(str):
return QCoreApplication.translate("OpenMWExportPlugin", str)
def __processMod(self, configFile, modName):
state = self.__organizer.modList().state(modName)
if (state & mobase.ModState.ACTIVE) != 0 or modName == "Overwrite":
path = self.__organizer.modList().getMod(modName).absolutePath()
configLine = self.__processDataPath(path)
configFile.write(configLine)
def __processDataPath(self, dataPath):
# boost::filesystem::path uses a weird format in order to round-trip being constructed from a stream correctly, even when quotation characters are in the path.
# Modern OpenMW versions don't require this unless the path begins or ends in whitespace, or begins with a double quote, but do it anyway for backwards compatibility.
processedPath = "data=\""
for character in dataPath:
if character == '&' or character == '"':
processedPath += "&"
processedPath += character
processedPath += "\"\n"
return processedPath
def __clearOpenMWConfig(self, configPath):
import tempfile
import os
import shutil
# copy the lines we want to keep to a temp file
tempFilePath = None
with tempfile.NamedTemporaryFile(mode="w", delete = False, encoding="utf-8") as f:
tempFilePath = f.name
lastLine = ""
with configPath.open("r", encoding="utf-8-sig") as openmwcfg:
for line in openmwcfg:
if not line.startswith("data=") and not line.startswith("content="):
f.write(line)
lastLine = line
# ensure the last line ended with a line break
if not lastLine.endswith("\n"):
f.write("\n")
# we can't move to Path.replace due to https://bugs.python.org/issue29805
os.remove(configPath)
shutil.move(tempFilePath, configPath)
def __getOpenMWConfigPath(self):
savedPath = Path(self.__organizer.pluginSetting(OpenMWExportPlugin.__NAME, OpenMWExportPlugin.__CONFIG_PATH))
alwaysUse = self.__organizer.pluginSetting(OpenMWExportPlugin.__NAME, OpenMWExportPlugin.__ALWAYS_USE_THIS_CONFIG_PATH)
if alwaysUse:
if savedPath.exists() and savedPath.is_file():
return savedPath
else:
#: {0} is the key for the setting that's being reset.
QMessageBox.information(self._parentWidget(), OpenMWExportPlugin.tr("Saved openmw.cfg path unavailable"), OpenMWExportPlugin.tr("Saved openmw.cfg path unavailable. Resetting {0}").format(OpenMWExportPlugin.__ALWAYS_USE_THIS_CONFIG_PATH))
self.__organizer.setPluginSetting(OpenMWExportPlugin.__NAME, OpenMWExportPlugin.__ALWAYS_USE_THIS_CONFIG_PATH, False)
defaultPath = Path(QStandardPaths.locate(QStandardPaths.StandardLocation.DocumentsLocation, str(Path("My Games", "OpenMW", "openmw.cfg"))))
messageBox = QMessageBox(self._parentWidget())
messageBox.setText(OpenMWExportPlugin.tr("Choose openmw.cfg path"))
#: <div style=\"white-space:pre\">{0}</div> is the saved path.
#: <div style=\"white-space:pre\">{1}</div> is the default path.
#: <br> is a line break between them.
messageBox.setInformativeText(OpenMWExportPlugin.tr("Saved:<div style=\"white-space:pre\">{0}</div><br>Default:<div style=\"white-space:pre\">{1}</div>").format(savedPath, defaultPath))
savedButton = messageBox.addButton(OpenMWExportPlugin.tr("Saved"), QMessageBox.ButtonRole.AcceptRole)
savedButton.setEnabled(savedPath.exists() and savedPath.is_file())
defaultButton = messageBox.addButton(OpenMWExportPlugin.tr("Default"), QMessageBox.ButtonRole.AcceptRole)
defaultButton.setEnabled(defaultPath.exists() and defaultPath.is_file())
browseButton = messageBox.addButton(OpenMWExportPlugin.tr("Browse"), QMessageBox.ButtonRole.AcceptRole)
rememberCheckBox = QCheckBox(OpenMWExportPlugin.tr("Always use this path"))
messageBox.setCheckBox(rememberCheckBox)
messageBox.exec()
clickedButton = messageBox.clickedButton()
if clickedButton == savedButton:
path = savedPath
elif clickedButton == defaultButton:
path = defaultPath
if ((not savedPath.exists() or not savedPath.is_file()) and savedPath != defaultPath) or rememberCheckBox.isChecked():
self.__organizer.setPluginSetting(OpenMWExportPlugin.__NAME, OpenMWExportPlugin.__CONFIG_PATH, str(defaultPath))
else:
path = Path(QFileDialog.getOpenFileName(self._parentWidget(), OpenMWExportPlugin.tr("Locate OpenMW Config File"), ".", "OpenMW Config File (openmw.cfg)")[0])
if savedPath != path:
self.__organizer.setPluginSetting(OpenMWExportPlugin.__NAME, OpenMWExportPlugin.__CONFIG_PATH, str(path))
if rememberCheckBox.isChecked():
self.__organizer.setPluginSetting(OpenMWExportPlugin.__NAME, OpenMWExportPlugin.__ALWAYS_USE_THIS_CONFIG_PATH, True)
return path
def __checkForUpdate(self, mainWindow):
self.__nexusBridge.requestDescription("Morrowind", 45642, None)
def __onDescriptionReceived(self, gameName, modID, userData, resultData):
version = mobase.VersionInfo(resultData["version"])
if self.version() < version:
response = QMessageBox.question(self._parentWidget(), OpenMWExportPlugin.tr("Plugin update available"), OpenMWExportPlugin.tr("{0} can be updated from version {1} to {2}. Do you want to open the download page in your browser?").format(self.displayName(), self.version(), version))
if response == QMessageBox.StandardButton.Yes:
QDesktopServices.openUrl(QUrl("https://www.nexusmods.com/morrowind/mods/45642"))
def createPlugin():
return OpenMWExportPlugin()