From 7ee008e150bc5bcf76082d726f719ee0fdfda982 Mon Sep 17 00:00:00 2001 From: SulfurNitride Date: Wed, 11 Feb 2026 02:37:39 -0600 Subject: 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 --- .../src/creation/creationgameplugins.cpp | 183 +++++ libs/game_bethesda/src/gamebryo/dummybsa.cpp | 198 +++++ .../src/gamebryo/gamebryobsainvalidation.cpp | 165 +++++ .../src/gamebryo/gamebryodataarchives.cpp | 68 ++ .../src/gamebryo/gamebryogameplugins.cpp | 246 ++++++ .../src/gamebryo/gamebryolocalsavegames.cpp | 138 ++++ .../src/gamebryo/gamebryomoddatachecker.cpp | 76 ++ .../src/gamebryo/gamebryomoddatacontent.cpp | 116 +++ .../src/gamebryo/gamebryosavegame.cpp | 607 +++++++++++++++ .../src/gamebryo/gamebryosavegameinfo.cpp | 100 +++ .../src/gamebryo/gamebryosavegameinfowidget.cpp | 199 +++++ .../src/gamebryo/gamebryoscriptextender.cpp | 46 ++ .../src/gamebryo/gamebryounmanagedmods.cpp | 51 ++ libs/game_bethesda/src/gamebryo/gamegamebryo.cpp | 823 +++++++++++++++++++++ .../src/games/enderal/enderalbsainvalidation.cpp | 16 + .../src/games/enderal/enderaldataarchives.cpp | 42 ++ .../src/games/enderal/enderalgameplugins.cpp | 123 +++ .../src/games/enderal/enderallocalsavegames.cpp | 24 + .../src/games/enderal/enderalsavegame.cpp | 101 +++ .../src/games/enderal/enderalscriptextender.cpp | 18 + .../src/games/enderal/gameenderal.cpp | 267 +++++++ .../src/games/enderalse/enderalsedataarchives.cpp | 64 ++ .../src/games/enderalse/enderalsegameplugins.cpp | 82 ++ .../games/enderalse/enderalselocalsavegames.cpp | 24 + .../src/games/enderalse/enderalsesavegame.cpp | 148 ++++ .../games/enderalse/enderalsescriptextender.cpp | 18 + .../src/games/enderalse/enderalseunmanagedmods.cpp | 29 + .../src/games/enderalse/gameenderalse.cpp | 347 +++++++++ .../src/games/fallout3/fallout3bsainvalidation.cpp | 16 + .../src/games/fallout3/fallout3dataarchives.cpp | 33 + .../src/games/fallout3/fallout3savegame.cpp | 72 ++ .../src/games/fallout3/fallout3scriptextender.cpp | 18 + .../src/games/fallout3/gamefallout3.cpp | 279 +++++++ .../src/games/fallout4/fallout4bsainvalidation.cpp | 68 ++ .../src/games/fallout4/fallout4dataarchives.cpp | 48 ++ .../src/games/fallout4/fallout4savegame.cpp | 106 +++ .../src/games/fallout4/fallout4scriptextender.cpp | 18 + .../src/games/fallout4/fallout4unmanagedmods.cpp | 63 ++ .../src/games/fallout4/gamefallout4.cpp | 334 +++++++++ .../fallout4london/fo4londonbsainvalidation.cpp | 68 ++ .../games/fallout4london/fo4londondataarchives.cpp | 48 ++ .../src/games/fallout4london/fo4londonsavegame.cpp | 108 +++ .../fallout4london/fo4londonscriptextender.cpp | 18 + .../fallout4london/fo4londonunmanagedmods.cpp | 63 ++ .../src/games/fallout4london/gamefo4london.cpp | 366 +++++++++ .../games/fallout4vr/fallout4vrdataarchives.cpp | 49 ++ .../src/games/fallout4vr/fallout4vrgameplugins.cpp | 15 + .../src/games/fallout4vr/fallout4vrsavegame.cpp | 108 +++ .../games/fallout4vr/fallout4vrscriptextender.cpp | 18 + .../games/fallout4vr/fallout4vrunmanagedmods.cpp | 63 ++ .../src/games/fallout4vr/gamefallout4vr.cpp | 251 +++++++ .../src/games/fallout76/fallout76dataarchives.cpp | 150 ++++ .../src/games/fallout76/fallout76savegame.cpp | 106 +++ .../src/games/fallout76/fallout76savegameinfo.cpp | 10 + .../games/fallout76/fallout76scriptextender.cpp | 18 + .../src/games/fallout76/fallout76unmanagedmods.cpp | 46 ++ .../src/games/fallout76/gamefallout76.cpp | 227 ++++++ .../games/falloutnv/falloutnvbsainvalidation.cpp | 16 + .../src/games/falloutnv/falloutnvdataarchives.cpp | 32 + .../src/games/falloutnv/falloutnvsavegame.cpp | 77 ++ .../games/falloutnv/falloutnvscriptextender.cpp | 18 + .../src/games/falloutnv/gamefalloutnv.cpp | 334 +++++++++ .../src/games/morrowind/gamemorrowind.cpp | 216 ++++++ .../games/morrowind/morrowindbsainvalidation.cpp | 16 + .../src/games/morrowind/morrowinddataarchives.cpp | 71 ++ .../src/games/morrowind/morrowindgameplugins.cpp | 197 +++++ .../games/morrowind/morrowindlocalsavegames.cpp | 56 ++ .../src/games/morrowind/morrowindsavegame.cpp | 183 +++++ .../src/games/morrowind/morrowindsavegameinfo.cpp | 18 + .../morrowind/morrowindsavegameinfowidget.cpp | 123 +++ libs/game_bethesda/src/games/nehrim/gamenehrim.cpp | 205 +++++ .../src/games/nehrim/nehrimbsainvalidation.cpp | 16 + .../src/games/nehrim/nehrimdataarchives.cpp | 31 + .../src/games/nehrim/nehrimmoddatachecker.cpp | 30 + .../src/games/nehrim/nehrimsavegame.cpp | 72 ++ .../src/games/nehrim/nehrimscriptextender.cpp | 20 + .../src/games/oblivion/gameoblivion.cpp | 213 ++++++ .../src/games/oblivion/oblivionbsainvalidation.cpp | 16 + .../src/games/oblivion/obliviondataarchives.cpp | 32 + .../src/games/oblivion/oblivionmoddatachecker.cpp | 30 + .../src/games/oblivion/oblivionsavegame.cpp | 74 ++ .../src/games/oblivion/oblivionscriptextender.cpp | 20 + libs/game_bethesda/src/games/skyrim/gameskyrim.cpp | 263 +++++++ .../src/games/skyrim/skyrimbsainvalidation.cpp | 16 + .../src/games/skyrim/skyrimdataarchives.cpp | 42 ++ .../src/games/skyrim/skyrimgameplugins.cpp | 123 +++ .../src/games/skyrim/skyrimsavegame.cpp | 105 +++ .../src/games/skyrim/skyrimscriptextender.cpp | 18 + .../src/games/skyrimse/gameskyrimse.cpp | 359 +++++++++ .../src/games/skyrimse/skyrimsedataarchives.cpp | 44 ++ .../src/games/skyrimse/skyrimsesavegame.cpp | 157 ++++ .../src/games/skyrimse/skyrimsescriptextender.cpp | 18 + .../src/games/skyrimse/skyrimseunmanagedmods.cpp | 29 + .../src/games/skyrimvr/gameskyrimvr.cpp | 306 ++++++++ .../src/games/skyrimvr/skyrimvrdataarchives.cpp | 45 ++ .../src/games/skyrimvr/skyrimvrgameplugins.cpp | 15 + .../src/games/skyrimvr/skyrimvrsavegame.cpp | 149 ++++ .../src/games/skyrimvr/skyrimvrscriptextender.cpp | 18 + .../src/games/skyrimvr/skyrimvrunmanagedmods.cpp | 29 + .../src/games/starfield/gamestarfield.cpp | 438 +++++++++++ .../games/starfield/starfieldbsainvalidation.cpp | 68 ++ .../src/games/starfield/starfielddataarchives.cpp | 98 +++ .../src/games/starfield/starfieldgameplugins.cpp | 35 + .../src/games/starfield/starfieldsavegame.cpp | 157 ++++ .../games/starfield/starfieldscriptextender.cpp | 18 + .../src/games/starfield/starfieldunmanagedmods.cpp | 132 ++++ .../src/games/ttw/falloutttwbsainvalidation.cpp | 16 + .../src/games/ttw/falloutttwdataarchives.cpp | 31 + .../src/games/ttw/falloutttwsavegame.cpp | 79 ++ .../src/games/ttw/falloutttwscriptextender.cpp | 18 + .../game_bethesda/src/games/ttw/gamefalloutttw.cpp | 364 +++++++++ 111 files changed, 12412 insertions(+) create mode 100644 libs/game_bethesda/src/creation/creationgameplugins.cpp create mode 100644 libs/game_bethesda/src/gamebryo/dummybsa.cpp create mode 100644 libs/game_bethesda/src/gamebryo/gamebryobsainvalidation.cpp create mode 100644 libs/game_bethesda/src/gamebryo/gamebryodataarchives.cpp create mode 100644 libs/game_bethesda/src/gamebryo/gamebryogameplugins.cpp create mode 100644 libs/game_bethesda/src/gamebryo/gamebryolocalsavegames.cpp create mode 100644 libs/game_bethesda/src/gamebryo/gamebryomoddatachecker.cpp create mode 100644 libs/game_bethesda/src/gamebryo/gamebryomoddatacontent.cpp create mode 100644 libs/game_bethesda/src/gamebryo/gamebryosavegame.cpp create mode 100644 libs/game_bethesda/src/gamebryo/gamebryosavegameinfo.cpp create mode 100644 libs/game_bethesda/src/gamebryo/gamebryosavegameinfowidget.cpp create mode 100644 libs/game_bethesda/src/gamebryo/gamebryoscriptextender.cpp create mode 100644 libs/game_bethesda/src/gamebryo/gamebryounmanagedmods.cpp create mode 100644 libs/game_bethesda/src/gamebryo/gamegamebryo.cpp create mode 100644 libs/game_bethesda/src/games/enderal/enderalbsainvalidation.cpp create mode 100644 libs/game_bethesda/src/games/enderal/enderaldataarchives.cpp create mode 100644 libs/game_bethesda/src/games/enderal/enderalgameplugins.cpp create mode 100644 libs/game_bethesda/src/games/enderal/enderallocalsavegames.cpp create mode 100644 libs/game_bethesda/src/games/enderal/enderalsavegame.cpp create mode 100644 libs/game_bethesda/src/games/enderal/enderalscriptextender.cpp create mode 100644 libs/game_bethesda/src/games/enderal/gameenderal.cpp create mode 100644 libs/game_bethesda/src/games/enderalse/enderalsedataarchives.cpp create mode 100644 libs/game_bethesda/src/games/enderalse/enderalsegameplugins.cpp create mode 100644 libs/game_bethesda/src/games/enderalse/enderalselocalsavegames.cpp create mode 100644 libs/game_bethesda/src/games/enderalse/enderalsesavegame.cpp create mode 100644 libs/game_bethesda/src/games/enderalse/enderalsescriptextender.cpp create mode 100644 libs/game_bethesda/src/games/enderalse/enderalseunmanagedmods.cpp create mode 100644 libs/game_bethesda/src/games/enderalse/gameenderalse.cpp create mode 100644 libs/game_bethesda/src/games/fallout3/fallout3bsainvalidation.cpp create mode 100644 libs/game_bethesda/src/games/fallout3/fallout3dataarchives.cpp create mode 100644 libs/game_bethesda/src/games/fallout3/fallout3savegame.cpp create mode 100644 libs/game_bethesda/src/games/fallout3/fallout3scriptextender.cpp create mode 100644 libs/game_bethesda/src/games/fallout3/gamefallout3.cpp create mode 100644 libs/game_bethesda/src/games/fallout4/fallout4bsainvalidation.cpp create mode 100644 libs/game_bethesda/src/games/fallout4/fallout4dataarchives.cpp create mode 100644 libs/game_bethesda/src/games/fallout4/fallout4savegame.cpp create mode 100644 libs/game_bethesda/src/games/fallout4/fallout4scriptextender.cpp create mode 100644 libs/game_bethesda/src/games/fallout4/fallout4unmanagedmods.cpp create mode 100644 libs/game_bethesda/src/games/fallout4/gamefallout4.cpp create mode 100644 libs/game_bethesda/src/games/fallout4london/fo4londonbsainvalidation.cpp create mode 100644 libs/game_bethesda/src/games/fallout4london/fo4londondataarchives.cpp create mode 100644 libs/game_bethesda/src/games/fallout4london/fo4londonsavegame.cpp create mode 100644 libs/game_bethesda/src/games/fallout4london/fo4londonscriptextender.cpp create mode 100644 libs/game_bethesda/src/games/fallout4london/fo4londonunmanagedmods.cpp create mode 100644 libs/game_bethesda/src/games/fallout4london/gamefo4london.cpp create mode 100644 libs/game_bethesda/src/games/fallout4vr/fallout4vrdataarchives.cpp create mode 100644 libs/game_bethesda/src/games/fallout4vr/fallout4vrgameplugins.cpp create mode 100644 libs/game_bethesda/src/games/fallout4vr/fallout4vrsavegame.cpp create mode 100644 libs/game_bethesda/src/games/fallout4vr/fallout4vrscriptextender.cpp create mode 100644 libs/game_bethesda/src/games/fallout4vr/fallout4vrunmanagedmods.cpp create mode 100644 libs/game_bethesda/src/games/fallout4vr/gamefallout4vr.cpp create mode 100644 libs/game_bethesda/src/games/fallout76/fallout76dataarchives.cpp create mode 100644 libs/game_bethesda/src/games/fallout76/fallout76savegame.cpp create mode 100644 libs/game_bethesda/src/games/fallout76/fallout76savegameinfo.cpp create mode 100644 libs/game_bethesda/src/games/fallout76/fallout76scriptextender.cpp create mode 100644 libs/game_bethesda/src/games/fallout76/fallout76unmanagedmods.cpp create mode 100644 libs/game_bethesda/src/games/fallout76/gamefallout76.cpp create mode 100644 libs/game_bethesda/src/games/falloutnv/falloutnvbsainvalidation.cpp create mode 100644 libs/game_bethesda/src/games/falloutnv/falloutnvdataarchives.cpp create mode 100644 libs/game_bethesda/src/games/falloutnv/falloutnvsavegame.cpp create mode 100644 libs/game_bethesda/src/games/falloutnv/falloutnvscriptextender.cpp create mode 100644 libs/game_bethesda/src/games/falloutnv/gamefalloutnv.cpp create mode 100644 libs/game_bethesda/src/games/morrowind/gamemorrowind.cpp create mode 100644 libs/game_bethesda/src/games/morrowind/morrowindbsainvalidation.cpp create mode 100644 libs/game_bethesda/src/games/morrowind/morrowinddataarchives.cpp create mode 100644 libs/game_bethesda/src/games/morrowind/morrowindgameplugins.cpp create mode 100644 libs/game_bethesda/src/games/morrowind/morrowindlocalsavegames.cpp create mode 100644 libs/game_bethesda/src/games/morrowind/morrowindsavegame.cpp create mode 100644 libs/game_bethesda/src/games/morrowind/morrowindsavegameinfo.cpp create mode 100644 libs/game_bethesda/src/games/morrowind/morrowindsavegameinfowidget.cpp create mode 100644 libs/game_bethesda/src/games/nehrim/gamenehrim.cpp create mode 100644 libs/game_bethesda/src/games/nehrim/nehrimbsainvalidation.cpp create mode 100644 libs/game_bethesda/src/games/nehrim/nehrimdataarchives.cpp create mode 100644 libs/game_bethesda/src/games/nehrim/nehrimmoddatachecker.cpp create mode 100644 libs/game_bethesda/src/games/nehrim/nehrimsavegame.cpp create mode 100644 libs/game_bethesda/src/games/nehrim/nehrimscriptextender.cpp create mode 100644 libs/game_bethesda/src/games/oblivion/gameoblivion.cpp create mode 100644 libs/game_bethesda/src/games/oblivion/oblivionbsainvalidation.cpp create mode 100644 libs/game_bethesda/src/games/oblivion/obliviondataarchives.cpp create mode 100644 libs/game_bethesda/src/games/oblivion/oblivionmoddatachecker.cpp create mode 100644 libs/game_bethesda/src/games/oblivion/oblivionsavegame.cpp create mode 100644 libs/game_bethesda/src/games/oblivion/oblivionscriptextender.cpp create mode 100644 libs/game_bethesda/src/games/skyrim/gameskyrim.cpp create mode 100644 libs/game_bethesda/src/games/skyrim/skyrimbsainvalidation.cpp create mode 100644 libs/game_bethesda/src/games/skyrim/skyrimdataarchives.cpp create mode 100644 libs/game_bethesda/src/games/skyrim/skyrimgameplugins.cpp create mode 100644 libs/game_bethesda/src/games/skyrim/skyrimsavegame.cpp create mode 100644 libs/game_bethesda/src/games/skyrim/skyrimscriptextender.cpp create mode 100644 libs/game_bethesda/src/games/skyrimse/gameskyrimse.cpp create mode 100644 libs/game_bethesda/src/games/skyrimse/skyrimsedataarchives.cpp create mode 100644 libs/game_bethesda/src/games/skyrimse/skyrimsesavegame.cpp create mode 100644 libs/game_bethesda/src/games/skyrimse/skyrimsescriptextender.cpp create mode 100644 libs/game_bethesda/src/games/skyrimse/skyrimseunmanagedmods.cpp create mode 100644 libs/game_bethesda/src/games/skyrimvr/gameskyrimvr.cpp create mode 100644 libs/game_bethesda/src/games/skyrimvr/skyrimvrdataarchives.cpp create mode 100644 libs/game_bethesda/src/games/skyrimvr/skyrimvrgameplugins.cpp create mode 100644 libs/game_bethesda/src/games/skyrimvr/skyrimvrsavegame.cpp create mode 100644 libs/game_bethesda/src/games/skyrimvr/skyrimvrscriptextender.cpp create mode 100644 libs/game_bethesda/src/games/skyrimvr/skyrimvrunmanagedmods.cpp create mode 100644 libs/game_bethesda/src/games/starfield/gamestarfield.cpp create mode 100644 libs/game_bethesda/src/games/starfield/starfieldbsainvalidation.cpp create mode 100644 libs/game_bethesda/src/games/starfield/starfielddataarchives.cpp create mode 100644 libs/game_bethesda/src/games/starfield/starfieldgameplugins.cpp create mode 100644 libs/game_bethesda/src/games/starfield/starfieldsavegame.cpp create mode 100644 libs/game_bethesda/src/games/starfield/starfieldscriptextender.cpp create mode 100644 libs/game_bethesda/src/games/starfield/starfieldunmanagedmods.cpp create mode 100644 libs/game_bethesda/src/games/ttw/falloutttwbsainvalidation.cpp create mode 100644 libs/game_bethesda/src/games/ttw/falloutttwdataarchives.cpp create mode 100644 libs/game_bethesda/src/games/ttw/falloutttwsavegame.cpp create mode 100644 libs/game_bethesda/src/games/ttw/falloutttwscriptextender.cpp create mode 100644 libs/game_bethesda/src/games/ttw/gamefalloutttw.cpp (limited to 'libs/game_bethesda/src/*.cpp') diff --git a/libs/game_bethesda/src/creation/creationgameplugins.cpp b/libs/game_bethesda/src/creation/creationgameplugins.cpp new file mode 100644 index 0000000..f5cef62 --- /dev/null +++ b/libs/game_bethesda/src/creation/creationgameplugins.cpp @@ -0,0 +1,183 @@ +#include "creationgameplugins.h" +#include +#include +#include +#include + +#include +#include +#include +#include + +using MOBase::IOrganizer; +using MOBase::IPluginGame; +using MOBase::IPluginList; +using MOBase::reportError; +using MOBase::SafeWriteFile; + +CreationGamePlugins::CreationGamePlugins(IOrganizer* organizer) + : GamebryoGamePlugins(organizer) +{} + +QStringList CreationGamePlugins::getLoadOrder() +{ + QString loadOrderPath = organizer()->profile()->absolutePath() + "/loadorder.txt"; + QString pluginsPath = organizer()->profile()->absolutePath() + "/plugins.txt"; + + 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) { + return readLoadOrderList(m_Organizer->pluginList(), loadOrderPath); + } else { + return readPluginList(m_Organizer->pluginList()); + } +} + +void CreationGamePlugins::writePluginList(const IPluginList* pluginList, + const QString& filePath) +{ + SafeWriteFile file(filePath); + + QStringEncoder encoder(QStringConverter::Encoding::System); + + file->resize(0); + + file->write( + encoder.encode("# This file was automatically generated by Mod Organizer.\r\n")); + + 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); + }); + + QStringList PrimaryPlugins = organizer()->managedGame()->primaryPlugins(); + QStringList DLCPlugins = organizer()->managedGame()->DLCPlugins(); + QSet ManagedMods = + QSet(PrimaryPlugins.begin(), PrimaryPlugins.end()); + QSet DLCSet = QSet(DLCPlugins.begin(), DLCPlugins.end()); + ManagedMods.subtract(DLCSet); + PrimaryPlugins.append(QList(ManagedMods.begin(), ManagedMods.end())); + + // TODO: do not write plugins in OFFICIAL_FILES container + for (const QString& pluginName : plugins) { + if (!PrimaryPlugins.contains(pluginName, Qt::CaseInsensitive)) { + if (pluginList->state(pluginName) == IPluginList::STATE_ACTIVE) { + auto result = encoder.encode(pluginName); + if (encoder.hasError()) { + invalidFileNames = true; + qCritical("invalid plugin name %s", qUtf8Printable(pluginName)); + } else { + file->write("*"); + file->write(result); + } + file->write("\r\n"); + ++writtenCount; + } else { + auto result = encoder.encode(pluginName); + if (encoder.hasError()) { + invalidFileNames = true; + qCritical("invalid plugin name %s", qUtf8Printable(pluginName)); + } else { + file->write(result); + } + file->write("\r\n"); + ++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.")); + } + + file->commit(); +} + +QStringList CreationGamePlugins::readPluginList(MOBase::IPluginList* pluginList) +{ + const auto plugins = pluginList->pluginNames(); + const auto primaryPlugins = organizer()->managedGame()->primaryPlugins(); + QStringList loadOrder(primaryPlugins); + + for (const QString& pluginName : loadOrder) { + if (pluginList->state(pluginName) != IPluginList::STATE_MISSING) { + pluginList->setState(pluginName, IPluginList::STATE_ACTIVE); + } + } + + QString filePath = organizer()->profile()->absolutePath() + "/plugins.txt"; + QFile file(filePath); + if (!file.open(QIODevice::ReadOnly)) { + qWarning("%s not found", qUtf8Printable(filePath)); + return loadOrder; + } + ON_BLOCK_EXIT([&]() { + file.close(); + }); + + if (file.size() == 0) { + // MO stores at least a header in the file. if it's completely empty the + // file is broken + qWarning("%s empty", qUtf8Printable(filePath)); + return loadOrder; + } + + QStringList pluginsFound; + while (!file.atEnd()) { + QByteArray line = file.readLine(); + QString pluginName; + if ((line.size() > 0) && (line.at(0) != '#')) { + pluginName = QStringEncoder(QStringConverter::Encoding::System) + .encode(line.trimmed().constData()); + } + if (!primaryPlugins.contains(pluginName, Qt::CaseInsensitive)) { + if (pluginName.startsWith('*')) { + pluginName.remove(0, 1); + if (pluginName.size() > 0) { + pluginList->setState(pluginName, IPluginList::STATE_ACTIVE); + pluginsFound.append(pluginName); + if (!loadOrder.contains(pluginName, Qt::CaseInsensitive)) { + loadOrder.append(pluginName); + } + } + } else { + if (pluginName.size() > 0) { + pluginList->setState(pluginName, IPluginList::STATE_INACTIVE); + pluginsFound.append(pluginName); + if (!loadOrder.contains(pluginName, Qt::CaseInsensitive)) { + loadOrder.append(pluginName); + } + } + } + } else { + pluginName.remove(0, 1); + pluginsFound.append(pluginName); + } + } + + file.close(); + + // set all plugins not found inactive + for (const auto& pluginName : plugins) { + if (!pluginsFound.contains(pluginName, Qt::CaseInsensitive)) { + pluginList->setState(pluginName, IPluginList::STATE_INACTIVE); + } + } + + return loadOrder; +} + +bool CreationGamePlugins::lightPluginsAreSupported() +{ + return true; +} diff --git a/libs/game_bethesda/src/gamebryo/dummybsa.cpp b/libs/game_bethesda/src/gamebryo/dummybsa.cpp new file mode 100644 index 0000000..7b0f877 --- /dev/null +++ b/libs/game_bethesda/src/gamebryo/dummybsa.cpp @@ -0,0 +1,198 @@ +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see . +*/ + +#include "dummybsa.h" +#include +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#else +#ifndef MAX_PATH +#define MAX_PATH 260 +#endif +#endif + +static void writeUlong(unsigned char* buffer, int offset, unsigned long value) +{ + union + { + unsigned long ulValue; + unsigned char cValue[4]; + }; + ulValue = value; + memcpy(buffer + offset, cValue, 4); +} + +static void writeUlonglong(unsigned char* buffer, int offset, unsigned long long value) +{ + union + { + unsigned long long ullValue; + unsigned char cValue[8]; + }; + ullValue = value; + memcpy(buffer + offset, cValue, 8); +} + +static unsigned long genHashInt(const unsigned char* pos, const unsigned char* end) +{ + unsigned long hash = 0; + for (; pos < end; ++pos) { + hash *= 0x1003f; + hash += *pos; + } + return hash; +} + +static unsigned long long genHash(const char* fileName) +{ + char fileNameLower[MAX_PATH + 1]; + int i = 0; + for (; i < MAX_PATH && fileName[i] != '\0'; ++i) { + fileNameLower[i] = static_cast(tolower(fileName[i])); + if (fileNameLower[i] == '\\') { + fileNameLower[i] = '/'; + } + } + fileNameLower[i] = '\0'; + + unsigned char* fileNameLowerU = reinterpret_cast(fileNameLower); + + char* ext = strrchr(fileNameLower, '.'); + if (ext == nullptr) { + ext = fileNameLower + strlen(fileNameLower); + } + unsigned char* extU = reinterpret_cast(ext); + + int length = ext - fileNameLower; + + unsigned long long hash = 0ULL; + + if (length > 0) { + hash = *(extU - 1) | ((length > 2 ? *(ext - 2) : 0) << 8) | (length << 16) | + (fileNameLowerU[0] << 24); + } + + if (strlen(ext) > 0) { + if (strcmp(ext + 1, "kf") == 0) { + hash |= 0x80; + } else if (strcmp(ext + 1, "nif") == 0) { + hash |= 0x8000; + } else if (strcmp(ext + 1, "dds") == 0) { + hash |= 0x8080; + } else if (strcmp(ext + 1, "wav") == 0) { + hash |= 0x80000000; + } + + unsigned long long temp = + static_cast(genHashInt(fileNameLowerU + 1, extU - 2)); + temp += static_cast(genHashInt(extU, extU + strlen(ext))); + + hash |= (temp & 0xFFFFFFFF) << 32; + } + return hash; +} + +DummyBSA::DummyBSA(unsigned long bsaVersion) + : m_Version(bsaVersion), m_FolderName(""), m_FileName("dummy.dds"), + m_TotalFileNameLength(0) +{} + +void DummyBSA::writeHeader(QFile& file) +{ + unsigned char header[] = { + 'B', 'S', 'A', '\0', // magic string + 0xDE, 0xAD, 0xBE, 0xEF, // version - insert later + 0x24, 0x00, 0x00, 0x00, // offset to folder recors. header size is static + 0xDE, 0xAD, 0xBE, 0xEF, // archive flags - insert later + 0x01, 0x00, 0x00, 0x00, // folder count + 0x01, 0x00, 0x00, 0x00, // file count + 0xDE, 0xAD, 0xBE, 0xEF, // total folder names length - insert later + 0xDE, 0xAD, 0xBE, 0xEF, // total file names length - insert later + 0xDE, 0xAD, 0xBE, 0xEF // file flags - insert later + }; + + writeUlong(header, 4, m_Version); + writeUlong(header, 12, 0x01 | 0x02); // has directories and has files. + writeUlong(header, 24, + static_cast(m_FolderName.length()) + + 1); // empty folder name + writeUlong(header, 28, m_TotalFileNameLength); // single character file name + + writeUlong(header, 32, 2); // has dds + + file.write(reinterpret_cast(header), sizeof(header)); +} + +void DummyBSA::writeFolderRecord(QFile& file, const std::string& folderName) +{ + unsigned char folderRecord[] = { + 0xDE, 0xAD, 0xBE, 0xEF, 0xDE, 0xAD, 0xBE, 0xEF, // folder hash + 0x01, 0x00, 0x00, 0x00, // file count + 0xDE, 0xAD, 0xBE, 0xEF, // offset to folder name + }; + // we'd usually have to sort folders be the hash value generated here + writeUlonglong(folderRecord, 0, genHash(folderName.c_str())); + writeUlong(folderRecord, 12, + 0x34 + m_TotalFileNameLength); // TODO: this should be calculated properly + + file.write(reinterpret_cast(folderRecord), sizeof(folderRecord)); +} + +void DummyBSA::writeFileRecord(QFile& file, const std::string& fileName) +{ + unsigned char fileRecord[] = { + 0xDE, 0xAD, 0xBE, 0xEF, 0xDE, 0xAD, 0xBE, 0xEF, // file name hash + 0xDE, 0xAD, 0xBE, 0xEF, // size + 0xDE, 0xAD, 0xBE, 0xEF, // offset to file data + }; + + // we'd usually have to sort files by the value generated here + writeUlonglong(fileRecord, 0, genHash(fileName.c_str())); + writeUlong(fileRecord, 8, 0); + writeUlong( + fileRecord, 12, + 0x44 + static_cast(fileName.length() + 1) + + 4); // after this record we expect the filename and 4 bytes of file size + + file.write(reinterpret_cast(fileRecord), sizeof(fileRecord)); +} + +void DummyBSA::writeFileRecordBlocks(QFile& file, const std::string& folderName) +{ + file.write(folderName.c_str(), folderName.length() + 1); + + writeFileRecord(file, m_FileName); +} + +void DummyBSA::write(const QString& fileName) +{ + QFile file(fileName); + file.open(QIODevice::WriteOnly); + + m_TotalFileNameLength = static_cast(m_FileName.length() + 1); + + writeHeader(file); + writeFolderRecord(file, m_FolderName); + writeFileRecordBlocks(file, m_FolderName); + file.write(m_FileName.c_str(), m_FileName.length() + 1); + char fileSize[] = {0x00, 0x00, 0x00, 0x00}; + file.write(fileSize, sizeof(fileSize)); + file.close(); +} diff --git a/libs/game_bethesda/src/gamebryo/gamebryobsainvalidation.cpp b/libs/game_bethesda/src/gamebryo/gamebryobsainvalidation.cpp new file mode 100644 index 0000000..6170305 --- /dev/null +++ b/libs/game_bethesda/src/gamebryo/gamebryobsainvalidation.cpp @@ -0,0 +1,165 @@ +#include "gamebryobsainvalidation.h" + +#include "dummybsa.h" +#include "iplugingame.h" +#include "iprofile.h" +#include "registry.h" +#include +#include + +#include +#include +#include + +#include "gamegamebryo.h" + +GamebryoBSAInvalidation::GamebryoBSAInvalidation(MOBase::DataArchives* dataArchives, + const QString& iniFilename, + MOBase::IPluginGame const* game) + : m_DataArchives(dataArchives), m_IniFileName(iniFilename), m_Game(game) +{} + +bool GamebryoBSAInvalidation::isInvalidationBSA(const QString& bsaName) +{ + static QStringList invalidation{invalidationBSAName()}; + + for (const QString& file : invalidation) { + if (file.compare(bsaName, Qt::CaseInsensitive) == 0) { + return true; + } + } + return false; +} + +void GamebryoBSAInvalidation::deactivate(MOBase::IProfile* profile) +{ + prepareProfile(profile); +} + +void GamebryoBSAInvalidation::activate(MOBase::IProfile* profile) +{ + prepareProfile(profile); +} + +// Case-insensitive file resolution (same logic as gamegamebryo.cpp). +static QString resolveIniPathCaseInsensitive(const QString& filePath) +{ +#ifdef _WIN32 + return filePath; +#else + if (QFileInfo(filePath).exists()) { + return filePath; + } + QFileInfo info(filePath); + QDir dir(info.path()); + if (!dir.exists()) { + return filePath; + } + const QString target = info.fileName(); + const QStringList entries = dir.entryList(QDir::Files | QDir::Hidden); + for (const QString& entry : entries) { + if (entry.compare(target, Qt::CaseInsensitive) == 0) { + return dir.absoluteFilePath(entry); + } + } + return filePath; +#endif +} + +bool GamebryoBSAInvalidation::prepareProfile(MOBase::IProfile* profile) +{ + bool dirty = false; + QString basePath = profile->localSettingsEnabled() + ? profile->absolutePath() + : m_Game->documentsDirectory().absolutePath(); + + // Ensure the target INI has adequate content before writing to it. + // On Linux, the game launcher may not work, leaving INI files missing + // or as empty stubs. Seed from the game's default INI if needed. + const auto* gamebryo = dynamic_cast(m_Game); + if (gamebryo) { + const_cast(gamebryo)->ensureIniFilesExist(basePath); + } + + // Resolve case-insensitively (e.g., fallout.ini vs Fallout.ini on Linux) + QString iniFilePath = resolveIniPathCaseInsensitive(basePath + "/" + m_IniFileName); + + // write bInvalidateOlderFiles = 1, if needed + QString setting = + GameGamebryo::readIniValue(iniFilePath, "Archive", "bInvalidateOlderFiles", "0"); + if (setting.toLong() != 1) { + dirty = true; + if (!MOBase::WriteRegistryValue("Archive", "bInvalidateOlderFiles", "1", + iniFilePath)) { + qWarning("failed to activate BSA invalidation in \"%s\"", + qUtf8Printable(m_IniFileName)); + } + } + + if (profile->invalidationActive(nullptr)) { + + // add the dummy bsa to the archive string, if needed + QStringList archives = m_DataArchives->archives(profile); + bool bsaInstalled = false; + for (const QString& archive : archives) { + if (isInvalidationBSA(archive)) { + bsaInstalled = true; + break; + } + } + if (!bsaInstalled) { + m_DataArchives->addArchive(profile, 0, invalidationBSAName()); + dirty = true; + } + + // create the dummy bsa if necessary + QString bsaFile = m_Game->dataDirectory().absoluteFilePath(invalidationBSAName()); + if (!QFile::exists(bsaFile)) { + DummyBSA bsa(bsaVersion()); + bsa.write(bsaFile); + dirty = true; + } + + // write SInvalidationFile = "", if needed + QString sInvalidation = GameGamebryo::readIniValue( + iniFilePath, "Archive", "SInvalidationFile", "ArchiveInvalidation.txt"); + if (sInvalidation != "") { + dirty = true; + if (!MOBase::WriteRegistryValue("Archive", "SInvalidationFile", "", iniFilePath)) { + qWarning("failed to activate BSA invalidation in \"%s\"", + qUtf8Printable(m_IniFileName)); + } + } + } else { + + // remove the dummy bsa from the archive string, if needed + QStringList archivesBefore = m_DataArchives->archives(profile); + for (const QString& archive : archivesBefore) { + if (isInvalidationBSA(archive)) { + m_DataArchives->removeArchive(profile, archive); + dirty = true; + } + } + + // delete the dummy bsa, if needed + QString bsaFile = m_Game->dataDirectory().absoluteFilePath(invalidationBSAName()); + if (QFile::exists(bsaFile)) { + MOBase::shellDeleteQuiet(bsaFile); + dirty = true; + } + + // write SInvalidationFile = "ArchiveInvalidation.txt", if needed + QString sInvalidation2 = + GameGamebryo::readIniValue(iniFilePath, "Archive", "SInvalidationFile", ""); + if (sInvalidation2 != "ArchiveInvalidation.txt") { + dirty = true; + if (!MOBase::WriteRegistryValue("Archive", "SInvalidationFile", + "ArchiveInvalidation.txt", iniFilePath)) { + qWarning("failed to activate BSA invalidation in \"%s\"", + qUtf8Printable(m_IniFileName)); + } + } + } + + return dirty; +} diff --git a/libs/game_bethesda/src/gamebryo/gamebryodataarchives.cpp b/libs/game_bethesda/src/gamebryo/gamebryodataarchives.cpp new file mode 100644 index 0000000..11e5408 --- /dev/null +++ b/libs/game_bethesda/src/gamebryo/gamebryodataarchives.cpp @@ -0,0 +1,68 @@ +#include "gamebryodataarchives.h" + +#include +#include + +#include "gamegamebryo.h" + +GamebryoDataArchives::GamebryoDataArchives(const GameGamebryo* game) : m_Game{game} {} + +QDir GamebryoDataArchives::gameDirectory() const +{ + return QDir(m_Game->gameDirectory()).absolutePath(); +} + +QDir GamebryoDataArchives::localGameDirectory() const +{ + return QDir(m_Game->myGamesPath()).absolutePath(); +} + +QStringList GamebryoDataArchives::getArchivesFromKey(const QString& iniFile, + const QString& key, + const int size) const +{ + QStringList result; + + QString value = GameGamebryo::readIniValue(iniFile, "Archive", key, ""); + if (!value.isEmpty()) { + result.append(value.split(',')); + } + + for (int i = 0; i < result.count(); ++i) { + result[i] = result[i].trimmed(); + } + return result; +} + +void GamebryoDataArchives::setArchivesToKey(const QString& iniFile, const QString& key, + const QString& value) +{ + if (!MOBase::WriteRegistryValue("Archive", key, value, iniFile)) { + qWarning("failed to set archives in \"%s\"", qUtf8Printable(iniFile)); + } +} + +void GamebryoDataArchives::addArchive(MOBase::IProfile* profile, int index, + const QString& archiveName) +{ + QStringList current = archives(profile); + if (current.contains(archiveName, Qt::CaseInsensitive)) { + return; + } + + current.insert(index != INT_MAX ? index : current.size(), archiveName); + + writeArchiveList(profile, current); +} + +void GamebryoDataArchives::removeArchive(MOBase::IProfile* profile, + const QString& archiveName) +{ + QStringList current = archives(profile); + if (!current.contains(archiveName, Qt::CaseInsensitive)) { + return; + } + current.removeAll(archiveName); + + writeArchiveList(profile, current); +} diff --git a/libs/game_bethesda/src/gamebryo/gamebryogameplugins.cpp b/libs/game_bethesda/src/gamebryo/gamebryogameplugins.cpp new file mode 100644 index 0000000..1accc4f --- /dev/null +++ b/libs/game_bethesda/src/gamebryo/gamebryogameplugins.cpp @@ -0,0 +1,246 @@ +#include "gamebryogameplugins.h" +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +using MOBase::IOrganizer; +using MOBase::IPluginList; +using MOBase::reportError; +using MOBase::SafeWriteFile; + +GamebryoGamePlugins::GamebryoGamePlugins(IOrganizer* organizer) : m_Organizer(organizer) +{} + +void GamebryoGamePlugins::writePluginLists(const IPluginList* pluginList) +{ + if (!m_LastRead.isValid()) { + // attempt to write uninitialized plugin lists + return; + } + + writePluginList(pluginList, m_Organizer->profile()->absolutePath() + "/plugins.txt"); + writeLoadOrderList(pluginList, + m_Organizer->profile()->absolutePath() + "/loadorder.txt"); + + m_LastRead = QDateTime::currentDateTime(); +} + +void GamebryoGamePlugins::readPluginLists(MOBase::IPluginList* pluginList) +{ + QString loadOrderPath = organizer()->profile()->absolutePath() + "/loadorder.txt"; + QString pluginsPath = organizer()->profile()->absolutePath() + "/plugins.txt"; + + 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(); +} + +QStringList GamebryoGamePlugins::getLoadOrder() +{ + QString loadOrderPath = organizer()->profile()->absolutePath() + "/loadorder.txt"; + QString pluginsPath = organizer()->profile()->absolutePath() + "/plugins.txt"; + + 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) { + return readLoadOrderList(m_Organizer->pluginList(), loadOrderPath); + } else { + return readPluginList(m_Organizer->pluginList()); + } +} + +void GamebryoGamePlugins::writePluginList(const MOBase::IPluginList* pluginList, + const QString& filePath) +{ + return writeList(pluginList, filePath, false); +} + +void GamebryoGamePlugins::writeLoadOrderList(const MOBase::IPluginList* pluginList, + const QString& filePath) +{ + return writeList(pluginList, filePath, true); +} + +void GamebryoGamePlugins::writeList(const IPluginList* pluginList, + const QString& filePath, bool loadOrder) +{ + SafeWriteFile file(filePath); + + QStringEncoder encoder = loadOrder + ? QStringEncoder(QStringConverter::Encoding::Utf8) + : QStringEncoder(QStringConverter::Encoding::System); + + file->resize(0); + + file->write( + encoder.encode("# This file was automatically generated by Mod Organizer.\r\n")); + + 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); + }); + + 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 { + file->write(result); + } + file->write("\r\n"); + ++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."); + } else { + file->commit(); + } +} + +QStringList GamebryoGamePlugins::readLoadOrderList(MOBase::IPluginList* pluginList, + const QString& filePath) +{ + QStringList pluginNames = organizer()->managedGame()->primaryPlugins(); + + std::set pluginLookup; + for (auto&& name : pluginNames) { + pluginLookup.insert(name.toLower()); + } + + const auto b = MOBase::forEachLineInFile(filePath, [&](QString s) { + if (!pluginLookup.contains(s.toLower())) { + pluginLookup.insert(s); + pluginNames.push_back(std::move(s)); + } + }); + + if (!b) { + return readPluginList(pluginList); + } + + return pluginNames; +} + +QStringList GamebryoGamePlugins::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(); + QStringList pluginsClone(plugins); + // Do not sort the primary plugins. Their load order should be locked as defined in + // "primaryPlugins". + for (const auto& plugin : pluginsClone) { + if (primary.contains(plugin, Qt::CaseInsensitive)) + plugins.removeAll(plugin); + } + + // Always use filetime loadorder to get the actual load order + QHash modificationTimes; + modificationTimes.reserve(plugins.size()); + for (const auto& pluginName : plugins) { + MOBase::IModInterface* mod = + organizer()->modList()->getMod(pluginList->origin(pluginName)); + QDir dir = mod ? mod->absolutePath() : organizer()->managedGame()->dataDirectory(); + QString absPath = dir.absoluteFilePath(pluginName); + modificationTimes.insert(pluginName, QFileInfo(absPath).lastModified()); + } + + std::ranges::sort( + plugins, [&modificationTimes](const QString& lhs, const QString& rhs) { + return modificationTimes.value(lhs) < modificationTimes.value(rhs); + }); + + // Determine plugin active state by the plugins.txt file. + bool pluginsTxtExists = true; + QString filePath = organizer()->profile()->absolutePath() + "/plugins.txt"; + QFile file(filePath); + if (!file.open(QIODevice::ReadOnly)) { + pluginsTxtExists = false; + } + ON_BLOCK_EXIT([&]() { + file.close(); + }); + + if (file.size() == 0) { + // MO stores at least a header in the file. if it's completely empty the + // file is broken + pluginsTxtExists = false; + } + + QStringList activePlugins; + if (pluginsTxtExists) { + while (!file.atEnd()) { + QByteArray line = file.readLine(); + QString pluginName; + if ((line.size() > 0) && (line.at(0) != '#')) { + QStringEncoder encoder(QStringConverter::Encoding::System); + pluginName = encoder.encode(line.trimmed().constData()); + } + if (pluginName.size() > 0) { + pluginList->setState(pluginName, IPluginList::STATE_ACTIVE); + activePlugins.push_back(pluginName); + } + } + + for (const auto& pluginName : plugins) { + if (!activePlugins.contains(pluginName, Qt::CaseInsensitive)) { + pluginList->setState(pluginName, IPluginList::STATE_INACTIVE); + } + } + } else { + for (const QString& pluginName : plugins) { + pluginList->setState(pluginName, IPluginList::STATE_INACTIVE); + } + } + + return primary + plugins; +} diff --git a/libs/game_bethesda/src/gamebryo/gamebryolocalsavegames.cpp b/libs/game_bethesda/src/gamebryo/gamebryolocalsavegames.cpp new file mode 100644 index 0000000..e4df0ea --- /dev/null +++ b/libs/game_bethesda/src/gamebryo/gamebryolocalsavegames.cpp @@ -0,0 +1,138 @@ +/* +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 "gamebryolocalsavegames.h" +#include "registry.h" +#include +#include +#include +#include + +#include + +#include "gamegamebryo.h" + +// Helper to remove a key from a Bethesda-style INI file +static void removeIniValue(const QString& iniFile, const QString& section, + const QString& key) +{ + QSettings settings(iniFile, QSettings::IniFormat); + settings.beginGroup(section); + settings.remove(key); + settings.endGroup(); + settings.sync(); +} + +GamebryoLocalSavegames::GamebryoLocalSavegames(const GameGamebryo* game, + const QString& iniFileName) + : m_Game{game}, m_IniFileName(iniFileName) +{} + +MappingType GamebryoLocalSavegames::mappings(const QDir& profileSaveDir) const +{ + return {{profileSaveDir.absolutePath(), localSavesDirectory().absolutePath(), true, + true}}; +} + +QString GamebryoLocalSavegames::localSavesDummy() const +{ + return "__MO_Saves\\"; +} + +QDir GamebryoLocalSavegames::localSavesDirectory() const +{ + return QDir(m_Game->myGamesPath()).absoluteFilePath(localSavesDummy()); +} + +QDir GamebryoLocalSavegames::localGameDirectory() const +{ + return QDir(m_Game->myGamesPath()).absolutePath(); +} + +bool GamebryoLocalSavegames::prepareProfile(MOBase::IProfile* profile) +{ + bool enable = profile->localSavesEnabled(); + + QString basePath = profile->localSettingsEnabled() + ? profile->absolutePath() + : localGameDirectory().absolutePath(); + QString iniFilePath = basePath + "/" + m_IniFileName; + QString saveIni = profile->absolutePath() + "/" + "savepath.ini"; + + // Get the current sLocalSavePath + QString currentPath = + GameGamebryo::readIniValue(iniFilePath, "General", "sLocalSavePath", "SKIP_ME"); + bool alreadyEnabled = (currentPath == localSavesDummy()); + + // Get the current bUseMyGamesDirectory + QString currentMyGames = GameGamebryo::readIniValue( + iniFilePath, "General", "bUseMyGamesDirectory", "SKIP_ME"); + + // Create the __MO_Saves directory if local saves are enabled and it doesn't exist + if (enable) { + QDir saves = localSavesDirectory(); + if (!saves.exists()) { + saves.mkdir("."); + } + } + + // Set the path to __MO_Saves if it's not already + if (enable && !alreadyEnabled) { + // If the path is not blank, save it to savepath.ini + if (currentPath != "SKIP_ME") { + MOBase::WriteRegistryValue("General", "sLocalSavePath", currentPath, saveIni); + } + if (currentMyGames != "SKIP_ME") { + MOBase::WriteRegistryValue("General", "bUseMyGamesDirectory", currentMyGames, + saveIni); + } + MOBase::WriteRegistryValue("General", "sLocalSavePath", localSavesDummy(), + iniFilePath); + MOBase::WriteRegistryValue("General", "bUseMyGamesDirectory", "1", iniFilePath); + } + + // Get rid of the local saves setting if it's still there + if (!enable && alreadyEnabled) { + // If savepath.ini exists, use it and delete it + if (QFile::exists(saveIni)) { + QString savedPath = + GameGamebryo::readIniValue(saveIni, "General", "sLocalSavePath", "DELETE_ME"); + QString savedMyGames = GameGamebryo::readIniValue( + saveIni, "General", "bUseMyGamesDirectory", "DELETE_ME"); + if (savedPath != "DELETE_ME") { + MOBase::WriteRegistryValue("General", "sLocalSavePath", savedPath, iniFilePath); + } else { + removeIniValue(iniFilePath, "General", "sLocalSavePath"); + } + if (savedMyGames != "DELETE_ME") { + MOBase::WriteRegistryValue("General", "bUseMyGamesDirectory", savedMyGames, + iniFilePath); + } else { + removeIniValue(iniFilePath, "General", "bUseMyGamesDirectory"); + } + QFile::remove(saveIni); + } + // Otherwise just delete the setting + else { + removeIniValue(iniFilePath, "General", "sLocalSavePath"); + removeIniValue(iniFilePath, "General", "bUseMyGamesDirectory"); + } + } + + return enable != alreadyEnabled; +} diff --git a/libs/game_bethesda/src/gamebryo/gamebryomoddatachecker.cpp b/libs/game_bethesda/src/gamebryo/gamebryomoddatachecker.cpp new file mode 100644 index 0000000..b5bace6 --- /dev/null +++ b/libs/game_bethesda/src/gamebryo/gamebryomoddatachecker.cpp @@ -0,0 +1,76 @@ +#include + +#include "gamebryomoddatachecker.h" + +/** + * @return the list of possible folder names in data. + */ +auto GamebryoModDataChecker::possibleFolderNames() const -> const FileNameSet& +{ + static FileNameSet result{"fonts", + "interface", + "menus", + "meshes", + "music", + "scripts", + "shaders", + "sound", + "strings", + "textures", + "trees", + "video", + "facegen", + "materials", + "skse", + "obse", + "mwse", + "nvse", + "fose", + "f4se", + "distantlod", + "asi", + "SkyProc Patchers", + "Tools", + "MCM", + "icons", + "bookart", + "distantland", + "mits", + "splash", + "dllplugins", + "CalienteTools", + "NetScriptFramework", + "shadersfx"}; + return result; +} + +/** + * @return the extensions of possible files in data. + */ +auto GamebryoModDataChecker::possibleFileExtensions() const -> const FileNameSet& +{ + static FileNameSet result{"esp", "esm", "esl", "bsa", "ba2", "modgroups", "ini"}; + return result; +} + +GamebryoModDataChecker::GamebryoModDataChecker(const GameGamebryo* game) : m_Game(game) +{} + +GamebryoModDataChecker::CheckReturn GamebryoModDataChecker::dataLooksValid( + std::shared_ptr fileTree) const +{ + auto& folders = possibleFolderNames(); + auto& suffixes = possibleFileExtensions(); + for (auto entry : *fileTree) { + if (entry->isDir()) { + if (folders.count(entry->name()) > 0) { + return CheckReturn::VALID; + } + } else { + if (suffixes.count(entry->suffix()) > 0) { + return CheckReturn::VALID; + } + } + } + return CheckReturn::INVALID; +} diff --git a/libs/game_bethesda/src/gamebryo/gamebryomoddatacontent.cpp b/libs/game_bethesda/src/gamebryo/gamebryomoddatacontent.cpp new file mode 100644 index 0000000..85b66b0 --- /dev/null +++ b/libs/game_bethesda/src/gamebryo/gamebryomoddatacontent.cpp @@ -0,0 +1,116 @@ +#include "gamebryomoddatacontent.h" + +#include +#include + +GamebryoModDataContent::GamebryoModDataContent( + MOBase::IGameFeatures const* gameFeatures) + : m_GameFeatures(gameFeatures), m_Enabled(CONTENT_MODGROUP + 1, true) +{} + +std::vector +GamebryoModDataContent::getAllContents() const +{ + static std::vector GAMEBRYO_CONTENTS{ + {CONTENT_PLUGIN, QT_TR_NOOP("Plugins (ESP/ESM/ESL)"), ":/MO/gui/content/plugin"}, + {CONTENT_OPTIONAL, QT_TR_NOOP("Optional Plugins"), "", true}, + {CONTENT_INTERFACE, QT_TR_NOOP("Interface"), ":/MO/gui/content/interface"}, + {CONTENT_MESH, QT_TR_NOOP("Meshes"), ":/MO/gui/content/mesh"}, + {CONTENT_BSA, QT_TR_NOOP("Bethesda Archive"), ":/MO/gui/content/bsa"}, + {CONTENT_SCRIPT, QT_TR_NOOP("Scripts (Papyrus)"), ":/MO/gui/content/script"}, + {CONTENT_SKSE, QT_TR_NOOP("Script Extender Plugin"), ":/MO/gui/content/skse"}, + {CONTENT_SKSE_FILES, QT_TR_NOOP("Script Extender Files"), "", true}, + {CONTENT_SKYPROC, QT_TR_NOOP("SkyProc Patcher"), ":/MO/gui/content/skyproc"}, + {CONTENT_SOUND, QT_TR_NOOP("Sound or Music"), ":/MO/gui/content/sound"}, + {CONTENT_TEXTURE, QT_TR_NOOP("Textures"), ":/MO/gui/content/texture"}, + {CONTENT_MCM, QT_TR_NOOP("MCM Configuration"), ":/MO/gui/content/menu"}, + {CONTENT_INI, QT_TR_NOOP("INI Files"), ":/MO/gui/content/inifile"}, + {CONTENT_FACEGEN, QT_TR_NOOP("FaceGen Data"), ":/MO/gui/content/facegen"}, + {CONTENT_MODGROUP, QT_TR_NOOP("ModGroup Files"), ":/MO/gui/content/modgroup"}}; + + // Copy the list of enabled contents: + std::vector contents; + std::copy_if(std::begin(GAMEBRYO_CONTENTS), std::end(GAMEBRYO_CONTENTS), + std::back_inserter(contents), [this](auto e) { + return m_Enabled[e.id()]; + }); + return contents; +} + +std::vector GamebryoModDataContent::getContentsFor( + std::shared_ptr fileTree) const +{ + std::vector contents; + + for (auto e : *fileTree) { + if (e->isFile()) { + auto suffix = e->suffix().toLower(); + if (m_Enabled[CONTENT_PLUGIN] && + (suffix == "esp" || suffix == "esm" || suffix == "esl")) { + contents.push_back(CONTENT_PLUGIN); + } else if (m_Enabled[CONTENT_BSA] && (suffix == "bsa" || suffix == "ba2")) { + contents.push_back(CONTENT_BSA); + } else if (m_Enabled[CONTENT_INI] && suffix == "ini" && + e->compare("meta.ini") != 0) { + contents.push_back(CONTENT_INI); + } else if (m_Enabled[CONTENT_MODGROUP] && suffix == "modgroups") { + contents.push_back(CONTENT_MODGROUP); + } + } else { + if (m_Enabled[CONTENT_TEXTURE] && + (e->compare("textures") == 0 || e->compare("icons") == 0 || + e->compare("bookart") == 0)) { + contents.push_back(CONTENT_TEXTURE); + } else if (m_Enabled[CONTENT_MESH] && e->compare("meshes") == 0) { + contents.push_back(CONTENT_MESH); + } else if (m_Enabled[CONTENT_INTERFACE] && + (e->compare("interface") == 0 || e->compare("menus") == 0)) { + contents.push_back(CONTENT_INTERFACE); + } else if (m_Enabled[CONTENT_SOUND] && e->compare("music") == 0 || + e->compare("sound") == 0) { + contents.push_back(CONTENT_SOUND); + } else if (m_Enabled[CONTENT_SCRIPT] && e->compare("scripts") == 0) { + contents.push_back(CONTENT_SCRIPT); + } else if (m_Enabled[CONTENT_SKYPROC] && e->compare("SkyProc Patchers") == 0) { + contents.push_back(CONTENT_SKYPROC); + } else if (m_Enabled[CONTENT_MCM] && e->compare("MCM") == 0) { + contents.push_back(CONTENT_MCM); + } else if (m_Enabled[CONTENT_OPTIONAL] && e->compare("Optional") == 0 && + e->astree()->size() > 0) { + contents.push_back(CONTENT_OPTIONAL); + } + } + } + + if (m_Enabled[CONTENT_FACEGEN]) { + auto e1 = fileTree->findDirectory("meshes/actors/character/facegendata"); + if (e1) { + contents.push_back(CONTENT_FACEGEN); + } else { + auto e2 = fileTree->findDirectory("textures/actors/character/facegendata"); + if (e2) { + contents.push_back(CONTENT_FACEGEN); + } + } + } + + auto extender = m_GameFeatures->gameFeature(); + if (extender != nullptr) { + auto e = fileTree->findDirectory(extender->PluginPath()); + if (e) { + if (m_Enabled[CONTENT_SKSE_FILES]) { + contents.push_back(CONTENT_SKSE_FILES); + } + if (m_Enabled[CONTENT_SKSE]) { + for (auto f : *e) { + if (f->hasSuffix("dll")) { + contents.push_back(CONTENT_SKSE); + break; + } + } + } + } + } + + return contents; +} diff --git a/libs/game_bethesda/src/gamebryo/gamebryosavegame.cpp b/libs/game_bethesda/src/gamebryo/gamebryosavegame.cpp new file mode 100644 index 0000000..523df0d --- /dev/null +++ b/libs/game_bethesda/src/gamebryo/gamebryosavegame.cpp @@ -0,0 +1,607 @@ +#include "gamebryosavegame.h" + +#include "iplugingame.h" +#include "log.h" +#include "scriptextender.h" + +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#endif +#include +#include + +#include +#include + +#include "gamegamebryo.h" +#include "imoinfo.h" + +#define CHUNK 16384 + +GamebryoSaveGame::GamebryoSaveGame(QString const& file, GameGamebryo const* game, + bool const lightEnabled, bool const mediumEnabled) + : m_FileName(file), m_CreationTime(QFileInfo(file).lastModified()), m_Game(game), + m_MediumEnabled(mediumEnabled), m_LightEnabled(lightEnabled), + m_DataFields([this]() { + return fetchDataFields(); + }) +{} + +GamebryoSaveGame::~GamebryoSaveGame() {} + +QString GamebryoSaveGame::getFilepath() const +{ + return m_FileName; +} + +QDateTime GamebryoSaveGame::getCreationTime() const +{ + return m_CreationTime; +} + +QString GamebryoSaveGame::getName() const +{ + return QObject::tr("%1, #%2, Level %3, %4") + .arg(m_PCName) + .arg(m_SaveNumber) + .arg(m_PCLevel) + .arg(m_PCLocation); +} + +QString GamebryoSaveGame::getSaveGroupIdentifier() const +{ + return m_PCName; +} + +QStringList GamebryoSaveGame::allFiles() const +{ + // This returns all valid files associated with this game + QStringList res = {m_FileName}; + auto e = m_Game->m_Organizer->gameFeatures()->gameFeature(); + if (e != nullptr) { + QFileInfo file(m_FileName); + QFileInfo SEfile(file.absolutePath() + "/" + file.completeBaseName() + "." + + m_Game->savegameSEExtension()); + if (SEfile.exists()) { + res.push_back(SEfile.absoluteFilePath()); + } + } + return res; +} + +bool GamebryoSaveGame::hasScriptExtenderFile() const +{ + QFileInfo file(m_FileName); + QFileInfo SEfile(file.absolutePath() + "/" + file.completeBaseName() + "." + + m_Game->savegameSEExtension()); + return SEfile.exists(); +} + +void GamebryoSaveGame::setCreationTime(_SYSTEMTIME const& ctime) +{ + QDate date; + date.setDate(ctime.wYear, ctime.wMonth, ctime.wDay); + QTime time; + time.setHMS(ctime.wHour, ctime.wMinute, ctime.wSecond, ctime.wMilliseconds); + + m_CreationTime = QDateTime(date, time, Qt::UTC); +} + +GamebryoSaveGame::FileWrapper::FileWrapper(QString const& filepath, + QString const& expected) + : m_File(filepath), m_HasFieldMarkers(false), + m_PluginString(StringType::TYPE_WSTRING), + m_PluginStringFormat(StringFormat::UTF8), m_NextChunk(0) +{ + if (!m_File.open(QIODevice::ReadOnly)) { + throw std::runtime_error( + QObject::tr("failed to open %1").arg(filepath).toUtf8().constData()); + } + + std::vector fileID(expected.length() + 1); + m_File.read(fileID.data(), expected.length()); + fileID[expected.length()] = '\0'; + + QString id(fileID.data()); + if (expected != id) { + throw std::runtime_error( + QObject::tr("wrong file format - expected %1 got \'%2\' for %3") + .arg(expected) + .arg(id) + .arg(filepath) + .toUtf8() + .constData()); + } +} + +void GamebryoSaveGame::FileWrapper::setHasFieldMarkers(bool state) +{ + m_HasFieldMarkers = state; +} + +void GamebryoSaveGame::FileWrapper::setPluginString(StringType type) +{ + m_PluginString = type; +} + +void GamebryoSaveGame::FileWrapper::setPluginStringFormat(StringFormat type) +{ + m_PluginStringFormat = type; +} + +void GamebryoSaveGame::FileWrapper::readQDataStream(QDataStream& data, void* buff, + std::size_t length) +{ + int read = data.readRawData(static_cast(buff), static_cast(length)); + bool result = true; + if (read != length && m_CompressionType == 1) { + bool result = readNextChunk(); + if (result) { + read += data.readRawData(static_cast(buff) + read, + static_cast(length - read)); + } + } + if (read != length || !result) { + throw std::runtime_error("unexpected end of file"); + } +} + +template +void GamebryoSaveGame::FileWrapper::readQDataStream(QDataStream& data, T& value) +{ + static_assert(std::is_trivial_v && std::is_standard_layout_v); + readQDataStream(data, &value, sizeof(T)); +} + +void GamebryoSaveGame::FileWrapper::skipQDataStream(QDataStream& data, + std::size_t length) +{ + int skip = data.skipRawData(static_cast(length)); + bool result = true; + if (skip != length && m_CompressionType == 1) { + result = readNextChunk(); + if (result) { + skip += data.skipRawData(static_cast(length - skip)); + } + } + if (skip != length || !result) { + throw std::runtime_error("unexpected end of file"); + } +} + +template <> +void GamebryoSaveGame::FileWrapper::read(QString& value) +{ + if (m_CompressionType == 0) { + unsigned short length; + if (m_PluginString == StringType::TYPE_BSTRING || + m_PluginString == StringType::TYPE_BZSTRING) { + unsigned char len; + read(len); + length = m_PluginString == StringType::TYPE_BZSTRING ? len + 1 : len; + } else { + read(length); + } + + if (m_HasFieldMarkers) { + skip(); + } + + QByteArray buffer; + buffer.resize(length); + + read(buffer.data(), + m_PluginString == StringType::TYPE_BZSTRING ? length - 1 : length); + + if (m_PluginString == StringType::TYPE_BZSTRING) + buffer[length - 1] = '\0'; + + if (m_HasFieldMarkers) { + skip(); + } + + if (m_PluginStringFormat == StringFormat::UTF8) + value = QString::fromUtf8(buffer.constData()); + else + value = QString::fromLocal8Bit(buffer.constData()); + } else if (m_CompressionType == 1 || m_CompressionType == 2) { + unsigned short length; + if (m_PluginString == StringType::TYPE_BSTRING || + m_PluginString == StringType::TYPE_BZSTRING) { + unsigned char len; + readQDataStream(*m_Data, len); + length = m_PluginString == StringType::TYPE_BZSTRING ? len + 1 : len; + } else { + readQDataStream(*m_Data, length); + } + + if (m_HasFieldMarkers) { + skip(); + } + + QByteArray buffer; + buffer.resize(length); + + readQDataStream(*m_Data, buffer.data(), + m_PluginString == StringType::TYPE_BZSTRING ? length - 1 : length); + + if (m_PluginString == StringType::TYPE_BZSTRING) + buffer[length - 1] = '\0'; + + if (m_HasFieldMarkers) { + m_Data->skipRawData(1); + } + + if (m_PluginStringFormat == StringFormat::UTF8) + value = QString::fromUtf8(buffer.constData()); + else + value = QString::fromLocal8Bit(buffer.constData()); + } else { + MOBase::log::warn("Please create an issue on the MO github labeled \"Found unknown " + "Compressed\" with your savefile attached"); + } +} + +void GamebryoSaveGame::FileWrapper::read(void* buff, std::size_t length) +{ + int read = m_File.read(static_cast(buff), length); + if (read != length) { + throw std::runtime_error("unexpected end of file"); + } +} + +QImage GamebryoSaveGame::FileWrapper::readImage(int scale, bool alpha) +{ + uint32_t width; + read(width); + uint32_t height; + read(height); + return readImage(width, height, scale, alpha); +} + +QImage GamebryoSaveGame::FileWrapper::readImage(uint32_t width, uint32_t height, int scale, + bool alpha) +{ + int bpp = alpha ? 4 : 3; + QScopedArrayPointer buffer(new unsigned char[width * height * bpp]); + read(buffer.data(), width * height * bpp); + QImage image(buffer.data(), width, height, + alpha ? QImage::Format_RGBA8888_Premultiplied : QImage::Format_RGB888); + + // We need to copy the image here because QImage does not make a copy of the + // buffer when constructed. + if (scale != 0) { + return image.copy().scaledToWidth(scale); + } else { + return image.copy(); + } +} + +void GamebryoSaveGame::FileWrapper::setCompressionType(uint16_t compressionType) +{ + m_CompressionType = compressionType; +} + +void GamebryoSaveGame::FileWrapper::closeCompressedData() +{ + if (m_CompressionType == 0) { + } else if (m_CompressionType == 1 || m_CompressionType == 2) { + m_NextChunk = 0; + m_UncompressedSize = 0; + m_Data->device()->close(); + delete m_Data; + } else + MOBase::log::warn("Please create an issue on the MO github labeled \"Found unknown " + "Compressed\" with your savefile attached"); +} + +bool GamebryoSaveGame::FileWrapper::openCompressedData(int bytesToIgnore) +{ + if (m_CompressionType == 0) { + if (bytesToIgnore > 0) // Just to make certain + skip(bytesToIgnore); + return false; + } else if (m_CompressionType == 1) { + read(m_NextChunk); + read(m_UncompressedSize); + QByteArray placeholder; + m_Data = new QDataStream(placeholder); + bool result = readNextChunk(); + if (result) + skipQDataStream(*m_Data, bytesToIgnore); + return result; + } else if (m_CompressionType == 2) { + uint32_t uncompressedSize; + read(uncompressedSize); + uint32_t compressedSize; + read(compressedSize); + QByteArray compressed; + compressed.resize(compressedSize); + read(compressed.data(), compressedSize); + QByteArray decompressed; + decompressed.resize(uncompressedSize); + LZ4_decompress_safe_partial(compressed.data(), decompressed.data(), compressedSize, + uncompressedSize, uncompressedSize); + compressed.clear(); + + m_Data = new QDataStream(decompressed); + skipQDataStream(*m_Data, bytesToIgnore); + + return true; + } else { + MOBase::log::warn("Please create an issue on the MO github labeled \"Found unknown " + "Compressed\" with your savefile attached"); + return false; + } +} + +bool GamebryoSaveGame::FileWrapper::readNextChunk() +{ + uint32_t have; + uint64_t read = 0; + std::unique_ptr inBuffer = std::make_unique(CHUNK); + std::unique_ptr outBuffer = std::make_unique(CHUNK); + QByteArray finalData; + m_Data->device()->close(); + delete m_Data; + z_stream stream{}; + try { + stream.zalloc = Z_NULL; + stream.zfree = Z_NULL; + stream.opaque = Z_NULL; + stream.avail_in = 0; + stream.next_in = Z_NULL; + if (m_NextChunk >= m_File.size() || finalData.size() == m_UncompressedSize) + return false; + m_File.seek(m_NextChunk); + int zlibRet = inflateInit2(&stream, 15 + 32); + if (zlibRet != Z_OK) { + return false; + } + do { + stream.avail_in = m_File.read(inBuffer.get(), CHUNK); + read += stream.avail_in; + if (!m_File.isReadable()) { + (void)inflateEnd(&stream); + return false; + } + if (stream.avail_in == 0) + break; + stream.next_in = reinterpret_cast(inBuffer.get()); + do { + stream.avail_out = CHUNK; + stream.next_out = reinterpret_cast(outBuffer.get()); + zlibRet = inflate(&stream, Z_NO_FLUSH); + if ((zlibRet != Z_OK) && (zlibRet != Z_STREAM_END) && + (zlibRet != Z_BUF_ERROR)) { + return false; + } + have = CHUNK - stream.avail_out; + finalData += QByteArray::fromRawData(outBuffer.get(), have); + } while (stream.avail_out == 0); + read -= stream.avail_in; + } while (zlibRet != Z_STREAM_END); + inflateEnd(&stream); + uint64_t remainder = (m_NextChunk + read) % 16; + uint64_t next = m_NextChunk + read + 16 - (remainder == 0 ? 16 : remainder); + m_NextChunk = next; + } catch (const std::exception&) { + inflateEnd(&stream); + return false; + } + m_Data = new QDataStream(finalData); + return true; +} + +uint8_t GamebryoSaveGame::FileWrapper::readChar(int bytesToIgnore) +{ + if (m_CompressionType == 0) { + if (bytesToIgnore > 0) // Just to make certain + skip(bytesToIgnore); + uint8_t version; + read(version); + return version; + } else if (m_CompressionType == 1 || m_CompressionType == 2) { + // decompression already done by readSaveGameVersion + skipQDataStream(*m_Data, bytesToIgnore); + + uint8_t version; + readQDataStream(*m_Data, version); + return version; + + } else { + MOBase::log::warn("Please create an issue on the MO github labeled \"Found unknown " + "Compressed\" with your savefile attached"); + return 0; + } +} + +uint16_t GamebryoSaveGame::FileWrapper::readShort(int bytesToIgnore) +{ + if (m_CompressionType == 0) { + if (bytesToIgnore > 0) // Just to make certain + skip(bytesToIgnore); + uint16_t size; + read(size); + return size; + } else if (m_CompressionType == 1 || m_CompressionType == 2) { + // decompression already done by readSaveGameVersion + skipQDataStream(*m_Data, bytesToIgnore); + + uint16_t size; + readQDataStream(*m_Data, size); + return size; + } else { + MOBase::log::warn("Please create an issue on the MO github labeled \"Found unknown " + "Compressed\" with your savefile attached"); + return 0; + } +} + +uint32_t GamebryoSaveGame::FileWrapper::readInt(int bytesToIgnore) +{ + if (m_CompressionType == 0) { + if (bytesToIgnore > 0) // Just to make certain + skip(bytesToIgnore); + uint32_t size; + read(size); + return size; + } else if (m_CompressionType == 1 || m_CompressionType == 2) { + // decompression already done by readSaveGameVersion + skipQDataStream(*m_Data, bytesToIgnore); + + uint32_t size; + readQDataStream(*m_Data, size); + return size; + } else { + MOBase::log::warn("Please create an issue on the MO github labeled \"Found unknown " + "Compressed\" with your savefile attached"); + return 0; + } +} + +uint64_t GamebryoSaveGame::FileWrapper::readLong(int bytesToIgnore) +{ + if (m_CompressionType == 0) { + if (bytesToIgnore > 0) // Just to make certain + skip(bytesToIgnore); + uint64_t size; + read(size); + return size; + } else if (m_CompressionType == 1 || m_CompressionType == 2) { + // decompression already done by readSaveGameVersion + skipQDataStream(*m_Data, bytesToIgnore); + + uint64_t size; + readQDataStream(*m_Data, size); + return size; + } else { + MOBase::log::warn("Please create an issue on the MO github labeled \"Found unknown " + "Compressed\" with your savefile attached"); + return 0; + } +} + +float_t GamebryoSaveGame::FileWrapper::readFloat(int bytesToIgnore) +{ + if (m_CompressionType == 0) { + if (bytesToIgnore > 0) // Just to make certain + skip(bytesToIgnore); + float_t value; + read(value); + return value; + } else if (m_CompressionType == 1 || m_CompressionType == 2) { + // decompression already done by readSaveGameVersion + skipQDataStream(*m_Data, bytesToIgnore); + + float_t value; + readQDataStream(*m_Data, value); + return value; + } else { + MOBase::log::warn("Please create an issue on the MO github labeled \"Found unknown " + "Compressed\" with your savefile attached"); + return 0; + } +} + +QStringList GamebryoSaveGame::FileWrapper::readPlugins(int bytesToIgnore, int extraData, + const QStringList& corePlugins) +{ + if (m_CompressionType == 0) { + if (bytesToIgnore > 0) // Just to make certain + skip(bytesToIgnore); + uint8_t count; + read(count); + return readPluginData(count, extraData, corePlugins); + } else if (m_CompressionType == 1 || m_CompressionType == 2) { + skipQDataStream(*m_Data, bytesToIgnore); + uint8_t count; + readQDataStream(*m_Data, count); + return readPluginData(count, extraData, corePlugins); + } + return {}; +} + +QStringList +GamebryoSaveGame::FileWrapper::readLightPlugins(int bytesToIgnore, int extraData, + const QStringList& corePlugins) +{ + if (m_CompressionType == 0) { + if (bytesToIgnore > 0) // Just to make certain + skip(bytesToIgnore); + uint16_t count; + read(count); + return readPluginData(count, extraData, corePlugins); + } else if (m_CompressionType == 1 || m_CompressionType == 2) { + skipQDataStream(*m_Data, bytesToIgnore); + uint16_t count; + readQDataStream(*m_Data, count); + return readPluginData(count, extraData, corePlugins); + } + return {}; +} + +QStringList +GamebryoSaveGame::FileWrapper::readMediumPlugins(int bytesToIgnore, int extraData, + const QStringList& corePlugins) +{ + if (m_CompressionType != 1) { + return {}; + } else { + skipQDataStream(*m_Data, bytesToIgnore); + uint32_t count; + readQDataStream(*m_Data, count); + return readPluginData(count, extraData, corePlugins); + } +} + +QStringList GamebryoSaveGame::FileWrapper::readPluginData(uint32_t count, int extraData, + const QStringList corePlugins) +{ + QStringList plugins; + plugins.reserve(count); + if (m_CompressionType == 0) { + for (std::size_t i = 0; i < count; ++i) { + QString name; + read(name); + plugins.push_back(name); + } + } else { + for (std::size_t i = 0; i < count; ++i) { + QString name; + read(name); + plugins.push_back(name); + bool isCustomPlugin; + if (extraData) { + if (extraData > 1) { + readQDataStream(*m_Data, isCustomPlugin); + } else { + isCustomPlugin = !corePlugins.contains(name); + } + if (isCustomPlugin) { + QString creationName; + QString creationId; + uint16_t flagsSize; + uint8_t isCreation; + read(creationName); + read(creationId); + readQDataStream(*m_Data, flagsSize); + skipQDataStream(*m_Data, flagsSize); + readQDataStream(*m_Data, isCreation); + } + } + } + } + return plugins; +} + +void GamebryoSaveGame::FileWrapper::close() +{ + m_File.close(); +} diff --git a/libs/game_bethesda/src/gamebryo/gamebryosavegameinfo.cpp b/libs/game_bethesda/src/gamebryo/gamebryosavegameinfo.cpp new file mode 100644 index 0000000..6d1289a --- /dev/null +++ b/libs/game_bethesda/src/gamebryo/gamebryosavegameinfo.cpp @@ -0,0 +1,100 @@ +#include "gamebryosavegameinfo.h" + +#include "gamebryosavegame.h" +#include "gamebryosavegameinfowidget.h" +#include "gamegamebryo.h" +#include "imodinterface.h" +#include "imoinfo.h" +#include "iplugingame.h" +#include "ipluginlist.h" + +#include +#include +#include + +GamebryoSaveGameInfo::GamebryoSaveGameInfo(GameGamebryo const* game) : m_Game(game) {} + +GamebryoSaveGameInfo::~GamebryoSaveGameInfo() {} + +GamebryoSaveGameInfo::MissingAssets +GamebryoSaveGameInfo::getMissingAssets(MOBase::ISaveGame const& save) const +{ + GamebryoSaveGame const& gamebryoSave = dynamic_cast(save); + MOBase::IOrganizer* organizerCore = m_Game->m_Organizer; + + // collect the list of missing plugins + MissingAssets missingAssets; + + for (QString const& pluginName : gamebryoSave.getPlugins()) { + switch (organizerCore->pluginList()->state(pluginName)) { + case MOBase::IPluginList::STATE_INACTIVE: + missingAssets[pluginName] = + ProvidingModules{organizerCore->pluginList()->origin(pluginName)}; + break; + case MOBase::IPluginList::STATE_MISSING: + missingAssets[pluginName] = ProvidingModules(); + break; + } + } + + for (QString const& pluginName : gamebryoSave.getLightPlugins()) { + switch (organizerCore->pluginList()->state(pluginName)) { + case MOBase::IPluginList::STATE_INACTIVE: + missingAssets[pluginName] = + ProvidingModules{organizerCore->pluginList()->origin(pluginName)}; + break; + case MOBase::IPluginList::STATE_MISSING: + missingAssets[pluginName] = ProvidingModules(); + break; + } + } + + // Find out any other mods that might contain the esp/esm + QStringList espFilter({"*.esp", "*.esl", "*.esm"}); + + QString dataDir(organizerCore->managedGame()->dataDirectory().absolutePath()); + + // Search normal mods. A note: This will also find mods in data. + for (QString const& mod : organizerCore->modList()->allModsByProfilePriority()) { + MOBase::IModInterface* modInfo = organizerCore->modList()->getMod(mod); + QStringList esps = QDir(modInfo->absolutePath()).entryList(espFilter); + for (QString const& esp : esps) { + MissingAssets::iterator iter = missingAssets.find(esp); + if (modInfo->absolutePath() == dataDir) { + // We have to prune esps that reside in the data directory, otherwise + // you get all the unmanaged mods listed as potential candidates for + // enabling + if (modInfo->name() != organizerCore->pluginList()->origin(esp)) { + continue; + } + } + if (iter != missingAssets.end()) { + if (!iter->contains(modInfo->name())) { + iter->push_back(modInfo->name()); + } + } + } + } + + // search in overwrite + { + QDir overwriteDir(organizerCore->overwritePath()); + QStringList esps = overwriteDir.entryList(espFilter); + for (const QString& esp : esps) { + MissingAssets::iterator iter = missingAssets.find(esp); + if (iter != missingAssets.end()) { + if (!iter->contains("")) { + iter->push_back(""); + } + } + } + } + + return missingAssets; +} + +MOBase::ISaveGameInfoWidget* +GamebryoSaveGameInfo::getSaveGameWidget(QWidget* parent) const +{ + return new GamebryoSaveGameInfoWidget(this, parent); +} diff --git a/libs/game_bethesda/src/gamebryo/gamebryosavegameinfowidget.cpp b/libs/game_bethesda/src/gamebryo/gamebryosavegameinfowidget.cpp new file mode 100644 index 0000000..8c95dad --- /dev/null +++ b/libs/game_bethesda/src/gamebryo/gamebryosavegameinfowidget.cpp @@ -0,0 +1,199 @@ +#include "gamebryosavegameinfowidget.h" +#include "ui_gamebryosavegameinfowidget.h" + +#include "gamebryosavegame.h" +#include "gamebryosavegameinfo.h" +#include "gamegamebryo.h" +#include "imoinfo.h" +#include "ipluginlist.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +GamebryoSaveGameInfoWidget::GamebryoSaveGameInfoWidget(GamebryoSaveGameInfo const* info, + QWidget* parent) + : MOBase::ISaveGameInfoWidget(parent), ui(new Ui::GamebryoSaveGameInfoWidget), + 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); +} + +GamebryoSaveGameInfoWidget::~GamebryoSaveGameInfoWidget() +{ + delete ui; +} + +void GamebryoSaveGameInfoWidget::setSave(MOBase::ISaveGame const& save) +{ + auto& gamebryoSave = dynamic_cast(save); + ui->saveNumLabel->setText(QString("%1").arg(gamebryoSave.getSaveNumber())); + ui->characterLabel->setText(gamebryoSave.getPCName()); + ui->locationLabel->setText(gamebryoSave.getPCLocation()); + ui->levelLabel->setText(QString("%1").arg(gamebryoSave.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 = gamebryoSave.getCreationTime().toLocalTime(); + ui->dateLabel->setText( + QLocale::system().toString(t.date(), QLocale::FormatType::ShortFormat) + " " + + QLocale::system().toString(t.time())); + ui->screenshotLabel->setPixmap(QPixmap::fromImage(gamebryoSave.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(); + if (gamebryoSave.hasScriptExtenderFile()) { + QLabel* scriptExtender = new QLabel(tr("Has Script Extender Data")); + QFont headerFont = scriptExtender->font(); + headerFont.setBold(true); + layout->addWidget(scriptExtender); + } + 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 : gamebryoSave.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); + } + if (gamebryoSave.isMediumEnabled()) { + QLabel* headerEsh = new QLabel(tr("Missing ESHs")); + QFont headerEshFont = headerEsh->font(); + QFont contentEshFont = headerEshFont; + headerEshFont.setItalic(true); + contentEshFont.setBold(true); + contentEshFont.setPointSize(7); + headerEsh->setFont(headerEshFont); + layout->addWidget(headerEsh); + int countEsh = 0; + for (QString const& pluginName : gamebryoSave.getMediumPlugins()) { + if (pluginList->state(pluginName) == MOBase::IPluginList::STATE_ACTIVE) { + continue; + } + + ++countEsh; + + if (countEsh > 7) { + break; + } + + QLabel* pluginLabel = new QLabel(pluginName); + pluginLabel->setIndent(10); + pluginLabel->setFont(contentFont); + layout->addWidget(pluginLabel); + } + if (countEsh > 7) { + QLabel* dotDotLabel = new QLabel("..."); + dotDotLabel->setIndent(10); + dotDotLabel->setFont(contentFont); + layout->addWidget(dotDotLabel); + } + if (countEsh == 0) { + QLabel* dotDotLabel = new QLabel(tr("None")); + dotDotLabel->setIndent(10); + dotDotLabel->setFont(contentFont); + layout->addWidget(dotDotLabel); + } + } + if (gamebryoSave.isLightEnabled()) { + QLabel* headerEsl = new QLabel(tr("Missing ESLs")); + QFont headerEslFont = headerEsl->font(); + QFont contentEslFont = headerEslFont; + headerEslFont.setItalic(true); + contentEslFont.setBold(true); + contentEslFont.setPointSize(7); + headerEsl->setFont(headerEslFont); + layout->addWidget(headerEsl); + int countEsl = 0; + for (QString const& pluginName : gamebryoSave.getLightPlugins()) { + if (pluginList->state(pluginName) == MOBase::IPluginList::STATE_ACTIVE) { + continue; + } + + ++countEsl; + + if (countEsl > 7) { + break; + } + + QLabel* pluginLabel = new QLabel(pluginName); + pluginLabel->setIndent(10); + pluginLabel->setFont(contentFont); + layout->addWidget(pluginLabel); + } + if (countEsl > 7) { + QLabel* dotDotLabel = new QLabel("..."); + dotDotLabel->setIndent(10); + dotDotLabel->setFont(contentFont); + layout->addWidget(dotDotLabel); + } + if (countEsl == 0) { + QLabel* dotDotLabel = new QLabel(tr("None")); + dotDotLabel->setIndent(10); + dotDotLabel->setFont(contentFont); + layout->addWidget(dotDotLabel); + } + } +} diff --git a/libs/game_bethesda/src/gamebryo/gamebryoscriptextender.cpp b/libs/game_bethesda/src/gamebryo/gamebryoscriptextender.cpp new file mode 100644 index 0000000..e3f709a --- /dev/null +++ b/libs/game_bethesda/src/gamebryo/gamebryoscriptextender.cpp @@ -0,0 +1,46 @@ +#include "gamebryoscriptextender.h" + +#include "gamegamebryo.h" + +#include "utility.h" + +#include +#include + +GamebryoScriptExtender::GamebryoScriptExtender(const GameGamebryo* game) : m_Game(game) +{} + +GamebryoScriptExtender::~GamebryoScriptExtender() {} + +QString GamebryoScriptExtender::loaderName() const +{ + return BinaryName(); +} + +QString GamebryoScriptExtender::loaderPath() const +{ + return m_Game->gameDirectory().absoluteFilePath(loaderName()); +} + +QString GamebryoScriptExtender::savegameExtension() const +{ + return m_Game->savegameSEExtension(); +} + +bool GamebryoScriptExtender::isInstalled() const +{ + // A note: It is possibly also OK if xxse_steam_loader.dll exists, but it's + // not clear why that would exist and the exe not if you'd installed it per + // instructions, and it'd mess up NCC installs a treat. + return m_Game->gameDirectory().exists(loaderName()); +} + +QString GamebryoScriptExtender::getExtenderVersion() const +{ + return MOBase::getFileVersion(m_Game->gameDirectory().absoluteFilePath(loaderName())); +} + +WORD GamebryoScriptExtender::getArch() const +{ + return m_Game->getArch(loaderName()); +} diff --git a/libs/game_bethesda/src/gamebryo/gamebryounmanagedmods.cpp b/libs/game_bethesda/src/gamebryo/gamebryounmanagedmods.cpp new file mode 100644 index 0000000..e5c7608 --- /dev/null +++ b/libs/game_bethesda/src/gamebryo/gamebryounmanagedmods.cpp @@ -0,0 +1,51 @@ +#include "gamebryounmanagedmods.h" +#include "gamegamebryo.h" +#include + +GamebryoUnmangedMods::GamebryoUnmangedMods(const GameGamebryo* game) : m_Game(game) {} + +GamebryoUnmangedMods::~GamebryoUnmangedMods() {} + +QStringList GamebryoUnmangedMods::mods(bool onlyOfficial) const +{ + QStringList result; + + QStringList dlcPlugins = m_Game->DLCPlugins(); + QStringList mainPlugins = m_Game->primaryPlugins(); + + QDir dataDir(m_Game->dataDirectory()); + for (const QString& fileName : dataDir.entryList({"*.esp", "*.esl", "*.esm"})) { + if (!mainPlugins.contains(fileName, Qt::CaseInsensitive) && + (!onlyOfficial || dlcPlugins.contains(fileName, Qt::CaseInsensitive))) { + result.append(fileName.chopped(4)); // trims the extension off + } + } + + return result; +} + +QString GamebryoUnmangedMods::displayName(const QString& modName) const +{ + return modName; +} + +QFileInfo GamebryoUnmangedMods::referenceFile(const QString& modName) const +{ + QFileInfoList files = + m_Game->dataDirectory().entryInfoList(QStringList() << modName + ".es*"); + if (files.size() > 0) { + return files.at(0); + } else { + return QFileInfo(); + } +} + +QStringList GamebryoUnmangedMods::secondaryFiles(const QString& modName) const +{ + QStringList archives; + QDir dataDir = m_Game->dataDirectory(); + for (const QString& archiveName : dataDir.entryList({modName + "*.bsa"})) { + archives.append(dataDir.absoluteFilePath(archiveName)); + } + return archives; +} diff --git a/libs/game_bethesda/src/gamebryo/gamegamebryo.cpp b/libs/game_bethesda/src/gamebryo/gamegamebryo.cpp new file mode 100644 index 0000000..ac9fe22 --- /dev/null +++ b/libs/game_bethesda/src/gamebryo/gamegamebryo.cpp @@ -0,0 +1,823 @@ +#include "gamegamebryo.h" + +#include "bsainvalidation.h" +#include "dataarchives.h" +#include "gamebryomoddatacontent.h" +#include "gamebryosavegame.h" +#include "gameplugins.h" +#include "iprofile.h" +#include "log.h" +#include "registry.h" +#include "savegameinfo.h" +#include "scopeguard.h" +#include "scriptextender.h" +#include "utility.h" +#include "vdf_parser.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#ifdef _WIN32 +#include +#include +#include +#include +#include +#endif + +#ifdef HAS_NAK_FFI +#include +#endif + +#include +#include +#include + +// Case-insensitive file path resolution for Linux. +// On Windows (case-insensitive FS), just returns the input path. +// On Linux, if the exact path doesn't exist, searches the parent directory +// for a file matching the name case-insensitively. +static QString resolveFileCaseInsensitive(const QString& filePath) +{ +#ifdef _WIN32 + return filePath; +#else + if (QFileInfo::exists(filePath)) { + return filePath; + } + + QFileInfo info(filePath); + QDir dir(info.path()); + if (!dir.exists()) { + return filePath; + } + + const QString target = info.fileName(); + const QStringList entries = + dir.entryList(QDir::Files | QDir::Hidden | QDir::System); + for (const QString& entry : entries) { + if (entry.compare(target, Qt::CaseInsensitive) == 0) { + return dir.absoluteFilePath(entry); + } + } + + return filePath; +#endif +} + +GameGamebryo::GameGamebryo() {} + +void GameGamebryo::detectGame() +{ + m_GamePath = identifyGamePath(); + m_MyGamesPath = determineMyGamesPath(gameName()); +} + +bool GameGamebryo::init(MOBase::IOrganizer* moInfo) +{ + m_Organizer = moInfo; + m_Organizer->onAboutToRun([this](const auto& binary) { + return prepareIni(binary); + }); + return true; +} + +bool GameGamebryo::isInstalled() const +{ + return !m_GamePath.isEmpty(); +} + +QIcon GameGamebryo::gameIcon() const +{ + return MOBase::iconForExecutable(gameDirectory().absoluteFilePath(binaryName())); +} + +QDir GameGamebryo::gameDirectory() const +{ + return QDir(m_GamePath); +} + +QDir GameGamebryo::dataDirectory() const +{ + return gameDirectory().absoluteFilePath("Data"); +} + +void GameGamebryo::setGamePath(const QString& path) +{ + m_GamePath = path; +} + +QDir GameGamebryo::documentsDirectory() const +{ + return m_MyGamesPath; +} + +QDir GameGamebryo::savesDirectory() const +{ + return QDir(myGamesPath() + "/Saves"); +} + +std::vector> +GameGamebryo::listSaves(QDir folder) const +{ + QStringList filters; + filters << QString("*.") + savegameExtension(); + + std::vector> saves; + for (auto info : folder.entryInfoList(filters, QDir::Files)) { + try { + saves.push_back(makeSaveGame(info.filePath())); + } catch (std::exception& e) { + MOBase::log::error("{}", e.what()); + continue; + } + } + + return saves; +} + +void GameGamebryo::setGameVariant(const QString& variant) +{ + m_GameVariant = variant; +} + +QString GameGamebryo::binaryName() const +{ + return gameShortName() + ".exe"; +} + +MOBase::IPluginGame::LoadOrderMechanism GameGamebryo::loadOrderMechanism() const +{ + return LoadOrderMechanism::FileTime; +} + +MOBase::IPluginGame::SortMechanism GameGamebryo::sortMechanism() const +{ + return SortMechanism::LOOT; +} + +bool GameGamebryo::looksValid(QDir const& path) const +{ + // Check for .exe for now. + return path.exists(binaryName()); +} + +QString GameGamebryo::gameVersion() const +{ + // We try the file version, but if it looks invalid (starts with the fallback + // version), we look the product version instead. If the product version is + // not empty, we use it. + QString binaryAbsPath = gameDirectory().absoluteFilePath(binaryName()); + QString version = MOBase::getFileVersion(binaryAbsPath); + if (version.startsWith(FALLBACK_GAME_VERSION)) { + QString pversion = MOBase::getProductVersion(binaryAbsPath); + if (!pversion.isEmpty()) { + version = pversion; + } + } + return version; +} + +QString GameGamebryo::getLauncherName() const +{ + return gameShortName() + "Launcher.exe"; +} + +WORD GameGamebryo::getArch(QString const& program) const +{ +#ifdef _WIN32 + WORD arch = 0; + // This *really* needs to be factored out + std::wstring app_name = + L"\\\\?\\" + + QDir::toNativeSeparators(this->gameDirectory().absoluteFilePath(program)) + .toStdWString(); + + WIN32_FIND_DATAW FindFileData; + HANDLE hFind = ::FindFirstFileW(app_name.c_str(), &FindFileData); + + // exit if the binary was not found + if (hFind == INVALID_HANDLE_VALUE) + return arch; + + HANDLE hFile = INVALID_HANDLE_VALUE; + HANDLE hMapping = INVALID_HANDLE_VALUE; + LPVOID addrHeader = nullptr; + PIMAGE_NT_HEADERS peHdr = nullptr; + + hFile = CreateFileW(app_name.c_str(), GENERIC_READ, FILE_SHARE_READ, NULL, + OPEN_EXISTING, FILE_ATTRIBUTE_READONLY, NULL); + if (hFile == INVALID_HANDLE_VALUE) + goto cleanup; + + hMapping = CreateFileMappingW(hFile, NULL, PAGE_READONLY | SEC_IMAGE, 0, 0, + program.toStdWString().c_str()); + if (hMapping == INVALID_HANDLE_VALUE) + goto cleanup; + + addrHeader = MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0); + if (addrHeader == NULL) + goto cleanup; // couldn't memory map the file + + peHdr = ImageNtHeader(addrHeader); + if (peHdr == NULL) + goto cleanup; // couldn't read the header + + arch = peHdr->FileHeader.Machine; + +cleanup: // release all of our handles + FindClose(hFind); + if (hFile != INVALID_HANDLE_VALUE) + CloseHandle(hFile); + if (hMapping != INVALID_HANDLE_VALUE) + CloseHandle(hMapping); + return arch; +#else + Q_UNUSED(program); + return 0; +#endif +} + +QFileInfo GameGamebryo::findInGameFolder(const QString& relativePath) const +{ + return QFileInfo(m_GamePath + "/" + relativePath); +} + +QString GameGamebryo::identifyGamePath() const +{ +#ifdef HAS_NAK_FFI + // Use NaK to detect game installations on Linux + NakGameList gameList = nak_detect_all_games(); + ON_BLOCK_EXIT([&]() { + nak_game_list_free(gameList); + }); + + QString shortName = gameShortName(); + QString fullName = gameName(); + + auto tokensMatch = [](const QString& detected, const QString& candidate) { + const QString detectedLower = detected.toLower(); + const QStringList tokens = + candidate.toLower().split(QRegularExpression("[^a-z0-9]+"), + Qt::SkipEmptyParts); + bool anyToken = false; + for (const QString& token : tokens) { + if (token.size() < 3) { + continue; + } + anyToken = true; + if (!detectedLower.contains(token)) { + return false; + } + } + return anyToken; + }; + + for (size_t i = 0; i < gameList.count; ++i) { + const NakGame& game = gameList.games[i]; + QString detectedName = QString::fromUtf8(game.name); + QString detectedPath = QString::fromUtf8(game.install_path); + if (detectedName.compare(fullName, Qt::CaseInsensitive) == 0 || + detectedName.compare(shortName, Qt::CaseInsensitive) == 0 || + detectedName.contains(fullName, Qt::CaseInsensitive) || + detectedName.contains(shortName, Qt::CaseInsensitive) || + tokensMatch(detectedName, fullName) || tokensMatch(detectedName, shortName)) { + if (looksValid(QDir(detectedPath))) { + return detectedPath; + } + } + } + return {}; +#elif defined(_WIN32) + QString path = "Software\\Bethesda Softworks\\" + gameShortName(); + return findInRegistry(HKEY_LOCAL_MACHINE, path.toStdWString().c_str(), + L"Installed Path"); +#else + return {}; +#endif +} + +bool GameGamebryo::prepareIni(const QString&) +{ + const auto profile = m_Organizer->profile(); + + QString basePath = profile->localSettingsEnabled() + ? profile->absolutePath() + : documentsDirectory().absolutePath(); + + // Ensure all INI files exist with adequate content before writing settings. + // On Linux, the game launcher often doesn't work properly and can't create + // the INI files. If an INI is missing or is a stub (< 200 bytes, meaning + // only MO2-written keys exist), seed it from the game's default INI template. + ensureIniFilesExist(basePath); + + if (!iniFiles().isEmpty()) { + // Resolve case-insensitively (e.g., fallout.ini vs Fallout.ini on Linux) + QString profileIni = + resolveFileCaseInsensitive(basePath + "/" + iniFiles()[0]); + + QString setting = readIniValue(profileIni, "Launcher", "bEnableFileSelection", "0"); + if (setting.toLong() != 1) { + MOBase::WriteRegistryValue("Launcher", "bEnableFileSelection", "1", profileIni); + } + } + + return true; +} + +void GameGamebryo::ensureIniFilesExist(const QString& basePath) +{ + // Make sure the target directory exists + QDir baseDir(basePath); + if (!baseDir.exists()) { + baseDir.mkpath("."); + } + + for (const QString& iniFile : iniFiles()) { + QString targetPath = basePath + "/" + iniFile; + QFileInfo targetInfo(targetPath); + + // Check if the target file already has adequate content + // (> 200 bytes = more than just MO2's BSA invalidation stub) + if (targetInfo.exists() && targetInfo.size() > 200) { + continue; + } + +#ifndef _WIN32 + // On Linux, search for a differently-cased version of this INI file + // (e.g., FalloutPrefs.ini when we expect falloutprefs.ini). + // We can't use resolveFileCaseInsensitive here because the exact-case + // file might exist as an empty stub while the real one has different case. + QString caseMismatchPath; + { + const QString target = targetInfo.fileName(); + const QStringList entries = + baseDir.entryList(QDir::Files | QDir::Hidden | QDir::System); + for (const QString& entry : entries) { + if (entry != target && + entry.compare(target, Qt::CaseInsensitive) == 0) { + caseMismatchPath = baseDir.absoluteFilePath(entry); + break; + } + } + } + + // If a differently-cased version exists with adequate content, + // replace the stub (if any) with a symlink to the real file. + if (!caseMismatchPath.isEmpty()) { + QFileInfo altInfo(caseMismatchPath); + if (altInfo.exists() && altInfo.size() > 200) { + if (targetInfo.exists()) { + QFile::remove(targetPath); + } + QFile::link(altInfo.fileName(), targetPath); + MOBase::log::info("Created case-matching symlink '{}' -> '{}'", + iniFile, altInfo.fileName()); + continue; + } + } +#endif + + // The INI doesn't exist or is a stub, and no adequate differently-cased + // version was found. Try to seed from the game's default INI template + // (e.g., fallout_default.ini, oblivion_default.ini). + QString baseName = QFileInfo(iniFile).completeBaseName(); // "fallout" + QString defaultIniName = baseName + "_default.ini"; + QString defaultIniPath = + resolveFileCaseInsensitive(gameDirectory().absoluteFilePath(defaultIniName)); + + if (QFileInfo::exists(defaultIniPath)) { + // Remove the stub file if it exists so we can replace it + if (targetInfo.exists()) { + QFile::remove(targetPath); + } + if (QFile::copy(defaultIniPath, targetPath)) { + // Make the copy writable + QFile::setPermissions( + targetPath, + QFile::permissions(targetPath) | QFile::WriteUser | QFile::WriteOwner); + MOBase::log::info("Seeded '{}' from default INI '{}'", iniFile, defaultIniPath); + } else { + MOBase::log::warn("Failed to copy default INI '{}' -> '{}'", + defaultIniPath, targetPath); + } + } + } +} + +QString GameGamebryo::readIniValue(const QString& iniFile, const QString& section, + const QString& key, const QString& defaultValue) +{ + // Read INI values directly without QSettings to avoid QSettings + // misinterpreting backslashes as line continuations. + QFile file(iniFile); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + return defaultValue; + } + + QString sectionHeader = "[" + section + "]"; + QTextStream in(&file); + bool inSection = false; + + while (!in.atEnd()) { + QString line = in.readLine().trimmed(); + + if (line.startsWith('[') && line.endsWith(']')) { + if (inSection) { + break; // Left the target section without finding key + } + if (line.compare(sectionHeader, Qt::CaseInsensitive) == 0) { + inSection = true; + } + continue; + } + + if (inSection && !line.isEmpty() && !line.startsWith(';') && !line.startsWith('#')) { + int eqPos = line.indexOf('='); + if (eqPos > 0) { + QString existingKey = line.left(eqPos).trimmed(); + if (existingKey.compare(key, Qt::CaseInsensitive) == 0) { + return line.mid(eqPos + 1).trimmed(); + } + } + } + } + + return defaultValue; +} + +QString GameGamebryo::selectedVariant() const +{ + return m_GameVariant; +} + +QString GameGamebryo::myGamesPath() const +{ + return m_MyGamesPath; +} + +/*static*/ QString GameGamebryo::getLootPath() +{ +#ifdef _WIN32 + return findInRegistry(HKEY_LOCAL_MACHINE, L"Software\\LOOT", L"Installed Path") + + "/Loot.exe"; +#else + // On Linux, look for loot in common locations + QString flatpakPath = QDir::homePath() + "/.var/app/io.github.loot.loot/data/LOOT/Loot"; + if (QFileInfo(flatpakPath).exists()) + return flatpakPath; + // Try system PATH + QString systemLoot = QStandardPaths::findExecutable("loot"); + if (!systemLoot.isEmpty()) + return systemLoot; + return "loot"; +#endif +} + +QString GameGamebryo::localAppFolder() +{ +#ifdef _WIN32 + QString result = getKnownFolderPath(FOLDERID_LocalAppData, false); + if (result.isEmpty()) { + // fallback: try the registry + result = getSpecialPath("Local AppData"); + } + return result; +#else + return QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation); +#endif +} + +void GameGamebryo::copyToProfile(QString const& sourcePath, + QDir const& destinationDirectory, + QString const& sourceFileName) +{ + copyToProfile(sourcePath, destinationDirectory, sourceFileName, sourceFileName); +} + +void GameGamebryo::copyToProfile(QString const& sourcePath, + QDir const& destinationDirectory, + QString const& sourceFileName, + QString const& destinationFileName) +{ + QString filePath = destinationDirectory.absoluteFilePath(destinationFileName); + if (!QFileInfo(filePath).exists()) { + QString sourceFile = sourcePath + "/" + sourceFileName; + // On Linux, try case-insensitive match if the exact source doesn't exist + sourceFile = resolveFileCaseInsensitive(sourceFile); + if (!MOBase::shellCopy(sourceFile, filePath)) { + // if copy file fails, create the file empty + QFile(filePath).open(QIODevice::WriteOnly); + } + } +} + +MappingType GameGamebryo::mappings() const +{ + MappingType result; + + for (const QString& profileFile : {"plugins.txt", "loadorder.txt"}) { + result.push_back({m_Organizer->profilePath() + "/" + profileFile, + localAppFolder() + "/" + gameShortName() + "/" + profileFile, + false}); + } + + return result; +} + +#ifdef _WIN32 +std::unique_ptr GameGamebryo::getRegValue(HKEY key, LPCWSTR path, LPCWSTR value, + DWORD flags, LPDWORD type) +{ + DWORD size = 0; + HKEY subKey; + LONG res = ::RegOpenKeyExW(key, path, 0, KEY_QUERY_VALUE | KEY_WOW64_32KEY, &subKey); + if (res != ERROR_SUCCESS) { + res = ::RegOpenKeyExW(key, path, 0, KEY_QUERY_VALUE | KEY_WOW64_64KEY, &subKey); + if (res != ERROR_SUCCESS) + return std::unique_ptr(); + } + res = ::RegGetValueW(subKey, L"", value, flags, type, nullptr, &size); + if (res == ERROR_FILE_NOT_FOUND || res == ERROR_UNSUPPORTED_TYPE) { + return std::unique_ptr(); + } + if (res != ERROR_SUCCESS && res != ERROR_MORE_DATA) { + throw MOBase::MyException( + QObject::tr("failed to query registry path (preflight): %1").arg(res, 0, 16)); + } + + std::unique_ptr result(new BYTE[size]); + res = ::RegGetValueW(subKey, L"", value, flags, type, result.get(), &size); + + if (res != ERROR_SUCCESS) { + throw MOBase::MyException( + QObject::tr("failed to query registry path (read): %1").arg(res, 0, 16)); + } + + return result; +} + +QString GameGamebryo::findInRegistry(HKEY baseKey, LPCWSTR path, LPCWSTR value) +{ + std::unique_ptr buffer = + getRegValue(baseKey, path, value, RRF_RT_REG_SZ | RRF_NOEXPAND); + + return QString::fromUtf16(reinterpret_cast(buffer.get())); +} + +QString GameGamebryo::getKnownFolderPath(REFKNOWNFOLDERID folderId, bool useDefault) +{ + PWSTR path = nullptr; + ON_BLOCK_EXIT([&]() { + if (path != nullptr) + ::CoTaskMemFree(path); + }); + + if (::SHGetKnownFolderPath(folderId, useDefault ? KF_FLAG_DEFAULT_PATH : 0, NULL, + &path) == S_OK) { + return QDir::fromNativeSeparators(QString::fromWCharArray(path)); + } else { + return QString(); + } +} + +QString GameGamebryo::getSpecialPath(const QString& name) +{ + QString base = findInRegistry( + HKEY_CURRENT_USER, + L"Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders", + name.toStdWString().c_str()); + + WCHAR temp[MAX_PATH]; + if (::ExpandEnvironmentStringsW(base.toStdWString().c_str(), temp, MAX_PATH) != 0) { + return QString::fromWCharArray(temp); + } else { + return base; + } +} +#endif // _WIN32 + +QString GameGamebryo::determineMyGamesPath(const QString& gameName) +{ + const QString pattern = "%1/My Games/" + gameName; + + auto tryDir = [&](const QString& dir) -> std::optional { + if (dir.isEmpty()) { + return {}; + } + + const auto path = pattern.arg(dir); + if (!QFileInfo(path).exists()) { + return {}; + } + + return path; + }; + +#ifdef _WIN32 + // a) this is the way it should work. get the configured My Documents directory + if (auto d = tryDir(getKnownFolderPath(FOLDERID_Documents, false))) { + return *d; + } + + // b) if there is no directory there, look in the default directory + if (auto d = tryDir(getKnownFolderPath(FOLDERID_Documents, true))) { + return *d; + } + + // c) finally, look in the registry. This is discouraged + if (auto d = tryDir(getSpecialPath("Personal"))) { + return *d; + } +#else + // On Linux, My Games is inside the Wine prefix's Documents folder. + // Check common Wine prefix locations for steamuser Documents. + QStringList prefixDocPaths; + + // First check the configured prefix from MO2 settings (most reliable). + const QString configuredPrefix = + QSettings().value("fluorine/prefix_path").toString().trimmed(); + if (!configuredPrefix.isEmpty()) { + const QString configuredDocs = + QDir(configuredPrefix).filePath("drive_c/users/steamuser/Documents"); + prefixDocPaths.append(configuredDocs); + } + + // Standard Steam Proton prefix paths + QString steamRoot = QDir::homePath() + "/.steam/steam/steamapps/compatdata"; + QDir compatDir(steamRoot); + if (compatDir.exists()) { + QStringList appIds = compatDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); + for (const QString& appId : appIds) { + prefixDocPaths.append(steamRoot + "/" + appId + + "/pfx/drive_c/users/steamuser/Documents"); + } + } + + // Also check XDG Documents (for native games or manual setups) + prefixDocPaths.append( + QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation)); + + for (const QString& docPath : prefixDocPaths) { + if (auto d = tryDir(docPath)) { + MOBase::log::debug("determineMyGamesPath: found '{}' for game '{}'", *d, gameName); + return *d; + } + } + + // If no existing directory was found, try to create it in the configured prefix + // so that the game launcher can populate it on first run. + if (!configuredPrefix.isEmpty()) { + const QString configuredDocs = + QDir(configuredPrefix).filePath("drive_c/users/steamuser/Documents"); + const QString newPath = pattern.arg(configuredDocs); + if (QDir().mkpath(newPath)) { + MOBase::log::info("determineMyGamesPath: created '{}' for game '{}'", newPath, + gameName); + return newPath; + } + } + + MOBase::log::warn("determineMyGamesPath: could not find My Games path for '{}'", + gameName); +#endif + + return {}; +} + +QString GameGamebryo::parseEpicGamesLocation(const QStringList& manifests) +{ +#ifdef _WIN32 + // Use the registry entry to find the EGL Data dir first, just in case something + // changes + QString manifestDir = findInRegistry( + HKEY_LOCAL_MACHINE, L"Software\\Epic Games\\EpicGamesLauncher", L"AppDataPath"); + if (manifestDir.isEmpty()) + manifestDir = getKnownFolderPath(FOLDERID_ProgramData, false) + + "\\Epic\\EpicGamesLauncher\\Data\\"; + manifestDir += "Manifests"; +#else + // Epic Games on Linux (via Heroic or Legendary) + QString manifestDir = QDir::homePath() + + "/.config/heroic/store_cache/egl_manifests"; + if (!QDir(manifestDir).exists()) { + // Try Legendary + manifestDir = QDir::homePath() + "/.config/legendary/installed.json"; + // Legendary uses a different format - skip for now + return ""; + } +#endif + + QDir epicManifests(manifestDir, "*.item", + QDir::SortFlags(QDir::Name | QDir::IgnoreCase), QDir::Files); + if (epicManifests.exists()) { + QDirIterator it(epicManifests); + while (it.hasNext()) { + QString manifestFile = it.next(); + QFile manifest(manifestFile); + + if (!manifest.open(QIODevice::ReadOnly)) { + qWarning("Couldn't open Epic Games manifest file."); + continue; + } + + QByteArray manifestData = manifest.readAll(); + + QJsonDocument manifestJson(QJsonDocument::fromJson(manifestData)); + + if (manifests.contains(manifestJson["AppName"].toString())) { + return manifestJson["InstallLocation"].toString(); + } + } + } + return ""; +} + +QString GameGamebryo::parseSteamLocation(const QString& appid, + const QString& directoryName) +{ +#ifdef HAS_NAK_FFI + // Use NaK to find Steam path + char* steamPathC = nak_find_steam_path(); + QString steamLocation; + if (steamPathC) { + steamLocation = QString::fromUtf8(steamPathC); + nak_string_free(steamPathC); + } +#elif defined(_WIN32) + QString path = "Software\\Valve\\Steam"; + QString steamLocation = + findInRegistry(HKEY_CURRENT_USER, path.toStdWString().c_str(), L"SteamPath"); +#else + // Fallback: common Steam locations on Linux + QString steamLocation = QDir::homePath() + "/.steam/steam"; + if (!QDir(steamLocation).exists()) { + steamLocation = QDir::homePath() + "/.local/share/Steam"; + } +#endif + + if (!steamLocation.isEmpty()) { + QString steamLibraryLocation; + QString steamLibraries(steamLocation + "/" + "config" + "/" + + "libraryfolders.vdf"); + if (QFile(steamLibraries).exists()) { + std::ifstream file(steamLibraries.toStdString()); + auto root = tyti::vdf::read(file); + for (auto child : root.childs) { + tyti::vdf::object* library = child.second.get(); + auto apps = library->childs["apps"]; + if (apps->attribs.contains(appid.toStdString())) { + steamLibraryLocation = QString::fromStdString(library->attribs["path"]); + break; + } + } + } + if (!steamLibraryLocation.isEmpty()) { + if (!directoryName.isEmpty()) { + QString gameLocation = steamLibraryLocation + "/" + "steamapps" + "/" + + "common" + "/" + directoryName; + if (QDir(gameLocation).exists()) { + return gameLocation; + } + } + + // Fallback: resolve install dir from appmanifest_.acf. + QString manifestPath = steamLibraryLocation + "/steamapps/appmanifest_" + appid + + ".acf"; + if (QFile::exists(manifestPath)) { + std::ifstream manifestFile(manifestPath.toStdString()); + auto manifest = tyti::vdf::read(manifestFile); + auto appStateIt = manifest.childs.find("AppState"); + if (appStateIt != manifest.childs.end()) { + const auto& attrs = appStateIt->second->attribs; + auto installdirIt = attrs.find("installdir"); + if (installdirIt != attrs.end()) { + QString installdir = QString::fromStdString(installdirIt->second); + QString gameLocation = steamLibraryLocation + "/steamapps/common/" + installdir; + if (QDir(gameLocation).exists()) { + return gameLocation; + } + } + } + } + } + } + return ""; +} + +void GameGamebryo::registerFeature(std::shared_ptr feature) +{ + // priority does not matter, this is a game plugin so will get lowest priority in MO2 + m_Organizer->gameFeatures()->registerFeature(this, feature, 0, true); +} diff --git a/libs/game_bethesda/src/games/enderal/enderalbsainvalidation.cpp b/libs/game_bethesda/src/games/enderal/enderalbsainvalidation.cpp new file mode 100644 index 0000000..5564075 --- /dev/null +++ b/libs/game_bethesda/src/games/enderal/enderalbsainvalidation.cpp @@ -0,0 +1,16 @@ +#include "enderalbsainvalidation.h" + +EnderalBSAInvalidation::EnderalBSAInvalidation(MOBase::DataArchives* dataArchives, + MOBase::IPluginGame const* game) + : GamebryoBSAInvalidation(dataArchives, "enderal.ini", game) +{} + +QString EnderalBSAInvalidation::invalidationBSAName() const +{ + return "Enderal - Invalidation.bsa"; +} + +unsigned long EnderalBSAInvalidation::bsaVersion() const +{ + return 0x68; +} diff --git a/libs/game_bethesda/src/games/enderal/enderaldataarchives.cpp b/libs/game_bethesda/src/games/enderal/enderaldataarchives.cpp new file mode 100644 index 0000000..3ac4ee0 --- /dev/null +++ b/libs/game_bethesda/src/games/enderal/enderaldataarchives.cpp @@ -0,0 +1,42 @@ +#include "enderaldataarchives.h" +#include + +QStringList EnderalDataArchives::vanillaArchives() const +{ + return {"Skyrim - Misc.bsa", "Skyrim - Shaders.bsa", "Skyrim - Textures.bsa", + "Skyrim - Interface.bsa", "Skyrim - Animations.bsa", "Skyrim - Meshes.bsa", + "Skyrim - Sounds.bsa", "E - Meshes.bsa", "E - Music.bsa", + "E - Scripts.bsa", "E - Sounds.bsa", "E - Textures1.bsa", + "E - Textures2.bsa", "E - Textures3.bsa", "L - Textures.bsa", + "L - Voices.bsa"}; +} + +QStringList EnderalDataArchives::archives(const MOBase::IProfile* profile) const +{ + QStringList result; + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("enderal.ini") + : localGameDirectory().absoluteFilePath("enderal.ini"); + result.append(getArchivesFromKey(iniFile, "SResourceArchiveList")); + result.append(getArchivesFromKey(iniFile, "SResourceArchiveList2")); + + return result; +} + +void EnderalDataArchives::writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) +{ + QString list = before.join(", "); + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("enderal.ini") + : localGameDirectory().absoluteFilePath("enderal.ini"); + if (list.length() > 255) { + int splitIdx = list.lastIndexOf(",", 256); + setArchivesToKey(iniFile, "SResourceArchiveList", list.mid(0, splitIdx)); + setArchivesToKey(iniFile, "SResourceArchiveList2", list.mid(splitIdx + 2)); + } else { + setArchivesToKey(iniFile, "SResourceArchiveList", list); + } +} diff --git a/libs/game_bethesda/src/games/enderal/enderalgameplugins.cpp b/libs/game_bethesda/src/games/enderal/enderalgameplugins.cpp new file mode 100644 index 0000000..cd8048e --- /dev/null +++ b/libs/game_bethesda/src/games/enderal/enderalgameplugins.cpp @@ -0,0 +1,123 @@ +#include "enderalgameplugins.h" +#include +#include +#include +#include + +#include +#include +#include + +using MOBase::IOrganizer; +using MOBase::IPluginGame; +using MOBase::IPluginList; +using MOBase::reportError; +using MOBase::SafeWriteFile; + +EnderalGamePlugins::EnderalGamePlugins(IOrganizer* organizer) + : GamebryoGamePlugins(organizer) +{} + +void EnderalGamePlugins::readPluginLists(MOBase::IPluginList* pluginList) +{ + QString loadOrderPath = organizer()->profile()->absolutePath() + "/loadorder.txt"; + QString pluginsPath = organizer()->profile()->absolutePath() + "/plugins.txt"; + + bool loadOrderIsNew = !m_LastRead.isValid() || !QFileInfo(loadOrderPath).exists() || + QFileInfo(loadOrderPath).lastModified() > m_LastRead; + bool pluginsIsNew = + !m_LastRead.isValid() || QFileInfo(pluginsPath).lastModified() > m_LastRead; + + if (pluginsIsNew && !loadOrderIsNew) { + // If the plugins is new but not loadorder, we must reparse the load order from the + // plugin files + + // removed because returned loadorder was incorrect and did not account for plugins + // that were already disabled before. + /*QStringList loadOrder = readPluginList(pluginList); + pluginList->setLoadOrder(loadOrder);*/ + + // Fix me: we are ignoring order changes in plugins.txt favouring loadorder.txt in + // all cases (plugins.txt should have precedence) + QStringList loadOrder = readLoadOrderList(pluginList, loadOrderPath); + pluginList->setLoadOrder(loadOrder); + readPluginList(pluginList); + } else { + // 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); + } + + m_LastRead = QDateTime::currentDateTime(); +} + +// TODO: return value is incorrect and should be ignored (it's not currently used +QStringList EnderalGamePlugins::readPluginList(MOBase::IPluginList* pluginList) +{ + QStringList plugins = pluginList->pluginNames(); + QStringList primaryPlugins = organizer()->managedGame()->primaryPlugins(); + QStringList loadOrder(plugins); + + for (const QString& pluginName : primaryPlugins) { + if (pluginList->state(pluginName) != IPluginList::STATE_MISSING) { + pluginList->setState(pluginName, IPluginList::STATE_ACTIVE); + } + } + + // 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 (primaryPlugins.contains(plugin, Qt::CaseInsensitive)) + plugins.removeAll(plugin); + } + + // Determine plugin active state by the plugins.txt file. + bool pluginsTxtExists = true; + QString filePath = organizer()->profile()->absolutePath() + "/plugins.txt"; + QFile file(filePath); + if (!file.open(QIODevice::ReadOnly)) { + pluginsTxtExists = false; + } + ON_BLOCK_EXIT([&]() { + qDebug("close %s", qUtf8Printable(filePath)); + file.close(); + }); + + if (file.size() == 0) { + // MO stores at least a header in the file. if it's completely empty the + // file is broken + pluginsTxtExists = false; + } + + if (pluginsTxtExists) { + while (!file.atEnd()) { + QByteArray line = file.readLine(); + QString pluginName; + if ((line.size() > 0) && (line.at(0) != '#')) { + pluginName = QStringEncoder(QStringConverter::Encoding::System) + .encode(line.trimmed().constData()); + } + if (pluginName.size() > 0) { + pluginList->setState(pluginName, IPluginList::STATE_ACTIVE); + plugins.removeAll(pluginName); + // we already have the old loadorder and we ignore the positions in plugins.txt + // (needs fix) loadOrder.append(pluginName); + } + } + + file.close(); + + // we removed each plugin found in the file, so what's left are inactive mods + for (const QString& pluginName : plugins) { + pluginList->setState(pluginName, IPluginList::STATE_INACTIVE); + } + } else { + for (const QString& pluginName : plugins) { + pluginList->setState(pluginName, IPluginList::STATE_INACTIVE); + } + } + + return loadOrder; +} diff --git a/libs/game_bethesda/src/games/enderal/enderallocalsavegames.cpp b/libs/game_bethesda/src/games/enderal/enderallocalsavegames.cpp new file mode 100644 index 0000000..e01783a --- /dev/null +++ b/libs/game_bethesda/src/games/enderal/enderallocalsavegames.cpp @@ -0,0 +1,24 @@ +/* +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 "enderallocalsavegames.h" + +QString EnderalLocalSavegames::localSavesDummy() const +{ + return "..\\Enderal\\__MO_Saves\\"; +} diff --git a/libs/game_bethesda/src/games/enderal/enderalsavegame.cpp b/libs/game_bethesda/src/games/enderal/enderalsavegame.cpp new file mode 100644 index 0000000..cd9beb3 --- /dev/null +++ b/libs/game_bethesda/src/games/enderal/enderalsavegame.cpp @@ -0,0 +1,101 @@ +#include "enderalsavegame.h" + +#ifdef _WIN32 +#include +#else +#include + +using SYSTEMTIME = _SYSTEMTIME; + +static void FileTimeToSystemTime(const FILETIME* ft, SYSTEMTIME* st) +{ + const uint64_t ticks = (static_cast(ft->dwHighDateTime) << 32) | + static_cast(ft->dwLowDateTime); + const int64_t unixSecs = static_cast(ticks / 10000000ULL) - 11644473600LL; + const uint64_t remainderHns = ticks % 10000000ULL; + + const QDateTime dt = QDateTime::fromSecsSinceEpoch(unixSecs, Qt::UTC); + const QDate d = dt.date(); + const QTime t = dt.time(); + + st->wYear = static_cast(d.year()); + st->wMonth = static_cast(d.month()); + st->wDayOfWeek = static_cast(d.dayOfWeek() % 7); + st->wDay = static_cast(d.day()); + st->wHour = static_cast(t.hour()); + st->wMinute = static_cast(t.minute()); + st->wSecond = static_cast(t.second()); + st->wMilliseconds = static_cast(remainderHns / 10000); +} +#endif + +#include "gameenderal.h" + +EnderalSaveGame::EnderalSaveGame(QString const& fileName, GameEnderal const* game) + : GamebryoSaveGame(fileName, game) +{ + FileWrapper file(getFilepath(), "TESV_SAVEGAME"); + + FILETIME ftime; + fetchInformationFields(file, m_SaveNumber, m_PCName, m_PCLevel, m_PCLocation, ftime); + + // A file time is a 64-bit value that represents the number of 100-nanosecond + // intervals that have elapsed since 12:00 A.M. January 1, 1601 Coordinated Universal + // Time (UTC). So we need to convert that to something useful + SYSTEMTIME ctime; + ::FileTimeToSystemTime(&ftime, &ctime); + setCreationTime(ctime); +} + +void EnderalSaveGame::fetchInformationFields( + FileWrapper& file, uint32_t& saveNumber, QString& playerName, + unsigned short& playerLevel, QString& playerLocation, FILETIME& creationTime) const +{ + file.skip(); // header size + file.skip(); // header version + file.read(saveNumber); + + file.read(playerName); + + uint32_t temp; + file.read(temp); + playerLevel = static_cast(temp); + + file.read(playerLocation); + + QString timeOfDay; + file.read(timeOfDay); + + QString race; + file.read(race); // race name (i.e. BretonRace) + + file.skip(); // Player gender (0 = male) + file.skip(2); // experience gathered, experience required + + file.read(creationTime); +} + +std::unique_ptr EnderalSaveGame::fetchDataFields() const +{ + FileWrapper file(getFilepath(), "TESV_SAVEGAME"); + std::unique_ptr fields = std::make_unique(); + + { + QString dummyName, dummyLocation; + unsigned short dummyLevel; + uint32_t dummySaveNumber; + FILETIME dummyTime; + + fetchInformationFields(file, dummySaveNumber, dummyName, dummyLevel, dummyLocation, + dummyTime); + } + + fields->Screenshot = file.readImage(); + + file.skip(); // form version + file.skip(); // plugin info size + + fields->Plugins = file.readPlugins(); + + return fields; +} diff --git a/libs/game_bethesda/src/games/enderal/enderalscriptextender.cpp b/libs/game_bethesda/src/games/enderal/enderalscriptextender.cpp new file mode 100644 index 0000000..ff3864b --- /dev/null +++ b/libs/game_bethesda/src/games/enderal/enderalscriptextender.cpp @@ -0,0 +1,18 @@ +#include "enderalscriptextender.h" + +#include +#include + +EnderalScriptExtender::EnderalScriptExtender(GameGamebryo const* game) + : GamebryoScriptExtender(game) +{} + +QString EnderalScriptExtender::BinaryName() const +{ + return "skse_loader.exe"; +} + +QString EnderalScriptExtender::PluginPath() const +{ + return "skse/plugins"; +} diff --git a/libs/game_bethesda/src/games/enderal/gameenderal.cpp b/libs/game_bethesda/src/games/enderal/gameenderal.cpp new file mode 100644 index 0000000..fb4e1d7 --- /dev/null +++ b/libs/game_bethesda/src/games/enderal/gameenderal.cpp @@ -0,0 +1,267 @@ +#include "gameenderal.h" + +#include "enderalbsainvalidation.h" +#include "enderaldataarchives.h" +#include "enderalgameplugins.h" +#include "enderallocalsavegames.h" +#include "enderalmoddatachecker.h" +#include "enderalmoddatacontent.h" +#include "enderalsavegame.h" +#include "enderalscriptextender.h" + +#include "executableinfo.h" +#include "pluginsetting.h" +#include "steamutility.h" +#include "utility.h" + +#include +#include +#include + +#include +#include +#include + +#include +#include + +#ifdef _WIN32 +#include +#include +#endif + +#include +#include +#include +#include +#include + +using namespace MOBase; + +GameEnderal::GameEnderal() {} + +bool GameEnderal::init(IOrganizer* moInfo) +{ + if (!GameGamebryo::init(moInfo)) { + return false; + } + + auto dataArchives = std::make_shared(this); + registerFeature(std::make_shared(this)); + registerFeature(dataArchives); + registerFeature(std::make_shared(dataArchives.get(), this)); + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(this, "enderal.ini")); + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(moInfo->gameFeatures())); + registerFeature(std::make_shared(moInfo)); + registerFeature(std::make_shared(this)); + + return true; +} + +QString GameEnderal::gameName() const +{ + return "Enderal"; +} + +QList GameEnderal::executables() const +{ + return QList() + //<< ExecutableInfo("SKSE", + // findInGameFolder(feature()->loaderName())) + //<< ExecutableInfo("SBW", findInGameFolder("SBW.exe")) + << ExecutableInfo("Enderal (SKSE)", findInGameFolder(binaryName())) + << ExecutableInfo("Enderal Launcher", findInGameFolder(getLauncherName())) + << ExecutableInfo("BOSS", findInGameFolder("BOSS/BOSS.exe")) + << ExecutableInfo("LOOT", QFileInfo(getLootPath())) + .withArgument("--game=\"Enderal\"") + << ExecutableInfo("Creation Kit", findInGameFolder("CreationKit.exe")) + .withSteamAppId("202480"); +} + +QList GameEnderal::executableForcedLoads() const +{ + return QList(); +} + +QString GameEnderal::name() const +{ + return "Enderal Support Plugin"; +} + +QString GameEnderal::localizedName() const +{ + return tr("Enderal Support Plugin"); +} + +QString GameEnderal::author() const +{ + return "AL12 & MO2 Team"; +} + +QString GameEnderal::description() const +{ + return tr("Adds support for the game Enderal"); +} + +MOBase::VersionInfo GameEnderal::version() const +{ + return VersionInfo(1, 3, 1, VersionInfo::RELEASE_FINAL); +} + +QList GameEnderal::settings() const +{ + QList results; + results.push_back( + PluginSetting("sse_downloads", "allow Skyrim SE downloads", QVariant(false))); + return results; +} + +void GameEnderal::initializeProfile(const QDir& path, ProfileSettings settings) const +{ + if (settings.testFlag(IPluginGame::MODS)) { + copyToProfile(localAppFolder() + "/enderal", path, "plugins.txt"); + } + + if (settings.testFlag(IPluginGame::CONFIGURATION)) { + if (settings.testFlag(IPluginGame::PREFER_DEFAULTS) || + !QFileInfo(myGamesPath() + "/enderal.ini").exists()) { + + // there is no default ini, actually they are going to put them in for us! + copyToProfile(gameDirectory().absolutePath(), path, "enderal_default.ini", + "enderal.ini"); + copyToProfile(gameDirectory().absolutePath(), path, "enderalprefs_default.ini", + "enderalprefs.ini"); + } else { + copyToProfile(myGamesPath(), path, "enderal.ini"); + copyToProfile(myGamesPath(), path, "enderalprefs.ini"); + } + } +} + +bool GameEnderal::looksValid(QDir const& path) const +{ + // we need to check both launcher and binary because the binary also exists for + // Skyrim and the launcher for Enderal SE + return path.exists(getLauncherName()) && path.exists(binaryName()); +} + +QIcon GameEnderal::gameIcon() const +{ + return MOBase::iconForExecutable(gameDirectory().absoluteFilePath(getLauncherName())); +} + +QString GameEnderal::savegameExtension() const +{ + return "ess"; +} + +QString GameEnderal::savegameSEExtension() const +{ + return "skse"; +} + +std::shared_ptr +GameEnderal::makeSaveGame(QString filepath) const +{ + return std::make_shared(filepath, this); +} + +QString GameEnderal::steamAPPId() const +{ + return "933480"; +} + +QStringList GameEnderal::primaryPlugins() const +{ + return {"Skyrim.esm", "Enderal - Forgotten Stories.esm", "Update.esm"}; +} + +QString GameEnderal::binaryName() const +{ + return "skse_loader.exe"; +} + +QString GameEnderal::getLauncherName() const +{ + return "Enderal Launcher.exe"; +} + +QString GameEnderal::gameShortName() const +{ + return "Enderal"; +} + +QString GameEnderal::gameNexusName() const +{ + return "enderal"; +} + +QStringList GameEnderal::primarySources() const +{ + return {"Skyrim"}; +} + +QStringList GameEnderal::validShortNames() const +{ + QStringList results; + results.push_back("Skyrim"); + if (m_Organizer->pluginSetting(name(), "sse_downloads").toBool()) { + results.push_back("SkyrimSE"); + } + return results; +} + +QStringList GameEnderal::iniFiles() const +{ + return {"enderal.ini", "enderalprefs.ini"}; +} + +QStringList GameEnderal::DLCPlugins() const +{ + return {}; +} + +IPluginGame::LoadOrderMechanism GameEnderal::loadOrderMechanism() const +{ + return LoadOrderMechanism::PluginsTxt; +} + +int GameEnderal::nexusModOrganizerID() const +{ + return 0; +} + +int GameEnderal::nexusGameID() const +{ + return 2736; +} + +QString GameEnderal::identifyGamePath() const +{ +#ifdef _WIN32 + QString path = "Software\\SureAI\\Enderal"; + QString result; + try { + result = + findInRegistry(HKEY_CURRENT_USER, path.toStdWString().c_str(), L"Install_Path"); + } catch (MOBase::MyException) { + result = MOBase::findSteamGame("Enderal", "Data\\Enderal - Forgotten Stories.esm"); + } + return result; +#else + // Prefer exact Steam app-id resolution for Enderal LE. + QString result = parseSteamLocation(steamAPPId(), "Enderal"); + if (!result.isEmpty() && looksValid(QDir(result))) { + return result; + } + + result = MOBase::findSteamGame("Enderal", "Data/Enderal - Forgotten Stories.esm"); + if (!result.isEmpty() && looksValid(QDir(result))) { + return result; + } + + return GameGamebryo::identifyGamePath(); +#endif +} diff --git a/libs/game_bethesda/src/games/enderalse/enderalsedataarchives.cpp b/libs/game_bethesda/src/games/enderalse/enderalsedataarchives.cpp new file mode 100644 index 0000000..606c688 --- /dev/null +++ b/libs/game_bethesda/src/games/enderalse/enderalsedataarchives.cpp @@ -0,0 +1,64 @@ +#include "enderalsedataarchives.h" + +#include "iprofile.h" +#include + +QStringList EnderalSEDataArchives::vanillaArchives() const +{ + return {"Skyrim - Textures0.bsa", + "Skyrim - Textures1.bsa", + "Skyrim - Textures2.bsa", + "Skyrim - Textures3.bsa", + "Skyrim - Textures4.bsa", + "Skyrim - Textures5.bsa", + "Skyrim - Textures6.bsa", + "Skyrim - Textures7.bsa", + "Skyrim - Textures8.bsa", + "Skyrim - Meshes0.bsa", + "Skyrim - Meshes1.bsa", + "Skyrim - Voices_en0.bsa", + "Skyrim - Sounds.bsa", + "Skyrim - Interface.bsa", + "Skyrim - Animations.bsa", + "Skyrim - Shaders.bsa", + "Skyrim - Misc.bsa", + "E - Meshes.bsa", + "E - SE.bsa", + "E - Scripts.bsa", + "E - Sounds.bsa", + "E - Textures1.bsa", + "E - Textures2.bsa", + "E - Textures3.bsa", + "L - Textures.bsa", + "L - Voices.bsa"}; +} + +QStringList EnderalSEDataArchives::archives(const MOBase::IProfile* profile) const +{ + QStringList result; + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("enderal.ini") + : localGameDirectory().absoluteFilePath("enderal.ini"); + result.append(getArchivesFromKey(iniFile, "SResourceArchiveList", 512)); + result.append(getArchivesFromKey(iniFile, "SResourceArchiveList2", 512)); + + return result; +} + +void EnderalSEDataArchives::writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) +{ + QString list = before.join(","); + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("enderal.ini") + : localGameDirectory().absoluteFilePath("enderal.ini"); + if (list.length() > 511) { + int splitIdx = list.lastIndexOf(",", 512); + setArchivesToKey(iniFile, "SResourceArchiveList", list.mid(0, splitIdx)); + setArchivesToKey(iniFile, "SResourceArchiveList2", list.mid(splitIdx + 2)); + } else { + setArchivesToKey(iniFile, "SResourceArchiveList", list); + } +} diff --git a/libs/game_bethesda/src/games/enderalse/enderalsegameplugins.cpp b/libs/game_bethesda/src/games/enderalse/enderalsegameplugins.cpp new file mode 100644 index 0000000..d7be8ce --- /dev/null +++ b/libs/game_bethesda/src/games/enderalse/enderalsegameplugins.cpp @@ -0,0 +1,82 @@ +#include "enderalsegameplugins.h" + +#include +#include +#include +#include + +#include + +using namespace MOBase; + +void EnderalSEGamePlugins::writePluginList(const MOBase::IPluginList* pluginList, + const QString& filePath) +{ + SafeWriteFile file(filePath); + + QStringEncoder encoder(QStringConverter::Encoding::System); + + file->resize(0); + + file->write( + encoder.encode("# This file was automatically generated by Mod Organizer.\r\n")); + + 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); + }); + + QStringList PrimaryPlugins = organizer()->managedGame()->primaryPlugins(); + QStringList DLCPlugins = organizer()->managedGame()->DLCPlugins(); + QSet ManagedMods = + QSet(PrimaryPlugins.begin(), PrimaryPlugins.end()); + QSet DLCSet = QSet(DLCPlugins.begin(), DLCPlugins.end()); + ManagedMods.subtract(DLCSet); + PrimaryPlugins.append(QList(ManagedMods.begin(), ManagedMods.end())); + + // we need to force some plugins because those are not force-loaded + // by the game but are considered primary plugins for users + file->write("*Enderal - Forgotten Stories.esm\r\n"); + file->write("*SkyUI_SE.esp\r\n"); + + // TODO: do not write plugins in OFFICIAL_FILES container + for (const QString& pluginName : plugins) { + if (!PrimaryPlugins.contains(pluginName, Qt::CaseInsensitive)) { + if (pluginList->state(pluginName) == IPluginList::STATE_ACTIVE) { + auto result = encoder.encode(pluginName); + if (encoder.hasError()) { + invalidFileNames = true; + qCritical("invalid plugin name %s", qUtf8Printable(pluginName)); + } else { + file->write("*"); + file->write(result); + } + file->write("\r\n"); + ++writtenCount; + } else { + auto result = encoder.encode(pluginName); + if (encoder.hasError()) { + invalidFileNames = true; + qCritical("invalid plugin name %s", qUtf8Printable(pluginName)); + } else { + file->write(result); + } + file->write("\r\n"); + ++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.")); + } + + file->commit(); +} diff --git a/libs/game_bethesda/src/games/enderalse/enderalselocalsavegames.cpp b/libs/game_bethesda/src/games/enderalse/enderalselocalsavegames.cpp new file mode 100644 index 0000000..fafa739 --- /dev/null +++ b/libs/game_bethesda/src/games/enderalse/enderalselocalsavegames.cpp @@ -0,0 +1,24 @@ +/* +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 "enderalselocalsavegames.h" + +QString EnderalSELocalSavegames::localSavesDummy() const +{ + return "..\\Enderal Special Edition\\__MO_Saves\\"; +} diff --git a/libs/game_bethesda/src/games/enderalse/enderalsesavegame.cpp b/libs/game_bethesda/src/games/enderalse/enderalsesavegame.cpp new file mode 100644 index 0000000..2662505 --- /dev/null +++ b/libs/game_bethesda/src/games/enderalse/enderalsesavegame.cpp @@ -0,0 +1,148 @@ +#include "enderalsesavegame.h" + +#ifdef _WIN32 +#include +#else +#include + +union _ULARGE_INTEGER { + struct { + uint32_t LowPart; + uint32_t HighPart; + }; + uint64_t QuadPart; +}; + +using SYSTEMTIME = _SYSTEMTIME; + +static void FileTimeToSystemTime(const FILETIME* ft, SYSTEMTIME* st) +{ + const uint64_t ticks = (static_cast(ft->dwHighDateTime) << 32) | + static_cast(ft->dwLowDateTime); + const int64_t unixSecs = static_cast(ticks / 10000000ULL) - 11644473600LL; + const uint64_t remainderHns = ticks % 10000000ULL; + + const QDateTime dt = QDateTime::fromSecsSinceEpoch(unixSecs, Qt::UTC); + const QDate d = dt.date(); + const QTime t = dt.time(); + + st->wYear = static_cast(d.year()); + st->wMonth = static_cast(d.month()); + st->wDayOfWeek = static_cast(d.dayOfWeek() % 7); + st->wDay = static_cast(d.day()); + st->wHour = static_cast(t.hour()); + st->wMinute = static_cast(t.minute()); + st->wSecond = static_cast(t.second()); + st->wMilliseconds = static_cast(remainderHns / 10000); +} +#endif + +EnderalSESaveGame::EnderalSESaveGame(QString const& fileName, GameEnderalSE const* game) + : GamebryoSaveGame(fileName, game, true) +{ + FileWrapper file(fileName, "TESV_SAVEGAME"); // 10bytes + + uint32_t version; + FILETIME ftime; + fetchInformationFields(file, version, m_PCName, m_PCLevel, m_PCLocation, m_SaveNumber, + ftime); + + // A file time is a 64-bit value that represents the number of 100-nanosecond + // intervals that have elapsed since 12:00 A.M. January 1, 1601 Coordinated Universal + // Time (UTC). So we need to convert that to something useful + + // For some reason, the file time is off by about 6 hours. + // So we need to subtract those 6 hours from the filetime. + _ULARGE_INTEGER time; + time.LowPart = ftime.dwLowDateTime; + time.HighPart = ftime.dwHighDateTime; + time.QuadPart -= 2.16e11; + ftime.dwHighDateTime = time.HighPart; + ftime.dwLowDateTime = time.LowPart; + + SYSTEMTIME ctime; + ::FileTimeToSystemTime(&ftime, &ctime); + + setCreationTime(ctime); +} + +void EnderalSESaveGame::fetchInformationFields( + FileWrapper& file, uint32_t& version, QString& playerName, + unsigned short& playerLevel, QString& playerLocation, uint32_t& saveNumber, + FILETIME& creationTime) const +{ + uint32_t headerSize; + file.read(headerSize); // header size "TESV_SAVEGAME" + file.read(version); + file.read(saveNumber); + file.read(playerName); + + uint32_t temp; + file.read(temp); + playerLevel = static_cast(temp); + file.read(playerLocation); + + QString timeOfDay; + file.read(timeOfDay); + + QString race; + file.read(race); // race name (i.e. BretonRace) + + file.skip(); // Player gender (0 = male) + file.skip(2); // experience gathered, experience required + + file.read(creationTime); // filetime +} + +std::unique_ptr EnderalSESaveGame::fetchDataFields() const +{ + FileWrapper file(getFilepath(), "TESV_SAVEGAME"); // 10bytes + + uint32_t version = 0; + { + QString dummyName, dummyLocation; + unsigned short dummyLevel; + uint32_t dummySaveNumber; + FILETIME dummyTime; + + fetchInformationFields(file, version, dummyName, dummyLevel, dummyLocation, + dummySaveNumber, dummyTime); + } + + std::unique_ptr fields = std::make_unique(); + + uint32_t width; + uint32_t height; + file.read(width); + file.read(height); + + bool alpha = false; + + // compatibility between LE and SE: + // SE has an additional uin16_t for compression + // SE uses an alpha channel, whereas LE does not + if (version == 12) { + uint16_t compressionType; + file.read(compressionType); + file.setCompressionType(compressionType); + alpha = true; + } + + fields->Screenshot = file.readImage(width, height, 320, alpha); + + file.openCompressedData(); + + uint8_t saveGameVersion = file.readChar(); + uint8_t pluginInfoSize = file.readChar(); + uint16_t other = file.readShort(); // Unknown + + fields->Plugins = file.readPlugins(1); // Just empty data + + if (saveGameVersion >= 78) { + fields->LightPlugins = file.readLightPlugins(); + } + + file.closeCompressedData(); + + return fields; +} diff --git a/libs/game_bethesda/src/games/enderalse/enderalsescriptextender.cpp b/libs/game_bethesda/src/games/enderalse/enderalsescriptextender.cpp new file mode 100644 index 0000000..f7a3113 --- /dev/null +++ b/libs/game_bethesda/src/games/enderalse/enderalsescriptextender.cpp @@ -0,0 +1,18 @@ +#include "enderalsescriptextender.h" + +#include +#include + +EnderalSEScriptExtender::EnderalSEScriptExtender(GameGamebryo const* game) + : GamebryoScriptExtender(game) +{} + +QString EnderalSEScriptExtender::BinaryName() const +{ + return "skse64_loader.exe"; +} + +QString EnderalSEScriptExtender::PluginPath() const +{ + return "skse/plugins"; +} diff --git a/libs/game_bethesda/src/games/enderalse/enderalseunmanagedmods.cpp b/libs/game_bethesda/src/games/enderalse/enderalseunmanagedmods.cpp new file mode 100644 index 0000000..9f11b44 --- /dev/null +++ b/libs/game_bethesda/src/games/enderalse/enderalseunmanagedmods.cpp @@ -0,0 +1,29 @@ +#include "enderalseunmanagedmods.h" + +EnderalSEUnmangedMods::EnderalSEUnmangedMods(const GameGamebryo* game) + : GamebryoUnmangedMods(game) +{} + +EnderalSEUnmangedMods::~EnderalSEUnmangedMods() {} + +QStringList EnderalSEUnmangedMods::mods(bool onlyOfficial) const +{ + QStringList result; + + QStringList pluginList = game()->primaryPlugins(); + QStringList otherPlugins = game()->DLCPlugins(); + otherPlugins.append(game()->CCPlugins()); + for (QString plugin : otherPlugins) { + pluginList.removeAll(plugin); + } + QDir dataDir(game()->dataDirectory()); + for (const QString& fileName : dataDir.entryList({"*.esp", "*.esl", "*.esm"})) { + if (!pluginList.contains(fileName, Qt::CaseInsensitive)) { + if (!onlyOfficial || pluginList.contains(fileName, Qt::CaseInsensitive)) { + result.append(fileName.chopped(4)); // trims the extension off + } + } + } + + return result; +} diff --git a/libs/game_bethesda/src/games/enderalse/gameenderalse.cpp b/libs/game_bethesda/src/games/enderalse/gameenderalse.cpp new file mode 100644 index 0000000..26587b8 --- /dev/null +++ b/libs/game_bethesda/src/games/enderalse/gameenderalse.cpp @@ -0,0 +1,347 @@ +#include "gameenderalse.h" + +#include "enderalsedataarchives.h" +#include "enderalsegameplugins.h" +#include "enderalselocalsavegames.h" +#include "enderalsemoddatachecker.h" +#include "enderalsemoddatacontent.h" +#include "enderalsesavegame.h" +#include "enderalsescriptextender.h" +#include "enderalseunmanagedmods.h" +#include "steamutility.h" + +#include "versioninfo.h" +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "scopeguard.h" +#include + +using namespace MOBase; + +GameEnderalSE::GameEnderalSE() {} + +void GameEnderalSE::setVariant(QString variant) +{ + m_GameVariant = variant; +} + +void GameEnderalSE::checkVariants() +{ + QFileInfo gog_dll(m_GamePath + "\\Galaxy64.dll"); + if (gog_dll.exists()) + setVariant("GOG"); + else + setVariant("Steam"); +} + +QDir GameEnderalSE::documentsDirectory() const +{ + return m_MyGamesPath; +} + +void GameEnderalSE::detectGame() +{ + m_GamePath = identifyGamePath(); + checkVariants(); + m_MyGamesPath = determineMyGamesPath(gameDirectoryName()); +} + +QString GameEnderalSE::identifyGamePath() const +{ +#ifdef _WIN32 + QMap paths = { + {"Software\\Bethesda Softworks\\" + gameName(), "Installed Path"}, + {"Software\\GOG.com\\Games\\1708684988", "path"}, + }; + QString result; + try { + for (auto& path : paths.toStdMap()) { + result = findInRegistry(HKEY_LOCAL_MACHINE, path.first.toStdWString().c_str(), + path.second.toStdWString().c_str()); + if (!result.isEmpty()) + break; + } + } catch (MOBase::MyException) { + result = MOBase::findSteamGame("Enderal Special Edition", + "Data\\Enderal - Forgotten Stories.esm"); + } + return result; +#else + // Prefer exact Steam app-id resolution for Enderal SE. + QString result = parseSteamLocation("976620", "Enderal Special Edition"); + if (!result.isEmpty() && looksValid(QDir(result))) { + return result; + } + + result = MOBase::findSteamGame("Enderal Special Edition", + "Data/Enderal - Forgotten Stories.esm"); + if (!result.isEmpty() && looksValid(QDir(result))) { + return result; + } + + return GameGamebryo::identifyGamePath(); +#endif +} + +void GameEnderalSE::setGamePath(const QString& path) +{ + m_GamePath = path; + checkVariants(); + m_MyGamesPath = determineMyGamesPath(gameDirectoryName()); +} + +QDir GameEnderalSE::savesDirectory() const +{ + return QDir(m_MyGamesPath + "/Saves"); +} + +bool GameEnderalSE::isInstalled() const +{ + return !m_GamePath.isEmpty(); +} + +bool GameEnderalSE::init(IOrganizer* moInfo) +{ + if (!GameGamebryo::init(moInfo)) { + return false; + } + + auto dataArchives = std::make_shared(this); + registerFeature(std::make_shared(this)); + registerFeature(dataArchives); + registerFeature(std::make_shared(this, "enderal.ini")); + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(moInfo->gameFeatures())); + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(moInfo)); + registerFeature(std::make_shared(this)); + + return true; +} + +QString GameEnderalSE::gameName() const +{ + return "Enderal Special Edition"; +} + +QString GameEnderalSE::gameDirectoryName() const +{ + if (selectedVariant() == "GOG") + return "Enderal Special Edition GOG"; + else + return "Enderal Special Edition"; +} + +QIcon GameEnderalSE::gameIcon() const +{ + return MOBase::iconForExecutable(gameDirectory().absoluteFilePath(getLauncherName())); +} + +QList GameEnderalSE::executables() const +{ + return QList() + << ExecutableInfo("Enderal Special Edition (SKSE)", + findInGameFolder(m_Organizer->gameFeatures() + ->gameFeature() + ->loaderName())) + << ExecutableInfo("Enderal Special Edition Launcher", + findInGameFolder(getLauncherName())) + << ExecutableInfo("Creation Kit", findInGameFolder("CreationKit.exe")) + .withSteamAppId("1946180") + << ExecutableInfo("LOOT", QFileInfo(getLootPath())) + .withArgument("--game=\"Enderal Special Edition\""); +} + +QList GameEnderalSE::executableForcedLoads() const +{ + return QList(); +} + +QString GameEnderalSE::binaryName() const +{ + return "skse64_loader.exe"; +} + +QString GameEnderalSE::getLauncherName() const +{ + return "Enderal Launcher.exe"; +} + +bool GameEnderalSE::looksValid(const QDir& folder) const +{ + // we need to check both launcher and binary because the binary also exists for + // Skyrim SE and the launcher for Enderal LE + return folder.exists(getLauncherName()) && folder.exists(binaryName()); +} + +QFileInfo GameEnderalSE::findInGameFolder(const QString& relativePath) const +{ + return QFileInfo(m_GamePath + "/" + relativePath); +} + +QString GameEnderalSE::name() const +{ + return "Enderal Special Edition Support Plugin"; +} + +QString GameEnderalSE::localizedName() const +{ + return tr("Enderal Special Edition Support Plugin"); +} + +QString GameEnderalSE::author() const +{ + return "Archost, ZachHaber & MO2 Team"; +} + +QString GameEnderalSE::description() const +{ + return tr("Adds support for the game Enderal Special Edition."); +} + +MOBase::VersionInfo GameEnderalSE::version() const +{ + return VersionInfo(1, 2, 0, VersionInfo::RELEASE_FINAL); +} + +QList GameEnderalSE::settings() const +{ + return QList(); +} + +void GameEnderalSE::initializeProfile(const QDir& path, ProfileSettings settings) const +{ + if (settings.testFlag(IPluginGame::MODS)) { + copyToProfile(localAppFolder() + gameDirectoryName(), path, "plugins.txt"); + } + + if (settings.testFlag(IPluginGame::CONFIGURATION)) { + if (settings.testFlag(IPluginGame::PREFER_DEFAULTS) || + !QFileInfo(myGamesPath() + "/Enderal.ini").exists()) { + + // there is no default ini, actually they are going to put them in for us! + copyToProfile(gameDirectory().absolutePath(), path, "enderal_default.ini", + "Enderal.ini"); + copyToProfile(gameDirectory().absolutePath(), path, "enderalprefs_default.ini", + "EnderalPrefs.ini"); + } else { + copyToProfile(myGamesPath(), path, "Enderal.ini"); + copyToProfile(myGamesPath(), path, "EnderalPrefs.ini"); + } + } +} + +QString GameEnderalSE::savegameExtension() const +{ + return "ess"; +} + +QString GameEnderalSE::savegameSEExtension() const +{ + return "skse"; +} + +std::shared_ptr +GameEnderalSE::makeSaveGame(QString filePath) const +{ + return std::make_shared(filePath, this); +} + +QString GameEnderalSE::steamAPPId() const +{ + if (selectedVariant() == "Steam") + return "976620"; + return ""; +} + +QStringList GameEnderalSE::primaryPlugins() const +{ + return {"skyrim.esm", "update.esm", "dawnguard.esm", "hearthfires.esm", + "dragonborn.esm", + + // these two plugins are considered "primary" for users but are not + // automatically loaded by the game so we need to force-write them + // to the plugin list + "enderal - forgotten stories.esm", "skyui_se.esp"}; +} + +QStringList GameEnderalSE::DLCPlugins() const +{ + return {}; +} + +QStringList GameEnderalSE::gameVariants() const +{ + return {"Steam", "GOG"}; +} + +QString GameEnderalSE::gameShortName() const +{ + return "EnderalSE"; +} + +QStringList GameEnderalSE::validShortNames() const +{ + return {"Skyrim", "SkyrimSE", "Enderal"}; +} + +QString GameEnderalSE::gameNexusName() const +{ + return "enderalspecialedition"; +} + +QStringList GameEnderalSE::iniFiles() const +{ + return {"Enderal.ini", "EnderalPrefs.ini"}; +} + +QStringList GameEnderalSE::CCPlugins() const +{ + return {}; +} + +IPluginGame::LoadOrderMechanism GameEnderalSE::loadOrderMechanism() const +{ + return IPluginGame::LoadOrderMechanism::PluginsTxt; +} + +int GameEnderalSE::nexusModOrganizerID() const +{ + return 0; +} + +int GameEnderalSE::nexusGameID() const +{ + return 3685; +} + +QDir GameEnderalSE::gameDirectory() const +{ + return QDir(m_GamePath); +} + +// Not to delete all the spaces... +MappingType GameEnderalSE::mappings() const +{ + MappingType result; + + for (const QString& profileFile : {"plugins.txt", "loadorder.txt"}) { + result.push_back({m_Organizer->profilePath() + "/" + profileFile, + localAppFolder() + "/" + gameDirectoryName() + "/" + profileFile, + false}); + } + + return result; +} diff --git a/libs/game_bethesda/src/games/fallout3/fallout3bsainvalidation.cpp b/libs/game_bethesda/src/games/fallout3/fallout3bsainvalidation.cpp new file mode 100644 index 0000000..ce981fb --- /dev/null +++ b/libs/game_bethesda/src/games/fallout3/fallout3bsainvalidation.cpp @@ -0,0 +1,16 @@ +#include "fallout3bsainvalidation.h" + +Fallout3BSAInvalidation::Fallout3BSAInvalidation(MOBase::DataArchives* dataArchives, + MOBase::IPluginGame const* game) + : GamebryoBSAInvalidation(dataArchives, "fallout.ini", game) +{} + +QString Fallout3BSAInvalidation::invalidationBSAName() const +{ + return "Fallout - Invalidation.bsa"; +} + +unsigned long Fallout3BSAInvalidation::bsaVersion() const +{ + return 0x68; +} diff --git a/libs/game_bethesda/src/games/fallout3/fallout3dataarchives.cpp b/libs/game_bethesda/src/games/fallout3/fallout3dataarchives.cpp new file mode 100644 index 0000000..ed6627b --- /dev/null +++ b/libs/game_bethesda/src/games/fallout3/fallout3dataarchives.cpp @@ -0,0 +1,33 @@ +#include "fallout3dataarchives.h" + +#include "iprofile.h" +#include + +QStringList Fallout3DataArchives::vanillaArchives() const +{ + return {"Fallout - Textures.bsa", "Fallout - Meshes.bsa", "Fallout - Voices.bsa", + "Fallout - Sound.bsa", "Fallout - MenuVoices.bsa", "Fallout - Misc.bsa"}; +} + +QStringList Fallout3DataArchives::archives(const MOBase::IProfile* profile) const +{ + QStringList result; + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("fallout.ini") + : localGameDirectory().absoluteFilePath("fallout.ini"); + result.append(getArchivesFromKey(iniFile, "SArchiveList")); + + return result; +} + +void Fallout3DataArchives::writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) +{ + QString list = before.join(", "); + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("fallout.ini") + : localGameDirectory().absoluteFilePath("fallout.ini"); + setArchivesToKey(iniFile, "SArchiveList", list); +} diff --git a/libs/game_bethesda/src/games/fallout3/fallout3savegame.cpp b/libs/game_bethesda/src/games/fallout3/fallout3savegame.cpp new file mode 100644 index 0000000..aa9bcde --- /dev/null +++ b/libs/game_bethesda/src/games/fallout3/fallout3savegame.cpp @@ -0,0 +1,72 @@ +#include "fallout3savegame.h" + +#include "gamefallout3.h" + +Fallout3SaveGame::Fallout3SaveGame(QString const& fileName, GameFallout3 const* game) + : GamebryoSaveGame(fileName, game) +{ + FileWrapper file(getFilepath(), "FO3SAVEGAME"); + + uint32_t width, height; + fetchInformationFields(file, width, height, m_SaveNumber, m_PCName, m_PCLevel, + m_PCLocation); +} + +void Fallout3SaveGame::fetchInformationFields(FileWrapper& file, uint32_t& width, + uint32_t& height, + uint32_t& saveNumber, + QString& playerName, + unsigned short& playerLevel, + QString& playerLocation) const +{ + file.skip(); // Save header size + + file.setHasFieldMarkers(true); + file.setPluginString(GamebryoSaveGame::StringType::TYPE_BZSTRING); + + file.skip(); // File version ? + file.skip(); // delimiter + + file.read(width); + file.read(height); + file.read(saveNumber); + file.read(playerName); + + QString whatthis; + file.read(whatthis); + + long level; + file.read(level); + playerLevel = level; + + file.read(playerLocation); +} + +std::unique_ptr Fallout3SaveGame::fetchDataFields() const +{ + FileWrapper file(getFilepath(), "FO3SAVEGAME"); + + std::unique_ptr fields = std::make_unique(); + + uint32_t width, height; + { + QString dummyName, dummyLocation; + unsigned short dummyLevel; + uint32_t dummySaveNumber; + + fetchInformationFields(file, width, height, dummySaveNumber, dummyName, dummyLevel, + dummyLocation); + } + + QString playtime; + file.read(playtime); + + fields->Screenshot = file.readImage(width, height, 256); + + file.skip(5); // unknown (1 byte), plugin size (4 bytes) + + file.setPluginString(GamebryoSaveGame::StringType::TYPE_BSTRING); + fields->Plugins = file.readPlugins(); + + return fields; +} diff --git a/libs/game_bethesda/src/games/fallout3/fallout3scriptextender.cpp b/libs/game_bethesda/src/games/fallout3/fallout3scriptextender.cpp new file mode 100644 index 0000000..608a305 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout3/fallout3scriptextender.cpp @@ -0,0 +1,18 @@ +#include "fallout3scriptextender.h" + +#include +#include + +Fallout3ScriptExtender::Fallout3ScriptExtender(GameGamebryo const* game) + : GamebryoScriptExtender(game) +{} + +QString Fallout3ScriptExtender::BinaryName() const +{ + return "fose_loader.exe"; +} + +QString Fallout3ScriptExtender::PluginPath() const +{ + return "fose/plugins"; +} diff --git a/libs/game_bethesda/src/games/fallout3/gamefallout3.cpp b/libs/game_bethesda/src/games/fallout3/gamefallout3.cpp new file mode 100644 index 0000000..f2f4562 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout3/gamefallout3.cpp @@ -0,0 +1,279 @@ +#include "gamefallout3.h" + +#include "fallout3bsainvalidation.h" +#include "fallout3dataarchives.h" +#include "fallout3moddatachecker.h" +#include "fallout3moddatacontent.h" +#include "fallout3savegame.h" +#include "fallout3scriptextender.h" + +#include "executableinfo.h" +#include "pluginsetting.h" +#include "versioninfo.h" +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace MOBase; + +GameFallout3::GameFallout3() {} + +bool GameFallout3::init(IOrganizer* moInfo) +{ + if (!GameGamebryo::init(moInfo)) { + return false; + } + + auto dataArchives = std::make_shared(this); + registerFeature(std::make_shared(this)); + registerFeature(dataArchives); + registerFeature(std::make_shared(dataArchives.get(), this)); + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(this, "fallout.ini")); + registerFeature(std::make_shared(this)); + registerFeature( + std::make_shared(m_Organizer->gameFeatures())); + registerFeature(std::make_shared(moInfo)); + registerFeature(std::make_shared(this)); + return true; +} + +QString GameFallout3::identifyVariant() const +{ + if (QFile::exists(m_GamePath + "/Fallout3ng.exe")) { + return "Low Violence"; + } else if (QFile::exists(m_GamePath + "/Galaxy.dll")) { + return "GOG"; + } else if (QFile::exists(m_GamePath + "/FalloutLauncherEpic.exe")) { + return "Epic Games"; + } else if (m_GamePath.endsWith("Fallout 3 goty")) { + return "Steam (Game of the Year)"; + } else { + return "Steam (Regular)"; + } +} + +QString GameFallout3::identifyGamePath() const +{ + // Steam (Regular) + auto result = parseSteamLocation("22300", "Fallout 3"); + + // Steam (Game of the Year) + if (result.isEmpty()) { + result = parseSteamLocation("22370", "Fallout 3 goty"); + } + + // Epic Games + if (result.isEmpty()) { + // Fallout 3: Game of the Year Edition: adeae8bbfc94427db57c7dfecce3f1d4 + result = parseEpicGamesLocation({"adeae8bbfc94427db57c7dfecce3f1d4"}); + if (QFileInfo(result).isDir()) { + QDir startPath = QDir(result); + auto subDirs = + startPath.entryList({"Fallout 3 GOTY*"}, QDir::Dirs | QDir::NoDotAndDotDot); + if (!subDirs.isEmpty()) + result = startPath.absoluteFilePath(subDirs.first()); + } + } + + // GOG (and Steam) + if (result.isEmpty()) { + result = GameGamebryo::identifyGamePath(); + } + + return result; +} + +QString GameFallout3::gameName() const +{ + return "Fallout 3"; +} + +void GameFallout3::detectGame() +{ + m_GamePath = identifyGamePath(); + setGameVariant(identifyVariant()); + m_MyGamesPath = determineMyGamesPath("Fallout3"); +} + +QList GameFallout3::executables() const +{ + return QList() + << ExecutableInfo("FOSE", + findInGameFolder(m_Organizer->gameFeatures() + ->gameFeature() + ->loaderName())) + << ExecutableInfo("Fallout 3", findInGameFolder(binaryName())) + << ExecutableInfo("Fallout Mod Manager", findInGameFolder("fomm/fomm.exe")) + << ExecutableInfo("Construction Kit", findInGameFolder("geck.exe")) + << ExecutableInfo("Fallout Launcher", findInGameFolder(getLauncherName())) + << ExecutableInfo("BOSS", findInGameFolder("BOSS/BOSS.exe")) + << ExecutableInfo("LOOT", QFileInfo(getLootPath())) + .withArgument("--game=\"Fallout3\""); +} + +QList GameFallout3::executableForcedLoads() const +{ + return QList(); +} + +QString GameFallout3::name() const +{ + return "Fallout 3 Support Plugin"; +} + +QString GameFallout3::localizedName() const +{ + return tr("Fallout 3 Support Plugin"); +} + +QString GameFallout3::author() const +{ + return "Tannin"; +} + +QString GameFallout3::description() const +{ + return tr("Adds support for the game Fallout 3."); +} + +MOBase::VersionInfo GameFallout3::version() const +{ + return VersionInfo(1, 4, 1, VersionInfo::RELEASE_FINAL); +} + +QList GameFallout3::settings() const +{ + return QList(); +} + +void GameFallout3::initializeProfile(const QDir& path, ProfileSettings settings) const +{ + if (settings.testFlag(IPluginGame::MODS)) { + copyToProfile(localAppFolder() + "/Fallout3", path, "plugins.txt"); + copyToProfile(localAppFolder() + "/Fallout3", path, "loadorder.txt"); + } + + if (settings.testFlag(IPluginGame::CONFIGURATION)) { + if (settings.testFlag(IPluginGame::PREFER_DEFAULTS) || + !QFileInfo(myGamesPath(), "fallout.ini").exists()) { + copyToProfile(gameDirectory().absolutePath(), path, "fallout_default.ini", + "fallout.ini"); + } else { + copyToProfile(myGamesPath(), path, "fallout.ini"); + } + + copyToProfile(myGamesPath(), path, "falloutprefs.ini"); + copyToProfile(myGamesPath(), path, "FalloutCustom.ini"); + copyToProfile(myGamesPath(), path, "custom.ini"); + copyToProfile(myGamesPath(), path, "GECKCustom.ini"); + copyToProfile(myGamesPath(), path, "GECKPrefs.ini"); + } +} + +QString GameFallout3::savegameExtension() const +{ + return "fos"; +} + +QString GameFallout3::savegameSEExtension() const +{ + return ""; +} + +std::shared_ptr +GameFallout3::makeSaveGame(QString filePath) const +{ + return std::make_shared(filePath, this); +} + +QString GameFallout3::steamAPPId() const +{ + if (selectedVariant() == "Steam (Game Of The Year)") { + return "22370"; + } else if (selectedVariant() == "Steam (Regular)") { + return "22300"; + } else { + return ""; + } +} + +QStringList GameFallout3::primaryPlugins() const +{ + return {"fallout3.esm"}; +} + +QStringList GameFallout3::gameVariants() const +{ + return {"Steam (Regular)", "Steam (Game Of The Year)", "Epic Games", "GOG", + "Low Violence"}; +} + +QString GameFallout3::binaryName() const +{ + if (selectedVariant() == "Low Violence") { + return "Fallout3ng.exe"; + } else { + return GameGamebryo::binaryName(); + } +} + +QString GameFallout3::gameShortName() const +{ + return "Fallout3"; +} + +QStringList GameFallout3::validShortNames() const +{ + return {"FalloutNV"}; +} + +QString GameFallout3::gameNexusName() const +{ + return "fallout3"; +} + +QStringList GameFallout3::iniFiles() const +{ + return {"fallout.ini", "falloutprefs.ini", "FalloutCustom.ini", "GECKCustom.ini", + "GECKPrefs.ini"}; +} + +QStringList GameFallout3::DLCPlugins() const +{ + return {"ThePitt.esm", "Anchorage.esm", "BrokenSteel.esm", "PointLookout.esm", + "Zeta.esm"}; +} + +int GameFallout3::nexusModOrganizerID() const +{ + return 16348; +} + +int GameFallout3::nexusGameID() const +{ + return 120; +} + +QString GameFallout3::getLauncherName() const +{ + const QMap names = { + {"Steam (Regular)", "Fallout3Launcher.exe"}, + {"Steam (Game of the Year)", "Fallout3Launcher.exe"}, + {"Epic Games", "FalloutLauncherEpic.exe"}, + {"GOG", "FalloutLauncher.exe"}, + {"Low Violence", "Fallout3Launcher.exe"}}; + + return names.value(selectedVariant()); +} diff --git a/libs/game_bethesda/src/games/fallout4/fallout4bsainvalidation.cpp b/libs/game_bethesda/src/games/fallout4/fallout4bsainvalidation.cpp new file mode 100644 index 0000000..7711522 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4/fallout4bsainvalidation.cpp @@ -0,0 +1,68 @@ +#include "fallout4bsainvalidation.h" + +#include "dummybsa.h" +#include "iplugingame.h" +#include "iprofile.h" +#include "registry.h" +#include +#include + +#include "gamegamebryo.h" + +Fallout4BSAInvalidation::Fallout4BSAInvalidation(MOBase::DataArchives* dataArchives, + MOBase::IPluginGame const* game) + : GamebryoBSAInvalidation(dataArchives, "Fallout4Custom.ini", game) +{ + m_IniFileName = "Fallout4Custom.ini"; + m_Game = game; +} + +bool Fallout4BSAInvalidation::isInvalidationBSA(const QString& bsaName) +{ + return false; +} + +QString Fallout4BSAInvalidation::invalidationBSAName() const +{ + return ""; +} + +unsigned long Fallout4BSAInvalidation::bsaVersion() const +{ + return 0x68; +} + +bool Fallout4BSAInvalidation::prepareProfile(MOBase::IProfile* profile) +{ + bool dirty = false; + QString basePath = profile->localSettingsEnabled() + ? profile->absolutePath() + : m_Game->documentsDirectory().absolutePath(); + QString iniFilePath = basePath + "/" + m_IniFileName; + + if (profile->invalidationActive(nullptr)) { + // write bInvalidateOlderFiles = 1, if needed + QString bInvalidateOlderFiles = GameGamebryo::readIniValue( + iniFilePath, "Archive", "bInvalidateOlderFiles", "0"); + if (bInvalidateOlderFiles.toLong() != 1) { + dirty = true; + if (!MOBase::WriteRegistryValue("Archive", "bInvalidateOlderFiles", "1", + iniFilePath)) { + qWarning("failed to override data directory in \"%s\"", + qUtf8Printable(m_IniFileName)); + } + } + QString sResourceDataDirsFinal = GameGamebryo::readIniValue( + iniFilePath, "Archive", "sResourceDataDirsFinal", "STRINGS\\"); + if (sResourceDataDirsFinal != "") { + dirty = true; + if (!MOBase::WriteRegistryValue("Archive", "sResourceDataDirsFinal", "", + iniFilePath)) { + qWarning("failed to override data directory in \"%s\"", + qUtf8Printable(m_IniFileName)); + } + } + } + + return dirty; +} diff --git a/libs/game_bethesda/src/games/fallout4/fallout4dataarchives.cpp b/libs/game_bethesda/src/games/fallout4/fallout4dataarchives.cpp new file mode 100644 index 0000000..c0931ce --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4/fallout4dataarchives.cpp @@ -0,0 +1,48 @@ +#include "fallout4dataarchives.h" + +#include "iprofile.h" +#include + +QStringList Fallout4DataArchives::vanillaArchives() const +{ + return {"Fallout4 - Textures1.ba2", "Fallout4 - Textures2.ba2", + "Fallout4 - Textures3.ba2", "Fallout4 - Textures4.ba2", + "Fallout4 - Textures5.ba2", "Fallout4 - Textures6.ba2", + "Fallout4 - Textures7.ba2", "Fallout4 - Textures8.ba2", + "Fallout4 - Textures9.ba2", "Fallout4 - Meshes.ba2", + "Fallout4 - MeshesExtra.ba2", "Fallout4 - Voices.ba2", + "Fallout4 - Sounds.ba2", "Fallout4 - Interface.ba2", + "Fallout4 - Animations.ba2", "Fallout4 - Materials.ba2", + "Fallout4 - Shaders.ba2", "Fallout4 - Startup.ba2", + "Fallout4 - Misc.ba2"}; +} + +QStringList Fallout4DataArchives::archives(const MOBase::IProfile* profile) const +{ + QStringList result; + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("fallout4.ini") + : localGameDirectory().absoluteFilePath("fallout4.ini"); + result.append(getArchivesFromKey(iniFile, "SResourceArchiveList")); + result.append(getArchivesFromKey(iniFile, "SResourceArchiveList2")); + + return result; +} + +void Fallout4DataArchives::writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) +{ + QString list = before.join(", "); + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("fallout4.ini") + : localGameDirectory().absoluteFilePath("fallout4.ini"); + if (list.length() > 255) { + int splitIdx = list.lastIndexOf(",", 256); + setArchivesToKey(iniFile, "SResourceArchiveList", list.mid(0, splitIdx)); + setArchivesToKey(iniFile, "SResourceArchiveList2", list.mid(splitIdx + 2)); + } else { + setArchivesToKey(iniFile, "SResourceArchiveList", list); + } +} diff --git a/libs/game_bethesda/src/games/fallout4/fallout4savegame.cpp b/libs/game_bethesda/src/games/fallout4/fallout4savegame.cpp new file mode 100644 index 0000000..2f88d1b --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4/fallout4savegame.cpp @@ -0,0 +1,106 @@ +#include "fallout4savegame.h" + +#ifdef _WIN32 +#include +#else +#include + +using SYSTEMTIME = _SYSTEMTIME; + +static void FileTimeToSystemTime(const FILETIME* ft, SYSTEMTIME* st) +{ + const uint64_t ticks = (static_cast(ft->dwHighDateTime) << 32) | + static_cast(ft->dwLowDateTime); + const int64_t unixSecs = static_cast(ticks / 10000000ULL) - 11644473600LL; + const uint64_t remainderHns = ticks % 10000000ULL; + + const QDateTime dt = QDateTime::fromSecsSinceEpoch(unixSecs, Qt::UTC); + const QDate d = dt.date(); + const QTime t = dt.time(); + + st->wYear = static_cast(d.year()); + st->wMonth = static_cast(d.month()); + st->wDayOfWeek = static_cast(d.dayOfWeek() % 7); + st->wDay = static_cast(d.day()); + st->wHour = static_cast(t.hour()); + st->wMinute = static_cast(t.minute()); + st->wSecond = static_cast(t.second()); + st->wMilliseconds = static_cast(remainderHns / 10000); +} +#endif + +#include "gamefallout4.h" + +Fallout4SaveGame::Fallout4SaveGame(QString const& fileName, GameFallout4 const* game) + : GamebryoSaveGame(fileName, game, true) +{ + FileWrapper file(getFilepath(), "FO4_SAVEGAME"); + + FILETIME creationTime; + fetchInformationFields(file, m_SaveNumber, m_PCName, m_PCLevel, m_PCLocation, + creationTime); + + // A file time is a 64-bit value that represents the number of 100-nanosecond + // intervals that have elapsed since 12:00 A.M. January 1, 1601 Coordinated Universal + // Time (UTC). So we need to convert that to something useful + SYSTEMTIME ctime; + ::FileTimeToSystemTime(&creationTime, &ctime); + + setCreationTime(ctime); +} + +void Fallout4SaveGame::fetchInformationFields( + FileWrapper& file, uint32_t& saveNumber, QString& playerName, + unsigned short& playerLevel, QString& playerLocation, FILETIME& creationTime) const +{ + file.skip(); // header size + file.skip(); // header version + file.read(saveNumber); + + file.read(playerName); + + uint32_t temp; + file.read(temp); + playerLevel = static_cast(temp); + file.read(playerLocation); + + QString ignore; + file.read(ignore); // playtime as ascii hh.mm.ss + file.read(ignore); // race name (i.e. BretonRace) + + file.skip(); // Player gender (0 = male) + file.skip(2); // experience gathered, experience required + + file.read(creationTime); +} + +std::unique_ptr Fallout4SaveGame::fetchDataFields() const +{ + FileWrapper file(getFilepath(), "FO4_SAVEGAME"); // 10bytes + + { + QString dummyName, dummyLocation; + unsigned short dummyLevel; + uint32_t dummySaveNumber; + FILETIME dummyTime; + + fetchInformationFields(file, dummySaveNumber, dummyName, dummyLevel, dummyLocation, + dummyTime); + } + + QString ignore; + std::unique_ptr fields = std::make_unique(); + + fields->Screenshot = file.readImage(384, true); + + uint8_t saveGameVersion = file.readChar(); + file.read(ignore); // game version + file.skip(); // plugin info size + + fields->Plugins = file.readPlugins(); + if (saveGameVersion >= 68) { + fields->LightPlugins = file.readLightPlugins(); + } + + return fields; +} diff --git a/libs/game_bethesda/src/games/fallout4/fallout4scriptextender.cpp b/libs/game_bethesda/src/games/fallout4/fallout4scriptextender.cpp new file mode 100644 index 0000000..1207d5e --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4/fallout4scriptextender.cpp @@ -0,0 +1,18 @@ +#include "fallout4scriptextender.h" + +#include +#include + +Fallout4ScriptExtender::Fallout4ScriptExtender(GameGamebryo const* game) + : GamebryoScriptExtender(game) +{} + +QString Fallout4ScriptExtender::BinaryName() const +{ + return "f4se_loader.exe"; +} + +QString Fallout4ScriptExtender::PluginPath() const +{ + return "f4se/plugins"; +} diff --git a/libs/game_bethesda/src/games/fallout4/fallout4unmanagedmods.cpp b/libs/game_bethesda/src/games/fallout4/fallout4unmanagedmods.cpp new file mode 100644 index 0000000..61b7b2a --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4/fallout4unmanagedmods.cpp @@ -0,0 +1,63 @@ +#include "fallout4unmanagedmods.h" + +Fallout4UnmangedMods::Fallout4UnmangedMods(const GameGamebryo* game) + : GamebryoUnmangedMods(game) +{} + +Fallout4UnmangedMods::~Fallout4UnmangedMods() {} + +QStringList Fallout4UnmangedMods::mods(bool onlyOfficial) const +{ + QStringList result; + + QStringList pluginList = game()->primaryPlugins(); + QStringList otherPlugins = game()->DLCPlugins(); + otherPlugins.append(game()->CCPlugins()); + for (QString plugin : otherPlugins) { + pluginList.removeAll(plugin); + } + QDir dataDir(game()->dataDirectory()); + for (const QString& fileName : dataDir.entryList({"*.esp", "*.esl", "*.esm"})) { + if (!pluginList.contains(fileName, Qt::CaseInsensitive)) { + if (!onlyOfficial || pluginList.contains(fileName, Qt::CaseInsensitive)) { + result.append(fileName.chopped(4)); // trims the extension off + } + } + } + + return result; +} + +QStringList Fallout4UnmangedMods::secondaryFiles(const QString& modName) const +{ + // file extension in FO4 is .ba2 instead of bsa + QStringList archives; + QDir dataDir = game()->dataDirectory(); + for (const QString& archiveName : dataDir.entryList({modName + "*.ba2"})) { + archives.append(dataDir.absoluteFilePath(archiveName)); + } + return archives; +} + +QString Fallout4UnmangedMods::displayName(const QString& modName) const +{ + // unlike in earlier games, in fallout 4 the file name doesn't correspond to + // the public name + if (modName.compare("dlcrobot", Qt::CaseInsensitive) == 0) { + return "Automatron"; + } else if (modName.compare("dlcworkshop01", Qt::CaseInsensitive) == 0) { + return "Wasteland Workshop"; + } else if (modName.compare("dlccoast", Qt::CaseInsensitive) == 0) { + return "Far Harbor"; + } else if (modName.compare("dlcworkshop02", Qt::CaseInsensitive) == 0) { + return "Contraptions Workshop"; + } else if (modName.compare("dlcworkshop03", Qt::CaseInsensitive) == 0) { + return "Vault-Tec Workshop"; + } else if (modName.compare("dlcnukaworld", Qt::CaseInsensitive) == 0) { + return "Nuka-World"; + } else if (modName.compare("dlcultrahighresolution", Qt::CaseInsensitive) == 0) { + return "Ultra High Resolution Texture Pack"; + } else { + return modName; + } +} diff --git a/libs/game_bethesda/src/games/fallout4/gamefallout4.cpp b/libs/game_bethesda/src/games/fallout4/gamefallout4.cpp new file mode 100644 index 0000000..8939a3c --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4/gamefallout4.cpp @@ -0,0 +1,334 @@ +#include "gamefallout4.h" + +#include "fallout4bsainvalidation.h" +#include "fallout4dataarchives.h" +#include "fallout4moddatachecker.h" +#include "fallout4moddatacontent.h" +#include "fallout4savegame.h" +#include "fallout4scriptextender.h" +#include "fallout4unmanagedmods.h" + +#include "versioninfo.h" +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "scopeguard.h" + +using namespace MOBase; + +const unsigned int GameFallout4::PROBLEM_TEST_FILE; + +GameFallout4::GameFallout4() {} + +bool GameFallout4::init(IOrganizer* moInfo) +{ + if (!GameGamebryo::init(moInfo)) { + return false; + } + + auto dataArchives = std::make_shared(this); + + registerFeature(std::make_shared(this)); + registerFeature(dataArchives); + registerFeature(std::make_shared(this, "fallout4custom.ini")); + registerFeature(std::make_shared(this)); + registerFeature( + std::make_shared(m_Organizer->gameFeatures())); + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(moInfo)); + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(dataArchives.get(), this)); + + return true; +} + +QString GameFallout4::gameName() const +{ + return "Fallout 4"; +} + +void GameFallout4::detectGame() +{ + m_GamePath = identifyGamePath(); + m_MyGamesPath = determineMyGamesPath("Fallout4"); +} + +QList GameFallout4::executables() const +{ + return QList() + << ExecutableInfo("F4SE", + findInGameFolder(m_Organizer->gameFeatures() + ->gameFeature() + ->loaderName())) + << ExecutableInfo("Fallout 4", findInGameFolder(binaryName())) + << ExecutableInfo("Fallout Launcher", findInGameFolder(getLauncherName())) + << ExecutableInfo("Creation Kit", findInGameFolder("CreationKit.exe")) + .withSteamAppId("1946160") + << ExecutableInfo("LOOT", QFileInfo(getLootPath())) + .withArgument("--game=\"Fallout4\""); +} + +QList GameFallout4::executableForcedLoads() const +{ + return QList(); +} + +QString GameFallout4::name() const +{ + return "Fallout 4 Support Plugin"; +} + +QString GameFallout4::localizedName() const +{ + return tr("Fallout 4 Support Plugin"); +} + +QString GameFallout4::author() const +{ + return "Tannin & MO2 Team"; +} + +QString GameFallout4::description() const +{ + return tr("Adds support for the game Fallout 4.\n" + "Splash by %1") + .arg("nekoyoubi"); +} + +MOBase::VersionInfo GameFallout4::version() const +{ + return VersionInfo(1, 8, 0, VersionInfo::RELEASE_FINAL); +} + +QList GameFallout4::settings() const +{ + return QList(); +} + +MappingType GameFallout4::mappings() const +{ + MappingType result; + if (testFilePlugins().isEmpty()) { + for (const QString& profileFile : {"plugins.txt", "loadorder.txt"}) { + result.push_back({m_Organizer->profilePath() + "/" + profileFile, + localAppFolder() + "/" + gameShortName() + "/" + profileFile, + false}); + } + } + return result; +} + +void GameFallout4::initializeProfile(const QDir& path, ProfileSettings settings) const +{ + if (settings.testFlag(IPluginGame::MODS)) { + copyToProfile(localAppFolder() + "/Fallout4", path, "plugins.txt"); + } + + if (settings.testFlag(IPluginGame::CONFIGURATION)) { + if (settings.testFlag(IPluginGame::PREFER_DEFAULTS) || + !QFileInfo(myGamesPath() + "/fallout4.ini").exists()) { + copyToProfile(gameDirectory().absolutePath(), path, "fallout4_default.ini", + "fallout4.ini"); + } else { + copyToProfile(myGamesPath(), path, "fallout4.ini"); + } + + copyToProfile(myGamesPath(), path, "fallout4prefs.ini"); + copyToProfile(myGamesPath(), path, "fallout4custom.ini"); + } +} + +QString GameFallout4::savegameExtension() const +{ + return "fos"; +} + +QString GameFallout4::savegameSEExtension() const +{ + return "f4se"; +} + +std::shared_ptr +GameFallout4::makeSaveGame(QString filePath) const +{ + return std::make_shared(filePath, this); +} + +QString GameFallout4::steamAPPId() const +{ + return "377160"; +} + +QStringList GameFallout4::testFilePlugins() const +{ + QStringList plugins; + if (m_Organizer != nullptr && m_Organizer->profile() != nullptr) { + QString customIni( + m_Organizer->profile()->absoluteIniFilePath("Fallout4Custom.ini")); + if (QFile(customIni).exists()) { + for (int i = 1; i <= 10; ++i) { + QString setting("sTestFile"); + setting += std::to_string(i); + QString plugin = GameGamebryo::readIniValue(customIni, "General", setting, ""); + if (!plugin.isEmpty() && !plugins.contains(plugin, Qt::CaseInsensitive)) { + plugins.append(plugin); + } + } + } + } + return plugins; +} + +QStringList GameFallout4::primaryPlugins() const +{ + QStringList plugins = {"fallout4.esm", "dlcrobot.esm", + "dlcworkshop01.esm", "dlccoast.esm", + "dlcworkshop02.esm", "dlcworkshop03.esm", + "dlcnukaworld.esm", "dlcultrahighresolution.esm"}; + + auto testPlugins = testFilePlugins(); + if (loadOrderMechanism() == LoadOrderMechanism::None) { + plugins << testPlugins; + } else { + plugins << CCPlugins(); + } + + return plugins; +} + +QStringList GameFallout4::gameVariants() const +{ + return {"Regular"}; +} + +QString GameFallout4::gameShortName() const +{ + return "Fallout4"; +} + +QString GameFallout4::gameNexusName() const +{ + return "fallout4"; +} + +QStringList GameFallout4::iniFiles() const +{ + return {"fallout4.ini", "fallout4prefs.ini", "fallout4custom.ini"}; +} + +QStringList GameFallout4::DLCPlugins() const +{ + return {"dlcrobot.esm", + "dlcworkshop01.esm", + "dlccoast.esm", + "dlcworkshop02.esm", + "dlcworkshop03.esm", + "dlcnukaworld.esm", + "dlcultrahighresolution.esm"}; +} + +QStringList GameFallout4::CCPlugins() const +{ + QStringList plugins = {}; + QFile file(gameDirectory().absoluteFilePath("Fallout4.ccc")); + if (file.open(QIODevice::ReadOnly)) { + ON_BLOCK_EXIT([&file]() { + file.close(); + }); + + if (file.size() == 0) { + return plugins; + } + while (!file.atEnd()) { + QByteArray line = file.readLine().trimmed(); + QString modName; + if ((line.size() > 0) && (line.at(0) != '#')) { + modName = QString::fromUtf8(line.constData()).toLower(); + } + + if (modName.size() > 0) { + if (!plugins.contains(modName, Qt::CaseInsensitive)) { + plugins.append(modName); + } + } + } + } + return plugins; +} + +IPluginGame::SortMechanism GameFallout4::sortMechanism() const +{ + if (!testFilePresent()) + return IPluginGame::SortMechanism::LOOT; + return IPluginGame::SortMechanism::NONE; +} + +IPluginGame::LoadOrderMechanism GameFallout4::loadOrderMechanism() const +{ + if (!testFilePresent()) + return IPluginGame::LoadOrderMechanism::PluginsTxt; + return IPluginGame::LoadOrderMechanism::None; +} + +int GameFallout4::nexusModOrganizerID() const +{ + return 28715; +} + +int GameFallout4::nexusGameID() const +{ + return 1151; +} + +// Start Diagnose +std::vector GameFallout4::activeProblems() const +{ + std::vector result; + if (m_Organizer->managedGame() == this) { + if (testFilePresent()) + result.push_back(PROBLEM_TEST_FILE); + } + return result; +} + +bool GameFallout4::testFilePresent() const +{ + if (!testFilePlugins().isEmpty()) + return true; + return false; +} + +QString GameFallout4::shortDescription(unsigned int key) const +{ + switch (key) { + case PROBLEM_TEST_FILE: + return tr("sTestFile entries are present"); + } + return QString(); +} + +QString GameFallout4::fullDescription(unsigned int key) const +{ + switch (key) { + case PROBLEM_TEST_FILE: { + return tr("

You have sTestFile settings in your " + "Fallout4Custom.ini. These must be removed or " + "the game will not read the plugins.txt file. " + "Management is disabled.

"); + } + } + return QString(); +} diff --git a/libs/game_bethesda/src/games/fallout4london/fo4londonbsainvalidation.cpp b/libs/game_bethesda/src/games/fallout4london/fo4londonbsainvalidation.cpp new file mode 100644 index 0000000..45df74f --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4london/fo4londonbsainvalidation.cpp @@ -0,0 +1,68 @@ +#include "fo4londonbsainvalidation.h" + +#include "dummybsa.h" +#include "iplugingame.h" +#include "iprofile.h" +#include "registry.h" +#include +#include + +#include "gamegamebryo.h" + +Fallout4LondonBSAInvalidation::Fallout4LondonBSAInvalidation( + MOBase::DataArchives* dataArchives, MOBase::IPluginGame const* game) + : GamebryoBSAInvalidation(dataArchives, "Fallout4Custom.ini", game) +{ + m_IniFileName = "Fallout4Custom.ini"; + m_Game = game; +} + +bool Fallout4LondonBSAInvalidation::isInvalidationBSA(const QString& bsaName) +{ + return false; +} + +QString Fallout4LondonBSAInvalidation::invalidationBSAName() const +{ + return ""; +} + +unsigned long Fallout4LondonBSAInvalidation::bsaVersion() const +{ + return 0x68; +} + +bool Fallout4LondonBSAInvalidation::prepareProfile(MOBase::IProfile* profile) +{ + bool dirty = false; + QString basePath = profile->localSettingsEnabled() + ? profile->absolutePath() + : m_Game->documentsDirectory().absolutePath(); + QString iniFilePath = basePath + "/" + m_IniFileName; + + if (profile->invalidationActive(nullptr)) { + // write bInvalidateOlderFiles = 1, if needed + QString bInvalidateOlderFiles = GameGamebryo::readIniValue( + iniFilePath, "Archive", "bInvalidateOlderFiles", "0"); + if (bInvalidateOlderFiles.toLong() != 1) { + dirty = true; + if (!MOBase::WriteRegistryValue("Archive", "bInvalidateOlderFiles", "1", + iniFilePath)) { + qWarning("failed to override data directory in \"%s\"", + qUtf8Printable(m_IniFileName)); + } + } + QString sResourceDataDirsFinal = GameGamebryo::readIniValue( + iniFilePath, "Archive", "sResourceDataDirsFinal", "STRINGS\\"); + if (sResourceDataDirsFinal != "") { + dirty = true; + if (!MOBase::WriteRegistryValue("Archive", "sResourceDataDirsFinal", "", + iniFilePath)) { + qWarning("failed to override data directory in \"%s\"", + qUtf8Printable(m_IniFileName)); + } + } + } + + return dirty; +} diff --git a/libs/game_bethesda/src/games/fallout4london/fo4londondataarchives.cpp b/libs/game_bethesda/src/games/fallout4london/fo4londondataarchives.cpp new file mode 100644 index 0000000..e631f75 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4london/fo4londondataarchives.cpp @@ -0,0 +1,48 @@ +#include "fo4londondataarchives.h" + +#include "iprofile.h" +#include + +QStringList Fallout4LondonDataArchives::vanillaArchives() const +{ + return {"Fallout4 - Textures1.ba2", "Fallout4 - Textures2.ba2", + "Fallout4 - Textures3.ba2", "Fallout4 - Textures4.ba2", + "Fallout4 - Textures5.ba2", "Fallout4 - Textures6.ba2", + "Fallout4 - Textures7.ba2", "Fallout4 - Textures8.ba2", + "Fallout4 - Textures9.ba2", "Fallout4 - Meshes.ba2", + "Fallout4 - MeshesExtra.ba2", "Fallout4 - Voices.ba2", + "Fallout4 - Sounds.ba2", "Fallout4 - Interface.ba2", + "Fallout4 - Animations.ba2", "Fallout4 - Materials.ba2", + "Fallout4 - Shaders.ba2", "Fallout4 - Startup.ba2", + "Fallout4 - Misc.ba2"}; +} + +QStringList Fallout4LondonDataArchives::archives(const MOBase::IProfile* profile) const +{ + QStringList result; + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("fallout4.ini") + : localGameDirectory().absoluteFilePath("fallout4.ini"); + result.append(getArchivesFromKey(iniFile, "SResourceArchiveList")); + result.append(getArchivesFromKey(iniFile, "SResourceArchiveList2")); + + return result; +} + +void Fallout4LondonDataArchives::writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) +{ + QString list = before.join(", "); + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("fallout4.ini") + : localGameDirectory().absoluteFilePath("fallout4.ini"); + if (list.length() > 255) { + int splitIdx = list.lastIndexOf(",", 256); + setArchivesToKey(iniFile, "SResourceArchiveList", list.mid(0, splitIdx)); + setArchivesToKey(iniFile, "SResourceArchiveList2", list.mid(splitIdx + 2)); + } else { + setArchivesToKey(iniFile, "SResourceArchiveList", list); + } +} diff --git a/libs/game_bethesda/src/games/fallout4london/fo4londonsavegame.cpp b/libs/game_bethesda/src/games/fallout4london/fo4londonsavegame.cpp new file mode 100644 index 0000000..ead2d1a --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4london/fo4londonsavegame.cpp @@ -0,0 +1,108 @@ +#include "fo4londonsavegame.h" + +#ifdef _WIN32 +#include +#else +#include + +using SYSTEMTIME = _SYSTEMTIME; + +static void FileTimeToSystemTime(const FILETIME* ft, SYSTEMTIME* st) +{ + const uint64_t ticks = (static_cast(ft->dwHighDateTime) << 32) | + static_cast(ft->dwLowDateTime); + const int64_t unixSecs = static_cast(ticks / 10000000ULL) - 11644473600LL; + const uint64_t remainderHns = ticks % 10000000ULL; + + const QDateTime dt = QDateTime::fromSecsSinceEpoch(unixSecs, Qt::UTC); + const QDate d = dt.date(); + const QTime t = dt.time(); + + st->wYear = static_cast(d.year()); + st->wMonth = static_cast(d.month()); + st->wDayOfWeek = static_cast(d.dayOfWeek() % 7); + st->wDay = static_cast(d.day()); + st->wHour = static_cast(t.hour()); + st->wMinute = static_cast(t.minute()); + st->wSecond = static_cast(t.second()); + st->wMilliseconds = static_cast(remainderHns / 10000); +} +#endif + +#include "gamefo4london.h" + +Fallout4LondonSaveGame::Fallout4LondonSaveGame(QString const& fileName, + GameFallout4London const* game) + : GamebryoSaveGame(fileName, game, true) +{ + FileWrapper file(getFilepath(), "FO4_SAVEGAME"); + + FILETIME creationTime; + fetchInformationFields(file, m_SaveNumber, m_PCName, m_PCLevel, m_PCLocation, + creationTime); + + // A file time is a 64-bit value that represents the number of 100-nanosecond + // intervals that have elapsed since 12:00 A.M. January 1, 1601 Coordinated Universal + // Time (UTC). So we need to convert that to something useful + SYSTEMTIME ctime; + ::FileTimeToSystemTime(&creationTime, &ctime); + + setCreationTime(ctime); +} + +void Fallout4LondonSaveGame::fetchInformationFields( + FileWrapper& file, uint32_t& saveNumber, QString& playerName, + unsigned short& playerLevel, QString& playerLocation, FILETIME& creationTime) const +{ + file.skip(); // header size + file.skip(); // header version + file.read(saveNumber); + + file.read(playerName); + + uint32_t temp; + file.read(temp); + playerLevel = static_cast(temp); + file.read(playerLocation); + + QString ignore; + file.read(ignore); // playtime as ascii hh.mm.ss + file.read(ignore); // race name (i.e. BretonRace) + + file.skip(); // Player gender (0 = male) + file.skip(2); // experience gathered, experience required + + file.read(creationTime); +} + +std::unique_ptr +Fallout4LondonSaveGame::fetchDataFields() const +{ + FileWrapper file(getFilepath(), "FO4_SAVEGAME"); // 10bytes + + { + QString dummyName, dummyLocation; + unsigned short dummyLevel; + uint32_t dummySaveNumber; + FILETIME dummyTime; + + fetchInformationFields(file, dummySaveNumber, dummyName, dummyLevel, dummyLocation, + dummyTime); + } + + QString ignore; + std::unique_ptr fields = std::make_unique(); + + fields->Screenshot = file.readImage(384, true); + + uint8_t saveGameVersion = file.readChar(); + file.read(ignore); // game version + file.skip(); // plugin info size + + fields->Plugins = file.readPlugins(); + if (saveGameVersion >= 68) { + fields->LightPlugins = file.readLightPlugins(); + } + + return fields; +} diff --git a/libs/game_bethesda/src/games/fallout4london/fo4londonscriptextender.cpp b/libs/game_bethesda/src/games/fallout4london/fo4londonscriptextender.cpp new file mode 100644 index 0000000..109ca51 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4london/fo4londonscriptextender.cpp @@ -0,0 +1,18 @@ +#include "fo4londonscriptextender.h" + +#include +#include + +Fallout4LondonScriptExtender::Fallout4LondonScriptExtender(GameGamebryo const* game) + : GamebryoScriptExtender(game) +{} + +QString Fallout4LondonScriptExtender::BinaryName() const +{ + return "f4se_loader.exe"; +} + +QString Fallout4LondonScriptExtender::PluginPath() const +{ + return "f4se/plugins"; +} diff --git a/libs/game_bethesda/src/games/fallout4london/fo4londonunmanagedmods.cpp b/libs/game_bethesda/src/games/fallout4london/fo4londonunmanagedmods.cpp new file mode 100644 index 0000000..f9a6631 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4london/fo4londonunmanagedmods.cpp @@ -0,0 +1,63 @@ +#include "fo4londonunmanagedmods.h" + +Fallout4LondonUnmangedMods::Fallout4LondonUnmangedMods(const GameGamebryo* game) + : GamebryoUnmangedMods(game) +{} + +Fallout4LondonUnmangedMods::~Fallout4LondonUnmangedMods() {} + +QStringList Fallout4LondonUnmangedMods::mods(bool onlyOfficial) const +{ + QStringList result; + + QStringList pluginList = game()->primaryPlugins(); + QStringList otherPlugins = game()->DLCPlugins(); + otherPlugins.append(game()->CCPlugins()); + for (QString plugin : otherPlugins) { + pluginList.removeAll(plugin); + } + QDir dataDir(game()->dataDirectory()); + for (const QString& fileName : dataDir.entryList({"*.esp", "*.esl", "*.esm"})) { + if (!pluginList.contains(fileName, Qt::CaseInsensitive)) { + if (!onlyOfficial || pluginList.contains(fileName, Qt::CaseInsensitive)) { + result.append(fileName.chopped(4)); // trims the extension off + } + } + } + + return result; +} + +QStringList Fallout4LondonUnmangedMods::secondaryFiles(const QString& modName) const +{ + // file extension in FO4 is .ba2 instead of bsa + QStringList archives; + QDir dataDir = game()->dataDirectory(); + for (const QString& archiveName : dataDir.entryList({modName + "*.ba2"})) { + archives.append(dataDir.absoluteFilePath(archiveName)); + } + return archives; +} + +QString Fallout4LondonUnmangedMods::displayName(const QString& modName) const +{ + // unlike in earlier games, in fallout 4 the file name doesn't correspond to + // the public name + if (modName.compare("dlcrobot", Qt::CaseInsensitive) == 0) { + return "Automatron"; + } else if (modName.compare("dlcworkshop01", Qt::CaseInsensitive) == 0) { + return "Wasteland Workshop"; + } else if (modName.compare("dlccoast", Qt::CaseInsensitive) == 0) { + return "Far Harbor"; + } else if (modName.compare("dlcworkshop02", Qt::CaseInsensitive) == 0) { + return "Contraptions Workshop"; + } else if (modName.compare("dlcworkshop03", Qt::CaseInsensitive) == 0) { + return "Vault-Tec Workshop"; + } else if (modName.compare("dlcnukaworld", Qt::CaseInsensitive) == 0) { + return "Nuka-World"; + } else if (modName.compare("dlcultrahighresolution", Qt::CaseInsensitive) == 0) { + return "Ultra High Resolution Texture Pack"; + } else { + return modName; + } +} diff --git a/libs/game_bethesda/src/games/fallout4london/gamefo4london.cpp b/libs/game_bethesda/src/games/fallout4london/gamefo4london.cpp new file mode 100644 index 0000000..0fa774a --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4london/gamefo4london.cpp @@ -0,0 +1,366 @@ +#include "gamefo4london.h" + +#include "fo4londonbsainvalidation.h" +#include "fo4londondataarchives.h" +#include "fo4londonmoddatachecker.h" +#include "fo4londonmoddatacontent.h" +#include "fo4londonsavegame.h" +#include "fo4londonscriptextender.h" +#include "fo4londonunmanagedmods.h" + +#include "versioninfo.h" +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "scopeguard.h" + +using namespace MOBase; + +const unsigned int GameFallout4London::PROBLEM_TEST_FILE; + +GameFallout4London::GameFallout4London() {} + +bool GameFallout4London::init(IOrganizer* moInfo) +{ + if (!GameGamebryo::init(moInfo)) { + return false; + } + + auto dataArchives = std::make_shared(this); + + registerFeature(std::make_shared(this)); + registerFeature(dataArchives); + registerFeature( + std::make_shared(this, "fo4londoncustom.ini")); + registerFeature(std::make_shared(this)); + registerFeature( + std::make_shared(m_Organizer->gameFeatures())); + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(moInfo)); + registerFeature(std::make_shared(this)); + registerFeature( + std::make_shared(dataArchives.get(), this)); + + return true; +} + +QString GameFallout4London::gameName() const +{ + return "Fallout 4 London"; +} + +void GameFallout4London::detectGame() +{ + m_GamePath = identifyGamePath(); + m_MyGamesPath = determineMyGamesPath("Fallout4"); +} + +QString GameFallout4London::identifyGamePath() const +{ +#ifdef _WIN32 + // TODO: Add GOG support + QString path = "Software\\Bethesda Softworks\\Fallout4"; + return findInRegistry(HKEY_LOCAL_MACHINE, path.toStdWString().c_str(), + L"Installed Path"); +#else + return GameGamebryo::identifyGamePath(); +#endif +} + +QList GameFallout4London::executables() const +{ + return QList() + << ExecutableInfo("F4SE", + findInGameFolder(m_Organizer->gameFeatures() + ->gameFeature() + ->loaderName())) + << ExecutableInfo("Fallout 4 London", findInGameFolder(binaryName())) + << ExecutableInfo("Fallout Launcher", findInGameFolder(getLauncherName())) + << ExecutableInfo("Creation Kit", findInGameFolder("CreationKit.exe")) + .withSteamAppId("1946160") + << ExecutableInfo("LOOT", QFileInfo(getLootPath())) + .withArgument("--game=\"Fallout4\""); +} + +QList GameFallout4London::executableForcedLoads() const +{ + return QList(); +} + +QString GameFallout4London::name() const +{ + return "Fallout 4 London Support Plugin"; +} + +QString GameFallout4London::localizedName() const +{ + return tr("Fallout 4 London Support Plugin"); +} + +QString GameFallout4London::author() const +{ + return "MO2 Team"; +} + +QString GameFallout4London::description() const +{ + return tr("Adds support for the game Fallout 4 London."); +} + +MOBase::VersionInfo GameFallout4London::version() const +{ + return VersionInfo(0, 0, 1, VersionInfo::RELEASE_PREALPHA); +} + +QList GameFallout4London::settings() const +{ + return QList(); +} + +MappingType GameFallout4London::mappings() const +{ + MappingType result; + if (testFilePlugins().isEmpty()) { + for (const QString& profileFile : {"plugins.txt", "loadorder.txt"}) { + result.push_back({m_Organizer->profilePath() + "/" + profileFile, + localAppFolder() + "/Fallout4/" + profileFile, false}); + } + } + return result; +} + +void GameFallout4London::initializeProfile(const QDir& path, + ProfileSettings settings) const +{ + if (settings.testFlag(IPluginGame::MODS)) { + copyToProfile(localAppFolder() + "/Fallout4", path, "plugins.txt"); + } + + if (settings.testFlag(IPluginGame::CONFIGURATION)) { + if (settings.testFlag(IPluginGame::PREFER_DEFAULTS) || + !QFileInfo(myGamesPath() + "/fallout4.ini").exists()) { + copyToProfile(gameDirectory().absolutePath(), path, "fallout4_default.ini", + "fallout4.ini"); + } else { + copyToProfile(myGamesPath(), path, "fallout4.ini"); + } + + copyToProfile(myGamesPath(), path, "fallout4prefs.ini"); + copyToProfile(myGamesPath(), path, "fallout4custom.ini"); + } +} + +QString GameFallout4London::savegameExtension() const +{ + return "fos"; +} + +QString GameFallout4London::savegameSEExtension() const +{ + return "f4se"; +} + +std::shared_ptr +GameFallout4London::makeSaveGame(QString filePath) const +{ + return std::make_shared(filePath, this); +} + +QString GameFallout4London::steamAPPId() const +{ + return "377160"; +} + +QStringList GameFallout4London::testFilePlugins() const +{ + QStringList plugins; + if (m_Organizer != nullptr && m_Organizer->profile() != nullptr) { + QString customIni( + m_Organizer->profile()->absoluteIniFilePath("Fallout4Custom.ini")); + if (QFile(customIni).exists()) { + for (int i = 1; i <= 10; ++i) { + QString setting("sTestFile"); + setting += std::to_string(i); + QString plugin = GameGamebryo::readIniValue(customIni, "General", setting, ""); + if (!plugin.isEmpty() && !plugins.contains(plugin, Qt::CaseInsensitive)) { + plugins.append(plugin); + } + } + } + } + return plugins; +} + +QStringList GameFallout4London::primaryPlugins() const +{ + QStringList plugins = {"fallout4.esm", "dlcrobot.esm", + "dlcworkshop01.esm", "dlccoast.esm", + "dlcworkshop02.esm", "dlcworkshop03.esm", + "dlcnukaworld.esm", "dlcultrahighresolution.esm"}; + + auto testPlugins = testFilePlugins(); + if (loadOrderMechanism() == LoadOrderMechanism::None) { + plugins << testPlugins; + } else { + plugins << CCPlugins(); + } + + return plugins; +} + +QStringList GameFallout4London::enabledPlugins() const +{ + return {"bakaframework.esm", "londonworldspace.esm", "londonworldspace-dlcblock.esp"}; +} + +QStringList GameFallout4London::gameVariants() const +{ + return {"Regular"}; +} + +QString GameFallout4London::gameShortName() const +{ + return "Fallout4London"; +} + +QStringList GameFallout4London::validShortNames() const +{ + return {"Fallout4"}; +} + +QString GameFallout4London::gameNexusName() const +{ + return "fallout4london"; +} + +QString GameFallout4London::binaryName() const +{ + return "Fallout4.exe"; +} + +QString GameFallout4London::getLauncherName() const +{ + return "Fallout4Launcher.exe"; +} + +QStringList GameFallout4London::iniFiles() const +{ + return {"fallout4.ini", "fallout4prefs.ini", "fallout4custom.ini"}; +} + +QStringList GameFallout4London::DLCPlugins() const +{ + return {"dlcrobot.esm", + "dlcworkshop01.esm", + "dlccoast.esm", + "dlcworkshop02.esm", + "dlcworkshop03.esm", + "dlcnukaworld.esm", + "dlcultrahighresolution.esm"}; +} + +QStringList GameFallout4London::CCPlugins() const +{ + QStringList plugins = {}; + QFile file(gameDirectory().absoluteFilePath("Fallout4.ccc")); + if (file.open(QIODevice::ReadOnly)) { + ON_BLOCK_EXIT([&file]() { + file.close(); + }); + + if (file.size() == 0) { + return plugins; + } + while (!file.atEnd()) { + QByteArray line = file.readLine().trimmed(); + QString modName; + if ((line.size() > 0) && (line.at(0) != '#')) { + modName = QString::fromUtf8(line.constData()).toLower(); + } + + if (modName.size() > 0) { + if (!plugins.contains(modName, Qt::CaseInsensitive)) { + plugins.append(modName); + } + } + } + } + return plugins; +} + +IPluginGame::SortMechanism GameFallout4London::sortMechanism() const +{ + if (!testFilePresent()) + return IPluginGame::SortMechanism::LOOT; + return IPluginGame::SortMechanism::NONE; +} + +IPluginGame::LoadOrderMechanism GameFallout4London::loadOrderMechanism() const +{ + if (!testFilePresent()) + return IPluginGame::LoadOrderMechanism::PluginsTxt; + return IPluginGame::LoadOrderMechanism::None; +} + +int GameFallout4London::nexusModOrganizerID() const +{ + return 28715; +} + +int GameFallout4London::nexusGameID() const +{ + return 1151; +} + +// Start Diagnose +std::vector GameFallout4London::activeProblems() const +{ + std::vector result; + if (m_Organizer->managedGame() == this) { + if (testFilePresent()) + result.push_back(PROBLEM_TEST_FILE); + } + return result; +} + +bool GameFallout4London::testFilePresent() const +{ + if (!testFilePlugins().isEmpty()) + return true; + return false; +} + +QString GameFallout4London::shortDescription(unsigned int key) const +{ + switch (key) { + case PROBLEM_TEST_FILE: + return tr("sTestFile entries are present"); + } + return QString(); +} + +QString GameFallout4London::fullDescription(unsigned int key) const +{ + switch (key) { + case PROBLEM_TEST_FILE: { + return tr("

You have sTestFile settings in your " + "Fallout4Custom.ini. These must be removed or " + "the game will not read the plugins.txt file. " + "Management is disabled.

"); + } + } + return QString(); +} diff --git a/libs/game_bethesda/src/games/fallout4vr/fallout4vrdataarchives.cpp b/libs/game_bethesda/src/games/fallout4vr/fallout4vrdataarchives.cpp new file mode 100644 index 0000000..58a9a09 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4vr/fallout4vrdataarchives.cpp @@ -0,0 +1,49 @@ +#include "fallout4vrdataarchives.h" + +#include "iprofile.h" +#include + +QStringList Fallout4VRDataArchives::vanillaArchives() const +{ + return {"Fallout4 - Textures1.ba2", "Fallout4 - Textures2.ba2", + "Fallout4 - Textures3.ba2", "Fallout4 - Textures4.ba2", + "Fallout4 - Textures5.ba2", "Fallout4 - Textures6.ba2", + "Fallout4 - Textures7.ba2", "Fallout4 - Textures8.ba2", + "Fallout4 - Textures9.ba2", "Fallout4 - Meshes.ba2", + "Fallout4 - MeshesExtra.ba2", "Fallout4 - Voices.ba2", + "Fallout4 - Sounds.ba2", "Fallout4 - Interface.ba2", + "Fallout4 - Animations.ba2", "Fallout4 - Materials.ba2", + "Fallout4 - Shaders.ba2", "Fallout4 - Startup.ba2", + "Fallout4 - Misc.ba2", "Fallout4_VR - Main.ba2", + "Fallout4_VR - Shaders.ba2", "Fallout4_VR - Textures.ba2"}; +} + +QStringList Fallout4VRDataArchives::archives(const MOBase::IProfile* profile) const +{ + QStringList result; + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("fallout4.ini") + : localGameDirectory().absoluteFilePath("fallout4.ini"); + result.append(getArchivesFromKey(iniFile, "SResourceArchiveList")); + result.append(getArchivesFromKey(iniFile, "SResourceArchiveList2")); + + return result; +} + +void Fallout4VRDataArchives::writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) +{ + QString list = before.join(", "); + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("fallout4.ini") + : localGameDirectory().absoluteFilePath("fallout4.ini"); + if (list.length() > 255) { + int splitIdx = list.lastIndexOf(",", 256); + setArchivesToKey(iniFile, "SResourceArchiveList", list.mid(0, splitIdx)); + setArchivesToKey(iniFile, "SResourceArchiveList2", list.mid(splitIdx + 2)); + } else { + setArchivesToKey(iniFile, "SResourceArchiveList", list); + } +} diff --git a/libs/game_bethesda/src/games/fallout4vr/fallout4vrgameplugins.cpp b/libs/game_bethesda/src/games/fallout4vr/fallout4vrgameplugins.cpp new file mode 100644 index 0000000..e40a3ae --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4vr/fallout4vrgameplugins.cpp @@ -0,0 +1,15 @@ +#include "fallout4vrgameplugins.h" + +using namespace MOBase; + +Fallout4VRGamePlugins::Fallout4VRGamePlugins(MOBase::IOrganizer* organizer) + : CreationGamePlugins(organizer) +{} + +bool Fallout4VRGamePlugins::lightPluginsAreSupported() +{ + auto files = m_Organizer->findFiles("f4se\\plugins", {"falloutvresl.dll"}); + if (files.isEmpty()) + return false; + return true; +} diff --git a/libs/game_bethesda/src/games/fallout4vr/fallout4vrsavegame.cpp b/libs/game_bethesda/src/games/fallout4vr/fallout4vrsavegame.cpp new file mode 100644 index 0000000..cf29aae --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4vr/fallout4vrsavegame.cpp @@ -0,0 +1,108 @@ +#include "fallout4vrsavegame.h" + +#ifdef _WIN32 +#include +#else +#include + +using SYSTEMTIME = _SYSTEMTIME; + +static void FileTimeToSystemTime(const FILETIME* ft, SYSTEMTIME* st) +{ + const uint64_t ticks = (static_cast(ft->dwHighDateTime) << 32) | + static_cast(ft->dwLowDateTime); + const int64_t unixSecs = static_cast(ticks / 10000000ULL) - 11644473600LL; + const uint64_t remainderHns = ticks % 10000000ULL; + + const QDateTime dt = QDateTime::fromSecsSinceEpoch(unixSecs, Qt::UTC); + const QDate d = dt.date(); + const QTime t = dt.time(); + + st->wYear = static_cast(d.year()); + st->wMonth = static_cast(d.month()); + st->wDayOfWeek = static_cast(d.dayOfWeek() % 7); + st->wDay = static_cast(d.day()); + st->wHour = static_cast(t.hour()); + st->wMinute = static_cast(t.minute()); + st->wSecond = static_cast(t.second()); + st->wMilliseconds = static_cast(remainderHns / 10000); +} +#endif + +#include "gamefallout4vr.h" + +Fallout4VRSaveGame::Fallout4VRSaveGame(QString const& fileName, + GameFallout4VR const* game) + : GamebryoSaveGame(fileName, game, true) +{ + FileWrapper file(getFilepath(), "FO4_SAVEGAME"); + + FILETIME creationTime; + fetchInformationFields(file, m_SaveNumber, m_PCName, m_PCLevel, m_PCLocation, + creationTime); + + // A file time is a 64-bit value that represents the number of 100-nanosecond + // intervals that have elapsed since 12:00 A.M. January 1, 1601 Coordinated Universal + // Time (UTC). So we need to convert that to something useful + SYSTEMTIME ctime; + ::FileTimeToSystemTime(&creationTime, &ctime); + + setCreationTime(ctime); +} + +void Fallout4VRSaveGame::fetchInformationFields( + FileWrapper& file, uint32_t& saveNumber, QString& playerName, + unsigned short& playerLevel, QString& playerLocation, FILETIME& creationTime) const +{ + file.skip(); // header size + file.skip(); // header version + file.read(saveNumber); + + file.read(playerName); + + uint32_t temp; + file.read(temp); + playerLevel = static_cast(temp); + file.read(playerLocation); + + QString ignore; + file.read(ignore); // playtime as ascii hh.mm.ss + file.read(ignore); // race name (i.e. BretonRace) + + file.skip(); // Player gender (0 = male) + file.skip(2); // experience gathered, experience required + + file.read(creationTime); +} + +std::unique_ptr +Fallout4VRSaveGame::fetchDataFields() const +{ + FileWrapper file(getFilepath(), "FO4_SAVEGAME"); // 10bytes + + { + QString dummyName, dummyLocation; + unsigned short dummyLevel; + uint32_t dummySaveNumber; + FILETIME dummyTime; + + fetchInformationFields(file, dummySaveNumber, dummyName, dummyLevel, dummyLocation, + dummyTime); + } + + QString ignore; + std::unique_ptr fields = std::make_unique(); + + fields->Screenshot = file.readImage(384, true); + + uint8_t saveGameVersion = file.readChar(); + file.read(ignore); // game version + file.skip(); // plugin info size + + fields->Plugins = file.readPlugins(); + if (saveGameVersion >= 68) { + fields->LightPlugins = file.readLightPlugins(); + } + + return fields; +} diff --git a/libs/game_bethesda/src/games/fallout4vr/fallout4vrscriptextender.cpp b/libs/game_bethesda/src/games/fallout4vr/fallout4vrscriptextender.cpp new file mode 100644 index 0000000..6508c6a --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4vr/fallout4vrscriptextender.cpp @@ -0,0 +1,18 @@ +#include "fallout4vrscriptextender.h" + +#include +#include + +Fallout4VRScriptExtender::Fallout4VRScriptExtender(GameGamebryo const* game) + : GamebryoScriptExtender(game) +{} + +QString Fallout4VRScriptExtender::BinaryName() const +{ + return "f4se_loader.exe"; +} + +QString Fallout4VRScriptExtender::PluginPath() const +{ + return "f4se/plugins"; +} diff --git a/libs/game_bethesda/src/games/fallout4vr/fallout4vrunmanagedmods.cpp b/libs/game_bethesda/src/games/fallout4vr/fallout4vrunmanagedmods.cpp new file mode 100644 index 0000000..a217ef0 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4vr/fallout4vrunmanagedmods.cpp @@ -0,0 +1,63 @@ +#include "fallout4vrunmanagedmods.h" + +Fallout4VRUnmangedMods::Fallout4VRUnmangedMods(const GameGamebryo* game) + : GamebryoUnmangedMods(game) +{} + +Fallout4VRUnmangedMods::~Fallout4VRUnmangedMods() {} + +QStringList Fallout4VRUnmangedMods::mods(bool onlyOfficial) const +{ + QStringList result; + + QStringList pluginList = game()->primaryPlugins(); + QStringList otherPlugins = game()->DLCPlugins(); + otherPlugins.append(game()->CCPlugins()); + for (QString plugin : otherPlugins) { + pluginList.removeAll(plugin); + } + QDir dataDir(game()->dataDirectory()); + for (const QString& fileName : dataDir.entryList({"*.esp", "*.esl", "*.esm"})) { + if (!pluginList.contains(fileName, Qt::CaseInsensitive)) { + if (!onlyOfficial || pluginList.contains(fileName, Qt::CaseInsensitive)) { + result.append(fileName.chopped(4)); // trims the extension off + } + } + } + + return result; +} + +QStringList Fallout4VRUnmangedMods::secondaryFiles(const QString& modName) const +{ + // file extension in FO4 is .ba2 instead of bsa + QStringList archives; + QDir dataDir = game()->dataDirectory(); + for (const QString& archiveName : dataDir.entryList({modName + "*.ba2"})) { + archives.append(dataDir.absoluteFilePath(archiveName)); + } + return archives; +} + +QString Fallout4VRUnmangedMods::displayName(const QString& modName) const +{ + // unlike in earlier games, in fallout 4 the file name doesn't correspond to + // the public name + if (modName.compare("dlcrobot", Qt::CaseInsensitive) == 0) { + return "Automatron"; + } else if (modName.compare("dlcworkshop01", Qt::CaseInsensitive) == 0) { + return "Wasteland Workshop"; + } else if (modName.compare("dlccoast", Qt::CaseInsensitive) == 0) { + return "Far Harbor"; + } else if (modName.compare("dlcworkshop02", Qt::CaseInsensitive) == 0) { + return "Contraptions Workshop"; + } else if (modName.compare("dlcworkshop03", Qt::CaseInsensitive) == 0) { + return "Vault-Tec Workshop"; + } else if (modName.compare("dlcnukaworld", Qt::CaseInsensitive) == 0) { + return "Nuka-World"; + } else if (modName.compare("dlcultrahighresolution", Qt::CaseInsensitive) == 0) { + return "Ultra High Resolution Texture Pack"; + } else { + return modName; + } +} diff --git a/libs/game_bethesda/src/games/fallout4vr/gamefallout4vr.cpp b/libs/game_bethesda/src/games/fallout4vr/gamefallout4vr.cpp new file mode 100644 index 0000000..bd2448b --- /dev/null +++ b/libs/game_bethesda/src/games/fallout4vr/gamefallout4vr.cpp @@ -0,0 +1,251 @@ +#include "gamefallout4vr.h" + +#include "fallout4vrdataarchives.h" +#include "fallout4vrgameplugins.h" +#include "fallout4vrmoddatachecker.h" +#include "fallout4vrmoddatacontent.h" +#include "fallout4vrsavegame.h" +#include "fallout4vrunmanagedmods.h" + +#include "versioninfo.h" +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "scopeguard.h" + +using namespace MOBase; + +GameFallout4VR::GameFallout4VR() {} + +bool GameFallout4VR::init(IOrganizer* moInfo) +{ + if (!GameGamebryo::init(moInfo)) { + return false; + } + + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(this, "fallout4custom.ini")); + registerFeature(std::make_shared(this)); + registerFeature( + std::make_shared(m_Organizer->gameFeatures())); + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(moInfo)); + registerFeature(std::make_shared(this)); + + return true; +} + +QString GameFallout4VR::gameName() const +{ + return "Fallout 4 VR"; +} + +void GameFallout4VR::detectGame() +{ + m_GamePath = identifyGamePath(); + m_MyGamesPath = determineMyGamesPath("Fallout4VR"); +} + +QList GameFallout4VR::executables() const +{ + return QList() + << ExecutableInfo("Fallout 4 VR", findInGameFolder(binaryName())) + << ExecutableInfo("Creation Kit", findInGameFolder("CreationKit.exe")) + << ExecutableInfo("LOOT", QFileInfo(getLootPath())) + .withArgument("--game=\"Fallout4VR\""); +} + +QList GameFallout4VR::executableForcedLoads() const +{ + return QList(); +} + +QString GameFallout4VR::name() const +{ + return "Fallout 4 VR Support Plugin"; +} + +QString GameFallout4VR::localizedName() const +{ + return tr("Fallout 4 VR Support Plugin"); +} + +QString GameFallout4VR::author() const +{ + return "MO2 Contibutors"; +} + +QString GameFallout4VR::description() const +{ + return tr("Adds support for the game Fallout 4 VR.\n" + "Splash by %1") + .arg("nekoyoubi"); +} + +MOBase::VersionInfo GameFallout4VR::version() const +{ + return VersionInfo(1, 6, 1, VersionInfo::RELEASE_FINAL); +} + +QList GameFallout4VR::settings() const +{ + return QList(); +} + +void GameFallout4VR::initializeProfile(const QDir& path, ProfileSettings settings) const +{ + if (settings.testFlag(IPluginGame::MODS)) { + copyToProfile(localAppFolder() + "/Fallout4VR", path, "plugins.txt"); + } + + if (settings.testFlag(IPluginGame::CONFIGURATION)) { + if (settings.testFlag(IPluginGame::PREFER_DEFAULTS) || + !QFileInfo(myGamesPath() + "/fallout4.ini").exists()) { + copyToProfile(gameDirectory().absolutePath(), path, "fallout4.ini"); + } else { + copyToProfile(myGamesPath(), path, "fallout4.ini"); + } + + copyToProfile(myGamesPath(), path, "fallout4prefs.ini"); + copyToProfile(myGamesPath(), path, "fallout4custom.ini"); + } +} + +QString GameFallout4VR::savegameExtension() const +{ + return "fos"; +} + +QString GameFallout4VR::savegameSEExtension() const +{ + return "f4se"; +} + +std::shared_ptr +GameFallout4VR::makeSaveGame(QString filePath) const +{ + return std::make_shared(filePath, this); +} + +QString GameFallout4VR::steamAPPId() const +{ + return "611660"; +} + +QStringList GameFallout4VR::primaryPlugins() const +{ + QStringList plugins = {"fallout4.esm", "fallout4_vr.esm"}; + + plugins.append(CCPlugins()); + + return plugins; +} + +QStringList GameFallout4VR::gameVariants() const +{ + return {"Regular"}; +} + +QString GameFallout4VR::gameShortName() const +{ + return "Fallout4VR"; +} + +QStringList GameFallout4VR::validShortNames() const +{ + return {"Fallout4"}; +} + +QString GameFallout4VR::gameNexusName() const +{ + return QString(); +} + +QStringList GameFallout4VR::iniFiles() const +{ + return {"fallout4.ini", "fallout4custom.ini", "fallout4prefs.ini"}; +} + +QStringList GameFallout4VR::DLCPlugins() const +{ + return {"dlcrobot.esm", + "dlcworkshop01.esm", + "dlccoast.esm", + "dlcworkshop02.esm", + "dlcworkshop03.esm", + "dlcnukaworld.esm", + "dlcultrahighresolution.esm"}; +} + +QStringList GameFallout4VR::CCPlugins() const +{ + QStringList plugins = {}; + QFile file(gameDirectory().absoluteFilePath("Fallout4.ccc")); + if (file.open(QIODevice::ReadOnly)) { + ON_BLOCK_EXIT([&file]() { + file.close(); + }); + + if (file.size() == 0) { + return plugins; + } + while (!file.atEnd()) { + QByteArray line = file.readLine().trimmed(); + QString modName; + if ((line.size() > 0) && (line.at(0) != '#')) { + modName = QString::fromUtf8(line.constData()).toLower(); + } + + if (modName.size() > 0) { + if (!plugins.contains(modName, Qt::CaseInsensitive)) { + plugins.append(modName); + } + } + } + } + return plugins; +} + +IPluginGame::LoadOrderMechanism GameFallout4VR::loadOrderMechanism() const +{ + return IPluginGame::LoadOrderMechanism::PluginsTxt; +} + +int GameFallout4VR::nexusModOrganizerID() const +{ + return 0; //... +} + +int GameFallout4VR::nexusGameID() const +{ + return 0; +} + +QString GameFallout4VR::getLauncherName() const +{ + return binaryName(); // Fallout 4 VR has no Launcher, so we just return the name of + // the game binary +} + +QString GameFallout4VR::identifyGamePath() const +{ +#ifdef _WIN32 + QString path = "Software\\Bethesda Softworks\\" + gameName(); + return findInRegistry(HKEY_LOCAL_MACHINE, path.toStdWString().c_str(), + L"Installed Path"); +#else + return GameGamebryo::identifyGamePath(); +#endif +} diff --git a/libs/game_bethesda/src/games/fallout76/fallout76dataarchives.cpp b/libs/game_bethesda/src/games/fallout76/fallout76dataarchives.cpp new file mode 100644 index 0000000..6827d08 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout76/fallout76dataarchives.cpp @@ -0,0 +1,150 @@ +#include "fallout76dataarchives.h" + +#include "iprofile.h" +#include + +#include + +QStringList Fallout76DataArchives::vanillaArchives() const +{ + return {"SeventySix - Animations.ba2", + "SeventySix - ATX_Main.ba2", + "SeventySix - ATX_Textures.ba2", + "SeventySix - EnlightenExteriors01.ba2", + "SeventySix - EnlightenExteriors02.ba2", + "SeventySix - EnlightenInteriors.ba2", + "SeventySix - GeneratedMeshes.ba2", + "SeventySix - GeneratedTextures.ba2", + "SeventySix - Interface.ba2", + "SeventySix - Localization.ba2", + "SeventySix - Materials.ba2", + "SeventySix - Meshes01.ba2", + "SeventySix - Meshes02.ba2", + "SeventySix - MeshesExtra.ba2", + "SeventySix - MiscClient.ba2", + "SeventySix - Shaders.ba2", + "SeventySix - Sounds01.ba2", + "SeventySix - Sounds02.ba2", + "SeventySix - Startup.ba2", + "SeventySix - Textures01.ba2", + "SeventySix - Textures02.ba2", + "SeventySix - Textures03.ba2", + "SeventySix - Textures04.ba2", + "SeventySix - Textures05.ba2", + "SeventySix - Textures06.ba2", + "SeventySix - Voices.ba2"}; +} + +QStringList Fallout76DataArchives::sResourceIndexFileList() const +{ + return {"SeventySix - Textures01.ba2", "SeventySix - Textures02.ba2", + "SeventySix - Textures03.ba2", "SeventySix - Textures04.ba2", + "SeventySix - Textures05.ba2", "SeventySix - Textures06.ba2"}; +} + +QStringList Fallout76DataArchives::sResourceStartUpArchiveList() const +{ + return {"SeventySix - Interface.ba2", "SeventySix - Localization.ba2", + "SeventySix - Shaders.ba2", "SeventySix - Startup.ba2"}; +} + +QStringList Fallout76DataArchives::SResourceArchiveMemoryCacheList() const +{ + return {"SeventySix - Interface.ba2", "SeventySix - Materials.ba2", + "SeventySix - MiscClient.ba2", "SeventySix - Shaders.ba2"}; +} + +QStringList Fallout76DataArchives::SResourceArchiveList() const +{ + return {"SeventySix - GeneratedMeshes.ba2", "SeventySix - Materials.ba2", + "SeventySix - Meshes01.ba2", "SeventySix - Meshes02.ba2", + "SeventySix - MeshesExtra.ba2", "SeventySix - MiscClient.ba2", + "SeventySix - Sounds01.ba2", "SeventySix - Sounds02.ba2", + "SeventySix - Startup.ba2", "SeventySix - Voices.ba2"}; +} + +QStringList Fallout76DataArchives::SResourceArchiveList2() const +{ + return {"SeventySix - Animations.ba2", "SeventySix - EnlightenInteriors.ba2", + "SeventySix - GeneratedTextures.ba2", "SeventySix - EnlightenExteriors01.ba2", + "SeventySix - EnlightenExteriors02.ba2"}; +} + +QStringList Fallout76DataArchives::sResourceArchive2List() const +{ + return {"SeventySix - ATX_Main.ba2", "SeventySix - ATX_Textures.ba2"}; +} + +QStringList Fallout76DataArchives::archives(const MOBase::IProfile* profile) const +{ + QStringList result; + + QString iniFile = + profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("Fallout76.ini") + : localGameDirectory().absoluteFilePath("Fallout76.ini"); + + result.append(getArchivesFromKey(iniFile, "sResourceIndexFileList")); + result.append(getArchivesFromKey(iniFile, "sResourceStartUpArchiveList")); + result.append(getArchivesFromKey(iniFile, "SResourceArchiveMemoryCacheList")); + result.append(getArchivesFromKey(iniFile, "SResourceArchiveList")); + result.append(getArchivesFromKey(iniFile, "SResourceArchiveList2")); + result.append(getArchivesFromKey(iniFile, "sResourceArchive2List")); + + return result; +} + +void Fallout76DataArchives::writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) +{ + QString iniFile = + profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("Fallout76.ini") + : localGameDirectory().absoluteFilePath("Fallout76.ini"); + + QStringList sResourceIndexFileList = {}; + QStringList sResourceStartUpArchiveList = {}; + QStringList SResourceArchiveMemoryCacheList = {}; + QStringList SResourceArchiveList = {}; + QStringList SResourceArchiveList2 = {}; + QStringList sResourceArchive2List = {}; + + for (int i = 0; i < before.size(); ++i) { + QString archive = before[i]; + if (archive.contains(QRegularExpression(" - Textures(\\d{2})\\.ba2$"))) { + sResourceIndexFileList.append(archive); + } else if (archive.contains(QRegularExpression( + " - (Interface|Localization|Shaders|Startup)\\.ba2$"))) { + sResourceStartUpArchiveList.append(archive); + } else if (archive.contains(QRegularExpression( + " - (Interface|Materials|MiscClient|Shaders)\\.ba2$"))) { + SResourceArchiveMemoryCacheList.append(archive); + } else if (archive.contains(QRegularExpression( + " - " + "(GeneratedMeshes|Materials|Meshes(\\d{2}|\\w+)?|MiscClient|" + "Sounds\\d{2}|Startup|Voices)\\.ba2$"))) { + SResourceArchiveList.append(archive); + } else if (archive.contains( + QRegularExpression(" - " + "(Animations|Enlighten(Interiors|Exteriors\\d{2})" + "|GeneratedTextures)\\.ba2$"))) { + SResourceArchiveList2.append(archive); + } else if (archive.contains(QRegularExpression(" - ATX_.*\\.ba2$"))) { + // if it is named after DLC, it has to go here + sResourceArchive2List.append(archive); + } else { + // if it did not fit any description above, it gets tacked on at the end + sResourceArchive2List.append(archive); + } + } + + setArchivesToKey(iniFile, "sResourceIndexFileList", + sResourceIndexFileList.join(", ")); + setArchivesToKey(iniFile, "sResourceStartUpArchiveList", + sResourceStartUpArchiveList.join(", ")); + setArchivesToKey(iniFile, "SResourceArchiveMemoryCacheList", + SResourceArchiveMemoryCacheList.join(", ")); + setArchivesToKey(iniFile, "SResourceArchiveList", SResourceArchiveList.join(", ")); + setArchivesToKey(iniFile, "SResourceArchiveList2", SResourceArchiveList2.join(", ")); + setArchivesToKey(iniFile, "sResourceArchive2List", sResourceArchive2List.join(", ")); +} diff --git a/libs/game_bethesda/src/games/fallout76/fallout76savegame.cpp b/libs/game_bethesda/src/games/fallout76/fallout76savegame.cpp new file mode 100644 index 0000000..66e8a00 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout76/fallout76savegame.cpp @@ -0,0 +1,106 @@ +#include "fallout76savegame.h" + +#ifdef _WIN32 +#include +#else +#include + +using SYSTEMTIME = _SYSTEMTIME; + +static void FileTimeToSystemTime(const FILETIME* ft, SYSTEMTIME* st) +{ + const uint64_t ticks = (static_cast(ft->dwHighDateTime) << 32) | + static_cast(ft->dwLowDateTime); + const int64_t unixSecs = static_cast(ticks / 10000000ULL) - 11644473600LL; + const uint64_t remainderHns = ticks % 10000000ULL; + + const QDateTime dt = QDateTime::fromSecsSinceEpoch(unixSecs, Qt::UTC); + const QDate d = dt.date(); + const QTime t = dt.time(); + + st->wYear = static_cast(d.year()); + st->wMonth = static_cast(d.month()); + st->wDayOfWeek = static_cast(d.dayOfWeek() % 7); + st->wDay = static_cast(d.day()); + st->wHour = static_cast(t.hour()); + st->wMinute = static_cast(t.minute()); + st->wSecond = static_cast(t.second()); + st->wMilliseconds = static_cast(remainderHns / 10000); +} +#endif + +#include "gamefallout76.h" + +Fallout76SaveGame::Fallout76SaveGame(QString const& fileName, GameFallout76 const* game) + : GamebryoSaveGame(fileName, game, true) +{ + FileWrapper file(fileName, "FO76_SAVEGAME"); + + FILETIME ftime; + fetchInformationFields(file, m_PCName, m_PCLevel, m_PCLocation, m_SaveNumber, ftime); + + // A file time is a 64-bit value that represents the number of 100-nanosecond + // intervals that have elapsed since 12:00 A.M. January 1, 1601 Coordinated Universal + // Time (UTC). So we need to convert that to something useful + SYSTEMTIME ctime; + ::FileTimeToSystemTime(&ftime, &ctime); + + setCreationTime(ctime); +} + +void Fallout76SaveGame::fetchInformationFields(FileWrapper& file, QString playerName, + unsigned short playerLevel, + QString playerLocation, + unsigned long saveNumber, + FILETIME& creationTime) const +{ + + file.skip(); // header size + file.skip(); // header version + file.read(saveNumber); + + file.read(playerName); + + unsigned long temp; + file.read(temp); + playerLevel = static_cast(temp); + file.read(playerLocation); + + QString ignore; + file.read(ignore); // playtime as ascii hh.mm.ss + file.read(ignore); // race name (i.e. BretonRace) + + file.skip(); // Player gender (0 = male) + file.skip(2); // experience gathered, experience required + + FILETIME ftime; + file.read(ftime); +} + +std::unique_ptr Fallout76SaveGame::fetchDataFields() const +{ + + FileWrapper file(getFilepath(), "TESV_SAVEGAME"); // 10bytes + { + + FILETIME ftime; + fetchInformationFields(file, m_PCName, m_PCLevel, m_PCLocation, m_SaveNumber, + ftime); + } + + std::unique_ptr fields = std::make_unique(); + + file.readImage(384, true); + + uint8_t saveGameVersion = file.readChar(); + QString ignore; + file.read(ignore); // game version + file.skip(); // plugin info size + + file.readPlugins(); + if (saveGameVersion >= 68) { + file.readLightPlugins(); + } + + return fields; +} diff --git a/libs/game_bethesda/src/games/fallout76/fallout76savegameinfo.cpp b/libs/game_bethesda/src/games/fallout76/fallout76savegameinfo.cpp new file mode 100644 index 0000000..c6cb621 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout76/fallout76savegameinfo.cpp @@ -0,0 +1,10 @@ +#include "fallout76savegameinfo.h" + +#include "fallout76savegame.h" +#include "gamegamebryo.h" + +Fallout76SaveGameInfo::Fallout76SaveGameInfo(GameGamebryo const* game) + : GamebryoSaveGameInfo(game) +{} + +Fallout76SaveGameInfo::~Fallout76SaveGameInfo() {} diff --git a/libs/game_bethesda/src/games/fallout76/fallout76scriptextender.cpp b/libs/game_bethesda/src/games/fallout76/fallout76scriptextender.cpp new file mode 100644 index 0000000..8a46c5e --- /dev/null +++ b/libs/game_bethesda/src/games/fallout76/fallout76scriptextender.cpp @@ -0,0 +1,18 @@ +#include "fallout76scriptextender.h" + +#include +#include + +Fallout76ScriptExtender::Fallout76ScriptExtender(GameGamebryo const* game) + : GamebryoScriptExtender(game) +{} + +QString Fallout76ScriptExtender::BinaryName() const +{ + return "f76se_loader.exe"; +} + +QString Fallout76ScriptExtender::PluginPath() const +{ + return "f76se/plugins"; +} diff --git a/libs/game_bethesda/src/games/fallout76/fallout76unmanagedmods.cpp b/libs/game_bethesda/src/games/fallout76/fallout76unmanagedmods.cpp new file mode 100644 index 0000000..1ddc21c --- /dev/null +++ b/libs/game_bethesda/src/games/fallout76/fallout76unmanagedmods.cpp @@ -0,0 +1,46 @@ +#include "fallout76unmanagedmods.h" + +Fallout76UnmangedMods::Fallout76UnmangedMods(const GameGamebryo* game) + : GamebryoUnmangedMods(game) +{} + +Fallout76UnmangedMods::~Fallout76UnmangedMods() {} + +QStringList Fallout76UnmangedMods::mods(bool onlyOfficial) const +{ + QStringList result; + + QStringList pluginList = game()->primaryPlugins(); + QStringList otherPlugins = game()->DLCPlugins(); + otherPlugins.append(game()->CCPlugins()); + for (QString plugin : otherPlugins) { + pluginList.removeAll(plugin); + } + QDir dataDir(game()->dataDirectory()); + for (const QString& fileName : dataDir.entryList({"*.esp", "*.esl", "*.esm"})) { + if (!pluginList.contains(fileName, Qt::CaseInsensitive)) { + if (!onlyOfficial || pluginList.contains(fileName, Qt::CaseInsensitive)) { + QFileInfo file(fileName); + result.append(file.baseName()); + } + } + } + + return result; +} + +QStringList Fallout76UnmangedMods::secondaryFiles(const QString& modName) const +{ + // file extension in FO76 is .ba2 instead of bsa + QStringList archives; + QDir dataDir = game()->dataDirectory(); + for (const QString& archiveName : dataDir.entryList({modName + "*.ba2"})) { + archives.append(dataDir.absoluteFilePath(archiveName)); + } + return archives; +} + +QString Fallout76UnmangedMods::displayName(const QString& modName) const +{ + return modName; +} diff --git a/libs/game_bethesda/src/games/fallout76/gamefallout76.cpp b/libs/game_bethesda/src/games/fallout76/gamefallout76.cpp new file mode 100644 index 0000000..bfeb507 --- /dev/null +++ b/libs/game_bethesda/src/games/fallout76/gamefallout76.cpp @@ -0,0 +1,227 @@ +#include "gamefallout76.h" + +#include "fallout76dataarchives.h" +#include "fallout76moddatachecker.h" +#include "fallout76moddatacontent.h" +#include "fallout76unmanagedmods.h" + +#include "versioninfo.h" +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "scopeguard.h" + +using namespace MOBase; + +GameFallout76::GameFallout76() {} + +bool GameFallout76::init(IOrganizer* moInfo) +{ + if (!GameGamebryo::init(moInfo)) { + return false; + } + + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(this)); + registerFeature( + std::make_shared(m_Organizer->gameFeatures())); + registerFeature(std::make_shared(moInfo)); + registerFeature(std::make_shared(this)); + + return true; +} + +QString GameFallout76::gameName() const +{ + return "Fallout 76"; +} + +void GameFallout76::detectGame() +{ + m_GamePath = identifyGamePath(); + m_MyGamesPath = determineMyGamesPath(gameName()); +} + +QList GameFallout76::executables() const +{ + return QList() + << ExecutableInfo("Fallout 76", findInGameFolder(binaryName())); +} + +QList GameFallout76::executableForcedLoads() const +{ + return {}; +} + +QString GameFallout76::name() const +{ + return "Fallout 76 Support Plugin"; +} + +QString GameFallout76::author() const +{ + return "Mod Organizer Team; EntranceJew"; +} + +QString GameFallout76::description() const +{ + return tr("Adds support for the game Fallout 76.\n" + "Splash by %1") + .arg("nekoyoubi"); +} + +MOBase::VersionInfo GameFallout76::version() const +{ + return VersionInfo(1, 0, 0, VersionInfo::RELEASE_FINAL); +} + +QList GameFallout76::settings() const +{ + return QList(); +} + +void GameFallout76::initializeProfile(const QDir& path, ProfileSettings settings) const +{ + if (settings.testFlag(IPluginGame::MODS)) { + copyToProfile(localAppFolder() + "/Fallout76", path, "plugins.txt"); + copyToProfile(localAppFolder() + "/Fallout76", path, "loadorder.txt"); + } + + if (settings.testFlag(IPluginGame::CONFIGURATION)) { + if (settings.testFlag(IPluginGame::PREFER_DEFAULTS) || + !QFileInfo(myGamesPath() + "/Fallout76.ini").exists()) { + copyToProfile(gameDirectory().absolutePath(), path, "Fallout76_default.ini", + "Fallout76.ini"); + } else { + copyToProfile(myGamesPath(), path, "Fallout76.ini"); + } + + copyToProfile(myGamesPath(), path, "Fallout76Prefs.ini"); + copyToProfile(myGamesPath(), path, "Fallout76Custom.ini"); + } +} + +QString GameFallout76::savegameExtension() const +{ + return ""; +} + +QString GameFallout76::savegameSEExtension() const +{ + return ""; +} + +std::vector> +GameFallout76::listSaves(QDir folder) const +{ + return {}; +} + +QString GameFallout76::identifyGamePath() const +{ + return parseSteamLocation(steamAPPId(), gameShortName()); +} + +std::shared_ptr GameFallout76::makeSaveGame(QString) const +{ + return nullptr; +} + +QString GameFallout76::steamAPPId() const +{ + return "1151340"; +} + +QStringList GameFallout76::primaryPlugins() const +{ + QStringList plugins = {"SeventySix.esm"}; + + plugins.append(CCPlugins()); + + return plugins; +} + +QStringList GameFallout76::gameVariants() const +{ + return {"Regular"}; +} + +QString GameFallout76::gameShortName() const +{ + return "Fallout76"; +} + +QString GameFallout76::gameNexusName() const +{ + return "Fallout76"; +} + +QStringList GameFallout76::iniFiles() const +{ + return {"Fallout76.ini", "Fallout76Prefs.ini", "Fallout76Custom.ini"}; +} + +QStringList GameFallout76::DLCPlugins() const +{ + return {}; +} + +QStringList GameFallout76::CCPlugins() const +{ + QStringList plugins = {}; + QFile file(gameDirectory().absoluteFilePath("Fallout76.ccc")); + if (file.open(QIODevice::ReadOnly)) { + ON_BLOCK_EXIT([&file]() { + file.close(); + }); + + if (file.size() == 0) { + return plugins; + } + while (!file.atEnd()) { + QByteArray line = file.readLine().trimmed(); + QString modName; + if ((line.size() > 0) && (line.at(0) != '#')) { + modName = QString::fromUtf8(line.constData()).toLower(); + } + + if (modName.size() > 0) { + if (!plugins.contains(modName, Qt::CaseInsensitive)) { + plugins.append(modName); + } + } + } + } + return plugins; +} + +IPluginGame::SortMechanism GameFallout76::sortMechanism() const +{ + return IPluginGame::SortMechanism::NONE; +} + +IPluginGame::LoadOrderMechanism GameFallout76::loadOrderMechanism() const +{ + return IPluginGame::LoadOrderMechanism::PluginsTxt; +} + +int GameFallout76::nexusModOrganizerID() const +{ + return -1; +} + +int GameFallout76::nexusGameID() const +{ + return 2590; +} diff --git a/libs/game_bethesda/src/games/falloutnv/falloutnvbsainvalidation.cpp b/libs/game_bethesda/src/games/falloutnv/falloutnvbsainvalidation.cpp new file mode 100644 index 0000000..d66591f --- /dev/null +++ b/libs/game_bethesda/src/games/falloutnv/falloutnvbsainvalidation.cpp @@ -0,0 +1,16 @@ +#include "falloutnvbsainvalidation.h" + +FalloutNVBSAInvalidation::FalloutNVBSAInvalidation(MOBase::DataArchives* dataArchives, + MOBase::IPluginGame const* game) + : GamebryoBSAInvalidation(dataArchives, "fallout.ini", game) +{} + +QString FalloutNVBSAInvalidation::invalidationBSAName() const +{ + return "Fallout - Invalidation.bsa"; +} + +unsigned long FalloutNVBSAInvalidation::bsaVersion() const +{ + return 0x68; +} diff --git a/libs/game_bethesda/src/games/falloutnv/falloutnvdataarchives.cpp b/libs/game_bethesda/src/games/falloutnv/falloutnvdataarchives.cpp new file mode 100644 index 0000000..7314561 --- /dev/null +++ b/libs/game_bethesda/src/games/falloutnv/falloutnvdataarchives.cpp @@ -0,0 +1,32 @@ +#include "falloutnvdataarchives.h" +#include + +QStringList FalloutNVDataArchives::vanillaArchives() const +{ + return {"Fallout - Textures.bsa", "Fallout - Textures2.bsa", "Fallout - Meshes.bsa", + "Fallout - Voices1.bsa", "Fallout - Sound.bsa", "Fallout - Misc.bsa"}; +} + +QStringList FalloutNVDataArchives::archives(const MOBase::IProfile* profile) const +{ + QStringList result; + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("fallout.ini") + : localGameDirectory().absoluteFilePath("fallout.ini"); + result.append(getArchivesFromKey(iniFile, "SArchiveList", + 8192)); // NVAC expands the maximum string limit + + return result; +} + +void FalloutNVDataArchives::writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) +{ + QString list = before.join(", "); + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("fallout.ini") + : localGameDirectory().absoluteFilePath("fallout.ini"); + setArchivesToKey(iniFile, "SArchiveList", list); +} diff --git a/libs/game_bethesda/src/games/falloutnv/falloutnvsavegame.cpp b/libs/game_bethesda/src/games/falloutnv/falloutnvsavegame.cpp new file mode 100644 index 0000000..e6bbc2d --- /dev/null +++ b/libs/game_bethesda/src/games/falloutnv/falloutnvsavegame.cpp @@ -0,0 +1,77 @@ +#include "falloutnvsavegame.h" + +#include "gamefalloutnv.h" + +FalloutNVSaveGame::FalloutNVSaveGame(QString const& fileName, GameFalloutNV const* game) + : GamebryoSaveGame(fileName, game) +{ + FileWrapper file(getFilepath(), "FO3SAVEGAME"); + uint32_t width, height; + fetchInformationFields(file, width, height, m_SaveNumber, m_PCName, m_PCLevel, + m_PCLocation); +} + +void FalloutNVSaveGame::fetchInformationFields(FileWrapper& file, uint32_t& width, + uint32_t& height, + uint32_t& saveNumber, + QString& playerName, + unsigned short& playerLevel, + QString& playerLocation) const +{ + file.skip(); // Save header size + + file.skip(); // File version? + file.skip(); // Delimiter + + // A huge wodge of text with no length but a delimiter. Given the null bytes + // in it I presume it's fixed length (64 bytes + delim) but I have no + // definite spec + for (unsigned char ignore = 0; ignore != 0x7c;) { + file.read(ignore); // unknown + } + + file.setHasFieldMarkers(true); + file.setPluginString(GamebryoSaveGame::StringType::TYPE_BZSTRING); + + file.read(width); + file.read(height); + file.read(saveNumber); + file.read(playerName); + + QString whatthis; + file.read(whatthis); + + long level; + file.read(level); + playerLevel = level; + file.read(playerLocation); +} + +std::unique_ptr FalloutNVSaveGame::fetchDataFields() const +{ + FileWrapper file(getFilepath(), "FO3SAVEGAME"); + + std::unique_ptr fields = std::make_unique(); + + uint32_t width, height; + { + QString dummyName, dummyLocation; + unsigned short dummyLevel; + uint32_t dummySaveNumber; + + fetchInformationFields(file, width, height, dummySaveNumber, dummyName, dummyLevel, + dummyLocation); + } + + QString playtime; + file.read(playtime); + + fields->Screenshot = file.readImage(width, height, 256); + + file.skip(5); // unknown (1 byte), plugin size (4 bytes) + + file.setPluginString(GamebryoSaveGame::StringType::TYPE_BSTRING); + fields->Plugins = file.readPlugins(); + + return fields; +} diff --git a/libs/game_bethesda/src/games/falloutnv/falloutnvscriptextender.cpp b/libs/game_bethesda/src/games/falloutnv/falloutnvscriptextender.cpp new file mode 100644 index 0000000..fc00c31 --- /dev/null +++ b/libs/game_bethesda/src/games/falloutnv/falloutnvscriptextender.cpp @@ -0,0 +1,18 @@ +#include "falloutnvscriptextender.h" + +#include +#include + +FalloutNVScriptExtender::FalloutNVScriptExtender(GameGamebryo const* game) + : GamebryoScriptExtender(game) +{} + +QString FalloutNVScriptExtender::BinaryName() const +{ + return "nvse_loader.exe"; +} + +QString FalloutNVScriptExtender::PluginPath() const +{ + return "nvse/plugins"; +} diff --git a/libs/game_bethesda/src/games/falloutnv/gamefalloutnv.cpp b/libs/game_bethesda/src/games/falloutnv/gamefalloutnv.cpp new file mode 100644 index 0000000..1e8e9e9 --- /dev/null +++ b/libs/game_bethesda/src/games/falloutnv/gamefalloutnv.cpp @@ -0,0 +1,334 @@ +#include "gamefalloutnv.h" + +#include "falloutnvbsainvalidation.h" +#include "falloutnvdataarchives.h" +#include "falloutnvmoddatachecker.h" +#include "falloutnvmoddatacontent.h" +#include "falloutnvsavegame.h" +#include "falloutnvscriptextender.h" + +#include "executableinfo.h" +#include "pluginsetting.h" +#include "versioninfo.h" +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace MOBase; + +GameFalloutNV::GameFalloutNV() {} + +bool GameFalloutNV::init(IOrganizer* moInfo) +{ + if (!GameGamebryo::init(moInfo)) { + return false; + } + + auto dataArchives = std::make_shared(this); + registerFeature(std::make_shared(this)); + registerFeature(dataArchives); + registerFeature(std::make_shared(dataArchives.get(), this)); + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(this, "fallout.ini")); + registerFeature(std::make_shared(this)); + registerFeature( + std::make_shared(m_Organizer->gameFeatures())); + registerFeature(std::make_shared(moInfo)); + registerFeature(std::make_shared(this)); + + return true; +} + +void GameFalloutNV::setVariant(QString variant) +{ + m_GameVariant = variant; +} + +void GameFalloutNV::checkVariants() +{ + QFileInfo gog_dll(m_GamePath + "\\Galaxy.dll"); + QFileInfo epic_dll(m_GamePath + "\\EOSSDK-Win32-Shipping.dll"); + if (gog_dll.exists()) + setVariant("GOG"); + else if (epic_dll.exists()) + setVariant("Epic Games"); + else + setVariant("Steam"); +} + +QDir GameFalloutNV::documentsDirectory() const +{ + return m_MyGamesPath; +} + +QString GameFalloutNV::identifyGamePath() const +{ + auto result = GameGamebryo::identifyGamePath(); // Default registry path + // EPIC Game Store + if (result.isEmpty()) { + /** + * Basegame: 5daeb974a22a435988892319b3a4f476 + * Dead Money: b290229eb58045cbab9501640f3278f3 + * Honest Hearts: 562d4a2c1b3147b089a7c453e3ddbcbe + * Old World Blues: c8dae1ab0570475a8b38a9041e614840 + * Lonesome Road: 4fa3d8d9b2cb4714a19a38d1a598be8f + * Gun Runners' Arsenal: 7dcfb9cd9d134728b2646466c34c7b3b + * Courier's Stash: ee9a44b4530942499ef1c8c390731fce + */ + result = parseEpicGamesLocation({"5daeb974a22a435988892319b3a4f476"}); + if (QFileInfo(result).isDir()) { + QDir startPath = QDir(result); + auto subDirs = startPath.entryList({"Fallout New Vegas*"}, + QDir::Dirs | QDir::NoDotAndDotDot); + if (!subDirs.isEmpty()) + result = startPath.absoluteFilePath(subDirs.first()); + } + } + return result; +} + +void GameFalloutNV::setGamePath(const QString& path) +{ + m_GamePath = path; + checkVariants(); + m_MyGamesPath = determineMyGamesPath(gameDirectoryName()); +} + +QDir GameFalloutNV::savesDirectory() const +{ + return QDir(m_MyGamesPath + "/Saves"); +} + +QString GameFalloutNV::myGamesPath() const +{ + return m_MyGamesPath; +} + +bool GameFalloutNV::isInstalled() const +{ + return !m_GamePath.isEmpty(); +} + +QString GameFalloutNV::gameName() const +{ + return "New Vegas"; +} + +QString GameFalloutNV::gameDirectoryName() const +{ + if (selectedVariant() == "Epic Games") + return "FalloutNV_Epic"; + else + return "FalloutNV"; +} + +void GameFalloutNV::detectGame() +{ + m_GamePath = identifyGamePath(); + checkVariants(); + m_MyGamesPath = determineMyGamesPath(gameDirectoryName()); +} + +QList GameFalloutNV::executables() const +{ + ExecutableInfo game("New Vegas", findInGameFolder(binaryName())); + ExecutableInfo launcher("Fallout Launcher", findInGameFolder(getLauncherName())); + QList extraExecutables = + QList() << ExecutableInfo("Fallout Mod Manager", + findInGameFolder("fomm/fomm.exe")) + << ExecutableInfo("BOSS", + findInGameFolder("BOSS/BOSS.exe")) + << ExecutableInfo("GECK", findInGameFolder("geck.exe")) + << ExecutableInfo("LOOT", QFileInfo(getLootPath())) + .withArgument("--game=\"FalloutNV\""); + if (selectedVariant() != "Epic Games") { + extraExecutables.prepend(ExecutableInfo( + "NVSE", findInGameFolder(m_Organizer->gameFeatures() + ->gameFeature() + ->loaderName()))); + } else { + game.withArgument("-EpicPortal"); + launcher.withArgument("-EpicPortal"); + } + QList executables = {game, launcher}; + executables += extraExecutables; + return executables; +} + +QList GameFalloutNV::executableForcedLoads() const +{ + return QList(); +} + +QString GameFalloutNV::name() const +{ + return "Fallout NV Support Plugin"; +} + +QString GameFalloutNV::localizedName() const +{ + return tr("Fallout NV Support Plugin"); +} + +QString GameFalloutNV::author() const +{ + return "Tannin & MO2 Team"; +} + +QString GameFalloutNV::description() const +{ + return tr("Adds support for the game Fallout New Vegas"); +} + +MOBase::VersionInfo GameFalloutNV::version() const +{ + return VersionInfo(1, 6, 0, VersionInfo::RELEASE_FINAL); +} + +QList GameFalloutNV::settings() const +{ + return QList() + << PluginSetting("enable_loot_sorting", + tr("While not recommended by the FNV modding community, " + "enables LOOT sorting"), + false); +} + +void GameFalloutNV::initializeProfile(const QDir& path, ProfileSettings settings) const +{ + if (settings.testFlag(IPluginGame::MODS)) { + copyToProfile(localAppFolder() + "/" + gameDirectoryName(), path, "plugins.txt"); + } + + if (settings.testFlag(IPluginGame::CONFIGURATION)) { + if (settings.testFlag(IPluginGame::PREFER_DEFAULTS) || + !QFileInfo(myGamesPath() + "/fallout.ini").exists()) { + copyToProfile(gameDirectory().absolutePath(), path, "fallout_default.ini", + "fallout.ini"); + } else { + copyToProfile(myGamesPath(), path, "fallout.ini"); + } + + copyToProfile(myGamesPath(), path, "falloutprefs.ini"); + copyToProfile(myGamesPath(), path, "falloutcustom.ini"); + copyToProfile(myGamesPath(), path, "GECKCustom.ini"); + copyToProfile(myGamesPath(), path, "GECKPrefs.ini"); + } +} + +QString GameFalloutNV::savegameExtension() const +{ + return "fos"; +} + +QString GameFalloutNV::savegameSEExtension() const +{ + return "nvse"; +} + +std::shared_ptr +GameFalloutNV::makeSaveGame(QString filePath) const +{ + return std::make_shared(filePath, this); +} + +QString GameFalloutNV::steamAPPId() const +{ + if (selectedVariant() == "Steam") { + if (m_GamePath.endsWith("enplczru")) { + return "22490"; + } else { + return "22380"; + } + } + return QString(); +} + +QStringList GameFalloutNV::primaryPlugins() const +{ + return {"falloutnv.esm"}; +} + +QStringList GameFalloutNV::gameVariants() const +{ + return {"Steam", "GOG", "Epic Games"}; +} + +QString GameFalloutNV::gameShortName() const +{ + return "FalloutNV"; +} + +QStringList GameFalloutNV::validShortNames() const +{ + return {"Fallout3"}; +} + +QString GameFalloutNV::gameNexusName() const +{ + return "newvegas"; +} + +QStringList GameFalloutNV::iniFiles() const +{ + return {"fallout.ini", "falloutprefs.ini", "falloutcustom.ini", "GECKCustom.ini", + "GECKPrefs.ini"}; +} + +QStringList GameFalloutNV::DLCPlugins() const +{ + return {"DeadMoney.esm", "HonestHearts.esm", "OldWorldBlues.esm", + "LonesomeRoad.esm", "GunRunnersArsenal.esm", "CaravanPack.esm", + "ClassicPack.esm", "MercenaryPack.esm", "TribalPack.esm"}; +} + +MOBase::IPluginGame::SortMechanism GameFalloutNV::sortMechanism() const +{ + if (m_Organizer->pluginSetting(name(), "enable_loot_sorting").toBool()) + return IPluginGame::SortMechanism::LOOT; + return IPluginGame::SortMechanism::NONE; +} + +int GameFalloutNV::nexusModOrganizerID() const +{ + return 42572; +} + +int GameFalloutNV::nexusGameID() const +{ + return 130; +} + +QDir GameFalloutNV::gameDirectory() const +{ + return QDir(m_GamePath); +} + +MappingType GameFalloutNV::mappings() const +{ + MappingType result; + + for (const QString& profileFile : {"plugins.txt", "loadorder.txt"}) { + result.push_back({m_Organizer->profilePath() + "/" + profileFile, + localAppFolder() + "/" + gameShortName() + "/" + profileFile, + false}); + if (selectedVariant() == "Epic Games") { + result.push_back( + {m_Organizer->profilePath() + "/" + profileFile, + localAppFolder() + "/" + gameDirectoryName() + "/" + profileFile, false}); + } + } + + return result; +} 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 + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +using namespace MOBase; + +GameMorrowind::GameMorrowind() {} + +bool GameMorrowind::init(IOrganizer* moInfo) +{ + if (!GameGamebryo::init(moInfo)) { + return false; + } + + auto dataArchives = std::make_shared(this); + registerFeature(dataArchives); + registerFeature(std::make_shared(dataArchives.get(), this)); + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(this)); + registerFeature( + std::make_shared(m_Organizer->gameFeatures())); + registerFeature(std::make_shared(moInfo)); + registerFeature(std::make_shared(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 GameMorrowind::executables() const +{ + return QList() + << 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 GameMorrowind::executableForcedLoads() const +{ + return QList(); +} + +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 GameMorrowind::settings() const +{ + return QList(); +} + +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 +GameMorrowind::makeSaveGame(QString filePath) const +{ + return std::make_shared(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/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/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 + +#include + +#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/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 +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +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/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 +#include +#include +#include + +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/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 +#include +#include + +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(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(3); // data size + file.skip(4); // HEDR tag + file.skip(); // header size + file.skip(); // header version + file.skip(); // following data chunk size? seems to be 9 groupings of 32 + // bytes + file.skip(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 saveNameBuffer(256); // 31 char save name with a null terminator + file.read(saveNameBuffer.data(), 256); + saveName = QString::fromLatin1(saveNameBuffer.data(), -1).trimmed(); + } + + file.skip(); // NumRecords (for the entire save) + std::vector 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(4); // DATA record + + // Typically size 8 - contains length of master data for version checking + file.read(len); // Length + file.skip(len); + + file.read(buffer.data(), 4); // Get next record type + plugins.push_back(name); + } + + // Start of GMDT + file.skip(); // size of record + + file.read(playerCurrentHealth); + file.read(playerMaxHealth); + + file.skip(); // current stam? + file.skip(); // max stam? + // file.skip(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 MorrowindSaveGame::fetchDataFields() const +{ + FileWrapper file(getFilepath(), "TES3"); + std::vector buffer(255); + + std::unique_ptr fields = std::make_unique(); + + { + QString dummy; + float dummyF; + fetchInformationFields(file, dummy, fields->Plugins, dummyF, dummyF, dummy, dummyF, + dummy); + } + + file.skip(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(4); // SCRS + file.skip(); // 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(); + std::vector buff(4); + file.read(buff.data(), 4); + while (QString::fromLatin1(buff.data(), 4) != "NPC_") { + uint32_t len; + file.read(len); + file.skip(8 + len); + file.read(buff.data(), 4); + } + while (QString::fromLatin1(buff.data(), 4) == "NPC_") { + uint32_t size; + file.read(size); + file.skip(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(len); + file.read(buff.data(), 4); + } + file.skip(); + file.read(fields->PCLevel); + } else { + file.skip(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/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(game); +} + +MorrowindSaveGameInfo::~MorrowindSaveGameInfo() {} + +MOBase::ISaveGameInfoWidget* +MorrowindSaveGameInfo::getSaveGameWidget(QWidget* parent) const +{ + return new MorrowindSaveGameInfoWidget(this, parent); +} 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +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(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/nehrim/gamenehrim.cpp b/libs/game_bethesda/src/games/nehrim/gamenehrim.cpp new file mode 100644 index 0000000..2e3c8c5 --- /dev/null +++ b/libs/game_bethesda/src/games/nehrim/gamenehrim.cpp @@ -0,0 +1,205 @@ +#include "gamenehrim.h" + +#include "nehrimbsainvalidation.h" +#include "nehrimdataarchives.h" +#include "nehrimmoddatachecker.h" +#include "nehrimmoddatacontent.h" +#include "nehrimsavegame.h" +#include "nehrimscriptextender.h" + +#include "executableinfo.h" +#include "pluginsetting.h" +#include +#include +#include +#include + +#include +#include +#include + +#include + +using namespace MOBase; + +GameNehrim::GameNehrim() {} + +bool GameNehrim::init(IOrganizer* moInfo) +{ + if (!GameGamebryo::init(moInfo)) { + return false; + } + + auto dataArchives = std::make_shared(this); + registerFeature(std::make_shared(this)); + registerFeature(dataArchives); + registerFeature(std::make_shared(dataArchives.get(), this)); + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(this, "oblivion.ini")); + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(m_Organizer->gameFeatures())); + registerFeature(std::make_shared(moInfo)); + registerFeature(std::make_shared(this)); + return true; +} + +QString GameNehrim::gameName() const +{ + return "Nehrim"; +} + +QList GameNehrim::executables() const +{ + return QList() + << ExecutableInfo("Nehrim", findInGameFolder("Oblivion.exe")) + << ExecutableInfo("Nehrim Launcher", findInGameFolder("NehrimLauncher.exe")) + << ExecutableInfo("Oblivion Mod Manager", + findInGameFolder("OblivionModManager.exe")) + << ExecutableInfo("BOSS", findInGameFolder("BOSS/BOSS.exe")) + << ExecutableInfo("LOOT", QFileInfo(getLootPath())) + .withArgument("--game=\"Nehrim\"") + << ExecutableInfo("Construction Set", + findInGameFolder("TESConstructionSet.exe")); +} + +QList GameNehrim::executableForcedLoads() const +{ + // TODO Search game directory for OBSE DLLs + return QList() + << ExecutableForcedLoadSetting("Oblivion.exe", "obse_1_2_416.dll") + .withForced() + .withEnabled() + << ExecutableForcedLoadSetting("TESConstructionSet.exe", "obse_editor_1_2.dll") + .withForced() + .withEnabled(); +} + +QString GameNehrim::name() const +{ + return "Nehrim Support Plugin"; +} + +QString GameNehrim::localizedName() const +{ + return tr("Nehrim Support Plugin"); +} + +QString GameNehrim::author() const +{ + return "Tannin & MO2 Team"; +} + +QString GameNehrim::description() const +{ + return tr("Adds support for the game Nehrim"); +} + +MOBase::VersionInfo GameNehrim::version() const +{ + return VersionInfo(1, 1, 1, VersionInfo::RELEASE_FINAL); +} + +QList GameNehrim::settings() const +{ + return QList(); +} + +void GameNehrim::initializeProfile(const QDir& path, ProfileSettings settings) const +{ + if (settings.testFlag(IPluginGame::MODS)) { + copyToProfile(localAppFolder() + "/Oblivion", path, "plugins.txt"); + } + + if (settings.testFlag(IPluginGame::CONFIGURATION)) { + if (settings.testFlag(IPluginGame::PREFER_DEFAULTS) || + !QFileInfo(myGamesPath() + "/oblivion.ini").exists()) { + copyToProfile(gameDirectory().absolutePath(), path, "oblivion_default.ini", + "oblivion.ini"); + } else { + copyToProfile(myGamesPath(), path, "oblivion.ini"); + } + + copyToProfile(myGamesPath(), path, "oblivionprefs.ini"); + } +} + +QString GameNehrim::savegameExtension() const +{ + return "ess"; +} + +QString GameNehrim::savegameSEExtension() const +{ + return "obse"; +} + +std::shared_ptr GameNehrim::makeSaveGame(QString filePath) const +{ + return std::make_shared(filePath, this); +} + +QString GameNehrim::steamAPPId() const +{ + return "22330"; +} + +QStringList GameNehrim::primaryPlugins() const +{ + return {"Nehrim.esm", "Translation.esp"}; +} + +QString GameNehrim::gameShortName() const +{ + return "Nehrim"; +} + +QString GameNehrim::gameNexusName() const +{ + return "Nehrim"; +} + +QStringList GameNehrim::iniFiles() const +{ + return {"oblivion.ini", "oblivionprefs.ini"}; +} + +QStringList GameNehrim::DLCPlugins() const +{ + return {}; +} + +int GameNehrim::nexusModOrganizerID() const +{ + return -1; +} + +int GameNehrim::nexusGameID() const +{ + return 3312; +} + +QStringList GameNehrim::primarySources() const +{ + return {"Oblivion"}; +} + +QStringList GameNehrim::validShortNames() const +{ + return {"Oblivion"}; +} + +QString GameNehrim::identifyGamePath() const +{ +#ifdef _WIN32 + QString path = "Software\\Bethesda Softworks\\Oblivion"; + return findInRegistry(HKEY_LOCAL_MACHINE, path.toStdWString().c_str(), + L"Installed Path"); +#else + return GameGamebryo::identifyGamePath(); +#endif +} + +QString GameNehrim::binaryName() const +{ + return "NehrimLauncher.exe"; +} diff --git a/libs/game_bethesda/src/games/nehrim/nehrimbsainvalidation.cpp b/libs/game_bethesda/src/games/nehrim/nehrimbsainvalidation.cpp new file mode 100644 index 0000000..ac89d54 --- /dev/null +++ b/libs/game_bethesda/src/games/nehrim/nehrimbsainvalidation.cpp @@ -0,0 +1,16 @@ +#include "nehrimbsainvalidation.h" + +NehrimBSAInvalidation::NehrimBSAInvalidation(MOBase::DataArchives* dataArchives, + MOBase::IPluginGame const* game) + : GamebryoBSAInvalidation(dataArchives, "oblivion.ini", game) +{} + +QString NehrimBSAInvalidation::invalidationBSAName() const +{ + return "Nehrim - Invalidation.bsa"; +} + +unsigned long NehrimBSAInvalidation::bsaVersion() const +{ + return 0x67; +} diff --git a/libs/game_bethesda/src/games/nehrim/nehrimdataarchives.cpp b/libs/game_bethesda/src/games/nehrim/nehrimdataarchives.cpp new file mode 100644 index 0000000..8fe5b06 --- /dev/null +++ b/libs/game_bethesda/src/games/nehrim/nehrimdataarchives.cpp @@ -0,0 +1,31 @@ +#include "nehrimdataarchives.h" +#include + +QStringList NehrimDataArchives::vanillaArchives() const +{ + return {"N - Meshes.bsa", "N - Textures1.bsa", "N - Textures2.bsa", "N - Misc.bsa", + "N - Sounds.bsa", "L - Voices.bsa", "L - Misc.bsa"}; +} + +QStringList NehrimDataArchives::archives(const MOBase::IProfile* profile) const +{ + QStringList result; + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("oblivion.ini") + : localGameDirectory().absoluteFilePath("oblivion.ini"); + result.append(getArchivesFromKey(iniFile, "SArchiveList")); + + return result; +} + +void NehrimDataArchives::writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) +{ + QString list = before.join(", "); + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("oblivion.ini") + : localGameDirectory().absoluteFilePath("oblivion.ini"); + setArchivesToKey(iniFile, "SArchiveList", list); +} diff --git a/libs/game_bethesda/src/games/nehrim/nehrimmoddatachecker.cpp b/libs/game_bethesda/src/games/nehrim/nehrimmoddatachecker.cpp new file mode 100644 index 0000000..3ec4eb0 --- /dev/null +++ b/libs/game_bethesda/src/games/nehrim/nehrimmoddatachecker.cpp @@ -0,0 +1,30 @@ +#include "nehrimmoddatachecker.h" + +MOBase::ModDataChecker::CheckReturn NehrimModDataChecker::dataLooksValid( + std::shared_ptr fileTree) const +{ + // Check with Gamebryo stuff: + auto check = GamebryoModDataChecker::dataLooksValid(fileTree); + if (check == CheckReturn::VALID) { + return check; + } + + // Check for OBSE_ files: + for (auto const& entry : *fileTree) { + if (entry->isDir() || !entry->name().startsWith("OBSE", Qt::CaseInsensitive)) { + return CheckReturn::INVALID; + } + } + + return CheckReturn::FIXABLE; +} + +std::shared_ptr +NehrimModDataChecker::fix(std::shared_ptr fileTree) const +{ + // If we arrive here, it means all files starts with OBSE. + auto data = fileTree->createOrphanTree(); + auto obse = data->addDirectory("OBSE/Plugins"); + obse->merge(fileTree); + return data; +} diff --git a/libs/game_bethesda/src/games/nehrim/nehrimsavegame.cpp b/libs/game_bethesda/src/games/nehrim/nehrimsavegame.cpp new file mode 100644 index 0000000..4a6f912 --- /dev/null +++ b/libs/game_bethesda/src/games/nehrim/nehrimsavegame.cpp @@ -0,0 +1,72 @@ +#include "nehrimsavegame.h" + +NehrimSaveGame::NehrimSaveGame(QString const& fileName, GameNehrim const* game) + : GamebryoSaveGame(fileName, game) +{ + FileWrapper file(getFilepath(), "TES4SAVEGAME"); + file.setPluginString(GamebryoSaveGame::StringType::TYPE_BSTRING); + + _SYSTEMTIME creationTime; + fetchInformationFields(file, m_SaveNumber, m_PCName, m_PCLevel, m_PCLocation, + creationTime); + setCreationTime(creationTime); +} + +void NehrimSaveGame::fetchInformationFields(FileWrapper& file, + uint32_t& saveNumber, + QString& playerName, + unsigned short& playerLevel, + QString& playerLocation, + _SYSTEMTIME& creationTime) const +{ + file.skip(); // Major version + file.skip(); // Minor version + + file.skip<_SYSTEMTIME>(); // exe last modified (!) + + file.skip(); // Header version + file.skip(); // Header size + + file.read(saveNumber); + + file.read(playerName); + file.read(playerLevel); + file.read(playerLocation); + + file.skip(); // game days + file.skip(); // game ticks + + // there is a save time stored here. So use it rather than the file time, which + // could have been copied. + // Note: This says it uses getlocaltime api to obtain it which is u/s - if so + // we should ignore this. + file.read(creationTime); +} + +std::unique_ptr NehrimSaveGame::fetchDataFields() const +{ + FileWrapper file(getFilepath(), "TES4SAVEGAME"); + file.setPluginString(GamebryoSaveGame::StringType::TYPE_BSTRING); + + std::unique_ptr fields = std::make_unique(); + + { + QString dummyName, dummyLocation; + unsigned short dummyLevel; + uint32_t dummySaveNumber; + _SYSTEMTIME dummyTime; + + fetchInformationFields(file, dummySaveNumber, dummyName, dummyLevel, dummyLocation, + dummyTime); + } + + // Note that screenshot size, width, height and data are apparently the same + // structure + file.skip(); // Screenshot size. + + fields->Screenshot = file.readImage(); + + fields->Plugins = file.readPlugins(); + + return fields; +} diff --git a/libs/game_bethesda/src/games/nehrim/nehrimscriptextender.cpp b/libs/game_bethesda/src/games/nehrim/nehrimscriptextender.cpp new file mode 100644 index 0000000..c50c59e --- /dev/null +++ b/libs/game_bethesda/src/games/nehrim/nehrimscriptextender.cpp @@ -0,0 +1,20 @@ +#include "nehrimscriptextender.h" + +#include +#include + +NehrimScriptExtender::NehrimScriptExtender(GameGamebryo const* game) + : GamebryoScriptExtender(game) +{} + +NehrimScriptExtender::~NehrimScriptExtender() {} + +QString NehrimScriptExtender::BinaryName() const +{ + return "obse_loader.exe"; +} + +QString NehrimScriptExtender::PluginPath() const +{ + return "obse/plugins"; +} diff --git a/libs/game_bethesda/src/games/oblivion/gameoblivion.cpp b/libs/game_bethesda/src/games/oblivion/gameoblivion.cpp new file mode 100644 index 0000000..9f7a174 --- /dev/null +++ b/libs/game_bethesda/src/games/oblivion/gameoblivion.cpp @@ -0,0 +1,213 @@ +#include "gameoblivion.h" + +#include "oblivionbsainvalidation.h" +#include "obliviondataarchives.h" +#include "oblivionmoddatachecker.h" +#include "oblivionmoddatacontent.h" +#include "oblivionsavegame.h" +#include "oblivionscriptextender.h" + +#include "executableinfo.h" +#include "pluginsetting.h" +#include "steamutility.h" +#include +#include +#include +#include + +#include +#include +#include + +#include + +using namespace MOBase; + +GameOblivion::GameOblivion() {} + +bool GameOblivion::init(IOrganizer* moInfo) +{ + if (!GameGamebryo::init(moInfo)) { + return false; + } + + auto dataArchives = std::make_shared(this); + registerFeature(std::make_shared(this)); + registerFeature(dataArchives); + registerFeature(std::make_shared(dataArchives.get(), this)); + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(this, "oblivion.ini")); + registerFeature(std::make_shared(this)); + registerFeature( + std::make_shared(m_Organizer->gameFeatures())); + registerFeature(std::make_shared(moInfo)); + registerFeature(std::make_shared(this)); + return true; +} + +QString GameOblivion::gameName() const +{ + return "Oblivion"; +} + +QList GameOblivion::executables() const +{ + return QList() + << ExecutableInfo("Oblivion", findInGameFolder(binaryName())) + << ExecutableInfo("Oblivion Launcher", findInGameFolder(getLauncherName())) + << ExecutableInfo("Oblivion Mod Manager", + findInGameFolder("OblivionModManager.exe")) + << ExecutableInfo("BOSS", findInGameFolder("BOSS/BOSS.exe")) + << ExecutableInfo("LOOT", QFileInfo(getLootPath())) + .withArgument("--game=\"Oblivion\"") + << ExecutableInfo("Construction Set", + findInGameFolder("TESConstructionSet.exe")); +} + +QList GameOblivion::executableForcedLoads() const +{ + return QList() + << ExecutableForcedLoadSetting("Oblivion.exe", "obse_1_2_416.dll") + .withForced() + .withEnabled() + << ExecutableForcedLoadSetting("TESConstructionSet.exe", "obse_editor_1_2.dll") + .withForced() + .withEnabled(); +} + +QString GameOblivion::name() const +{ + return "Oblivion Support Plugin"; +} + +QString GameOblivion::localizedName() const +{ + return tr("Oblivion Support Plugin"); +} + +QString GameOblivion::author() const +{ + return "Tannin & MO2 Team"; +} + +QString GameOblivion::description() const +{ + return tr("Adds support for the game Oblivion"); +} + +MOBase::VersionInfo GameOblivion::version() const +{ + return VersionInfo(1, 6, 1, VersionInfo::RELEASE_FINAL); +} + +QList GameOblivion::settings() const +{ + return {PluginSetting("nehrim_downloads", "allow Nehrim downloads", QVariant(false))}; +} + +void GameOblivion::initializeProfile(const QDir& path, ProfileSettings settings) const +{ + if (settings.testFlag(IPluginGame::MODS)) { + copyToProfile(localAppFolder() + "/Oblivion", path, "plugins.txt"); + } + + if (settings.testFlag(IPluginGame::CONFIGURATION)) { + if (settings.testFlag(IPluginGame::PREFER_DEFAULTS) || + !QFileInfo(myGamesPath() + "/oblivion.ini").exists()) { + copyToProfile(gameDirectory().absolutePath(), path, "oblivion_default.ini", + "oblivion.ini"); + } else { + copyToProfile(myGamesPath(), path, "oblivion.ini"); + } + + copyToProfile(myGamesPath(), path, "oblivionprefs.ini"); + } +} + +QString GameOblivion::savegameExtension() const +{ + return "ess"; +} + +QString GameOblivion::savegameSEExtension() const +{ + return "obse"; +} + +std::shared_ptr +GameOblivion::makeSaveGame(QString filePath) const +{ + return std::make_shared(filePath, this); +} + +QString GameOblivion::steamAPPId() const +{ + return "22330"; +} + +QStringList GameOblivion::primaryPlugins() const +{ + return {"oblivion.esm", "update.esm"}; +} + +QString GameOblivion::gameShortName() const +{ + return "Oblivion"; +} + +QStringList GameOblivion::validShortNames() const +{ + QStringList shortNames; + if (m_Organizer->pluginSetting(name(), "nehrim_downloads").toBool()) { + shortNames.append("Nehrim"); + } + return shortNames; +} + +QString GameOblivion::gameNexusName() const +{ + return "Oblivion"; +} + +QStringList GameOblivion::iniFiles() const +{ + return {"oblivion.ini", "oblivionprefs.ini"}; +} + +QStringList GameOblivion::DLCPlugins() const +{ + return {"DLCBattlehornCastle.esp", "DLCShiveringIsles.esp", "Knights.esp", + "DLCFrostcrag.esp", "DLCSpellTomes.esp", "DLCMehrunesRazor.esp", + "DLCOrrery.esp", "DLCThievesDen.esp", "DLCVileLair.esp", + "DLCHorseArmor.esp"}; +} + +int GameOblivion::nexusModOrganizerID() const +{ + return 38277; +} + +int GameOblivion::nexusGameID() const +{ + return 101; +} + +QString GameOblivion::identifyGamePath() const +{ +#ifdef _WIN32 + return GameGamebryo::identifyGamePath(); +#else + // Prefer Steam app-id resolution to avoid matching Oblivion Remastered. + QString path = parseSteamLocation(steamAPPId(), "Oblivion"); + if (!path.isEmpty() && looksValid(QDir(path))) { + return path; + } + + path = MOBase::findSteamGame("Oblivion", "Oblivion.exe"); + if (!path.isEmpty() && looksValid(QDir(path))) { + return path; + } + + return GameGamebryo::identifyGamePath(); +#endif +} diff --git a/libs/game_bethesda/src/games/oblivion/oblivionbsainvalidation.cpp b/libs/game_bethesda/src/games/oblivion/oblivionbsainvalidation.cpp new file mode 100644 index 0000000..864d37c --- /dev/null +++ b/libs/game_bethesda/src/games/oblivion/oblivionbsainvalidation.cpp @@ -0,0 +1,16 @@ +#include "oblivionbsainvalidation.h" + +OblivionBSAInvalidation::OblivionBSAInvalidation(MOBase::DataArchives* dataArchives, + MOBase::IPluginGame const* game) + : GamebryoBSAInvalidation(dataArchives, "oblivion.ini", game) +{} + +QString OblivionBSAInvalidation::invalidationBSAName() const +{ + return "Oblivion - Invalidation.bsa"; +} + +unsigned long OblivionBSAInvalidation::bsaVersion() const +{ + return 0x67; +} diff --git a/libs/game_bethesda/src/games/oblivion/obliviondataarchives.cpp b/libs/game_bethesda/src/games/oblivion/obliviondataarchives.cpp new file mode 100644 index 0000000..47c3dd4 --- /dev/null +++ b/libs/game_bethesda/src/games/oblivion/obliviondataarchives.cpp @@ -0,0 +1,32 @@ +#include "obliviondataarchives.h" +#include + +QStringList OblivionDataArchives::vanillaArchives() const +{ + return {"Oblivion - Misc.bsa", "Oblivion - Textures - Compressed.bsa", + "Oblivion - Meshes.bsa", "Oblivion - Sounds.bsa", + "Oblivion - Voices1.bsa", "Oblivion - Voices2.bsa"}; +} + +QStringList OblivionDataArchives::archives(const MOBase::IProfile* profile) const +{ + QStringList result; + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("oblivion.ini") + : localGameDirectory().absoluteFilePath("oblivion.ini"); + result.append(getArchivesFromKey(iniFile, "SArchiveList")); + + return result; +} + +void OblivionDataArchives::writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) +{ + QString list = before.join(", "); + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("oblivion.ini") + : localGameDirectory().absoluteFilePath("oblivion.ini"); + setArchivesToKey(iniFile, "SArchiveList", list); +} diff --git a/libs/game_bethesda/src/games/oblivion/oblivionmoddatachecker.cpp b/libs/game_bethesda/src/games/oblivion/oblivionmoddatachecker.cpp new file mode 100644 index 0000000..30a74ee --- /dev/null +++ b/libs/game_bethesda/src/games/oblivion/oblivionmoddatachecker.cpp @@ -0,0 +1,30 @@ +#include "oblivionmoddatachecker.h" + +MOBase::ModDataChecker::CheckReturn OblivionModDataChecker::dataLooksValid( + std::shared_ptr fileTree) const +{ + // Check with Gamebryo stuff: + auto check = GamebryoModDataChecker::dataLooksValid(fileTree); + if (check == CheckReturn::VALID) { + return check; + } + + // Check for OBSE_ files: + for (auto const& entry : *fileTree) { + if (entry->isDir() || !entry->name().startsWith("OBSE", Qt::CaseInsensitive)) { + return CheckReturn::INVALID; + } + } + + return CheckReturn::FIXABLE; +} + +std::shared_ptr +OblivionModDataChecker::fix(std::shared_ptr fileTree) const +{ + // If we arrive here, it means all files starts with OBSE. + auto data = fileTree->createOrphanTree(); + auto obse = data->addDirectory("OBSE/Plugins"); + obse->merge(fileTree); + return data; +} diff --git a/libs/game_bethesda/src/games/oblivion/oblivionsavegame.cpp b/libs/game_bethesda/src/games/oblivion/oblivionsavegame.cpp new file mode 100644 index 0000000..2f26714 --- /dev/null +++ b/libs/game_bethesda/src/games/oblivion/oblivionsavegame.cpp @@ -0,0 +1,74 @@ +#include "oblivionsavegame.h" + +OblivionSaveGame::OblivionSaveGame(QString const& fileName, GameOblivion const* game) + : GamebryoSaveGame(fileName, game) +{ + FileWrapper file(getFilepath(), "TES4SAVEGAME"); + file.setPluginString(GamebryoSaveGame::StringType::TYPE_BSTRING); + + _SYSTEMTIME creationTime; + fetchInformationFields(file, m_SaveNumber, m_PCName, m_PCLevel, m_PCLocation, + creationTime); + setCreationTime(creationTime); +} + +void OblivionSaveGame::fetchInformationFields(FileWrapper& file, + uint32_t& saveNumber, + QString& playerName, + unsigned short& playerLevel, + QString& playerLocation, + _SYSTEMTIME& creationTime) const +{ + file.skip(); // Major version + file.skip(); // Minor version + + file.skip<_SYSTEMTIME>(); // exe last modified (!) + + file.skip(); // Header version + file.skip(); // Header size + + file.read(saveNumber); + + file.read(playerName); + file.read(playerLevel); + file.read(playerLocation); + + file.skip(); // game days + file.skip(); // game ticks + + // there is a save time stored here. So use it rather than the file time, which + // could have been copied. + // Note: This says it uses getlocaltime api to obtain it which is u/s - if so + // we should ignore this. + file.read(creationTime); +} + +std::unique_ptr OblivionSaveGame::fetchDataFields() const +{ + FileWrapper file(getFilepath(), "TES4SAVEGAME"); + file.setPluginString(GamebryoSaveGame::StringType::TYPE_BSTRING); + + std::unique_ptr fields = std::make_unique(); + + { + QString dummyName, dummyLocation; + unsigned short dummyLevel; + uint32_t dummySaveNumber; + _SYSTEMTIME dummyTime; + + fetchInformationFields(file, dummySaveNumber, dummyName, dummyLevel, dummyLocation, + dummyTime); + } + + // Note that screenshot size, width, height and data are apparently the same + // structure + uint32_t imageSize; + file.read(imageSize); // Screenshot size. + if (imageSize > 0) { + fields->Screenshot = file.readImage(); + } + + fields->Plugins = file.readPlugins(); + + return fields; +} diff --git a/libs/game_bethesda/src/games/oblivion/oblivionscriptextender.cpp b/libs/game_bethesda/src/games/oblivion/oblivionscriptextender.cpp new file mode 100644 index 0000000..f021a2e --- /dev/null +++ b/libs/game_bethesda/src/games/oblivion/oblivionscriptextender.cpp @@ -0,0 +1,20 @@ +#include "oblivionscriptextender.h" + +#include +#include + +OblivionScriptExtender::OblivionScriptExtender(GameGamebryo const* game) + : GamebryoScriptExtender(game) +{} + +OblivionScriptExtender::~OblivionScriptExtender() {} + +QString OblivionScriptExtender::BinaryName() const +{ + return "obse_loader.exe"; +} + +QString OblivionScriptExtender::PluginPath() const +{ + return "obse/plugins"; +} diff --git a/libs/game_bethesda/src/games/skyrim/gameskyrim.cpp b/libs/game_bethesda/src/games/skyrim/gameskyrim.cpp new file mode 100644 index 0000000..4a0053e --- /dev/null +++ b/libs/game_bethesda/src/games/skyrim/gameskyrim.cpp @@ -0,0 +1,263 @@ +#include "gameskyrim.h" + +#include "skyrimbsainvalidation.h" +#include "skyrimdataarchives.h" +#include "skyrimgameplugins.h" +#include "skyrimmoddatachecker.h" +#include "skyrimmoddatacontent.h" +#include "skyrimsavegame.h" +#include "skyrimscriptextender.h" + +#include "executableinfo.h" +#include "pluginsetting.h" + +#include +#include +#include +#include + +#include +#include +#include + +#include + +#ifdef _WIN32 +#include +#include +#endif + +#include +#include +#include +#include +#include + +using namespace MOBase; + +GameSkyrim::GameSkyrim() {} + +bool GameSkyrim::init(IOrganizer* moInfo) +{ + if (!GameGamebryo::init(moInfo)) { + return false; + } + + auto dataArchives = std::make_shared(this); + registerFeature(std::make_shared(this)); + registerFeature(dataArchives); + registerFeature(std::make_shared(dataArchives.get(), this)); + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(this, "skyrim.ini")); + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(m_Organizer->gameFeatures())); + registerFeature(std::make_shared(moInfo)); + registerFeature(std::make_shared(this)); + + return true; +} + +QString GameSkyrim::gameName() const +{ + return "Skyrim"; +} + +QList GameSkyrim::executables() const +{ + return QList() + << ExecutableInfo("SKSE", + findInGameFolder(m_Organizer->gameFeatures() + ->gameFeature() + ->loaderName())) + << ExecutableInfo("SBW", findInGameFolder("SBW.exe")) + << ExecutableInfo("Skyrim", findInGameFolder(binaryName())) + << ExecutableInfo("Skyrim Launcher", findInGameFolder(getLauncherName())) + << ExecutableInfo("BOSS", findInGameFolder("BOSS/BOSS.exe")) + << ExecutableInfo("LOOT", QFileInfo(getLootPath())) + .withArgument("--game=\"Skyrim\"") + << ExecutableInfo("Creation Kit", findInGameFolder("CreationKit.exe")) + .withSteamAppId("202480"); +} + +QList GameSkyrim::executableForcedLoads() const +{ + return QList(); +} + +QString GameSkyrim::name() const +{ + return "Skyrim Support Plugin"; +} + +QString GameSkyrim::localizedName() const +{ + return tr("Skyrim Support Plugin"); +} + +QString GameSkyrim::author() const +{ + return "Tannin & MO2 Team"; +} + +QString GameSkyrim::description() const +{ + return tr("Adds support for the game Skyrim"); +} + +MOBase::VersionInfo GameSkyrim::version() const +{ + return VersionInfo(1, 6, 1, VersionInfo::RELEASE_FINAL); +} + +QList GameSkyrim::settings() const +{ + QList results; + results.push_back( + PluginSetting("sse_downloads", "allow Skyrim SE downloads", QVariant(false))); + return results; +} + +void GameSkyrim::initializeProfile(const QDir& path, ProfileSettings settings) const +{ + if (settings.testFlag(IPluginGame::MODS)) { + copyToProfile(localAppFolder() + "/Skyrim", path, "plugins.txt"); + } + + if (settings.testFlag(IPluginGame::CONFIGURATION)) { + if (settings.testFlag(IPluginGame::PREFER_DEFAULTS) || + !QFileInfo(myGamesPath() + "/skyrim.ini").exists()) { + copyToProfile(gameDirectory().absolutePath(), path, "skyrim_default.ini", + "skyrim.ini"); + } else { + copyToProfile(myGamesPath(), path, "skyrim.ini"); + } + + copyToProfile(myGamesPath(), path, "skyrimprefs.ini"); + } +} + +QString GameSkyrim::savegameExtension() const +{ + return "ess"; +} + +QString GameSkyrim::savegameSEExtension() const +{ + return "skse"; +} + +std::shared_ptr GameSkyrim::makeSaveGame(QString filepath) const +{ + return std::make_shared(filepath, this); +} + +QString GameSkyrim::steamAPPId() const +{ + return "72850"; +} + +QStringList GameSkyrim::primaryPlugins() const +{ + return {"skyrim.esm", "update.esm"}; +} + +QString GameSkyrim::binaryName() const +{ + return "TESV.exe"; +} + +QString GameSkyrim::gameShortName() const +{ + return "Skyrim"; +} + +QString GameSkyrim::gameNexusName() const +{ + return "skyrim"; +} + +QStringList GameSkyrim::validShortNames() const +{ + QStringList results; + if (m_Organizer->pluginSetting(name(), "sse_downloads").toBool()) { + results.push_back("SkyrimSE"); + } + return results; +} + +QStringList GameSkyrim::iniFiles() const +{ + return {"skyrim.ini", "skyrimprefs.ini"}; +} + +QStringList GameSkyrim::DLCPlugins() const +{ + return {"Dawnguard.esm", + "Dragonborn.esm", + "HearthFires.esm", + "HighResTexturePack01.esp", + "HighResTexturePack02.esp", + "HighResTexturePack03.esp"}; +} + +namespace +{ +// Note: This is ripped off from shared/util. And in an upcoming move, the fomod +// installer requires something similar. I suspect I should abstract this out +// into gamebryo (or lower level) + +#ifdef _WIN32 +VS_FIXEDFILEINFO GetFileVersion(const std::wstring& fileName) +{ + DWORD handle = 0UL; + DWORD size = ::GetFileVersionInfoSizeW(fileName.c_str(), &handle); + if (size == 0) { + throw std::runtime_error("failed to determine file version info size"); + } + + std::vector buffer(size); + handle = 0UL; + if (!::GetFileVersionInfoW(fileName.c_str(), handle, size, buffer.data())) { + throw std::runtime_error("failed to determine file version info"); + } + + void* versionInfoPtr = nullptr; + UINT versionInfoLength = 0; + if (!::VerQueryValue(buffer.data(), L"\\", &versionInfoPtr, &versionInfoLength)) { + throw std::runtime_error("failed to determine file version"); + } + + return *static_cast(versionInfoPtr); +} +#endif + +} // namespace + +IPluginGame::LoadOrderMechanism GameSkyrim::loadOrderMechanism() const +{ +#ifdef _WIN32 + try { + std::wstring fileName = + gameDirectory().absoluteFilePath(binaryName()).toStdWString().c_str(); + VS_FIXEDFILEINFO versionInfo = ::GetFileVersion(fileName); + if ((versionInfo.dwFileVersionMS > 0x10004) || // version >= 1.5.x? + ((versionInfo.dwFileVersionMS == 0x10004) && + (versionInfo.dwFileVersionLS >= 0x1A0000))) { // version >= ?.4.26 + return LoadOrderMechanism::PluginsTxt; + } + } catch (const std::exception& e) { + qCritical() << "TESV.exe is invalid: " << e.what(); + } +#endif + return LoadOrderMechanism::FileTime; +} + +int GameSkyrim::nexusModOrganizerID() const +{ + return 0; +} + +int GameSkyrim::nexusGameID() const +{ + return 110; +} diff --git a/libs/game_bethesda/src/games/skyrim/skyrimbsainvalidation.cpp b/libs/game_bethesda/src/games/skyrim/skyrimbsainvalidation.cpp new file mode 100644 index 0000000..e056f10 --- /dev/null +++ b/libs/game_bethesda/src/games/skyrim/skyrimbsainvalidation.cpp @@ -0,0 +1,16 @@ +#include "skyrimbsainvalidation.h" + +SkyrimBSAInvalidation::SkyrimBSAInvalidation(MOBase::DataArchives* dataArchives, + MOBase::IPluginGame const* game) + : GamebryoBSAInvalidation(dataArchives, "skyrim.ini", game) +{} + +QString SkyrimBSAInvalidation::invalidationBSAName() const +{ + return "Skyrim - Invalidation.bsa"; +} + +unsigned long SkyrimBSAInvalidation::bsaVersion() const +{ + return 0x68; +} diff --git a/libs/game_bethesda/src/games/skyrim/skyrimdataarchives.cpp b/libs/game_bethesda/src/games/skyrim/skyrimdataarchives.cpp new file mode 100644 index 0000000..6422175 --- /dev/null +++ b/libs/game_bethesda/src/games/skyrim/skyrimdataarchives.cpp @@ -0,0 +1,42 @@ +#include "skyrimdataarchives.h" +#include + +QStringList SkyrimDataArchives::vanillaArchives() const +{ + return {"Skyrim - Misc.bsa", "Skyrim - Shaders.bsa", + "Skyrim - Textures.bsa", "HighResTexturePack01.bsa", + "HighResTexturePack02.bsa", "HighResTexturePack03.bsa", + "Skyrim - Interface.bsa", "Skyrim - Animations.bsa", + "Skyrim - Meshes.bsa", "Skyrim - Sounds.bsa", + "Skyrim - Voices.bsa", "Skyrim - VoicesExtra.bsa"}; +} + +QStringList SkyrimDataArchives::archives(const MOBase::IProfile* profile) const +{ + QStringList result; + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("skyrim.ini") + : localGameDirectory().absoluteFilePath("skyrim.ini"); + result.append(getArchivesFromKey(iniFile, "SResourceArchiveList")); + result.append(getArchivesFromKey(iniFile, "SResourceArchiveList2")); + + return result; +} + +void SkyrimDataArchives::writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) +{ + QString list = before.join(", "); + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("skyrim.ini") + : localGameDirectory().absoluteFilePath("skyrim.ini"); + if (list.length() > 255) { + int splitIdx = list.lastIndexOf(",", 256); + setArchivesToKey(iniFile, "SResourceArchiveList", list.mid(0, splitIdx)); + setArchivesToKey(iniFile, "SResourceArchiveList2", list.mid(splitIdx + 2)); + } else { + setArchivesToKey(iniFile, "SResourceArchiveList", list); + } +} diff --git a/libs/game_bethesda/src/games/skyrim/skyrimgameplugins.cpp b/libs/game_bethesda/src/games/skyrim/skyrimgameplugins.cpp new file mode 100644 index 0000000..88aac81 --- /dev/null +++ b/libs/game_bethesda/src/games/skyrim/skyrimgameplugins.cpp @@ -0,0 +1,123 @@ +#include "skyrimgameplugins.h" +#include +#include +#include +#include + +#include +#include +#include + +using MOBase::IOrganizer; +using MOBase::IPluginGame; +using MOBase::IPluginList; +using MOBase::reportError; +using MOBase::SafeWriteFile; + +SkyrimGamePlugins::SkyrimGamePlugins(IOrganizer* organizer) + : GamebryoGamePlugins(organizer) +{} + +void SkyrimGamePlugins::readPluginLists(MOBase::IPluginList* pluginList) +{ + QString loadOrderPath = organizer()->profile()->absolutePath() + "/loadorder.txt"; + QString pluginsPath = organizer()->profile()->absolutePath() + "/plugins.txt"; + + bool loadOrderIsNew = !m_LastRead.isValid() || !QFileInfo(loadOrderPath).exists() || + QFileInfo(loadOrderPath).lastModified() > m_LastRead; + bool pluginsIsNew = + !m_LastRead.isValid() || QFileInfo(pluginsPath).lastModified() > m_LastRead; + + if (pluginsIsNew && !loadOrderIsNew) { + // If the plugins is new but not loadorder, we must reparse the load order from the + // plugin files + + // removed because returned loadorder was incorrect and did not account for plugins + // that were already disabled before. + /*QStringList loadOrder = readPluginList(pluginList); + pluginList->setLoadOrder(loadOrder);*/ + + // Fix me: we are ignoring order changes in plugins.txt favouring loadorder.txt in + // all cases (plugins.txt shuld have precedence) + QStringList loadOrder = readLoadOrderList(pluginList, loadOrderPath); + pluginList->setLoadOrder(loadOrder); + readPluginList(pluginList); + } else { + // 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); + } + + m_LastRead = QDateTime::currentDateTime(); +} + +// TODO: return value is incorrect and should be ignored (it's not currently used +QStringList SkyrimGamePlugins::readPluginList(MOBase::IPluginList* pluginList) +{ + QStringList plugins = pluginList->pluginNames(); + QStringList primaryPlugins = organizer()->managedGame()->primaryPlugins(); + QStringList loadOrder(plugins); + + for (const QString& pluginName : primaryPlugins) { + if (pluginList->state(pluginName) != IPluginList::STATE_MISSING) { + pluginList->setState(pluginName, IPluginList::STATE_ACTIVE); + } + } + + // 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 (primaryPlugins.contains(plugin, Qt::CaseInsensitive)) + plugins.removeAll(plugin); + } + + // Determine plugin active state by the plugins.txt file. + bool pluginsTxtExists = true; + QString filePath = organizer()->profile()->absolutePath() + "/plugins.txt"; + QFile file(filePath); + if (!file.open(QIODevice::ReadOnly)) { + pluginsTxtExists = false; + } + ON_BLOCK_EXIT([&]() { + qDebug("close %s", qUtf8Printable(filePath)); + file.close(); + }); + + if (file.size() == 0) { + // MO stores at least a header in the file. if it's completely empty the + // file is broken + pluginsTxtExists = false; + } + + if (pluginsTxtExists) { + while (!file.atEnd()) { + QByteArray line = file.readLine(); + QString pluginName; + if ((line.size() > 0) && (line.at(0) != '#')) { + pluginName = QStringEncoder(QStringConverter::Encoding::System) + .encode(line.trimmed().constData()); + } + if (pluginName.size() > 0) { + pluginList->setState(pluginName, IPluginList::STATE_ACTIVE); + plugins.removeAll(pluginName); + // we already have the old loadorder and we ignore the positions in plugins.txt + // (needs fix) loadOrder.append(pluginName); + } + } + + file.close(); + + // we removed each plugin found in the file, so what's left are inactive mods + for (const QString& pluginName : plugins) { + pluginList->setState(pluginName, IPluginList::STATE_INACTIVE); + } + } else { + for (const QString& pluginName : plugins) { + pluginList->setState(pluginName, IPluginList::STATE_INACTIVE); + } + } + + return loadOrder; +} diff --git a/libs/game_bethesda/src/games/skyrim/skyrimsavegame.cpp b/libs/game_bethesda/src/games/skyrim/skyrimsavegame.cpp new file mode 100644 index 0000000..bd16036 --- /dev/null +++ b/libs/game_bethesda/src/games/skyrim/skyrimsavegame.cpp @@ -0,0 +1,105 @@ +#include "skyrimsavegame.h" + +#ifdef _WIN32 +#include +#else +#include + +// SYSTEMTIME is _SYSTEMTIME (defined in gamebryosavegame.h for Linux) +using SYSTEMTIME = _SYSTEMTIME; + +// Portable FileTimeToSystemTime: converts Windows FILETIME to SYSTEMTIME +static void FileTimeToSystemTime(const FILETIME* ft, SYSTEMTIME* st) +{ + const uint64_t ticks = (static_cast(ft->dwHighDateTime) << 32) | + static_cast(ft->dwLowDateTime); + const int64_t unixSecs = static_cast(ticks / 10000000ULL) - 11644473600LL; + const uint64_t remainderHns = ticks % 10000000ULL; // leftover 100ns units + + const QDateTime dt = QDateTime::fromSecsSinceEpoch(unixSecs, Qt::UTC); + const QDate d = dt.date(); + const QTime t = dt.time(); + + st->wYear = static_cast(d.year()); + st->wMonth = static_cast(d.month()); + st->wDayOfWeek = static_cast(d.dayOfWeek() % 7); // Qt Mon=1..Sun=7 -> Win Sun=0..Sat=6 + st->wDay = static_cast(d.day()); + st->wHour = static_cast(t.hour()); + st->wMinute = static_cast(t.minute()); + st->wSecond = static_cast(t.second()); + st->wMilliseconds = static_cast(remainderHns / 10000); +} +#endif + +#include "gameskyrim.h" + +SkyrimSaveGame::SkyrimSaveGame(QString const& fileName, GameSkyrim const* game) + : GamebryoSaveGame(fileName, game) +{ + FileWrapper file(getFilepath(), "TESV_SAVEGAME"); + + FILETIME ftime; + fetchInformationFields(file, m_SaveNumber, m_PCName, m_PCLevel, m_PCLocation, ftime); + + // A file time is a 64-bit value that represents the number of 100-nanosecond + // intervals that have elapsed since 12:00 A.M. January 1, 1601 Coordinated Universal + // Time (UTC). So we need to convert that to something useful + SYSTEMTIME ctime; + ::FileTimeToSystemTime(&ftime, &ctime); + setCreationTime(ctime); +} + +void SkyrimSaveGame::fetchInformationFields( + FileWrapper& file, uint32_t& saveNumber, QString& playerName, + unsigned short& playerLevel, QString& playerLocation, FILETIME& creationTime) const +{ + file.skip(); // header size + file.skip(); // header version + file.read(saveNumber); + + file.read(playerName); + + uint32_t temp; + file.read(temp); + playerLevel = static_cast(temp); + + file.setPluginStringFormat(GamebryoSaveGame::StringFormat::LOCAL8BIT); + file.read(playerLocation); + file.setPluginStringFormat(GamebryoSaveGame::StringFormat::UTF8); + + QString timeOfDay; + file.read(timeOfDay); + + QString race; + file.read(race); // race name (i.e. BretonRace) + + file.skip(); // Player gender (0 = male) + file.skip(2); // experience gathered, experience required + + file.read(creationTime); +} + +std::unique_ptr SkyrimSaveGame::fetchDataFields() const +{ + FileWrapper file(getFilepath(), "TESV_SAVEGAME"); + std::unique_ptr fields = std::make_unique(); + + { + QString dummyName, dummyLocation; + unsigned short dummyLevel; + uint32_t dummySaveNumber; + FILETIME dummyTime; + + fetchInformationFields(file, dummySaveNumber, dummyName, dummyLevel, dummyLocation, + dummyTime); + } + + fields->Screenshot = file.readImage(); + + file.skip(); // form version + file.skip(); // plugin info size + + fields->Plugins = file.readPlugins(); + + return fields; +} diff --git a/libs/game_bethesda/src/games/skyrim/skyrimscriptextender.cpp b/libs/game_bethesda/src/games/skyrim/skyrimscriptextender.cpp new file mode 100644 index 0000000..38e17ed --- /dev/null +++ b/libs/game_bethesda/src/games/skyrim/skyrimscriptextender.cpp @@ -0,0 +1,18 @@ +#include "skyrimscriptextender.h" + +#include +#include + +SkyrimScriptExtender::SkyrimScriptExtender(GameGamebryo const* game) + : GamebryoScriptExtender(game) +{} + +QString SkyrimScriptExtender::BinaryName() const +{ + return "skse_loader.exe"; +} + +QString SkyrimScriptExtender::PluginPath() const +{ + return "skse/plugins"; +} diff --git a/libs/game_bethesda/src/games/skyrimse/gameskyrimse.cpp b/libs/game_bethesda/src/games/skyrimse/gameskyrimse.cpp new file mode 100644 index 0000000..5452239 --- /dev/null +++ b/libs/game_bethesda/src/games/skyrimse/gameskyrimse.cpp @@ -0,0 +1,359 @@ +#include "gameskyrimse.h" + +#include "skyrimsedataarchives.h" +#include "skyrimsemoddatachecker.h" +#include "skyrimsemoddatacontent.h" +#include "skyrimsesavegame.h" +#include "skyrimsescriptextender.h" +#include "skyrimseunmanagedmods.h" + +#include "versioninfo.h" +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "scopeguard.h" +#include + +using namespace MOBase; + +GameSkyrimSE::GameSkyrimSE() {} + +void GameSkyrimSE::setVariant(QString variant) +{ + m_GameVariant = variant; +} + +void GameSkyrimSE::checkVariants() +{ + QFileInfo gog_dll(m_GamePath + "/Galaxy64.dll"); + QFileInfo epic_dll(m_GamePath + "/EOSSDK-Win64-Shipping.dll"); + if (gog_dll.exists()) + setVariant("GOG"); + else if (epic_dll.exists()) + setVariant("Epic Games"); + else + setVariant("Steam"); +} + +QDir GameSkyrimSE::documentsDirectory() const +{ + return m_MyGamesPath; +} + +void GameSkyrimSE::detectGame() +{ + m_GamePath = identifyGamePath(); + checkVariants(); + m_MyGamesPath = determineMyGamesPath(gameDirectoryName()); +} + +QString GameSkyrimSE::identifyGamePath() const +{ +#ifdef _WIN32 + QMap paths = { + {"Software\\Bethesda Softworks\\" + gameName(), "Installed Path"}, + {"Software\\GOG.com\\Games\\1162721350", "path"}, + {"Software\\GOG.com\\Games\\1711230643", "path"}, + }; + + QString result; + for (auto& path : paths.toStdMap()) { + result = findInRegistry(HKEY_LOCAL_MACHINE, path.first.toStdWString().c_str(), + path.second.toStdWString().c_str()); + if (!result.isEmpty()) + break; + } + + // Check Epic Games Manifests + // AppName: ac82db5035584c7f8a2c548d98c86b2c + // AE Update: 5d600e4f59974aeba0259c7734134e27 + if (result.isEmpty()) { + result = parseEpicGamesLocation( + {"ac82db5035584c7f8a2c548d98c86b2c", "5d600e4f59974aeba0259c7734134e27"}); + } + + return result; +#else + // On Linux, use base class NaK detection (handles Steam via game_finder) + QString result = GameGamebryo::identifyGamePath(); + + // Check Epic Games (Heroic/Legendary on Linux) + if (result.isEmpty()) { + result = parseEpicGamesLocation( + {"ac82db5035584c7f8a2c548d98c86b2c", "5d600e4f59974aeba0259c7734134e27"}); + } + + // Try Steam VDF location parsing as fallback + if (result.isEmpty()) { + result = parseSteamLocation("489830", "Skyrim Special Edition"); + } + + return result; +#endif +} + +void GameSkyrimSE::setGamePath(const QString& path) +{ + m_GamePath = path; + checkVariants(); + m_MyGamesPath = determineMyGamesPath(gameDirectoryName()); +} + +QDir GameSkyrimSE::savesDirectory() const +{ + return QDir(m_MyGamesPath + "/Saves"); +} + +QString GameSkyrimSE::myGamesPath() const +{ + return m_MyGamesPath; +} + +bool GameSkyrimSE::isInstalled() const +{ + return !m_GamePath.isEmpty(); +} + +bool GameSkyrimSE::init(IOrganizer* moInfo) +{ + if (!GameGamebryo::init(moInfo)) { + return false; + } + + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(this, "Skyrimcustom.ini")); + registerFeature(std::make_shared(this)); + registerFeature( + std::make_shared(m_Organizer->gameFeatures())); + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(moInfo)); + registerFeature(std::make_shared(this)); + + return true; +} + +QString GameSkyrimSE::gameName() const +{ + return "Skyrim Special Edition"; +} + +QString GameSkyrimSE::gameDirectoryName() const +{ + if (selectedVariant() == "GOG") + return "Skyrim Special Edition GOG"; + else if (selectedVariant() == "Epic Games") + return "Skyrim Special Edition EPIC"; + else + return "Skyrim Special Edition"; +} + +QList GameSkyrimSE::executables() const +{ + return QList() + << ExecutableInfo("SKSE", + findInGameFolder(m_Organizer->gameFeatures() + ->gameFeature() + ->loaderName())) + << ExecutableInfo("Skyrim Special Edition", findInGameFolder(binaryName())) + << ExecutableInfo("Skyrim Special Edition Launcher", + findInGameFolder(getLauncherName())) + << ExecutableInfo("Creation Kit", findInGameFolder("CreationKit.exe")) + .withSteamAppId("1946180") + << ExecutableInfo("LOOT", QFileInfo(getLootPath())) + .withArgument("--game=\"Skyrim Special Edition\""); +} + +QList GameSkyrimSE::executableForcedLoads() const +{ + return QList(); +} + +QFileInfo GameSkyrimSE::findInGameFolder(const QString& relativePath) const +{ + return QFileInfo(m_GamePath + "/" + relativePath); +} + +QString GameSkyrimSE::name() const +{ + return "Skyrim Special Edition Support Plugin"; +} + +QString GameSkyrimSE::localizedName() const +{ + return tr("Skyrim Special Edition Support Plugin"); +} + +QString GameSkyrimSE::author() const +{ + return "MO2 Team, Orig: Archost & ZachHaber"; +} + +QString GameSkyrimSE::description() const +{ + return tr("Adds support for the game Skyrim Special Edition."); +} + +MOBase::VersionInfo GameSkyrimSE::version() const +{ + return VersionInfo(1, 7, 1, VersionInfo::RELEASE_FINAL); +} + +QList GameSkyrimSE::settings() const +{ + return {PluginSetting("enderal_downloads", "allow Enderal and Enderal SE downloads", + QVariant(false))}; +} + +void GameSkyrimSE::initializeProfile(const QDir& path, ProfileSettings settings) const +{ + if (settings.testFlag(IPluginGame::MODS)) { + copyToProfile(localAppFolder() + "/" + gameDirectoryName(), path, "plugins.txt"); + } + + if (settings.testFlag(IPluginGame::CONFIGURATION)) { + if (settings.testFlag(IPluginGame::PREFER_DEFAULTS) || + !QFileInfo(myGamesPath() + "/skyrim.ini").exists()) { + copyToProfile(gameDirectory().absolutePath(), path, "skyrim_default.ini", + "skyrim.ini"); + } else { + copyToProfile(myGamesPath(), path, "skyrim.ini"); + } + + copyToProfile(myGamesPath(), path, "skyrimprefs.ini"); + copyToProfile(myGamesPath(), path, "skyrimcustom.ini"); + } +} + +QString GameSkyrimSE::savegameExtension() const +{ + return "ess"; +} + +QString GameSkyrimSE::savegameSEExtension() const +{ + return "skse"; +} + +std::shared_ptr +GameSkyrimSE::makeSaveGame(QString filePath) const +{ + return std::make_shared(filePath, this); +} + +QString GameSkyrimSE::steamAPPId() const +{ + if (selectedVariant() == "Steam") + return "489830"; + return QString(); +} + +QStringList GameSkyrimSE::primaryPlugins() const +{ + QStringList plugins = {"skyrim.esm", "update.esm", "dawnguard.esm", "hearthfires.esm", + "dragonborn.esm"}; + + plugins.append(CCPlugins()); + + return plugins; +} + +QStringList GameSkyrimSE::gameVariants() const +{ + return {"Steam", "GOG", "Epic Games"}; +} + +QString GameSkyrimSE::gameShortName() const +{ + return "SkyrimSE"; +} + +QStringList GameSkyrimSE::validShortNames() const +{ + QStringList shortNames{"Skyrim"}; + if (m_Organizer->pluginSetting(name(), "enderal_downloads").toBool()) { + shortNames.append({"Enderal", "EnderalSE"}); + } + return shortNames; +} + +QString GameSkyrimSE::gameNexusName() const +{ + return "skyrimspecialedition"; +} + +QStringList GameSkyrimSE::iniFiles() const +{ + return {"skyrim.ini", "skyrimprefs.ini", "skyrimcustom.ini"}; +} + +QStringList GameSkyrimSE::DLCPlugins() const +{ + return {"dawnguard.esm", "hearthfires.esm", "dragonborn.esm"}; +} + +QStringList GameSkyrimSE::CCPlugins() const +{ + QStringList plugins; + std::set pluginsLookup; + + const QString path = gameDirectory().filePath("Skyrim.ccc"); + + MOBase::forEachLineInFile(path, [&](QString s) { + const auto lc = s.toLower(); + if (!pluginsLookup.contains(lc)) { + pluginsLookup.insert(lc); + plugins.append(std::move(s)); + } + }); + + return plugins; +} + +IPluginGame::LoadOrderMechanism GameSkyrimSE::loadOrderMechanism() const +{ + return IPluginGame::LoadOrderMechanism::PluginsTxt; +} + +int GameSkyrimSE::nexusModOrganizerID() const +{ + return 6194; //... Should be 0? +} + +int GameSkyrimSE::nexusGameID() const +{ + return 1704; // 1704 +} + +QDir GameSkyrimSE::gameDirectory() const +{ + return QDir(m_GamePath); +} + +// Not to delete all the spaces... +MappingType GameSkyrimSE::mappings() const +{ + MappingType result; + + for (const QString& profileFile : {"plugins.txt", "loadorder.txt"}) { + result.push_back({m_Organizer->profilePath() + "/" + profileFile, + localAppFolder() + "/" + gameDirectoryName() + "/" + profileFile, + false}); + } + + return result; +} diff --git a/libs/game_bethesda/src/games/skyrimse/skyrimsedataarchives.cpp b/libs/game_bethesda/src/games/skyrimse/skyrimsedataarchives.cpp new file mode 100644 index 0000000..a4b5869 --- /dev/null +++ b/libs/game_bethesda/src/games/skyrimse/skyrimsedataarchives.cpp @@ -0,0 +1,44 @@ +#include "skyrimsedataarchives.h" + +#include "iprofile.h" +#include + +QStringList SkyrimSEDataArchives::vanillaArchives() const +{ + return {"Skyrim - Textures0.bsa", "Skyrim - Textures1.bsa", "Skyrim - Textures2.bsa", + "Skyrim - Textures3.bsa", "Skyrim - Textures4.bsa", "Skyrim - Textures5.bsa", + "Skyrim - Textures6.bsa", "Skyrim - Textures7.bsa", "Skyrim - Textures8.bsa", + "Skyrim - Meshes0.bsa", "Skyrim - Meshes1.bsa", "Skyrim - Voices_en0.bsa", + "Skyrim - Sounds.bsa", "Skyrim - Interface.bsa", "Skyrim - Animations.bsa", + "Skyrim - Shaders.bsa", "Skyrim - Misc.bsa"}; +} + +QStringList SkyrimSEDataArchives::archives(const MOBase::IProfile* profile) const +{ + QStringList result; + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("skyrim.ini") + : localGameDirectory().absoluteFilePath("skyrim.ini"); + result.append(getArchivesFromKey(iniFile, "SResourceArchiveList")); + result.append(getArchivesFromKey(iniFile, "SResourceArchiveList2")); + + return result; +} + +void SkyrimSEDataArchives::writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) +{ + QString list = before.join(", "); + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("skyrim.ini") + : localGameDirectory().absoluteFilePath("skyrim.ini"); + if (list.length() > 255) { + int splitIdx = list.lastIndexOf(",", 256); + setArchivesToKey(iniFile, "SResourceArchiveList", list.mid(0, splitIdx)); + setArchivesToKey(iniFile, "SResourceArchiveList2", list.mid(splitIdx + 2)); + } else { + setArchivesToKey(iniFile, "SResourceArchiveList", list); + } +} diff --git a/libs/game_bethesda/src/games/skyrimse/skyrimsesavegame.cpp b/libs/game_bethesda/src/games/skyrimse/skyrimsesavegame.cpp new file mode 100644 index 0000000..d68ba19 --- /dev/null +++ b/libs/game_bethesda/src/games/skyrimse/skyrimsesavegame.cpp @@ -0,0 +1,157 @@ +#include "skyrimsesavegame.h" + +#ifdef _WIN32 +#include +#else +#include + +// Windows ULARGE_INTEGER union for 64-bit FILETIME math +union _ULARGE_INTEGER { + struct { + uint32_t LowPart; + uint32_t HighPart; + }; + uint64_t QuadPart; +}; + +// SYSTEMTIME is _SYSTEMTIME (defined in gamebryosavegame.h for Linux) +using SYSTEMTIME = _SYSTEMTIME; + +// Portable FileTimeToSystemTime: converts Windows FILETIME to SYSTEMTIME +// FILETIME = 100ns intervals since Jan 1, 1601 UTC +static void FileTimeToSystemTime(const FILETIME* ft, SYSTEMTIME* st) +{ + _ULARGE_INTEGER ull; + ull.LowPart = ft->dwLowDateTime; + ull.HighPart = ft->dwHighDateTime; + + // 11644473600 seconds between 1601-01-01 and 1970-01-01 epochs + int64_t unixSecs = static_cast(ull.QuadPart / 10000000ULL) - 11644473600LL; + uint64_t remainderHns = ull.QuadPart % 10000000ULL; // leftover 100ns units + + QDateTime dt = QDateTime::fromSecsSinceEpoch(unixSecs, Qt::UTC); + QDate d = dt.date(); + QTime t = dt.time(); + + st->wYear = static_cast(d.year()); + st->wMonth = static_cast(d.month()); + st->wDayOfWeek = static_cast(d.dayOfWeek() % 7); // Qt Mon=1..Sun=7 → Win Sun=0..Sat=6 + st->wDay = static_cast(d.day()); + st->wHour = static_cast(t.hour()); + st->wMinute = static_cast(t.minute()); + st->wSecond = static_cast(t.second()); + st->wMilliseconds = static_cast(remainderHns / 10000); +} +#endif + +SkyrimSESaveGame::SkyrimSESaveGame(QString const& fileName, GameSkyrimSE const* game) + : GamebryoSaveGame(fileName, game, true) +{ + FileWrapper file(fileName, "TESV_SAVEGAME"); // 10bytes + + uint32_t version; + FILETIME ftime; + fetchInformationFields(file, version, m_PCName, m_PCLevel, m_PCLocation, m_SaveNumber, + ftime); + + // A file time is a 64-bit value that represents the number of 100-nanosecond + // intervals that have elapsed since 12:00 A.M. January 1, 1601 Coordinated Universal + // Time (UTC). So we need to convert that to something useful + + // For some reason, the file time is off by about 6 hours. + // So we need to subtract those 6 hours from the filetime. + _ULARGE_INTEGER time; + time.LowPart = ftime.dwLowDateTime; + time.HighPart = ftime.dwHighDateTime; + time.QuadPart -= 216000000000ULL; // 6 hours in 100ns intervals + ftime.dwHighDateTime = time.HighPart; + ftime.dwLowDateTime = time.LowPart; + + SYSTEMTIME ctime; + ::FileTimeToSystemTime(&ftime, &ctime); + + setCreationTime(ctime); +} + +void SkyrimSESaveGame::fetchInformationFields(FileWrapper& file, uint32_t& version, + QString& playerName, + unsigned short& playerLevel, + QString& playerLocation, + uint32_t& saveNumber, + FILETIME& creationTime) const +{ + uint32_t headerSize; + file.read(headerSize); // header size "TESV_SAVEGAME" + file.read(version); + file.read(saveNumber); + file.read(playerName); + + uint32_t temp; + file.read(temp); + playerLevel = static_cast(temp); + file.read(playerLocation); + + QString timeOfDay; + file.read(timeOfDay); + + QString race; + file.read(race); // race name (i.e. BretonRace) + + file.skip(); // Player gender (0 = male) + file.skip(2); // experience gathered, experience required + + file.read(creationTime); // filetime +} + +std::unique_ptr SkyrimSESaveGame::fetchDataFields() const +{ + FileWrapper file(getFilepath(), "TESV_SAVEGAME"); // 10bytes + + uint32_t version = 0; + { + QString dummyName, dummyLocation; + unsigned short dummyLevel; + uint32_t dummySaveNumber; + FILETIME dummyTime; + + fetchInformationFields(file, version, dummyName, dummyLevel, dummyLocation, + dummySaveNumber, dummyTime); + } + + std::unique_ptr fields = std::make_unique(); + + uint32_t width; + uint32_t height; + file.read(width); + file.read(height); + + bool alpha = false; + + // compatibility between LE and SE: + // SE has an additional uin16_t for compression + // SE uses an alpha channel, whereas LE does not + uint16_t compressionType = 0; + if (version == 12) { + file.read(compressionType); + alpha = true; + } + + fields->Screenshot = file.readImage(width, height, 320, alpha); + + file.setCompressionType(compressionType); + file.openCompressedData(); + + uint8_t saveGameVersion = file.readChar(); + uint8_t pluginInfoSize = file.readChar(); + uint16_t other = file.readShort(); // Unknown + + fields->Plugins = file.readPlugins(1); // Just empty data + + if (saveGameVersion >= 78) { + fields->LightPlugins = file.readLightPlugins(); + } + + file.closeCompressedData(); + + return fields; +} diff --git a/libs/game_bethesda/src/games/skyrimse/skyrimsescriptextender.cpp b/libs/game_bethesda/src/games/skyrimse/skyrimsescriptextender.cpp new file mode 100644 index 0000000..2dd5cff --- /dev/null +++ b/libs/game_bethesda/src/games/skyrimse/skyrimsescriptextender.cpp @@ -0,0 +1,18 @@ +#include "skyrimsescriptextender.h" + +#include +#include + +SkyrimSEScriptExtender::SkyrimSEScriptExtender(GameGamebryo const* game) + : GamebryoScriptExtender(game) +{} + +QString SkyrimSEScriptExtender::BinaryName() const +{ + return "skse64_loader.exe"; +} + +QString SkyrimSEScriptExtender::PluginPath() const +{ + return "skse/plugins"; +} diff --git a/libs/game_bethesda/src/games/skyrimse/skyrimseunmanagedmods.cpp b/libs/game_bethesda/src/games/skyrimse/skyrimseunmanagedmods.cpp new file mode 100644 index 0000000..da1fa02 --- /dev/null +++ b/libs/game_bethesda/src/games/skyrimse/skyrimseunmanagedmods.cpp @@ -0,0 +1,29 @@ +#include "skyrimseunmanagedmods.h" + +SkyrimSEUnmangedMods::SkyrimSEUnmangedMods(const GameGamebryo* game) + : GamebryoUnmangedMods(game) +{} + +SkyrimSEUnmangedMods::~SkyrimSEUnmangedMods() {} + +QStringList SkyrimSEUnmangedMods::mods(bool onlyOfficial) const +{ + QStringList result; + + QStringList pluginList = game()->primaryPlugins(); + QStringList otherPlugins = game()->DLCPlugins(); + otherPlugins.append(game()->CCPlugins()); + for (QString plugin : otherPlugins) { + pluginList.removeAll(plugin); + } + QDir dataDir(game()->dataDirectory()); + for (const QString& fileName : dataDir.entryList({"*.esp", "*.esl", "*.esm"})) { + if (!pluginList.contains(fileName, Qt::CaseInsensitive)) { + if (!onlyOfficial || pluginList.contains(fileName, Qt::CaseInsensitive)) { + result.append(fileName.chopped(4)); // trims the extension off + } + } + } + + return result; +} diff --git a/libs/game_bethesda/src/games/skyrimvr/gameskyrimvr.cpp b/libs/game_bethesda/src/games/skyrimvr/gameskyrimvr.cpp new file mode 100644 index 0000000..67db6d0 --- /dev/null +++ b/libs/game_bethesda/src/games/skyrimvr/gameskyrimvr.cpp @@ -0,0 +1,306 @@ +#include "gameskyrimvr.h" + +#include "skyrimvrdataarchives.h" +#include "skyrimvrgameplugins.h" +#include "skyrimvrmoddatachecker.h" +#include "skyrimvrmoddatacontent.h" +#include "skyrimvrsavegame.h" +#include "skyrimvrscriptextender.h" +#include "skyrimvrunmanagedmods.h" + +#include "versioninfo.h" +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "scopeguard.h" +#include + +using namespace MOBase; + +GameSkyrimVR::GameSkyrimVR() {} + +void GameSkyrimVR::setGamePath(const QString& path) +{ + m_GamePath = path; +} + +QDir GameSkyrimVR::documentsDirectory() const +{ + return m_MyGamesPath; +} + +QString GameSkyrimVR::identifyGamePath() const +{ +#ifdef _WIN32 + QString path = "Software\\Bethesda Softworks\\" + gameName(); + return findInRegistry(HKEY_LOCAL_MACHINE, path.toStdWString().c_str(), + L"Installed Path"); +#else + // Use the shared Linux detection logic from the base class (NaK + fallbacks). + return GameGamebryo::identifyGamePath(); +#endif +} + +QDir GameSkyrimVR::savesDirectory() const +{ + return QDir(m_MyGamesPath + "/Saves"); +} + +QString GameSkyrimVR::myGamesPath() const +{ + return m_MyGamesPath; +} + +bool GameSkyrimVR::isInstalled() const +{ + return !m_GamePath.isEmpty(); +} + +bool GameSkyrimVR::init(IOrganizer* moInfo) +{ + if (!GameGamebryo::init(moInfo)) { + return false; + } + + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(this, "SkyrimVR.ini")); + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(this)); + registerFeature( + std::make_shared(m_Organizer->gameFeatures())); + registerFeature(std::make_shared(moInfo)); + registerFeature(std::make_shared(this)); + + return true; +} + +QString GameSkyrimVR::gameName() const +{ + return "Skyrim VR"; +} + +QList GameSkyrimVR::executables() const +{ + return QList() + << ExecutableInfo("SKSE", + findInGameFolder(m_Organizer->gameFeatures() + ->gameFeature() + ->loaderName())) + << ExecutableInfo("Skyrim VR", findInGameFolder(binaryName())) + << ExecutableInfo("Creation Kit", findInGameFolder("CreationKit.exe")) + << ExecutableInfo("LOOT", QFileInfo(getLootPath())) + .withArgument("--game=\"Skyrim VR\""); +} + +QList GameSkyrimVR::executableForcedLoads() const +{ + return QList(); +} + +QFileInfo GameSkyrimVR::findInGameFolder(const QString& relativePath) const +{ + return QFileInfo(m_GamePath + "/" + relativePath); +} + +QString GameSkyrimVR::name() const +{ + return "Skyrim VR Support Plugin"; +} + +QString GameSkyrimVR::localizedName() const +{ + return tr("Skyrim VR Support Plugin"); +} + +QString GameSkyrimVR::author() const +{ + return "Brixified & MO2 Team"; +} + +QString GameSkyrimVR::description() const +{ + return tr("Adds support for the game Skyrim VR."); +} + +MOBase::VersionInfo GameSkyrimVR::version() const +{ + return VersionInfo(1, 5, 1, VersionInfo::RELEASE_FINAL); +} + +QList GameSkyrimVR::settings() const +{ + return {PluginSetting("enderal_downloads", "allow Enderal and Enderal SE downloads", + QVariant(false))}; +} + +void GameSkyrimVR::initializeProfile(const QDir& path, ProfileSettings settings) const +{ + if (settings.testFlag(IPluginGame::MODS)) { + copyToProfile(localAppFolder() + "/Skyrim VR", path, "plugins.txt"); + } + + if (settings.testFlag(IPluginGame::CONFIGURATION)) { + if (settings.testFlag(IPluginGame::PREFER_DEFAULTS) || + !QFileInfo(myGamesPath() + "/skyrimvr.ini").exists()) { + copyToProfile(gameDirectory().absolutePath(), path, "skyrim.ini", "skyrimvr.ini"); + } else { + copyToProfile(myGamesPath(), path, "skyrimvr.ini"); + } + + copyToProfile(myGamesPath(), path, "skyrimprefs.ini"); + } +} + +QString GameSkyrimVR::savegameExtension() const +{ + return "ess"; +} + +QString GameSkyrimVR::savegameSEExtension() const +{ + return "skse"; +} + +std::shared_ptr +GameSkyrimVR::makeSaveGame(QString filePath) const +{ + return std::make_shared(filePath, this); +} + +QString GameSkyrimVR::steamAPPId() const +{ + return "611670"; +} + +QStringList GameSkyrimVR::primaryPlugins() const +{ + QStringList plugins = {"skyrim.esm", "update.esm", "dawnguard.esm", + "hearthfires.esm", "dragonborn.esm", "skyrimvr.esm"}; + + plugins.append(CCPlugins()); + + return plugins; +} + +QStringList GameSkyrimVR::gameVariants() const +{ + return {"Regular"}; +} + +QString GameSkyrimVR::gameShortName() const +{ + return "SkyrimVR"; +} + +QStringList GameSkyrimVR::primarySources() const +{ + return {"SkyrimSE"}; +} + +QStringList GameSkyrimVR::validShortNames() const +{ + QStringList shortNames{"Skyrim", "SkyrimSE"}; + if (m_Organizer->pluginSetting(name(), "enderal_downloads").toBool()) { + shortNames.append({"Enderal", "EnderalSE"}); + } + return shortNames; +} + +QString GameSkyrimVR::gameNexusName() const +{ + return QString(); +} + +QStringList GameSkyrimVR::iniFiles() const +{ + return {"skyrimvr.ini", "skyrimprefs.ini"}; +} + +QStringList GameSkyrimVR::DLCPlugins() const +{ + return {"dawnguard.esm", "hearthfires.esm", "dragonborn.esm"}; +} + +QStringList GameSkyrimVR::CCPlugins() const +{ + QStringList plugins = {}; + QFile file(gameDirectory().filePath("Skyrim.ccc")); + if (file.open(QIODevice::ReadOnly)) { + ON_BLOCK_EXIT([&file]() { + file.close(); + }); + + if (file.size() == 0) { + return plugins; + } + while (!file.atEnd()) { + QByteArray line = file.readLine().trimmed(); + QString modName; + if ((line.size() > 0) && (line.at(0) != '#')) { + modName = QString::fromUtf8(line.constData()).toLower(); + } + + if (modName.size() > 0) { + if (!plugins.contains(modName, Qt::CaseInsensitive)) { + plugins.append(modName); + } + } + } + } + return plugins; +} + +IPluginGame::LoadOrderMechanism GameSkyrimVR::loadOrderMechanism() const +{ + return IPluginGame::LoadOrderMechanism::PluginsTxt; +} + +MOBase::IPluginGame::SortMechanism GameSkyrimVR::sortMechanism() const +{ + return SortMechanism::LOOT; +} + +int GameSkyrimVR::nexusModOrganizerID() const +{ + return 0; +} + +int GameSkyrimVR::nexusGameID() const +{ + return 0; +} + +QString GameSkyrimVR::getLauncherName() const +{ + return binaryName(); // Skyrim VR has no Launcher, so we just return the name of the + // game binary +} + +QDir GameSkyrimVR::gameDirectory() const +{ + return QDir(m_GamePath); +} + +// Not to delete all the spaces... +MappingType GameSkyrimVR::mappings() const +{ + MappingType result; + + for (const QString& profileFile : {"plugins.txt", "loadorder.txt"}) { + result.push_back({m_Organizer->profilePath() + "/" + profileFile, + localAppFolder() + "/" + gameName() + "/" + profileFile, false}); + } + + return result; +} diff --git a/libs/game_bethesda/src/games/skyrimvr/skyrimvrdataarchives.cpp b/libs/game_bethesda/src/games/skyrimvr/skyrimvrdataarchives.cpp new file mode 100644 index 0000000..752aa15 --- /dev/null +++ b/libs/game_bethesda/src/games/skyrimvr/skyrimvrdataarchives.cpp @@ -0,0 +1,45 @@ +#include "skyrimvrdataarchives.h" + +#include "iprofile.h" +#include + +QStringList SkyrimVRDataArchives::vanillaArchives() const +{ + return {"Skyrim - Textures0.bsa", "Skyrim - Textures1.bsa", "Skyrim - Textures2.bsa", + "Skyrim - Textures3.bsa", "Skyrim - Textures4.bsa", "Skyrim - Textures5.bsa", + "Skyrim - Textures6.bsa", "Skyrim - Textures7.bsa", "Skyrim - Textures8.bsa", + "Skyrim - Meshes0.bsa", "Skyrim - Meshes1.bsa", "Skyrim - Voices_en0.bsa", + "Skyrim - Sounds.bsa", "Skyrim - Interface.bsa", "Skyrim - Animations.bsa", + "Skyrim - Shaders.bsa", "Skyrim - Misc.bsa", "Skyrim - Patch.bsa", + "Skyrim_VR - Main.bsa"}; +} + +QStringList SkyrimVRDataArchives::archives(const MOBase::IProfile* profile) const +{ + QStringList result; + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("skyrimvr.ini") + : localGameDirectory().absoluteFilePath("skyrimvr.ini"); + result.append(getArchivesFromKey(iniFile, "SResourceArchiveList")); + result.append(getArchivesFromKey(iniFile, "SResourceArchiveList2")); + + return result; +} + +void SkyrimVRDataArchives::writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) +{ + QString list = before.join(", "); + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("skyrimvr.ini") + : localGameDirectory().absoluteFilePath("skyrimvr.ini"); + if (list.length() > 255) { + int splitIdx = list.lastIndexOf(",", 256); + setArchivesToKey(iniFile, "SResourceArchiveList", list.mid(0, splitIdx)); + setArchivesToKey(iniFile, "SResourceArchiveList2", list.mid(splitIdx + 2)); + } else { + setArchivesToKey(iniFile, "SResourceArchiveList", list); + } +} diff --git a/libs/game_bethesda/src/games/skyrimvr/skyrimvrgameplugins.cpp b/libs/game_bethesda/src/games/skyrimvr/skyrimvrgameplugins.cpp new file mode 100644 index 0000000..1524ffe --- /dev/null +++ b/libs/game_bethesda/src/games/skyrimvr/skyrimvrgameplugins.cpp @@ -0,0 +1,15 @@ +#include "skyrimvrgameplugins.h" + +using namespace MOBase; + +SkyrimVRGamePlugins::SkyrimVRGamePlugins(MOBase::IOrganizer* organizer) + : CreationGamePlugins(organizer) +{} + +bool SkyrimVRGamePlugins::lightPluginsAreSupported() +{ + auto files = m_Organizer->findFiles("skse\\plugins", {"skyrimvresl.dll"}); + if (files.isEmpty()) + return false; + return true; +} diff --git a/libs/game_bethesda/src/games/skyrimvr/skyrimvrsavegame.cpp b/libs/game_bethesda/src/games/skyrimvr/skyrimvrsavegame.cpp new file mode 100644 index 0000000..1670dc8 --- /dev/null +++ b/libs/game_bethesda/src/games/skyrimvr/skyrimvrsavegame.cpp @@ -0,0 +1,149 @@ +#include "skyrimvrsavegame.h" + +#ifdef _WIN32 +#include +#else +#include + +// Windows ULARGE_INTEGER union for 64-bit FILETIME math +union _ULARGE_INTEGER { + struct { + uint32_t LowPart; + uint32_t HighPart; + }; + uint64_t QuadPart; +}; + +// SYSTEMTIME is _SYSTEMTIME (defined in gamebryosavegame.h for Linux) +using SYSTEMTIME = _SYSTEMTIME; + +// Portable FileTimeToSystemTime: converts Windows FILETIME to SYSTEMTIME +static void FileTimeToSystemTime(const FILETIME* ft, SYSTEMTIME* st) +{ + const uint64_t ticks = (static_cast(ft->dwHighDateTime) << 32) | + static_cast(ft->dwLowDateTime); + const int64_t unixSecs = static_cast(ticks / 10000000ULL) - 11644473600LL; + const uint64_t remainderHns = ticks % 10000000ULL; // leftover 100ns units + + const QDateTime dt = QDateTime::fromSecsSinceEpoch(unixSecs, Qt::UTC); + const QDate d = dt.date(); + const QTime t = dt.time(); + + st->wYear = static_cast(d.year()); + st->wMonth = static_cast(d.month()); + st->wDayOfWeek = static_cast(d.dayOfWeek() % 7); // Qt Mon=1..Sun=7 -> Win Sun=0..Sat=6 + st->wDay = static_cast(d.day()); + st->wHour = static_cast(t.hour()); + st->wMinute = static_cast(t.minute()); + st->wSecond = static_cast(t.second()); + st->wMilliseconds = static_cast(remainderHns / 10000); +} +#endif + +#include "gameskyrimvr.h" + +SkyrimVRSaveGame::SkyrimVRSaveGame(QString const& fileName, GameSkyrimVR const* game) + : GamebryoSaveGame(fileName, game, true) +{ + FileWrapper file(fileName, "TESV_SAVEGAME"); // 10bytes + + uint32_t version; + FILETIME ftime; + fetchInformationFields(file, version, m_PCName, m_PCLevel, m_PCLocation, m_SaveNumber, + ftime); + + // A file time is a 64-bit value that represents the number of 100-nanosecond + // intervals that have elapsed since 12:00 A.M. January 1, 1601 Coordinated Universal + // Time (UTC). So we need to convert that to something useful + + // For some reason, the file time is off by about 6 hours. + // So we need to subtract those 6 hours from the filetime. + _ULARGE_INTEGER time; + time.LowPart = ftime.dwLowDateTime; + time.HighPart = ftime.dwHighDateTime; + time.QuadPart -= 2.16e11; + ftime.dwHighDateTime = time.HighPart; + ftime.dwLowDateTime = time.LowPart; + + SYSTEMTIME ctime; + ::FileTimeToSystemTime(&ftime, &ctime); + + setCreationTime(ctime); +} + +void SkyrimVRSaveGame::fetchInformationFields(FileWrapper& file, uint32_t& version, + QString& playerName, + unsigned short& playerLevel, + QString& playerLocation, + uint32_t& saveNumber, + FILETIME& creationTime) const +{ + uint32_t headerSize; + file.read(headerSize); // header size "TESV_SAVEGAME" + file.read(version); // header version 74 (original Skyrim is 79) + file.read(saveNumber); + + file.read(playerName); + + uint32_t temp; + file.read(temp); + playerLevel = static_cast(temp); + + file.read(playerLocation); + + QString timeOfDay; + file.read(timeOfDay); + + QString race; + file.read(race); // race name (i.e. BretonRace) + + file.skip(); // Player gender (0 = male) + file.skip(2); // experience gathered, experience required + + file.read(creationTime); // filetime +} + +std::unique_ptr SkyrimVRSaveGame::fetchDataFields() const +{ + FileWrapper file(getFilepath(), "TESV_SAVEGAME"); // 10bytes + + uint32_t version = 0; + { + QString dummyName, dummyLocation; + unsigned short dummyLevel; + uint32_t dummySaveNumber; + FILETIME dummyTime; + + fetchInformationFields(file, version, dummyName, dummyLevel, dummyLocation, + dummySaveNumber, dummyTime); + } + + std::unique_ptr fields = std::make_unique(); + + uint32_t width; + uint32_t height; + file.read(width); + file.read(height); + + uint16_t compressionType; + file.read(compressionType); + file.setCompressionType(compressionType); + + fields->Screenshot = file.readImage(width, height, 320, true); + + file.openCompressedData(); + + uint8_t saveGameVersion = file.readChar(); + uint8_t pluginInfoSize = file.readChar(); + uint16_t other = file.readShort(); // Unknown + + fields->Plugins = file.readPlugins(1); // Just empty data + + if (saveGameVersion >= 78) { + fields->LightPlugins = file.readLightPlugins(); + } + + file.closeCompressedData(); + + return fields; +} diff --git a/libs/game_bethesda/src/games/skyrimvr/skyrimvrscriptextender.cpp b/libs/game_bethesda/src/games/skyrimvr/skyrimvrscriptextender.cpp new file mode 100644 index 0000000..4d7433d --- /dev/null +++ b/libs/game_bethesda/src/games/skyrimvr/skyrimvrscriptextender.cpp @@ -0,0 +1,18 @@ +#include "skyrimvrscriptextender.h" + +#include +#include + +SkyrimVRScriptExtender::SkyrimVRScriptExtender(GameGamebryo const* game) + : GamebryoScriptExtender(game) +{} + +QString SkyrimVRScriptExtender::BinaryName() const +{ + return "sksevr_loader.exe"; +} + +QString SkyrimVRScriptExtender::PluginPath() const +{ + return "skse/plugins"; +} diff --git a/libs/game_bethesda/src/games/skyrimvr/skyrimvrunmanagedmods.cpp b/libs/game_bethesda/src/games/skyrimvr/skyrimvrunmanagedmods.cpp new file mode 100644 index 0000000..dd6a158 --- /dev/null +++ b/libs/game_bethesda/src/games/skyrimvr/skyrimvrunmanagedmods.cpp @@ -0,0 +1,29 @@ +#include "skyrimvrunmanagedmods.h" + +SkyrimVRUnmangedMods::SkyrimVRUnmangedMods(const GameGamebryo* game) + : GamebryoUnmangedMods(game) +{} + +SkyrimVRUnmangedMods::~SkyrimVRUnmangedMods() {} + +QStringList SkyrimVRUnmangedMods::mods(bool onlyOfficial) const +{ + QStringList result; + + QStringList pluginList = game()->primaryPlugins(); + QStringList otherPlugins = game()->DLCPlugins(); + otherPlugins.append(game()->CCPlugins()); + for (QString plugin : otherPlugins) { + pluginList.removeAll(plugin); + } + QDir dataDir(game()->dataDirectory()); + for (const QString& fileName : dataDir.entryList({"*.esp", "*.esl", "*.esm"})) { + if (!pluginList.contains(fileName, Qt::CaseInsensitive)) { + if (!onlyOfficial || pluginList.contains(fileName, Qt::CaseInsensitive)) { + result.append(fileName.chopped(4)); // trims the extension off + } + } + } + + return result; +} diff --git a/libs/game_bethesda/src/games/starfield/gamestarfield.cpp b/libs/game_bethesda/src/games/starfield/gamestarfield.cpp new file mode 100644 index 0000000..c6c211a --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/gamestarfield.cpp @@ -0,0 +1,438 @@ +#include "gamestarfield.h" + +#include "ipluginlist.h" + +#include "starfieldbsainvalidation.h" +#include "starfielddataarchives.h" +#include "starfieldgameplugins.h" +#include "starfieldmoddatachecker.h" +#include "starfieldmoddatacontent.h" +#include "starfieldsavegame.h" +#include "starfieldscriptextender.h" +#include "starfieldunmanagedmods.h" + +#include "versioninfo.h" +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "utility.h" + +using namespace MOBase; + +const unsigned int GameStarfield::PROBLEM_ESP; +const unsigned int GameStarfield::PROBLEM_TEST_FILE; + +GameStarfield::GameStarfield() {} + +bool GameStarfield::init(IOrganizer* moInfo) +{ + if (!GameGamebryo::init(moInfo)) { + return false; + } + + auto dataArchives = std::make_shared(this); + registerFeature(std::make_shared(this)); + registerFeature(dataArchives); + registerFeature( + std::make_shared(this, "StarfieldCustom.ini")); + registerFeature(std::make_shared(this)); + registerFeature( + std::make_shared(m_Organizer->gameFeatures())); + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(moInfo)); + registerFeature(std::make_shared(this, localAppFolder())); + registerFeature(std::make_shared(dataArchives.get(), this)); + + return true; +} + +QString GameStarfield::gameName() const +{ + return "Starfield"; +} + +void GameStarfield::detectGame() +{ + m_GamePath = identifyGamePath(); + m_MyGamesPath = determineMyGamesPath("Starfield"); +} + +QString GameStarfield::identifyGamePath() const +{ + return parseSteamLocation(steamAPPId(), gameName()); +} + +QDir GameStarfield::dataDirectory() const +{ + QDir dataDir = documentsDirectory().absoluteFilePath("Data"); + if (!dataDir.exists()) + dataDir.mkdir(dataDir.path()); + return documentsDirectory().absoluteFilePath("Data"); +} + +QMap GameStarfield::secondaryDataDirectories() const +{ + QMap directories; + directories.insert("game_data", gameDirectory().absoluteFilePath("Data")); + return directories; +} + +QList GameStarfield::executables() const +{ + return QList() + << ExecutableInfo("SFSE", + findInGameFolder(m_Organizer->gameFeatures() + ->gameFeature() + ->loaderName())) + << ExecutableInfo("Starfield", findInGameFolder(binaryName())) + << ExecutableInfo("Creation Kit", findInGameFolder("CreationKit.exe")) + .withSteamAppId("2722710") + << ExecutableInfo("LOOT", QFileInfo(getLootPath())) + .withArgument("--game=\"Starfield\""); +} + +QList GameStarfield::executableForcedLoads() const +{ + return QList(); +} + +QString GameStarfield::name() const +{ + return "Starfield Support Plugin"; +} + +QString GameStarfield::localizedName() const +{ + return tr("Starfield Support Plugin"); +} + +QString GameStarfield::author() const +{ + return "Silarn"; +} + +QString GameStarfield::description() const +{ + return tr("Adds support for the game Starfield."); +} + +MOBase::VersionInfo GameStarfield::version() const +{ + return VersionInfo(1, 1, 0, VersionInfo::RELEASE_FINAL); +} + +QList GameStarfield::settings() const +{ + return QList() + << PluginSetting( + "enable_esp_warning", + tr("Show a warning when ESP plugins are enabled in the load order."), + true) + << PluginSetting("enable_management_warnings", + tr("Show a warning when plugins.txt management is invalid."), + true); +} + +MappingType GameStarfield::mappings() const +{ + MappingType result; + if (testFilePlugins().isEmpty()) { + for (const QString& profileFile : {"plugins.txt", "loadorder.txt"}) { + result.push_back({m_Organizer->profilePath() + "/" + profileFile, + localAppFolder() + "/" + gameShortName() + "/" + profileFile, + false}); + } + } + return result; +} + +void GameStarfield::initializeProfile(const QDir& path, ProfileSettings settings) const +{ + if (settings.testFlag(IPluginGame::MODS)) { + copyToProfile(localAppFolder() + "/Starfield", path, "plugins.txt"); + } + + if (settings.testFlag(IPluginGame::CONFIGURATION)) { + copyToProfile(myGamesPath(), path, "StarfieldPrefs.ini"); + copyToProfile(myGamesPath(), path, "StarfieldCustom.ini"); + } +} + +QString GameStarfield::savegameExtension() const +{ + return "sfs"; +} + +QString GameStarfield::savegameSEExtension() const +{ + return "sfse"; +} + +std::shared_ptr +GameStarfield::makeSaveGame(QString filePath) const +{ + return std::make_shared(filePath, this); +} + +QString GameStarfield::steamAPPId() const +{ + return "1716740"; +} + +QStringList GameStarfield::testFilePlugins() const +{ + QStringList plugins; + if (m_Organizer != nullptr && m_Organizer->profile() != nullptr) { + QString customIni( + m_Organizer->profile()->absoluteIniFilePath("StarfieldCustom.ini")); + if (QFile(customIni).exists()) { + for (int i = 1; i <= 10; ++i) { + QString setting("sTestFile"); + setting += std::to_string(i); + QString plugin = GameGamebryo::readIniValue(customIni, "General", setting, ""); + if (!plugin.isEmpty() && !plugins.contains(plugin, Qt::CaseInsensitive)) { + plugins.append(plugin); + } + } + } + } + return plugins; +} + +QStringList GameStarfield::primaryPlugins() const +{ + QStringList plugins = {"Starfield.esm", "Constellation.esm", + "ShatteredSpace.esm", "OldMars.esm", + "SFBGS003.esm", "SFBGS004.esm", + "SFBGS006.esm", "SFBGS007.esm", + "SFBGS008.esm", "BlueprintShips-Starfield.esm"}; + + for (auto plugin : CCCPlugins()) { + if (!plugins.contains(plugin, Qt::CaseInsensitive)) { + plugins.append(plugin); + } + } + + auto testPlugins = testFilePlugins(); + if (loadOrderMechanism() == LoadOrderMechanism::None) { + plugins << enabledPlugins(); + plugins << testPlugins; + } + + plugins.removeDuplicates(); + + return plugins; +} + +QStringList GameStarfield::enabledPlugins() const +{ + return {}; +} + +QStringList GameStarfield::gameVariants() const +{ + return {"Regular"}; +} + +QString GameStarfield::gameShortName() const +{ + return "Starfield"; +} + +QString GameStarfield::gameNexusName() const +{ + return "starfield"; +} + +QStringList GameStarfield::iniFiles() const +{ + return {"StarfieldPrefs.ini", "StarfieldCustom.ini"}; +} + +bool GameStarfield::prepareIni(const QString& exec) +{ + return true; // no need to write to Starfield.ini +} + +QStringList GameStarfield::DLCPlugins() const +{ + return {"Constellation.esm", "ShatteredSpace.esm"}; +} + +QStringList GameStarfield::CCCPlugins() const +{ + // While the CCC file appears to be mostly legacy, we need to parse it since the game + // will still read it and there are some compatibility reason to use it for + // force-loading the core game plugins. + QStringList plugins = {}; + if (!testFilePresent()) { + QFile myDocsCCCFile(myGamesPath() + "/Starfield.ccc"); + QFile gameCCCFile(gameDirectory().absoluteFilePath("Starfield.ccc")); + QFile* file; + if (myDocsCCCFile.exists()) { + file = &myDocsCCCFile; + } else { + file = &gameCCCFile; + } + if (file->open(QIODevice::ReadOnly)) { + if (file->size() > 0) { + while (!file->atEnd()) { + QByteArray line = file->readLine().trimmed(); + QString modName; + if ((line.size() > 0) && (line.at(0) != '#')) { + modName = QString::fromUtf8(line.constData()); + } + if (modName.size() > 0) { + plugins.append(modName); + } + } + } + } + } + return plugins; +} + +QStringList GameStarfield::CCPlugins() const +{ + QStringList plugins = {}; + std::shared_ptr unmanagedMods = + std::static_pointer_cast( + m_Organizer->gameFeatures()->gameFeature()); + + // The ContentCatalog.txt appears to be the main repository where Starfiled stores + // info about the installed Creations. We parse this to correctly mark unmanaged mods + // as Creations. The StarfieldUnmanagedMods class handles parsing mod names and files. + if (unmanagedMods.get()) { + auto contentCatalog = unmanagedMods->parseContentCatalog(); + for (const auto& mod : contentCatalog) { + if (!plugins.contains(mod.first, Qt::CaseInsensitive)) { + plugins.append(mod.first); + } + } + } + return plugins; +} + +IPluginGame::SortMechanism GameStarfield::sortMechanism() const +{ + return IPluginGame::SortMechanism::LOOT; +} + +IPluginGame::LoadOrderMechanism GameStarfield::loadOrderMechanism() const +{ + if (!testFilePresent()) + return IPluginGame::LoadOrderMechanism::PluginsTxt; + return IPluginGame::LoadOrderMechanism::None; +} + +int GameStarfield::nexusModOrganizerID() const +{ + return 0; +} + +int GameStarfield::nexusGameID() const +{ + return 4187; +} + +// Start Diagnose +std::vector GameStarfield::activeProblems() const +{ + std::vector result; + if (m_Organizer->managedGame() == this) { + if (m_Organizer->pluginSetting(name(), "enable_esp_warning").toBool() && + activeESP()) + result.push_back(PROBLEM_ESP); + if (m_Organizer->pluginSetting(name(), "enable_management_warnings").toBool()) { + if (testFilePresent()) + result.push_back(PROBLEM_TEST_FILE); + } + } + return result; +} + +bool GameStarfield::activeESP() const +{ + m_Active_ESPs.clear(); + std::set enabledPlugins; + + QStringList esps = m_Organizer->findFiles("", [](const QString& fileName) -> bool { + return fileName.endsWith(".esp", FileNameComparator::CaseSensitivity); + }); + + for (const QString& esp : esps) { + QString baseName = QFileInfo(esp).fileName(); + if (m_Organizer->pluginList()->state(baseName) == IPluginList::STATE_ACTIVE) { + m_Active_ESPs.insert(baseName); + } + } + + if (!m_Active_ESPs.empty()) + return true; + return false; +} + +bool GameStarfield::testFilePresent() const +{ + if (!testFilePlugins().isEmpty()) + return true; + return false; +} + +QString GameStarfield::shortDescription(unsigned int key) const +{ + switch (key) { + case PROBLEM_ESP: + return tr("You have active ESP plugins in Starfield"); + case PROBLEM_TEST_FILE: + return tr("sTestFile entries are present"); + } + return ""; +} + +QString GameStarfield::fullDescription(unsigned int key) const +{ + switch (key) { + case PROBLEM_ESP: { + QString espInfo = SetJoin(m_Active_ESPs, ", "); + return tr("

ESP plugins are not ideal for Starfield. In addition to being unable " + "to sort them alongside ESM or master-flagged plugins, certain record " + "references are always kept loaded by the game. This consumes " + "unnecessary resources and limits the game's ability to load what it " + "needs.

" + "

Ideally, plugins should be saved as ESM files upon release. It can " + "also be released as an ESL plugin, however there are additional " + "concerns with the way light plugins are currently handled and should " + "only be used when absolutely certain about what you're doing.

" + "

Notably, xEdit does not currently support saving ESP files.

" + "

Current ESPs:

%1

") + .arg(espInfo); + } + case PROBLEM_TEST_FILE: { + return tr("

You have plugin managment enabled but you still have sTestFile " + "settings in your StarfieldCustom.ini. These must be removed or the game " + "will not read the plugins.txt file. Management is still disabled.

"); + } + } + return ""; +} + +bool GameStarfield::hasGuidedFix(unsigned int key) const +{ + return false; +} + +void GameStarfield::startGuidedFix(unsigned int key) const {} diff --git a/libs/game_bethesda/src/games/starfield/starfieldbsainvalidation.cpp b/libs/game_bethesda/src/games/starfield/starfieldbsainvalidation.cpp new file mode 100644 index 0000000..f6e803b --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/starfieldbsainvalidation.cpp @@ -0,0 +1,68 @@ +#include "starfieldbsainvalidation.h" + +#include "dummybsa.h" +#include "iplugingame.h" +#include "iprofile.h" +#include "registry.h" +#include +#include + +#include "gamegamebryo.h" + +StarfieldBSAInvalidation::StarfieldBSAInvalidation(MOBase::DataArchives* dataArchives, + MOBase::IPluginGame const* game) + : GamebryoBSAInvalidation(dataArchives, "StarfieldCustom.ini", game) +{ + m_IniFileName = "StarfieldCustom.ini"; + m_Game = game; +} + +bool StarfieldBSAInvalidation::isInvalidationBSA(const QString& bsaName) +{ + return false; +} + +QString StarfieldBSAInvalidation::invalidationBSAName() const +{ + return ""; +} + +unsigned long StarfieldBSAInvalidation::bsaVersion() const +{ + return 0x68; +} + +bool StarfieldBSAInvalidation::prepareProfile(MOBase::IProfile* profile) +{ + bool dirty = false; + QString basePath = profile->localSettingsEnabled() + ? profile->absolutePath() + : m_Game->documentsDirectory().absolutePath(); + QString iniFilePath = basePath + "/" + m_IniFileName; + + if (profile->invalidationActive(nullptr)) { + // write bInvalidateOlderFiles = 1, if needed + QString bInvalidateOlderFiles = GameGamebryo::readIniValue( + iniFilePath, "Archive", "bInvalidateOlderFiles", "0"); + if (bInvalidateOlderFiles.toLong() != 1) { + dirty = true; + if (!MOBase::WriteRegistryValue("Archive", "bInvalidateOlderFiles", "1", + iniFilePath)) { + qWarning("failed to override data directory in \"%s\"", + qUtf8Printable(m_IniFileName)); + } + } + QString sResourceDataDirsFinal = GameGamebryo::readIniValue( + iniFilePath, "Archive", "sResourceDataDirsFinal", "STRINGS\\"); + if (sResourceDataDirsFinal != "") { + dirty = true; + if (!MOBase::WriteRegistryValue("Archive", "sResourceDataDirsFinal", "", + iniFilePath)) { + qWarning("failed to override data directory in \"%s\"", + qUtf8Printable(m_IniFileName)); + } + } + } + + return dirty; +} diff --git a/libs/game_bethesda/src/games/starfield/starfielddataarchives.cpp b/libs/game_bethesda/src/games/starfield/starfielddataarchives.cpp new file mode 100644 index 0000000..d7eadae --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/starfielddataarchives.cpp @@ -0,0 +1,98 @@ +#include "starfielddataarchives.h" + +#include "iprofile.h" +#include + +QStringList StarfieldDataArchives::vanillaArchives() const +{ + return {"Starfield - Animations.ba2", + "Starfield - DensityMaps.ba2", + "Starfield - FaceAnimation01.ba2", + "Starfield - FaceAnimation02.ba2", + "Starfield - FaceAnimation03.ba2", + "Starfield - FaceAnimation04.ba2", + "Starfield - FaceAnimationPatch.ba2", + "Starfield - FaceMeshes.ba2", + "Starfield - GeneratedTextures.ba2", + "Starfield - Interface.ba2", + "Starfield - Localization.ba2", + "Starfield - LODMeshes.ba2", + "Starfield - LODMeshesPatch.ba2", + "Starfield - LODTextures.ba2", + "Starfield - Materials.ba2", + "Starfield - Meshes01.ba2", + "Starfield - Meshes02.ba2", + "Starfield - MeshesPatch.ba2", + "Starfield - Misc.ba2", + "Starfield - Particles.ba2", + "Starfield - ParticlesTestData.ba2", + "Starfield - PlanetData.ba2", + "Starfield - Shaders.ba2", + "Starfield - ShadersBeta.ba2", + "Starfield - Terrain01.ba2", + "Starfield - Terrain02.ba2", + "Starfield - Terrain03.ba2", + "Starfield - Terrain04.ba2", + "Starfield - TerrainPatch.ba2", + "Starfield - Textures01.ba2", + "Starfield - Textures02.ba2", + "Starfield - Textures03.ba2", + "Starfield - Textures04.ba2", + "Starfield - Textures05.ba2", + "Starfield - Textures06.ba2", + "Starfield - Textures07.ba2", + "Starfield - Textures08.ba2", + "Starfield - Textures09.ba2", + "Starfield - Textures10.ba2", + "Starfield - Textures11.ba2", + "Starfield - TexturesPatch.ba2", + "Starfield - Voices01.ba2", + "Starfield - Voices02.ba2", + "Starfield - VoicesPatch.ba2", + "Starfield - WwiseSounds01.ba2", + "Starfield - WwiseSounds02.ba2", + "Starfield - WwiseSounds03.ba2", + "Starfield - WwiseSounds04.ba2", + "Starfield - WwiseSounds05.ba2", + "Starfield - WwiseSoundsPatch.ba2", + "Constellation - Localization.ba2", + "Constellation - Textures.ba2", + "OldMars - Localization.ba2", + "OldMars - Textures.ba2", + "BlueprintShips-Starfield - Localization.ba2"}; +} + +QStringList StarfieldDataArchives::archives(const MOBase::IProfile* profile) const +{ + QStringList result; + + QString defaultIniFile = gameDirectory().absoluteFilePath("Starfield.ini"); + QString customIniFile = + profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("StarfieldCustom.ini") + : localGameDirectory().absoluteFilePath("StarfieldCustom.ini"); + QStringList archiveSettings = {"SResourceArchiveList", "sResourceIndexFileList", + "SResourceArchiveMemoryCacheList", + "sResourceStartUpArchiveList", + "sResourceEnglishVoiceList"}; + for (auto setting : archiveSettings) { + auto archives = getArchivesFromKey(customIniFile, setting, 4096); + if (archives.isEmpty()) + archives = getArchivesFromKey(defaultIniFile, setting, 4096); + result.append(archives); + } + + return result; +} + +void StarfieldDataArchives::writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) +{} + +void StarfieldDataArchives::addArchive(MOBase::IProfile* profile, int index, + const QString& archiveName) +{} + +void StarfieldDataArchives::removeArchive(MOBase::IProfile* profile, + const QString& archiveName) +{} diff --git a/libs/game_bethesda/src/games/starfield/starfieldgameplugins.cpp b/libs/game_bethesda/src/games/starfield/starfieldgameplugins.cpp new file mode 100644 index 0000000..6b4e6ab --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/starfieldgameplugins.cpp @@ -0,0 +1,35 @@ +#include "starfieldgameplugins.h" + +using namespace MOBase; + +StarfieldGamePlugins::StarfieldGamePlugins(MOBase::IOrganizer* organizer) + : CreationGamePlugins(organizer) +{} + +bool StarfieldGamePlugins::mediumPluginsAreSupported() +{ + return true; +} + +bool StarfieldGamePlugins::blueprintPluginsAreSupported() +{ + return true; +} + +void StarfieldGamePlugins::writePluginList(const IPluginList* pluginList, + const QString& filePath) +{ + if (m_Organizer->managedGame()->loadOrderMechanism() == + MOBase::IPluginGame::LoadOrderMechanism::PluginsTxt) { + CreationGamePlugins::writePluginList(pluginList, filePath); + } +} + +QStringList StarfieldGamePlugins::readPluginList(MOBase::IPluginList* pluginList) +{ + if (m_Organizer->managedGame()->loadOrderMechanism() == + MOBase::IPluginGame::LoadOrderMechanism::PluginsTxt) { + return CreationGamePlugins::readPluginList(pluginList); + } + return {}; +} diff --git a/libs/game_bethesda/src/games/starfield/starfieldsavegame.cpp b/libs/game_bethesda/src/games/starfield/starfieldsavegame.cpp new file mode 100644 index 0000000..49653cb --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/starfieldsavegame.cpp @@ -0,0 +1,157 @@ +#include "starfieldsavegame.h" + +#ifdef _WIN32 +#include +#else +#include + +using SYSTEMTIME = _SYSTEMTIME; + +static void FileTimeToSystemTime(const FILETIME* ft, SYSTEMTIME* st) +{ + const uint64_t ticks = (static_cast(ft->dwHighDateTime) << 32) | + static_cast(ft->dwLowDateTime); + const int64_t unixSecs = static_cast(ticks / 10000000ULL) - 11644473600LL; + const uint64_t remainderHns = ticks % 10000000ULL; + + const QDateTime dt = QDateTime::fromSecsSinceEpoch(unixSecs, Qt::UTC); + const QDate d = dt.date(); + const QTime t = dt.time(); + + st->wYear = static_cast(d.year()); + st->wMonth = static_cast(d.month()); + st->wDayOfWeek = static_cast(d.dayOfWeek() % 7); + st->wDay = static_cast(d.day()); + st->wHour = static_cast(t.hour()); + st->wMinute = static_cast(t.minute()); + st->wSecond = static_cast(t.second()); + st->wMilliseconds = static_cast(remainderHns / 10000); +} +#endif + +#include "gamestarfield.h" + +StarfieldSaveGame::StarfieldSaveGame(QString const& fileName, GameStarfield const* game) + : GamebryoSaveGame(fileName, game, true, true) +{ + FileWrapper file(getFilepath(), "BCPS"); + + getData(file); + FILETIME creationTime; + unsigned char saveVersion; + fetchInformationFields(file, m_SaveNumber, saveVersion, m_PCName, m_PCLevel, + m_PCLocation, creationTime); + file.closeCompressedData(); + file.close(); + + // A file time is a 64-bit value that represents the number of 100-nanosecond + // intervals that have elapsed since 12:00 A.M. January 1, 1601 Coordinated Universal + // Time (UTC). So we need to convert that to something useful + SYSTEMTIME ctime; + ::FileTimeToSystemTime(&creationTime, &ctime); + + setCreationTime(ctime); +} + +void StarfieldSaveGame::getData(FileWrapper& file) const +{ + file.skip(); // header version + file.skip(); // chunk compressed size array start location + file.skip(); // unknown (0?) + file.setCompressionType(1); + /* + * Parse following variables then begin decompressing data + * - 64-bit int = compressed data start location + * - 64-bit int = complete uncompressed data size + */ + file.openCompressedData(); + /* + * Remaining headers before start of compressed data: + * - 32-bit float (version? appears to be 2.0) + * - 64-bit int - size of uncompressed chunks (250 KiB) + * - 64-bit int - size of byte rows? (16 bytes) used to determine start of each + * compressed chunk + * - 32-bit int - number of chunks? + * - 'ZIP ' - denotes start of chunk compressed size array + * - compressed size array - array of 32-bit ints containing the compressed size of + * each compressed chunk (see number of chunks above) + */ +} + +void StarfieldSaveGame::fetchInformationFields( + FileWrapper& file, uint32_t& saveNumber, unsigned char& saveVersion, + QString& playerName, unsigned short& playerLevel, QString& playerLocation, + FILETIME& creationTime) const +{ + char fileID[12]; // SFS_SAVEGAME + unsigned int headerSize; + unsigned int version; + // file.read(fileID, 12); + headerSize = file.readInt(12); + version = file.readInt(); + saveVersion = file.readChar(); + saveNumber = file.readInt(); + file.read(playerName); + + unsigned int temp; + temp = file.readInt(); + playerLevel = static_cast(temp); + file.read(playerLocation); + + QString ignore; + file.read(ignore); // playtime as ascii hh.mm.ss + file.read(ignore); // race name (i.e. BretonRace) + + unsigned short gender; + gender = file.readShort(); // Player gender (0 = male) + float experience, experienceRequired; + experience = file.readFloat(); + experienceRequired = file.readFloat(); + + unsigned long long time = file.readLong(); + creationTime.dwLowDateTime = (DWORD)time; + creationTime.dwHighDateTime = time >> 32; +} + +std::unique_ptr StarfieldSaveGame::fetchDataFields() const +{ + FileWrapper file(getFilepath(), "BCPS"); // 10bytes + + getData(file); + FILETIME creationTime; + unsigned char saveVersion; + + { + QString dummyName, dummyLocation; + unsigned short dummyLevel; + uint32_t dummySaveNumber; + FILETIME dummyTime; + + fetchInformationFields(file, dummySaveNumber, saveVersion, dummyName, dummyLevel, + dummyLocation, dummyTime); + } + + int extraInfo = 0; + if (saveVersion >= 122) + extraInfo = 1; + if (saveVersion >= 140) + extraInfo = 2; + QStringList gamePlugins = m_Game->primaryPlugins() + m_Game->enabledPlugins(); + + QString ignore; + std::unique_ptr fields = std::make_unique(); + + file.readChar(12); + file.read(ignore); // game version + file.read(ignore); // game version again? + file.readInt(); // plugin info size + + fields->Plugins = file.readPlugins(0, extraInfo, gamePlugins); + fields->LightPlugins = file.readLightPlugins(0, extraInfo, gamePlugins); + if (saveVersion >= 122) + fields->MediumPlugins = file.readMediumPlugins(0, extraInfo, gamePlugins); + file.closeCompressedData(); + file.close(); + + return fields; +} diff --git a/libs/game_bethesda/src/games/starfield/starfieldscriptextender.cpp b/libs/game_bethesda/src/games/starfield/starfieldscriptextender.cpp new file mode 100644 index 0000000..15cf6cd --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/starfieldscriptextender.cpp @@ -0,0 +1,18 @@ +#include "starfieldscriptextender.h" + +#include +#include + +StarfieldScriptExtender::StarfieldScriptExtender(GameGamebryo const* game) + : GamebryoScriptExtender(game) +{} + +QString StarfieldScriptExtender::BinaryName() const +{ + return "sfse_loader.exe"; +} + +QString StarfieldScriptExtender::PluginPath() const +{ + return "sfse/plugins"; +} diff --git a/libs/game_bethesda/src/games/starfield/starfieldunmanagedmods.cpp b/libs/game_bethesda/src/games/starfield/starfieldunmanagedmods.cpp new file mode 100644 index 0000000..30abb38 --- /dev/null +++ b/libs/game_bethesda/src/games/starfield/starfieldunmanagedmods.cpp @@ -0,0 +1,132 @@ +#include "starfieldunmanagedmods.h" + +#include "log.h" + +#include +#include +#include +#include + +StarfieldUnmanagedMods::StarfieldUnmanagedMods(const GameStarfield* game, + const QString& appDataFolder) + : GamebryoUnmangedMods(game), m_AppDataFolder(appDataFolder) +{} + +StarfieldUnmanagedMods::~StarfieldUnmanagedMods() {} + +QStringList StarfieldUnmanagedMods::mods(bool onlyOfficial) const +{ + QStringList result; + + QStringList pluginList = game()->primaryPlugins() + game()->enabledPlugins(); + QStringList otherPlugins = game()->DLCPlugins(); + otherPlugins.append(game()->CCPlugins()); + for (QString plugin : otherPlugins) { + pluginList.removeAll(plugin); + } + QMap directories = {{"data", game()->dataDirectory()}}; + directories.insert(game()->secondaryDataDirectories()); + for (QDir directory : directories) { + for (const QString& fileName : directory.entryList({"*.esp", "*.esl", "*.esm"})) { + if (!pluginList.contains(fileName, Qt::CaseInsensitive)) { + if (!onlyOfficial || pluginList.contains(fileName, Qt::CaseInsensitive)) { + result.append(fileName.chopped(4)); // trims the extension off + } + } + } + } + + return result; +} + +QFileInfo StarfieldUnmanagedMods::referenceFile(const QString& modName) const +{ + QFileInfoList files; + QMap directories = {{"data", game()->dataDirectory()}}; + directories.insert(game()->secondaryDataDirectories()); + for (QDir directory : directories) { + files += directory.entryInfoList(QStringList() << modName + ".es*"); + } + if (files.size() > 0) { + return files.at(0); + } else { + return QFileInfo(); + } +} + +std::map +StarfieldUnmanagedMods::parseContentCatalog() const +{ + QFile content(m_AppDataFolder + "/" + game()->gameShortName() + + "/ContentCatalog.txt"); + std::map contentCatalog; + if (content.open(QIODevice::OpenModeFlag::ReadOnly)) { + auto contentData = content.readAll(); + QString convertedData = QString::fromLatin1(contentData); + contentData = convertedData.toUtf8(); + QJsonParseError jsonError; + QJsonDocument contentDoc = QJsonDocument::fromJson(contentData, &jsonError); + if (jsonError.error) { + MOBase::log::warn(QObject::tr("ContentCatalog.txt appears to be corrupt: %1") + .arg(jsonError.errorString())); + } else { + QJsonObject contentObj = contentDoc.object(); + for (const auto& mod : contentObj.keys()) { + if (mod == "ContentCatalog") + continue; + auto modInfo = contentObj.value(mod).toObject(); + QStringList pluginList; + QStringList files; + for (const auto& file : modInfo.value("Files").toArray()) { + QString fileName = file.toString(); + files.append(fileName); + if (fileName.endsWith(".esm", Qt::CaseInsensitive) || + fileName.endsWith(".esl", Qt::CaseInsensitive) || + fileName.endsWith(".esp", Qt::CaseInsensitive)) { + pluginList.append(fileName); + } + } + for (const auto& plugin : pluginList) { + contentCatalog[plugin] = ContentCatalog(); + contentCatalog[plugin].files = files; + contentCatalog[plugin].name = modInfo.value("Title").toString(); + } + } + } + } + return contentCatalog; +} + +QStringList StarfieldUnmanagedMods::secondaryFiles(const QString& modName) const +{ + QStringList files; + auto contentCatalog = parseContentCatalog(); + for (const auto& mod : contentCatalog) { + if (mod.first.startsWith(modName, Qt::CaseInsensitive)) { + files += mod.second.files; + break; + } + } + // file extension in FO4 is .ba2 instead of bsa + QMap directories = {{"data", game()->dataDirectory()}}; + directories.insert(game()->secondaryDataDirectories()); + for (QDir directory : directories) { + for (const QString& archiveName : directory.entryList({modName + "*.ba2"})) { + files.append(directory.absoluteFilePath(archiveName)); + } + } + return files; +} + +QString StarfieldUnmanagedMods::displayName(const QString& modName) const +{ + auto contentCatalog = parseContentCatalog(); + for (const auto& mod : contentCatalog) { + if (mod.first.startsWith(modName, Qt::CaseInsensitive)) { + return mod.second.name; + } + } + // unlike in earlier games, in fallout 4 the file name doesn't correspond to + // the public name + return modName; +} diff --git a/libs/game_bethesda/src/games/ttw/falloutttwbsainvalidation.cpp b/libs/game_bethesda/src/games/ttw/falloutttwbsainvalidation.cpp new file mode 100644 index 0000000..67a7817 --- /dev/null +++ b/libs/game_bethesda/src/games/ttw/falloutttwbsainvalidation.cpp @@ -0,0 +1,16 @@ +#include "falloutttwbsainvalidation.h" + +FalloutTTWBSAInvalidation::FalloutTTWBSAInvalidation(MOBase::DataArchives* dataArchives, + MOBase::IPluginGame const* game) + : GamebryoBSAInvalidation(dataArchives, "fallout.ini", game) +{} + +QString FalloutTTWBSAInvalidation::invalidationBSAName() const +{ + return "Fallout - Invalidation.bsa"; +} + +unsigned long FalloutTTWBSAInvalidation::bsaVersion() const +{ + return 0x68; +} diff --git a/libs/game_bethesda/src/games/ttw/falloutttwdataarchives.cpp b/libs/game_bethesda/src/games/ttw/falloutttwdataarchives.cpp new file mode 100644 index 0000000..3ffb076 --- /dev/null +++ b/libs/game_bethesda/src/games/ttw/falloutttwdataarchives.cpp @@ -0,0 +1,31 @@ +#include "falloutttwdataarchives.h" +#include + +QStringList FalloutTTWDataArchives::vanillaArchives() const +{ + return {"Fallout - Textures.bsa", "Fallout - Textures2.bsa", "Fallout - Meshes.bsa", + "Fallout - Voices1.bsa", "Fallout - Sound.bsa", "Fallout - Misc.bsa"}; +} + +QStringList FalloutTTWDataArchives::archives(const MOBase::IProfile* profile) const +{ + QStringList result; + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("fallout.ini") + : localGameDirectory().absoluteFilePath("fallout.ini"); + result.append(getArchivesFromKey(iniFile, "SArchiveList")); + + return result; +} + +void FalloutTTWDataArchives::writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) +{ + QString list = before.join(", "); + + QString iniFile = profile->localSettingsEnabled() + ? QDir(profile->absolutePath()).absoluteFilePath("fallout.ini") + : localGameDirectory().absoluteFilePath("fallout.ini"); + setArchivesToKey(iniFile, "SArchiveList", list); +} diff --git a/libs/game_bethesda/src/games/ttw/falloutttwsavegame.cpp b/libs/game_bethesda/src/games/ttw/falloutttwsavegame.cpp new file mode 100644 index 0000000..9f41082 --- /dev/null +++ b/libs/game_bethesda/src/games/ttw/falloutttwsavegame.cpp @@ -0,0 +1,79 @@ +#include "falloutttwsavegame.h" + +#include "gamefalloutttw.h" + +FalloutTTWSaveGame::FalloutTTWSaveGame(QString const& fileName, + GameFalloutTTW const* game) + : GamebryoSaveGame(fileName, game) +{ + FileWrapper file(getFilepath(), "FO3SAVEGAME"); + uint32_t width, height; + fetchInformationFields(file, width, height, m_SaveNumber, m_PCName, m_PCLevel, + m_PCLocation); +} + +void FalloutTTWSaveGame::fetchInformationFields(FileWrapper& file, uint32_t& width, + uint32_t& height, + uint32_t& saveNumber, + QString& playerName, + unsigned short& playerLevel, + QString& playerLocation) const +{ + file.skip(); // Save header size + + file.skip(); // File version? + file.skip(); // Delimiter + + // A huge wodge of text with no length but a delimiter. Given the null bytes + // in it I presume it's fixed length (64 bytes + delim) but I have no + // definite spec + for (unsigned char ignore = 0; ignore != 0x7c;) { + file.read(ignore); // unknown + } + + file.setHasFieldMarkers(true); + file.setPluginString(GamebryoSaveGame::StringType::TYPE_BZSTRING); + + file.read(width); + file.read(height); + file.read(saveNumber); + file.read(playerName); + + QString whatthis; + file.read(whatthis); + + long level; + file.read(level); + playerLevel = level; + file.read(playerLocation); +} + +std::unique_ptr +FalloutTTWSaveGame::fetchDataFields() const +{ + FileWrapper file(getFilepath(), "FO3SAVEGAME"); + + std::unique_ptr fields = std::make_unique(); + + uint32_t width, height; + { + QString dummyName, dummyLocation; + unsigned short dummyLevel; + uint32_t dummySaveNumber; + + fetchInformationFields(file, width, height, dummySaveNumber, dummyName, dummyLevel, + dummyLocation); + } + + QString playtime; + file.read(playtime); + + fields->Screenshot = file.readImage(width, height, 256); + + file.skip(5); // unknown (1 byte), plugin size (4 bytes) + + file.setPluginString(GamebryoSaveGame::StringType::TYPE_BSTRING); + fields->Plugins = file.readPlugins(); + + return fields; +} diff --git a/libs/game_bethesda/src/games/ttw/falloutttwscriptextender.cpp b/libs/game_bethesda/src/games/ttw/falloutttwscriptextender.cpp new file mode 100644 index 0000000..ab9474e --- /dev/null +++ b/libs/game_bethesda/src/games/ttw/falloutttwscriptextender.cpp @@ -0,0 +1,18 @@ +#include "falloutttwscriptextender.h" + +#include +#include + +FalloutTTWScriptExtender::FalloutTTWScriptExtender(GameGamebryo const* game) + : GamebryoScriptExtender(game) +{} + +QString FalloutTTWScriptExtender::BinaryName() const +{ + return "nvse_loader.exe"; +} + +QString FalloutTTWScriptExtender::PluginPath() const +{ + return "nvse/plugins"; +} diff --git a/libs/game_bethesda/src/games/ttw/gamefalloutttw.cpp b/libs/game_bethesda/src/games/ttw/gamefalloutttw.cpp new file mode 100644 index 0000000..34eb1b2 --- /dev/null +++ b/libs/game_bethesda/src/games/ttw/gamefalloutttw.cpp @@ -0,0 +1,364 @@ +#include "gamefalloutttw.h" + +#include "falloutttwbsainvalidation.h" +#include "falloutttwdataarchives.h" +#include "falloutttwmoddatachecker.h" +#include "falloutttwmoddatacontent.h" +#include "falloutttwsavegame.h" +#include "falloutttwscriptextender.h" + +#include "executableinfo.h" +#include "pluginsetting.h" +#include "versioninfo.h" +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace MOBase; + +GameFalloutTTW::GameFalloutTTW() {} + +bool GameFalloutTTW::init(IOrganizer* moInfo) +{ + if (!GameGamebryo::init(moInfo)) { + return false; + } + + auto dataArchives = std::make_shared(this); + registerFeature(std::make_shared(this)); + registerFeature(dataArchives); + registerFeature( + std::make_shared(dataArchives.get(), this)); + registerFeature(std::make_shared(this)); + registerFeature(std::make_shared(this, "fallout.ini")); + registerFeature(std::make_shared(this)); + registerFeature( + std::make_shared(m_Organizer->gameFeatures())); + registerFeature(std::make_shared(moInfo)); + registerFeature(std::make_shared(this)); + + return true; +} + +void GameFalloutTTW::setVariant(QString variant) +{ + m_GameVariant = variant; +} + +void GameFalloutTTW::checkVariants() +{ + QFileInfo gog_dll(m_GamePath + "\\Galaxy.dll"); + QFileInfo epic_dll(m_GamePath + "\\EOSSDK-Win32-Shipping.dll"); + if (gog_dll.exists()) + setVariant("GOG"); + else if (epic_dll.exists()) + setVariant("Epic Games"); + else + setVariant("Steam"); +} + +QDir GameFalloutTTW::documentsDirectory() const +{ + return m_MyGamesPath; +} + +QString GameFalloutTTW::identifyGamePath() const +{ + QString result; +#ifdef _WIN32 + QString path = "Software\\Bethesda Softworks\\FalloutNV"; + result = findInRegistry(HKEY_LOCAL_MACHINE, path.toStdWString().c_str(), + L"Installed Path"); +#else + result = GameGamebryo::identifyGamePath(); +#endif + // EPIC Game Store + if (result.isEmpty()) { + /** + * Basegame: 5daeb974a22a435988892319b3a4f476 + * Dead Money: b290229eb58045cbab9501640f3278f3 + * Honest Hearts: 562d4a2c1b3147b089a7c453e3ddbcbe + * Old World Blues: c8dae1ab0570475a8b38a9041e614840 + * Lonesome Road: 4fa3d8d9b2cb4714a19a38d1a598be8f + * Gun Runners' Arsenal: 7dcfb9cd9d134728b2646466c34c7b3b + * Courier's Stash: ee9a44b4530942499ef1c8c390731fce + */ + result = parseEpicGamesLocation({"5daeb974a22a435988892319b3a4f476"}); + if (QFileInfo(result).isDir()) { + QDir startPath = QDir(result); + auto subDirs = startPath.entryList({"Fallout New Vegas*"}, + QDir::Dirs | QDir::NoDotAndDotDot); + if (!subDirs.isEmpty()) + result += "/" + subDirs.first(); + } + } + return result; +} + +void GameFalloutTTW::setGamePath(const QString& path) +{ + m_GamePath = path; + checkVariants(); + m_MyGamesPath = determineMyGamesPath(gameDirectoryName()); +} + +QDir GameFalloutTTW::savesDirectory() const +{ + return QDir(m_MyGamesPath + "/Saves"); +} + +QString GameFalloutTTW::myGamesPath() const +{ + return m_MyGamesPath; +} + +bool GameFalloutTTW::isInstalled() const +{ + return !m_GamePath.isEmpty(); +} + +QString GameFalloutTTW::gameName() const +{ + return "TTW"; +} + +QString GameFalloutTTW::displayGameName() const +{ + return "Tale of Two Wastelands"; +} + +QString GameFalloutTTW::gameDirectoryName() const +{ + if (selectedVariant() == "Epic Games") + return "FalloutNV_Epic"; + else + return "FalloutNV"; +} + +void GameFalloutTTW::detectGame() +{ + m_GamePath = identifyGamePath(); + checkVariants(); + m_MyGamesPath = determineMyGamesPath(gameDirectoryName()); +} + +QList GameFalloutTTW::executables() const +{ + ExecutableInfo game("Tale of Two Wastelands", findInGameFolder(binaryName())); + ExecutableInfo launcher("Fallout Launcher", findInGameFolder(getLauncherName())); + QList extraExecutables = + QList() << ExecutableInfo("GECK", findInGameFolder("geck.exe")) + << ExecutableInfo("LOOT", QFileInfo(getLootPath())) + .withArgument("--game=\"FalloutNV\""); + if (selectedVariant() != "Epic Games") { + extraExecutables.prepend(ExecutableInfo( + "NVSE", findInGameFolder(m_Organizer->gameFeatures() + ->gameFeature() + ->loaderName()))); + } else { + game.withArgument("-EpicPortal"); + launcher.withArgument("-EpicPortal"); + } + QList executables = {game, launcher}; + executables += extraExecutables; + return executables; +} + +QList GameFalloutTTW::executableForcedLoads() const +{ + return QList(); +} + +QString GameFalloutTTW::name() const +{ + return "Fallout TTW Support Plugin"; +} + +QString GameFalloutTTW::localizedName() const +{ + return tr("Fallout TTW Support Plugin"); +} + +QString GameFalloutTTW::author() const +{ + return "SuperSandro2000 & MO2 Team"; +} + +QString GameFalloutTTW::description() const +{ + return tr("Adds support for the game Fallout TTW"); +} + +MOBase::VersionInfo GameFalloutTTW::version() const +{ + return VersionInfo(1, 6, 0, VersionInfo::RELEASE_FINAL); +} + +QList GameFalloutTTW::settings() const +{ + return QList() + << PluginSetting("enable_loot_sorting", + tr("While not recommended by the TTW modding community, " + "enables LOOT sorting"), + false); +} + +void GameFalloutTTW::initializeProfile(const QDir& path, ProfileSettings settings) const +{ + if (settings.testFlag(IPluginGame::MODS)) { + copyToProfile(localAppFolder() + "/" + gameDirectoryName(), path, "plugins.txt"); + } + + if (settings.testFlag(IPluginGame::CONFIGURATION)) { + if (settings.testFlag(IPluginGame::PREFER_DEFAULTS) || + !QFileInfo(myGamesPath() + "/fallout.ini").exists()) { + copyToProfile(gameDirectory().absolutePath(), path, "fallout_default.ini", + "fallout.ini"); + } else { + copyToProfile(myGamesPath(), path, "fallout.ini"); + } + + copyToProfile(myGamesPath(), path, "falloutprefs.ini"); + copyToProfile(myGamesPath(), path, "falloutcustom.ini"); + copyToProfile(myGamesPath(), path, "GECKCustom.ini"); + copyToProfile(myGamesPath(), path, "GECKPrefs.ini"); + } +} + +QString GameFalloutTTW::savegameExtension() const +{ + return "fos"; +} + +QString GameFalloutTTW::savegameSEExtension() const +{ + return "nvse"; +} + +std::shared_ptr +GameFalloutTTW::makeSaveGame(QString filePath) const +{ + return std::make_shared(filePath, this); +} + +QString GameFalloutTTW::steamAPPId() const +{ + if (selectedVariant() == "Steam") { + if (m_GamePath.endsWith("enplczru")) { + return "22490"; + } else { + return "22380"; + } + } + return QString(); +} + +QStringList GameFalloutTTW::primaryPlugins() const +{ + return {"falloutnv.esm", "deadmoney.esm", "honesthearts.esm", + "oldworldblues.esm", "lonesomeroad.esm", "gunrunnersarsenal.esm", + "fallout3.esm", "anchorage.esm", "thepitt.esm", + "brokensteel.esm", "pointlookout.esm", "zeta.esm", + "caravanpack.esm", "classicpack.esm", "mercenarypack.esm", + "tribalpack.esm", "taleoftwowastelands.esm", "YUPTTW.esm"}; +} + +QStringList GameFalloutTTW::gameVariants() const +{ + return {"Steam", "GOG", "Epic Games"}; +} + +QString GameFalloutTTW::binaryName() const +{ + return "FalloutNV.exe"; +} + +QString GameFalloutTTW::gameShortName() const +{ + return "TTW"; +} + +QStringList GameFalloutTTW::primarySources() const +{ + return {"FalloutNV"}; +} + +QStringList GameFalloutTTW::validShortNames() const +{ + return {"FalloutNV", "Fallout3"}; +} + +QString GameFalloutTTW::gameNexusName() const +{ + return ""; +} + +QStringList GameFalloutTTW::iniFiles() const +{ + return {"fallout.ini", "falloutprefs.ini", "falloutcustom.ini", "GECKCustom.ini", + "GECKPrefs.ini"}; +} + +QStringList GameFalloutTTW::DLCPlugins() const +{ + return {}; +} + +MOBase::IPluginGame::SortMechanism GameFalloutTTW::sortMechanism() const +{ + if (m_Organizer->pluginSetting(name(), "enable_loot_sorting").toBool()) + return IPluginGame::SortMechanism::LOOT; + return IPluginGame::SortMechanism::NONE; +} + +QString GameFalloutTTW::lootGameName() const +{ + return "FalloutNV"; +} + +int GameFalloutTTW::nexusModOrganizerID() const +{ + return 0; +} + +int GameFalloutTTW::nexusGameID() const +{ + return 0; +} + +QDir GameFalloutTTW::gameDirectory() const +{ + return QDir(m_GamePath); +} + +QString GameFalloutTTW::getLauncherName() const +{ + return "FalloutNVLauncher.exe"; +} + +MappingType GameFalloutTTW::mappings() const +{ + MappingType result; + + for (const QString& profileFile : {"plugins.txt", "loadorder.txt"}) { + result.push_back({m_Organizer->profilePath() + "/" + profileFile, + localAppFolder() + "/FalloutNV/" + profileFile, false}); + if (selectedVariant() == "Epic Games") { + result.push_back( + {m_Organizer->profilePath() + "/" + profileFile, + localAppFolder() + "/" + gameDirectoryName() + "/" + profileFile, false}); + } + } + return result; +} -- cgit v1.3.1