aboutsummaryrefslogtreecommitdiff
path: root/libs/game_bethesda/src/games/morrowind
diff options
context:
space:
mode:
authorSulfurNitride <SulfurNitride@users.noreply.github.com>2026-02-11 02:37:39 -0600
committerSulfurNitride <SulfurNitride@users.noreply.github.com>2026-02-11 02:37:39 -0600
commit7ee008e150bc5bcf76082d726f719ee0fdfda982 (patch)
tree27fb39be241fdb5ac2734c574de678977d1856d0 /libs/game_bethesda/src/games/morrowind
Fluorine Manager: full Linux port of Mod Organizer 2
Complete native Linux port with FUSE-based virtual filesystem, Proton/umu-run integration, and Flatpak packaging. Key features: - FUSE VFS replacing Windows USVFS (in-process + standalone helper for Flatpak) - Proton/GE-Proton/umu-run launcher with env var forwarding - Flatpak support (sandbox-aware VFS, NXM handler, umu-run) - Wine prefix management UI - Case-insensitive path resolution for Linux filesystems - QSettings-safe INI handling (avoids Bethesda INI corruption) - Portable instance support with auto-generated launcher scripts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'libs/game_bethesda/src/games/morrowind')
-rw-r--r--libs/game_bethesda/src/games/morrowind/CMakeLists.txt27
-rw-r--r--libs/game_bethesda/src/games/morrowind/game_morrowind_en.ts69
-rw-r--r--libs/game_bethesda/src/games/morrowind/gamemorrowind.cpp216
-rw-r--r--libs/game_bethesda/src/games/morrowind/gamemorrowind.h62
-rw-r--r--libs/game_bethesda/src/games/morrowind/gamemorrowind.json1
-rw-r--r--libs/game_bethesda/src/games/morrowind/morrowind.qrc5
-rw-r--r--libs/game_bethesda/src/games/morrowind/morrowindbsainvalidation.cpp16
-rw-r--r--libs/game_bethesda/src/games/morrowind/morrowindbsainvalidation.h20
-rw-r--r--libs/game_bethesda/src/games/morrowind/morrowinddataarchives.cpp71
-rw-r--r--libs/game_bethesda/src/games/morrowind/morrowinddataarchives.h28
-rw-r--r--libs/game_bethesda/src/games/morrowind/morrowindgameplugins.cpp197
-rw-r--r--libs/game_bethesda/src/games/morrowind/morrowindgameplugins.h28
-rw-r--r--libs/game_bethesda/src/games/morrowind/morrowindlocalsavegames.cpp56
-rw-r--r--libs/game_bethesda/src/games/morrowind/morrowindlocalsavegames.h41
-rw-r--r--libs/game_bethesda/src/games/morrowind/morrowindmoddatachecker.h26
-rw-r--r--libs/game_bethesda/src/games/morrowind/morrowindmoddatacontent.h24
-rw-r--r--libs/game_bethesda/src/games/morrowind/morrowindsavegame.cpp183
-rw-r--r--libs/game_bethesda/src/games/morrowind/morrowindsavegame.h57
-rw-r--r--libs/game_bethesda/src/games/morrowind/morrowindsavegameinfo.cpp18
-rw-r--r--libs/game_bethesda/src/games/morrowind/morrowindsavegameinfo.h22
-rw-r--r--libs/game_bethesda/src/games/morrowind/morrowindsavegameinfowidget.cpp123
-rw-r--r--libs/game_bethesda/src/games/morrowind/morrowindsavegameinfowidget.h31
-rw-r--r--libs/game_bethesda/src/games/morrowind/morrowindsavegameinfowidget.ui248
-rw-r--r--libs/game_bethesda/src/games/morrowind/splash.pngbin0 -> 83336 bytes
24 files changed, 1569 insertions, 0 deletions
diff --git a/libs/game_bethesda/src/games/morrowind/CMakeLists.txt b/libs/game_bethesda/src/games/morrowind/CMakeLists.txt
new file mode 100644
index 0000000..5e92f4b
--- /dev/null
+++ b/libs/game_bethesda/src/games/morrowind/CMakeLists.txt
@@ -0,0 +1,27 @@
+cmake_minimum_required(VERSION 3.16)
+
+add_library(game_morrowind SHARED
+ gamemorrowind.cpp
+ gamemorrowind.h
+ morrowindbsainvalidation.cpp
+ morrowindbsainvalidation.h
+ morrowinddataarchives.cpp
+ morrowinddataarchives.h
+ morrowindgameplugins.cpp
+ morrowindgameplugins.h
+ morrowindlocalsavegames.cpp
+ morrowindlocalsavegames.h
+ morrowindmoddatachecker.h
+ morrowindmoddatacontent.h
+ morrowindsavegame.cpp
+ morrowindsavegame.h
+ morrowindsavegameinfo.cpp
+ morrowindsavegameinfo.h
+ morrowindsavegameinfowidget.cpp
+ morrowindsavegameinfowidget.h
+ morrowindsavegameinfowidget.ui
+ )
+mo2_configure_plugin(game_morrowind NO_SOURCES WARNINGS 4)
+mo2_default_source_group()
+target_link_libraries(game_morrowind PRIVATE game_gamebryo)
+mo2_install_plugin(game_morrowind)
diff --git a/libs/game_bethesda/src/games/morrowind/game_morrowind_en.ts b/libs/game_bethesda/src/games/morrowind/game_morrowind_en.ts
new file mode 100644
index 0000000..c6961c4
--- /dev/null
+++ b/libs/game_bethesda/src/games/morrowind/game_morrowind_en.ts
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="en_US">
+<context>
+ <name>GameMorrowind</name>
+ <message>
+ <location filename="gamemorrowind.cpp" line="106"/>
+ <source>Morrowind Support Plugin</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="gamemorrowind.cpp" line="116"/>
+ <source>Adds support for the game Morrowind.
+Splash by %1</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>MorrowindSaveGameInfoWidget</name>
+ <message>
+ <location filename="morrowindsavegameinfowidget.ui" line="53"/>
+ <source>Save #</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="morrowindsavegameinfowidget.ui" line="78"/>
+ <source>Character</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="morrowindsavegameinfowidget.ui" line="103"/>
+ <source>Level</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="morrowindsavegameinfowidget.ui" line="128"/>
+ <source>Health</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="morrowindsavegameinfowidget.ui" line="153"/>
+ <source>Location</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="morrowindsavegameinfowidget.ui" line="178"/>
+ <source>Date</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="morrowindsavegameinfowidget.cpp" line="85"/>
+ <source>Missing ESPs</source>
+ <translation type="unfinished"></translation>
+ </message>
+ <message>
+ <location filename="morrowindsavegameinfowidget.cpp" line="118"/>
+ <source>None</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+<context>
+ <name>QObject</name>
+ <message>
+ <location filename="morrowindgameplugins.cpp" line="120"/>
+ <source>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.</source>
+ <translation type="unfinished"></translation>
+ </message>
+</context>
+</TS>
diff --git a/libs/game_bethesda/src/games/morrowind/gamemorrowind.cpp b/libs/game_bethesda/src/games/morrowind/gamemorrowind.cpp
new file mode 100644
index 0000000..faa3a3b
--- /dev/null
+++ b/libs/game_bethesda/src/games/morrowind/gamemorrowind.cpp
@@ -0,0 +1,216 @@
+#include "gamemorrowind.h"
+
+#include "morrowindbsainvalidation.h"
+#include "morrowinddataarchives.h"
+#include "morrowindgameplugins.h"
+#include "morrowindlocalsavegames.h"
+#include "morrowindmoddatachecker.h"
+#include "morrowindmoddatacontent.h"
+#include "morrowindsavegame.h"
+#include "morrowindsavegameinfo.h"
+
+#include "executableinfo.h"
+#include "pluginsetting.h"
+#include "steamutility.h"
+
+#include <gamebryounmanagedmods.h>
+
+#include <QCoreApplication>
+#include <QDebug>
+#include <QFileInfo>
+
+#include <QtDebug>
+
+#include <exception>
+#include <memory>
+#include <stdexcept>
+#include <string>
+#include <vector>
+
+using namespace MOBase;
+
+GameMorrowind::GameMorrowind() {}
+
+bool GameMorrowind::init(IOrganizer* moInfo)
+{
+ if (!GameGamebryo::init(moInfo)) {
+ return false;
+ }
+
+ auto dataArchives = std::make_shared<MorrowindDataArchives>(this);
+ registerFeature(dataArchives);
+ registerFeature(std::make_shared<MorrowindBSAInvalidation>(dataArchives.get(), this));
+ registerFeature(std::make_shared<MorrowindSaveGameInfo>(this));
+ registerFeature(std::make_shared<MorrowindLocalSavegames>(this));
+ registerFeature(std::make_shared<MorrowindModDataChecker>(this));
+ registerFeature(
+ std::make_shared<MorrowindModDataContent>(m_Organizer->gameFeatures()));
+ registerFeature(std::make_shared<MorrowindGamePlugins>(moInfo));
+ registerFeature(std::make_shared<GamebryoUnmangedMods>(this));
+
+ return true;
+}
+
+QString GameMorrowind::gameName() const
+{
+ return "Morrowind";
+}
+
+QString GameMorrowind::getLauncherName() const
+{
+ return "Morrowind Launcher.exe";
+}
+
+QDir GameMorrowind::dataDirectory() const
+{
+ return gameDirectory().absoluteFilePath("Data Files");
+}
+
+QDir GameMorrowind::savesDirectory() const
+{
+ return QDir(gameDirectory().absoluteFilePath("Saves"));
+}
+
+QDir GameMorrowind::documentsDirectory() const
+{
+ return gameDirectory();
+}
+
+QList<ExecutableInfo> GameMorrowind::executables() const
+{
+ return QList<ExecutableInfo>()
+ << ExecutableInfo("MWSE (Launcher Method)",
+ findInGameFolder("MWSE Launcher.exe"))
+ << ExecutableInfo("Morrowind", findInGameFolder(binaryName()))
+ << ExecutableInfo("Morrowind Launcher", findInGameFolder(getLauncherName()))
+ << ExecutableInfo("MGE XE", findInGameFolder("MGEXEgui.exe"))
+ << ExecutableInfo("LOOT", QFileInfo(getLootPath()))
+ .withArgument("--game=\"Morrowind\"");
+}
+
+QList<ExecutableForcedLoadSetting> GameMorrowind::executableForcedLoads() const
+{
+ return QList<ExecutableForcedLoadSetting>();
+}
+
+QString GameMorrowind::name() const
+{
+ return "Morrowind Support Plugin";
+}
+
+QString GameMorrowind::localizedName() const
+{
+ return tr("Morrowind Support Plugin");
+}
+
+QString GameMorrowind::author() const
+{
+ return "Schilduin & MO2 Team";
+}
+
+QString GameMorrowind::description() const
+{
+ return tr("Adds support for the game Morrowind.\n"
+ "Splash by %1")
+ .arg("AnyOldName3");
+}
+
+MOBase::VersionInfo GameMorrowind::version() const
+{
+ return VersionInfo(1, 5, 1, VersionInfo::RELEASE_FINAL);
+}
+
+QList<PluginSetting> GameMorrowind::settings() const
+{
+ return QList<PluginSetting>();
+}
+
+void GameMorrowind::initializeProfile(const QDir& path, ProfileSettings settings) const
+{
+ if (settings.testFlag(IPluginGame::MODS)) {
+ copyToProfile(localAppFolder() + "/Morrowind", path, "plugins.txt");
+ }
+
+ if (settings.testFlag(IPluginGame::CONFIGURATION)) {
+ copyToProfile(gameDirectory().absolutePath(), path, "Morrowind.ini");
+ }
+}
+
+QString GameMorrowind::savegameExtension() const
+{
+ return "ess";
+}
+
+QString GameMorrowind::savegameSEExtension() const
+{
+ return "mwse";
+}
+
+std::shared_ptr<const GamebryoSaveGame>
+GameMorrowind::makeSaveGame(QString filePath) const
+{
+ return std::make_shared<const MorrowindSaveGame>(filePath, this);
+}
+
+QString GameMorrowind::steamAPPId() const
+{
+ return "22320";
+}
+
+QStringList GameMorrowind::primaryPlugins() const
+{
+ return {"morrowind.esm"};
+}
+
+QString GameMorrowind::binaryName() const
+{
+ return "Morrowind.exe";
+}
+
+QString GameMorrowind::gameShortName() const
+{
+ return "Morrowind";
+}
+
+QString GameMorrowind::gameNexusName() const
+{
+ return "Morrowind";
+}
+
+QStringList GameMorrowind::iniFiles() const
+{
+ return {"morrowind.ini"};
+}
+
+QStringList GameMorrowind::DLCPlugins() const
+{
+ return {"Tribunal.esm", "Bloodmoon.esm"};
+}
+
+MOBase::IPluginGame::SortMechanism GameMorrowind::sortMechanism() const
+{
+ return SortMechanism::LOOT;
+}
+
+int GameMorrowind::nexusModOrganizerID() const
+{
+ return 1334;
+}
+
+int GameMorrowind::nexusGameID() const
+{
+ return 100;
+}
+
+QString GameMorrowind::identifyGamePath() const
+{
+ QString path = GameGamebryo::identifyGamePath();
+ if (path.isEmpty()) {
+ // Resolve by Steam app id first (handles non-standard install directory names).
+ path = parseSteamLocation(steamAPPId(), "");
+ }
+ if (path.isEmpty()) {
+ path = MOBase::findSteamGame("Morrowind", "Data Files/Morrowind.esm");
+ }
+ return path;
+}
diff --git a/libs/game_bethesda/src/games/morrowind/gamemorrowind.h b/libs/game_bethesda/src/games/morrowind/gamemorrowind.h
new file mode 100644
index 0000000..a6f6862
--- /dev/null
+++ b/libs/game_bethesda/src/games/morrowind/gamemorrowind.h
@@ -0,0 +1,62 @@
+#ifndef GAMEMORROWIND_H
+#define GAMEMORROWIND_H
+
+#include "gamegamebryo.h"
+
+#include <QObject>
+#include <QtGlobal>
+
+class GameMorrowind : public GameGamebryo
+{
+ Q_OBJECT
+#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)
+ Q_PLUGIN_METADATA(IID "com.schilduin.GameMorrowind" FILE "gamemorrowind.json")
+#endif
+
+ friend class MorrowindSaveGameInfo;
+ friend class MorrowindSaveGameInfoWidget;
+
+public:
+ GameMorrowind();
+
+ virtual bool init(MOBase::IOrganizer* moInfo) override;
+
+public: // IPluginGame interface
+ virtual QString gameName() const override;
+ virtual QString getLauncherName() const override;
+ virtual QDir dataDirectory() const override;
+ virtual QDir savesDirectory() const override;
+ virtual QDir documentsDirectory() const override;
+ virtual QList<MOBase::ExecutableInfo> executables() const override;
+ virtual QList<MOBase::ExecutableForcedLoadSetting>
+ executableForcedLoads() const override;
+ virtual void initializeProfile(const QDir& path,
+ ProfileSettings settings) const override;
+ virtual QString steamAPPId() const override;
+ virtual QStringList primaryPlugins() const override;
+ virtual QString binaryName() const override;
+ virtual QString gameShortName() const override;
+ virtual QString gameNexusName() const override;
+ virtual QStringList iniFiles() const override;
+ virtual QStringList DLCPlugins() const override;
+ virtual SortMechanism sortMechanism() const override;
+ virtual int nexusModOrganizerID() const override;
+ virtual int nexusGameID() const override;
+ virtual QString identifyGamePath() const;
+
+public: // IPlugin interface
+ virtual QString name() const override;
+ virtual QString localizedName() const override;
+ virtual QString author() const override;
+ virtual QString description() const override;
+ virtual MOBase::VersionInfo version() const override;
+ virtual QList<MOBase::PluginSetting> settings() const override;
+
+protected:
+ virtual QString savegameExtension() const override;
+ virtual QString savegameSEExtension() const override;
+ virtual std::shared_ptr<const GamebryoSaveGame>
+ makeSaveGame(QString filepath) const override;
+};
+
+#endif // GAMEMORROWIND_H
diff --git a/libs/game_bethesda/src/games/morrowind/gamemorrowind.json b/libs/game_bethesda/src/games/morrowind/gamemorrowind.json
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/libs/game_bethesda/src/games/morrowind/gamemorrowind.json
@@ -0,0 +1 @@
+{}
diff --git a/libs/game_bethesda/src/games/morrowind/morrowind.qrc b/libs/game_bethesda/src/games/morrowind/morrowind.qrc
new file mode 100644
index 0000000..a459b56
--- /dev/null
+++ b/libs/game_bethesda/src/games/morrowind/morrowind.qrc
@@ -0,0 +1,5 @@
+<RCC>
+ <qresource prefix="/Morrowind">
+ <file alias="splash">splash.png</file>
+ </qresource>
+</RCC>
diff --git a/libs/game_bethesda/src/games/morrowind/morrowindbsainvalidation.cpp b/libs/game_bethesda/src/games/morrowind/morrowindbsainvalidation.cpp
new file mode 100644
index 0000000..9b63d4c
--- /dev/null
+++ b/libs/game_bethesda/src/games/morrowind/morrowindbsainvalidation.cpp
@@ -0,0 +1,16 @@
+#include "morrowindbsainvalidation.h"
+
+MorrowindBSAInvalidation::MorrowindBSAInvalidation(MOBase::DataArchives* dataArchives,
+ MOBase::IPluginGame const* game)
+ : GamebryoBSAInvalidation(dataArchives, "morrowind.ini", game)
+{}
+
+QString MorrowindBSAInvalidation::invalidationBSAName() const
+{
+ return "Morrowind - Invalidation.bsa";
+}
+
+unsigned long MorrowindBSAInvalidation::bsaVersion() const
+{
+ return 0x68;
+}
diff --git a/libs/game_bethesda/src/games/morrowind/morrowindbsainvalidation.h b/libs/game_bethesda/src/games/morrowind/morrowindbsainvalidation.h
new file mode 100644
index 0000000..8395abe
--- /dev/null
+++ b/libs/game_bethesda/src/games/morrowind/morrowindbsainvalidation.h
@@ -0,0 +1,20 @@
+#ifndef MORROWINDBSAINVALIDATION_H
+#define MORROWINDBSAINVALIDATION_H
+
+#include "gamebryobsainvalidation.h"
+#include "morrowinddataarchives.h"
+
+#include <memory>
+
+class MorrowindBSAInvalidation : public GamebryoBSAInvalidation
+{
+public:
+ MorrowindBSAInvalidation(MOBase::DataArchives* dataArchives,
+ MOBase::IPluginGame const* game);
+
+private:
+ virtual QString invalidationBSAName() const override;
+ virtual unsigned long bsaVersion() const override;
+};
+
+#endif // MORROWINDBSAINVALIDATION_H
diff --git a/libs/game_bethesda/src/games/morrowind/morrowinddataarchives.cpp b/libs/game_bethesda/src/games/morrowind/morrowinddataarchives.cpp
new file mode 100644
index 0000000..57a5944
--- /dev/null
+++ b/libs/game_bethesda/src/games/morrowind/morrowinddataarchives.cpp
@@ -0,0 +1,71 @@
+#include "morrowinddataarchives.h"
+#include "registry.h"
+#include <utility.h>
+
+#include <QSettings>
+
+#include "gamegamebryo.h"
+
+QStringList MorrowindDataArchives::vanillaArchives() const
+{
+ return {"Morrowind.bsa"};
+}
+
+QStringList MorrowindDataArchives::getArchives(const QString& iniFile) const
+{
+ QStringList result;
+ QSettings settings(iniFile, QSettings::IniFormat);
+
+ QString key = "Archive ";
+ int i = 0;
+ while (true) {
+ QString value =
+ settings.value("Archives/" + key + QString::number(i), "").toString().trimmed();
+ if (value.isEmpty()) {
+ break;
+ }
+ result.append(value);
+ i++;
+ }
+
+ return result;
+}
+
+void MorrowindDataArchives::setArchives(const QString& iniFile, const QStringList& list)
+{
+ QSettings settings(iniFile, QSettings::IniFormat);
+ settings.remove("Archives");
+
+ QString key = "Archive ";
+ int writtenCount = 0;
+ foreach (const QString& value, list) {
+ if (!MOBase::WriteRegistryValue(
+ "Archives", key + QString::number(writtenCount), value, iniFile)) {
+ qWarning("failed to set archives in \"%s\"", qUtf8Printable(iniFile));
+ }
+ ++writtenCount;
+ }
+}
+
+QStringList MorrowindDataArchives::archives(const MOBase::IProfile* profile) const
+{
+ QStringList result;
+
+ QString iniFile =
+ profile->localSettingsEnabled()
+ ? QDir(profile->absolutePath()).absoluteFilePath("morrowind.ini")
+ : gameDirectory().absoluteFilePath("morrowind.ini");
+ result.append(getArchives(iniFile));
+
+ return result;
+}
+
+void MorrowindDataArchives::writeArchiveList(MOBase::IProfile* profile,
+ const QStringList& before)
+{
+ QString iniFile =
+ profile->localSettingsEnabled()
+ ? QDir(profile->absolutePath()).absoluteFilePath("morrowind.ini")
+ : gameDirectory().absoluteFilePath("morrowind.ini");
+ setArchives(iniFile, before);
+}
diff --git a/libs/game_bethesda/src/games/morrowind/morrowinddataarchives.h b/libs/game_bethesda/src/games/morrowind/morrowinddataarchives.h
new file mode 100644
index 0000000..7ee0c1d
--- /dev/null
+++ b/libs/game_bethesda/src/games/morrowind/morrowinddataarchives.h
@@ -0,0 +1,28 @@
+#ifndef MORROWINDDATAARCHIVES_H
+#define MORROWINDDATAARCHIVES_H
+
+#include <QDir>
+#include <QString>
+#include <QStringList>
+#include <gamebryodataarchives.h>
+#include <iplugingame.h>
+#include <iprofile.h>
+
+class MorrowindDataArchives : public GamebryoDataArchives
+{
+public:
+ using GamebryoDataArchives::GamebryoDataArchives;
+
+ virtual QStringList vanillaArchives() const override;
+ virtual QStringList archives(const MOBase::IProfile* profile) const override;
+
+protected:
+ QStringList getArchives(const QString& iniFile) const;
+ void setArchives(const QString& iniFile, const QStringList& list);
+
+private:
+ virtual void writeArchiveList(MOBase::IProfile* profile,
+ const QStringList& before) override;
+};
+
+#endif // MORROWINDDATAARCHIVES_H
diff --git a/libs/game_bethesda/src/games/morrowind/morrowindgameplugins.cpp b/libs/game_bethesda/src/games/morrowind/morrowindgameplugins.cpp
new file mode 100644
index 0000000..27665a4
--- /dev/null
+++ b/libs/game_bethesda/src/games/morrowind/morrowindgameplugins.cpp
@@ -0,0 +1,197 @@
+#include "morrowindgameplugins.h"
+#include "registry.h"
+#include <imodinterface.h>
+#include <iplugingame.h>
+#include <ipluginlist.h>
+#include <report.h>
+#include <scopeguard.h>
+#include <utility.h>
+
+#include <QDateTime>
+#include <QDir>
+#include <QSettings>
+#include <QString>
+#include <QStringEncoder>
+#include <QStringList>
+
+using MOBase::IOrganizer;
+using MOBase::IPluginList;
+using MOBase::reportError;
+
+MorrowindGamePlugins::MorrowindGamePlugins(IOrganizer* organizer)
+ : GamebryoGamePlugins(organizer)
+{}
+
+void MorrowindGamePlugins::writePluginLists(const IPluginList* pluginList)
+{
+ if (!m_LastRead.isValid()) {
+ // attempt to write uninitialized plugin lists
+ return;
+ }
+
+ if (organizer()->profile()->localSettingsEnabled()) {
+ writePluginList(pluginList,
+ organizer()->profile()->absolutePath() + "/Morrowind.ini");
+ } else {
+ writePluginList(pluginList,
+ organizer()->managedGame()->gameDirectory().absolutePath() +
+ "/Morrowind.ini");
+ }
+
+ writeLoadOrderList(pluginList,
+ organizer()->profile()->absolutePath() + "/loadorder.txt");
+
+ m_LastRead = QDateTime::currentDateTime();
+}
+
+void MorrowindGamePlugins::readPluginLists(MOBase::IPluginList* pluginList)
+{
+ QString loadOrderPath = organizer()->profile()->absolutePath() + "/loadorder.txt";
+
+ QString pluginsPath = organizer()->profile()->absolutePath() + "/Morrowind.ini";
+ if (!organizer()->profile()->localSettingsEnabled()) {
+ pluginsPath =
+ organizer()->managedGame()->gameDirectory().absolutePath() + "/Morrowind.ini";
+ }
+
+ bool loadOrderIsNew = !m_LastRead.isValid() || !QFileInfo(loadOrderPath).exists() ||
+ QFileInfo(loadOrderPath).lastModified() > m_LastRead;
+ bool pluginsIsNew =
+ !m_LastRead.isValid() || QFileInfo(pluginsPath).lastModified() > m_LastRead;
+
+ if (loadOrderIsNew || !pluginsIsNew) {
+ // read both files if they are both new or both older than the last read
+ QStringList loadOrder = readLoadOrderList(pluginList, loadOrderPath);
+ pluginList->setLoadOrder(loadOrder);
+ readPluginList(pluginList);
+ } else {
+ // If the plugins is new but not loadorder, we must reparse the load order from the
+ // plugin files
+ QStringList loadOrder = readPluginList(pluginList);
+ pluginList->setLoadOrder(loadOrder);
+ }
+
+ m_LastRead = QDateTime::currentDateTime();
+}
+
+void MorrowindGamePlugins::writePluginList(const MOBase::IPluginList* pluginList,
+ const QString& filePath)
+{
+ return writeList(pluginList, filePath, false);
+}
+
+void MorrowindGamePlugins::writeList(const IPluginList* pluginList,
+ const QString& filePath, bool loadOrder)
+{
+ QStringEncoder encoder = loadOrder
+ ? QStringEncoder(QStringConverter::Encoding::Utf8)
+ : QStringEncoder(QStringConverter::Encoding::System);
+
+ QSettings settings(filePath, QSettings::IniFormat);
+ settings.remove("Game Files");
+
+ bool invalidFileNames = false;
+ int writtenCount = 0;
+
+ QStringList plugins = pluginList->pluginNames();
+ std::sort(plugins.begin(), plugins.end(),
+ [pluginList](const QString& lhs, const QString& rhs) {
+ return pluginList->priority(lhs) < pluginList->priority(rhs);
+ });
+ QString key = "GameFile";
+ for (const QString& pluginName : plugins) {
+ if (loadOrder || (pluginList->state(pluginName) == IPluginList::STATE_ACTIVE)) {
+ auto result = encoder.encode(pluginName);
+ if (encoder.hasError()) {
+ invalidFileNames = true;
+ qCritical("invalid plugin name %s", qUtf8Printable(pluginName));
+ } else {
+ if (!MOBase::WriteRegistryValue(
+ "Game Files", key + QString::number(writtenCount), pluginName,
+ filePath)) {
+ qWarning("failed to set game files in \"%s\"", qUtf8Printable(filePath));
+ }
+ }
+ ++writtenCount;
+ }
+ }
+
+ if (invalidFileNames) {
+ reportError(QObject::tr("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 (writtenCount == 0) {
+ qWarning("plugin list would be empty, this is almost certainly wrong. Not "
+ "saving.");
+ }
+}
+
+QStringList MorrowindGamePlugins::readPluginList(MOBase::IPluginList* pluginList)
+{
+ QStringList primary = organizer()->managedGame()->primaryPlugins();
+ for (const QString& pluginName : primary) {
+ if (pluginList->state(pluginName) != IPluginList::STATE_MISSING) {
+ pluginList->setState(pluginName, IPluginList::STATE_ACTIVE);
+ }
+ }
+ QStringList plugins = pluginList->pluginNames();
+ // Do not sort the primary plugins. Their load order should be locked as defined in
+ // "primaryPlugins".
+ const QStringList pluginsClone(plugins);
+ for (QString plugin : pluginsClone) {
+ if (primary.contains(plugin, Qt::CaseInsensitive))
+ plugins.removeAll(plugin);
+ }
+
+ // Always use filetime loadorder to get the actual load order
+ std::sort(plugins.begin(), plugins.end(),
+ [&](const QString& lhs, const QString& rhs) {
+ MOBase::IModInterface* lhm =
+ organizer()->modList()->getMod(pluginList->origin(lhs));
+ MOBase::IModInterface* rhm =
+ organizer()->modList()->getMod(pluginList->origin(rhs));
+ QDir lhd = organizer()->managedGame()->dataDirectory();
+ QDir rhd = organizer()->managedGame()->dataDirectory();
+ if (lhm != nullptr)
+ lhd = lhm->absolutePath();
+ if (rhm != nullptr)
+ rhd = rhm->absolutePath();
+ QString lhp = lhd.absoluteFilePath(lhs);
+ QString rhp = rhd.absoluteFilePath(rhs);
+ return QFileInfo(lhp).lastModified() < QFileInfo(rhp).lastModified();
+ });
+
+ QString filePath = organizer()->profile()->absolutePath() + "/Morrowind.ini";
+ if (!organizer()->profile()->localSettingsEnabled()) {
+ filePath =
+ organizer()->managedGame()->gameDirectory().absolutePath() + "/Morrowind.ini";
+ }
+ QStringList activePlugins;
+ QStringList inactivePlugins;
+ QSettings settings(filePath, QSettings::IniFormat);
+ QString key = "GameFile";
+ int i = 0;
+ while (true) {
+ QString pluginName =
+ settings.value("Game Files/" + key + QString::number(i), "").toString().trimmed();
+ if (pluginName.isEmpty()) {
+ break;
+ }
+ pluginList->setState(pluginName, IPluginList::STATE_ACTIVE);
+ activePlugins.push_back(pluginName);
+ i++;
+ }
+
+ // we removed each plugin found in the file, so what's left are inactive mods
+ for (const QString& pluginName : plugins)
+ if (!activePlugins.contains(pluginName))
+ inactivePlugins.push_back(pluginName);
+
+ for (const QString& pluginName : inactivePlugins)
+ pluginList->setState(pluginName, IPluginList::STATE_INACTIVE);
+
+ return primary + plugins;
+}
diff --git a/libs/game_bethesda/src/games/morrowind/morrowindgameplugins.h b/libs/game_bethesda/src/games/morrowind/morrowindgameplugins.h
new file mode 100644
index 0000000..37bc219
--- /dev/null
+++ b/libs/game_bethesda/src/games/morrowind/morrowindgameplugins.h
@@ -0,0 +1,28 @@
+#ifndef MORROWINDGAMEPLUGINS_H
+#define MORROWINDGAMEPLUGINS_H
+
+#include <gamebryogameplugins.h>
+
+class MorrowindGamePlugins : public GamebryoGamePlugins
+{
+
+public:
+ MorrowindGamePlugins(MOBase::IOrganizer* organizer);
+
+ virtual void writePluginLists(const MOBase::IPluginList* pluginList) override;
+ virtual void readPluginLists(MOBase::IPluginList* pluginList) override;
+
+protected:
+ virtual void writePluginList(const MOBase::IPluginList* pluginList,
+ const QString& filePath) override;
+ virtual QStringList readPluginList(MOBase::IPluginList* pluginList) override;
+
+private:
+ virtual void writeList(const MOBase::IPluginList* pluginList, const QString& filePath,
+ bool loadOrder);
+
+private:
+ QDateTime m_LastRead;
+};
+
+#endif // MORROWINDGAMEPLUGINS_H
diff --git a/libs/game_bethesda/src/games/morrowind/morrowindlocalsavegames.cpp b/libs/game_bethesda/src/games/morrowind/morrowindlocalsavegames.cpp
new file mode 100644
index 0000000..7499a58
--- /dev/null
+++ b/libs/game_bethesda/src/games/morrowind/morrowindlocalsavegames.cpp
@@ -0,0 +1,56 @@
+/*
+Copyright (C) 2015 Sebastian Herbord. All rights reserved.
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 3 of the License, or (at your option) any later version.
+
+This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+*/
+
+#include "morrowindlocalsavegames.h"
+#include <QtDebug>
+#include <iprofile.h>
+#include <stddef.h>
+#include <string>
+
+MorrowindLocalSavegames::MorrowindLocalSavegames(const MOBase::IPluginGame* game)
+ : m_GamePlugin(game)
+{}
+
+bool MorrowindLocalSavegames::prepareProfile(MOBase::IProfile* profile)
+{
+ bool dirty = false;
+
+ if (profile->localSavesEnabled()) {
+ if (m_GamePlugin->gameDirectory().exists("Saves")) {
+ if (!m_GamePlugin->gameDirectory().rename("Saves", "_Saves")) {
+ qCritical("Unable to enable Morrowind local save games!");
+ }
+ dirty = true;
+ }
+ } else {
+ if (m_GamePlugin->gameDirectory().exists("_Saves")) {
+ if (!m_GamePlugin->gameDirectory().rename("_Saves", "Saves")) {
+ qCritical("Unable to disable Morrowind local save games!");
+ }
+ dirty = true;
+ }
+ }
+
+ return dirty;
+}
+
+MappingType MorrowindLocalSavegames::mappings(const QDir& profileSaveDir) const
+{
+ return {{profileSaveDir.absolutePath(),
+ m_GamePlugin->gameDirectory().absoluteFilePath("Saves"), true, true}};
+}
diff --git a/libs/game_bethesda/src/games/morrowind/morrowindlocalsavegames.h b/libs/game_bethesda/src/games/morrowind/morrowindlocalsavegames.h
new file mode 100644
index 0000000..e391e12
--- /dev/null
+++ b/libs/game_bethesda/src/games/morrowind/morrowindlocalsavegames.h
@@ -0,0 +1,41 @@
+/*
+Copyright (C) 2015 Sebastian Herbord. All rights reserved.
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 3 of the License, or (at your option) any later version.
+
+This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+*/
+
+#ifndef MORROWINDLOCALSAVEGAMES_H
+#define MORROWINDLOCALSAVEGAMES_H
+
+#include <localsavegames.h>
+
+#include "iplugingame.h"
+#include <QDir>
+#include <QString>
+
+class MorrowindLocalSavegames : public MOBase::LocalSavegames
+{
+
+public:
+ MorrowindLocalSavegames(const MOBase::IPluginGame* game);
+
+ virtual MappingType mappings(const QDir& profileSaveDir) const override;
+ virtual bool prepareProfile(MOBase::IProfile* profile) override;
+
+private:
+ const MOBase::IPluginGame* m_GamePlugin;
+};
+
+#endif // MORROWINDLOCALSAVEGAMES_H
diff --git a/libs/game_bethesda/src/games/morrowind/morrowindmoddatachecker.h b/libs/game_bethesda/src/games/morrowind/morrowindmoddatachecker.h
new file mode 100644
index 0000000..bd158e0
--- /dev/null
+++ b/libs/game_bethesda/src/games/morrowind/morrowindmoddatachecker.h
@@ -0,0 +1,26 @@
+#ifndef MORROWIND_MODATACHECKER_H
+#define MORROWIND_MODATACHECKER_H
+
+#include <gamebryomoddatachecker.h>
+
+class MorrowindModDataChecker : public GamebryoModDataChecker
+{
+public:
+ using GamebryoModDataChecker::GamebryoModDataChecker;
+
+protected:
+ virtual const FileNameSet& possibleFolderNames() const override
+ {
+ static FileNameSet result{"fonts", "meshes", "music", "shaders", "sound",
+ "textures", "video", "mwse", "distantland", "mits",
+ "icons", "bookart", "splash", "animations"};
+ return result;
+ }
+ virtual const FileNameSet& possibleFileExtensions() const override
+ {
+ static FileNameSet result{"esp", "esm", "bsa", "modgroups"};
+ return result;
+ }
+};
+
+#endif // MORROWIND_MODATACHECKER_H
diff --git a/libs/game_bethesda/src/games/morrowind/morrowindmoddatacontent.h b/libs/game_bethesda/src/games/morrowind/morrowindmoddatacontent.h
new file mode 100644
index 0000000..b8c9af5
--- /dev/null
+++ b/libs/game_bethesda/src/games/morrowind/morrowindmoddatacontent.h
@@ -0,0 +1,24 @@
+#ifndef MORROWIND_MODDATACONTENT_H
+#define MORROWIND_MODDATACONTENT_H
+
+#include <gamebryomoddatacontent.h>
+#include <ifiletree.h>
+
+class MorrowindModDataContent : public GamebryoModDataContent
+{
+public:
+ /**
+ *
+ */
+ MorrowindModDataContent(MOBase::IGameFeatures const* gameFeatures)
+ : GamebryoModDataContent(gameFeatures)
+ {
+ // Just need to disable some contents:
+ m_Enabled[CONTENT_MCM] = false;
+ m_Enabled[CONTENT_SKYPROC] = false;
+ m_Enabled[CONTENT_INTERFACE] = false;
+ m_Enabled[CONTENT_SCRIPT] = false;
+ }
+};
+
+#endif // MORROWIND_MODDATACONTENT_H
diff --git a/libs/game_bethesda/src/games/morrowind/morrowindsavegame.cpp b/libs/game_bethesda/src/games/morrowind/morrowindsavegame.cpp
new file mode 100644
index 0000000..9588563
--- /dev/null
+++ b/libs/game_bethesda/src/games/morrowind/morrowindsavegame.cpp
@@ -0,0 +1,183 @@
+#include "morrowindsavegame.h"
+
+#include <QPixmap>
+#include <QRegularExpression>
+#include <filesystem>
+
+MorrowindSaveGame::MorrowindSaveGame(QString const& fileName, GameMorrowind const* game)
+ : GamebryoSaveGame(fileName, game)
+{
+ std::filesystem::path realFile(fileName.toStdWString());
+ QString realFileName = QString::fromStdWString(realFile.filename().wstring());
+ m_SaveNumber = realFileName.mid(4, 5).remove(QRegularExpression("0+$")).toInt();
+
+ FileWrapper file(fileName, "TES3");
+ QStringList dummyPlugins;
+ fetchInformationFields(file, m_SaveName, dummyPlugins, m_PCCurrentHealth,
+ m_PCCMaxHealth, m_PCLocation, m_GameDays, m_PCName);
+}
+
+QString MorrowindSaveGame::getName() const
+{
+ return QString("%1, #%2, %3").arg(m_PCName).arg(m_SaveNumber).arg(m_PCLocation);
+}
+
+unsigned short MorrowindSaveGame::getPCLevel() const
+{
+ return dynamic_cast<MorrowindDataFields*>(m_DataFields.value().get())->PCLevel;
+}
+
+// Fetch easy-to-access information.
+void MorrowindSaveGame::fetchInformationFields(FileWrapper& file, QString& saveName,
+ QStringList& plugins,
+ float& playerCurrentHealth,
+ float& playerMaxHealth,
+ QString& playerLocation, float& gameDays,
+ QString& playerName) const
+{
+ file.skip<uint32_t>(3); // data size
+ file.skip<unsigned char>(4); // HEDR tag
+ file.skip<uint32_t>(); // header size
+ file.skip<float>(); // header version
+ file.skip<uint32_t>(); // following data chunk size? seems to be 9 groupings of 32
+ // bytes
+ file.skip<unsigned char>(32); // Author empty for save files
+
+ // The defined save name. This is technically the description, but is likely only
+ // 31+\0 chars max.
+ {
+ std::vector<char> saveNameBuffer(256); // 31 char save name with a null terminator
+ file.read(saveNameBuffer.data(), 256);
+ saveName = QString::fromLatin1(saveNameBuffer.data(), -1).trimmed();
+ }
+
+ file.skip<uint32_t>(); // NumRecords (for the entire save)
+ std::vector<char> buffer(255);
+ file.read(buffer.data(), 4);
+ // Parse the MAST/DATA records
+ while (QString::fromLatin1(buffer.data(), 4) == "MAST") {
+ uint32_t len;
+ file.read(len); // Length of master name
+ file.read(buffer.data(), len); // Name of master
+ QString name = QString::fromLatin1(buffer.data(), len - 1);
+ file.skip<unsigned char>(4); // DATA record
+
+ // Typically size 8 - contains length of master data for version checking
+ file.read(len); // Length
+ file.skip<unsigned char>(len);
+
+ file.read(buffer.data(), 4); // Get next record type
+ plugins.push_back(name);
+ }
+
+ // Start of GMDT
+ file.skip<uint32_t>(); // size of record
+
+ file.read(playerCurrentHealth);
+ file.read(playerMaxHealth);
+
+ file.skip<double>(); // current stam?
+ file.skip<double>(); // max stam?
+ // file.skip<double>(2); // unknown values
+
+ std::fill(buffer.begin(), buffer.end(), '\0');
+ file.read(buffer.data(), 64);
+ playerLocation = QString::fromLatin1(buffer.data(), -1).trimmed();
+
+ file.read(gameDays);
+
+ std::fill(buffer.begin(), buffer.end(), '\0');
+ file.read(buffer.data(), 32);
+ playerName = QString::fromLatin1(buffer.data(), -1).trimmed();
+
+ // End of GMDT
+}
+
+std::unique_ptr<GamebryoSaveGame::DataFields> MorrowindSaveGame::fetchDataFields() const
+{
+ FileWrapper file(getFilepath(), "TES3");
+ std::vector<char> buffer(255);
+
+ std::unique_ptr<MorrowindDataFields> fields = std::make_unique<MorrowindDataFields>();
+
+ {
+ QString dummy;
+ float dummyF;
+ fetchInformationFields(file, dummy, fields->Plugins, dummyF, dummyF, dummy, dummyF,
+ dummy);
+ }
+
+ file.skip<unsigned char>(28); // Skip the SCRD
+ // I believe this tells the engine what color each pixel represents and the bitness of
+ // the image
+
+ // Start of screenshot
+ file.skip<unsigned char>(4); // SCRS
+ file.skip<uint32_t>(); // Size of screenshot always 65536 (128x128x4) RGBA8888
+
+ QImage image = readImageBGRA(file, 128, 128, 0, 1);
+ fields->Screenshot = image.scaled(252, 192);
+
+ // definitively have to use another method to access the player level
+ // it is stored in the fifth byte of the NPDT subrecord of the first NPC_ record
+
+ // Globals, Scripts, Regions
+ // file.skip<unsigned char>();
+ std::vector<char> buff(4);
+ file.read(buff.data(), 4);
+ while (QString::fromLatin1(buff.data(), 4) != "NPC_") {
+ uint32_t len;
+ file.read(len);
+ file.skip<unsigned char>(8 + len);
+ file.read(buff.data(), 4);
+ }
+ while (QString::fromLatin1(buff.data(), 4) == "NPC_") {
+ uint32_t size;
+ file.read(size);
+ file.skip<unsigned long>(3);
+ uint32_t len;
+ file.read(len);
+ file.read(buffer.data(), len);
+ if (QString::fromLatin1(buffer.data(), len - 1) == "player") {
+ file.read(buff.data(), 4);
+ while (QString::fromLatin1(buff.data(), 4) != "NPDT") {
+ uint32_t len;
+ file.read(len);
+ file.skip<unsigned char>(len);
+ file.read(buff.data(), 4);
+ }
+ file.skip<unsigned long>();
+ file.read(fields->PCLevel);
+ } else {
+ file.skip<unsigned char>(size - len - 8);
+ }
+ }
+
+ return fields;
+}
+
+QImage MorrowindSaveGame::readImageBGRA(GamebryoSaveGame::FileWrapper& file,
+ unsigned long width, unsigned long height,
+ int scale = 0, bool alpha = false) const
+{
+ QImage image(width, height, QImage::Format_RGBA8888);
+ for (unsigned long h = 0; h < width; h++) {
+ for (unsigned long w = 0; w < width; w++) {
+ uint8_t blue;
+ file.read(blue);
+ uint8_t green;
+ file.read(green);
+ uint8_t red;
+ file.read(red);
+ uint8_t alpha;
+ file.read(alpha);
+ alpha = 255 - alpha;
+ QColor color(red, green, blue, alpha);
+ image.setPixel(w, h, color.rgba());
+ }
+ }
+ if (scale != 0)
+ return image.copy().scaledToWidth(scale);
+ else
+ return image.copy();
+}
diff --git a/libs/game_bethesda/src/games/morrowind/morrowindsavegame.h b/libs/game_bethesda/src/games/morrowind/morrowindsavegame.h
new file mode 100644
index 0000000..b4ea960
--- /dev/null
+++ b/libs/game_bethesda/src/games/morrowind/morrowindsavegame.h
@@ -0,0 +1,57 @@
+#ifndef MORROWINDSAVEGAME_H
+#define MORROWINDSAVEGAME_H
+
+#include "gamebryosavegame.h"
+#include "gamemorrowind.h"
+
+namespace MOBase
+{
+class IPluginGame;
+}
+
+class MorrowindSaveGame : public GamebryoSaveGame
+{
+public:
+ MorrowindSaveGame(QString const& fileName, GameMorrowind const* game);
+
+public: // ISaveGame interface
+ // We need to override getName() because we do not read the level at
+ // the beginning.
+ virtual QString getName() const override;
+
+ // The PC level is not pre-fetch for morrowind.
+ unsigned short getPCLevel() const override;
+
+public:
+ // Simple getters
+ QString getSaveName() const { return m_SaveName; }
+ float getPCCurrentHealth() const { return m_PCCurrentHealth; }
+ float getPCMaxHealth() const { return m_PCCMaxHealth; }
+ float getGameDays() const { return m_GameDays; }
+
+protected:
+ QString m_SaveName;
+ float m_PCCurrentHealth;
+ float m_PCCMaxHealth;
+ float m_GameDays;
+
+protected:
+ QImage readImageBGRA(GamebryoSaveGame::FileWrapper& file, unsigned long width,
+ unsigned long height, int scale, bool alpha) const;
+
+ // We need to add the PC level here.
+ struct MorrowindDataFields : public DataFields
+ {
+ unsigned short PCLevel = 0;
+ };
+
+ // Fetch easy-to-access information.
+ void fetchInformationFields(FileWrapper& file, QString& saveName,
+ QStringList& plugins, float& playerCurrentHealth,
+ float& playerMaxHealth, QString& playerLocation,
+ float& gameDays, QString& playerName) const;
+
+ std::unique_ptr<DataFields> fetchDataFields() const override;
+};
+
+#endif // MORROWINDSAVEGAME_H
diff --git a/libs/game_bethesda/src/games/morrowind/morrowindsavegameinfo.cpp b/libs/game_bethesda/src/games/morrowind/morrowindsavegameinfo.cpp
new file mode 100644
index 0000000..746ea50
--- /dev/null
+++ b/libs/game_bethesda/src/games/morrowind/morrowindsavegameinfo.cpp
@@ -0,0 +1,18 @@
+#include "morrowindsavegameinfo.h"
+#include "gamegamebryo.h"
+#include "morrowindsavegame.h"
+#include "morrowindsavegameinfowidget.h"
+
+MorrowindSaveGameInfo::MorrowindSaveGameInfo(GameGamebryo const* game)
+ : GamebryoSaveGameInfo(game)
+{
+ m_Game = dynamic_cast<GameMorrowind const*>(game);
+}
+
+MorrowindSaveGameInfo::~MorrowindSaveGameInfo() {}
+
+MOBase::ISaveGameInfoWidget*
+MorrowindSaveGameInfo::getSaveGameWidget(QWidget* parent) const
+{
+ return new MorrowindSaveGameInfoWidget(this, parent);
+}
diff --git a/libs/game_bethesda/src/games/morrowind/morrowindsavegameinfo.h b/libs/game_bethesda/src/games/morrowind/morrowindsavegameinfo.h
new file mode 100644
index 0000000..ff4bc55
--- /dev/null
+++ b/libs/game_bethesda/src/games/morrowind/morrowindsavegameinfo.h
@@ -0,0 +1,22 @@
+#ifndef MORROWINDSAVEGAMEINFO_H
+#define MORROWINDSAVEGAMEINFO_H
+
+#include "gamebryosavegameinfo.h"
+#include "gamemorrowind.h"
+
+class GameGamebryo;
+
+class MorrowindSaveGameInfo : public GamebryoSaveGameInfo
+{
+public:
+ MorrowindSaveGameInfo(GameGamebryo const* game);
+ ~MorrowindSaveGameInfo();
+
+ virtual MOBase::ISaveGameInfoWidget* getSaveGameWidget(QWidget*) const override;
+
+protected:
+ friend class MorrowindSaveGameInfoWidget;
+ GameMorrowind const* m_Game;
+};
+
+#endif // MORROWINDSAVEGAMEINFO_H
diff --git a/libs/game_bethesda/src/games/morrowind/morrowindsavegameinfowidget.cpp b/libs/game_bethesda/src/games/morrowind/morrowindsavegameinfowidget.cpp
new file mode 100644
index 0000000..9e098fb
--- /dev/null
+++ b/libs/game_bethesda/src/games/morrowind/morrowindsavegameinfowidget.cpp
@@ -0,0 +1,123 @@
+#include "morrowindsavegameinfowidget.h"
+#include "ui_morrowindsavegameinfowidget.h"
+
+#include "gamemorrowind.h"
+#include "imoinfo.h"
+#include "ipluginlist.h"
+#include "morrowindsavegame.h"
+#include "morrowindsavegameinfo.h"
+
+#include <QDate>
+#include <QDateTime>
+#include <QFont>
+#include <QFrame>
+#include <QLabel>
+#include <QLayout>
+#include <QLayoutItem>
+#include <QPixmap>
+#include <QString>
+#include <QStyle>
+#include <QTime>
+#include <QVBoxLayout>
+
+#include <Qt>
+#include <QtGlobal>
+
+#include <memory>
+
+MorrowindSaveGameInfoWidget::MorrowindSaveGameInfoWidget(
+ MorrowindSaveGameInfo const* info, QWidget* parent)
+ : MOBase::ISaveGameInfoWidget(parent), ui(new Ui::MorrowindSaveGameInfoWidget),
+ m_Info(info)
+{
+ ui->setupUi(this);
+ this->setWindowFlags(Qt::ToolTip | Qt::BypassGraphicsProxyWidget);
+ setWindowOpacity(style()->styleHint(QStyle::SH_ToolTipLabel_Opacity, 0, this) /
+ qreal(255.0));
+ ui->gameFrame->setStyleSheet("background-color: transparent;");
+
+ QVBoxLayout* gameLayout = new QVBoxLayout();
+ gameLayout->setContentsMargins(0, 0, 0, 0);
+ gameLayout->setSpacing(2);
+ ui->gameFrame->setLayout(gameLayout);
+}
+
+MorrowindSaveGameInfoWidget::~MorrowindSaveGameInfoWidget()
+{
+ delete ui;
+}
+
+void MorrowindSaveGameInfoWidget::setSave(MOBase::ISaveGame const& save)
+{
+ auto const& morrowindSave = dynamic_cast<MorrowindSaveGame const&>(save);
+
+ ui->saveNameLabel->setText(QString("%1 (Day %2)")
+ .arg(morrowindSave.getSaveName())
+ .arg(morrowindSave.getGameDays()));
+ ui->saveNumLabel->setText(QString("%1").arg(morrowindSave.getSaveNumber()));
+ ui->healthLabel->setText(QString("%1 / %2")
+ .arg(round(morrowindSave.getPCCurrentHealth()))
+ .arg(morrowindSave.getPCMaxHealth()));
+ ui->characterLabel->setText(morrowindSave.getPCName());
+ ui->locationLabel->setText(morrowindSave.getPCLocation());
+ ui->levelLabel->setText(QString("%1").arg(morrowindSave.getPCLevel()));
+
+ // This somewhat contorted code is because on my system at least, the
+ // old way of doing this appears to give short date and long time.
+ QDateTime t = morrowindSave.getCreationTime();
+ ui->dateLabel->setText(
+ QLocale::system().toString(t.date(), QLocale::FormatType::ShortFormat) + " " +
+ QLocale::system().toString(t.time(), QLocale::FormatType::ShortFormat));
+ ui->screenshotLabel->setPixmap(QPixmap::fromImage(morrowindSave.getScreenshot()));
+ if (ui->gameFrame->layout() != nullptr) {
+ QLayoutItem* item = nullptr;
+ while ((item = ui->gameFrame->layout()->takeAt(0)) != nullptr) {
+ delete item->widget();
+ delete item;
+ }
+ ui->gameFrame->layout()->setSizeConstraint(QLayout::SetFixedSize);
+ }
+
+ // Resize box to new content
+ this->resize(0, 0);
+
+ QLayout* layout = ui->gameFrame->layout();
+ QLabel* header = new QLabel(tr("Missing ESPs"));
+ QFont headerFont = header->font();
+ QFont contentFont = headerFont;
+ headerFont.setItalic(true);
+ contentFont.setBold(true);
+ contentFont.setPointSize(7);
+ header->setFont(headerFont);
+ layout->addWidget(header);
+ int count = 0;
+ MOBase::IPluginList* pluginList = m_Info->m_Game->m_Organizer->pluginList();
+ for (QString const& pluginName : morrowindSave.getPlugins()) {
+ if (pluginList->state(pluginName) == MOBase::IPluginList::STATE_ACTIVE) {
+ continue;
+ }
+
+ ++count;
+
+ if (count > 7) {
+ break;
+ }
+
+ QLabel* pluginLabel = new QLabel(pluginName);
+ pluginLabel->setIndent(10);
+ pluginLabel->setFont(contentFont);
+ layout->addWidget(pluginLabel);
+ }
+ if (count > 7) {
+ QLabel* dotDotLabel = new QLabel("...");
+ dotDotLabel->setIndent(10);
+ dotDotLabel->setFont(contentFont);
+ layout->addWidget(dotDotLabel);
+ }
+ if (count == 0) {
+ QLabel* dotDotLabel = new QLabel(tr("None"));
+ dotDotLabel->setIndent(10);
+ dotDotLabel->setFont(contentFont);
+ layout->addWidget(dotDotLabel);
+ }
+}
diff --git a/libs/game_bethesda/src/games/morrowind/morrowindsavegameinfowidget.h b/libs/game_bethesda/src/games/morrowind/morrowindsavegameinfowidget.h
new file mode 100644
index 0000000..af27bc3
--- /dev/null
+++ b/libs/game_bethesda/src/games/morrowind/morrowindsavegameinfowidget.h
@@ -0,0 +1,31 @@
+#ifndef MORROWINDSAVEGAMEINFOWIDGET_H
+#define MORROWINDSAVEGAMEINFOWIDGET_H
+
+#include "isavegameinfowidget.h"
+#include "morrowindsavegameinfo.h"
+
+#include <QObject>
+
+class GamebryoGame;
+
+namespace Ui
+{
+class MorrowindSaveGameInfoWidget;
+}
+
+class MorrowindSaveGameInfoWidget : public MOBase::ISaveGameInfoWidget
+{
+ Q_OBJECT
+
+public:
+ MorrowindSaveGameInfoWidget(MorrowindSaveGameInfo const* info, QWidget* parent);
+ ~MorrowindSaveGameInfoWidget();
+
+ virtual void setSave(MOBase::ISaveGame const&) override;
+
+private:
+ Ui::MorrowindSaveGameInfoWidget* ui;
+ MorrowindSaveGameInfo const* m_Info;
+};
+
+#endif // MORROWINDSAVEGAMEINFOWIDGET_H
diff --git a/libs/game_bethesda/src/games/morrowind/morrowindsavegameinfowidget.ui b/libs/game_bethesda/src/games/morrowind/morrowindsavegameinfowidget.ui
new file mode 100644
index 0000000..9ba6ba1
--- /dev/null
+++ b/libs/game_bethesda/src/games/morrowind/morrowindsavegameinfowidget.ui
@@ -0,0 +1,248 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>MorrowindSaveGameInfoWidget</class>
+ <widget class="QWidget" name="MorrowindSaveGameInfoWidget">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>400</width>
+ <height>300</height>
+ </rect>
+ </property>
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="windowTitle">
+ <string/>
+ </property>
+ <property name="toolTip">
+ <string notr="true"/>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QLabel" name="saveNameLabel">
+ <property name="font">
+ <font>
+ <pointsize>10</pointsize>
+ <weight>75</weight>
+ <bold>true</bold>
+ </font>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <layout class="QFormLayout" name="formLayout">
+ <property name="fieldGrowthPolicy">
+ <enum>QFormLayout::AllNonFixedFieldsGrow</enum>
+ </property>
+ <item row="0" column="0">
+ <widget class="QLabel" name="label">
+ <property name="font">
+ <font>
+ <italic>true</italic>
+ </font>
+ </property>
+ <property name="text">
+ <string>Save #</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLabel" name="saveNumLabel">
+ <property name="font">
+ <font>
+ <weight>75</weight>
+ <bold>true</bold>
+ </font>
+ </property>
+ <property name="text">
+ <string notr="true"/>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="label_2">
+ <property name="font">
+ <font>
+ <italic>true</italic>
+ </font>
+ </property>
+ <property name="text">
+ <string>Character</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QLabel" name="characterLabel">
+ <property name="font">
+ <font>
+ <weight>75</weight>
+ <bold>true</bold>
+ </font>
+ </property>
+ <property name="text">
+ <string notr="true"/>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="label_3">
+ <property name="font">
+ <font>
+ <italic>true</italic>
+ </font>
+ </property>
+ <property name="text">
+ <string>Level</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QLabel" name="levelLabel">
+ <property name="font">
+ <font>
+ <weight>75</weight>
+ <bold>true</bold>
+ </font>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="0">
+ <widget class="QLabel" name="label_4">
+ <property name="font">
+ <font>
+ <italic>true</italic>
+ </font>
+ </property>
+ <property name="text">
+ <string>Health</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="1">
+ <widget class="QLabel" name="healthLabel">
+ <property name="font">
+ <font>
+ <weight>75</weight>
+ <bold>true</bold>
+ </font>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ </widget>
+ </item>
+ <item row="4" column="0">
+ <widget class="QLabel" name="label_5">
+ <property name="font">
+ <font>
+ <italic>true</italic>
+ </font>
+ </property>
+ <property name="text">
+ <string>Location</string>
+ </property>
+ </widget>
+ </item>
+ <item row="4" column="1">
+ <widget class="QLabel" name="locationLabel">
+ <property name="font">
+ <font>
+ <weight>75</weight>
+ <bold>true</bold>
+ </font>
+ </property>
+ <property name="text">
+ <string notr="true"/>
+ </property>
+ </widget>
+ </item>
+ <item row="5" column="0">
+ <widget class="QLabel" name="label_6">
+ <property name="font">
+ <font>
+ <italic>true</italic>
+ </font>
+ </property>
+ <property name="text">
+ <string>Date</string>
+ </property>
+ </widget>
+ </item>
+ <item row="5" column="1">
+ <widget class="QLabel" name="dateLabel">
+ <property name="font">
+ <font>
+ <weight>75</weight>
+ <bold>true</bold>
+ </font>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QFrame" name="gameFrame">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Minimum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="frameShape">
+ <enum>QFrame::StyledPanel</enum>
+ </property>
+ <property name="frameShadow">
+ <enum>QFrame::Raised</enum>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="screenshotLabel">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="MinimumExpanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>16777215</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="autoFillBackground">
+ <bool>false</bool>
+ </property>
+ <property name="text">
+ <string notr="true"/>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/libs/game_bethesda/src/games/morrowind/splash.png b/libs/game_bethesda/src/games/morrowind/splash.png
new file mode 100644
index 0000000..7914180
--- /dev/null
+++ b/libs/game_bethesda/src/games/morrowind/splash.png
Binary files differ