diff options
| author | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-02-11 02:37:39 -0600 |
|---|---|---|
| committer | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-02-11 02:37:39 -0600 |
| commit | 7ee008e150bc5bcf76082d726f719ee0fdfda982 (patch) | |
| tree | 27fb39be241fdb5ac2734c574de678977d1856d0 /libs/game_bethesda/src/games/skyrimse | |
Fluorine Manager: full Linux port of Mod Organizer 2
Complete native Linux port with FUSE-based virtual filesystem,
Proton/umu-run integration, and Flatpak packaging.
Key features:
- FUSE VFS replacing Windows USVFS (in-process + standalone helper for Flatpak)
- Proton/GE-Proton/umu-run launcher with env var forwarding
- Flatpak support (sandbox-aware VFS, NXM handler, umu-run)
- Wine prefix management UI
- Case-insensitive path resolution for Linux filesystems
- QSettings-safe INI handling (avoids Bethesda INI corruption)
- Portable instance support with auto-generated launcher scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'libs/game_bethesda/src/games/skyrimse')
14 files changed, 881 insertions, 0 deletions
diff --git a/libs/game_bethesda/src/games/skyrimse/CMakeLists.txt b/libs/game_bethesda/src/games/skyrimse/CMakeLists.txt new file mode 100644 index 0000000..2ec2b79 --- /dev/null +++ b/libs/game_bethesda/src/games/skyrimse/CMakeLists.txt @@ -0,0 +1,36 @@ +cmake_minimum_required(VERSION 3.16) + +find_package(Qt6 COMPONENTS Widgets REQUIRED) + +set(CMAKE_AUTOMOC ON) + +add_library(game_skyrimse SHARED + gameskyrimse.cpp + gameskyrimse.h + skyrimsedataarchives.cpp + skyrimsedataarchives.h + skyrimsemoddatachecker.h + skyrimsemoddatacontent.h + skyrimsesavegame.cpp + skyrimsesavegame.h + skyrimsescriptextender.cpp + skyrimsescriptextender.h + skyrimseunmanagedmods.cpp + skyrimseunmanagedmods.h +) + +target_link_libraries(game_skyrimse PRIVATE game_creation) + +# Keep uibase bare includes after system headers to avoid strings.h collision. +target_compile_options(game_skyrimse PRIVATE + "-idirafter" "${CMAKE_SOURCE_DIR}/libs/uibase/include/uibase" +) + +set_property(TARGET game_skyrimse APPEND PROPERTY AUTOMOC_MOC_OPTIONS + "-I${CMAKE_SOURCE_DIR}/libs/uibase/include/uibase" +) + +# Install the .so plugin next to the organizer binary +install(TARGETS game_skyrimse + LIBRARY DESTINATION plugins +) diff --git a/libs/game_bethesda/src/games/skyrimse/game_skyrimse_en.ts b/libs/game_bethesda/src/games/skyrimse/game_skyrimse_en.ts new file mode 100644 index 0000000..53d72ca --- /dev/null +++ b/libs/game_bethesda/src/games/skyrimse/game_skyrimse_en.ts @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="utf-8"?> +<!DOCTYPE TS> +<TS version="2.1" language="en_US"> +<context> + <name>GameSkyrimSE</name> + <message> + <location filename="gameskyrimse.cpp" line="180"/> + <source>Skyrim Special Edition Support Plugin</source> + <translation type="unfinished"></translation> + </message> + <message> + <location filename="gameskyrimse.cpp" line="190"/> + <source>Adds support for the game Skyrim Special Edition.</source> + <translation type="unfinished"></translation> + </message> +</context> +</TS> 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 <creationgameplugins.h> +#include <executableinfo.h> +#include <gamebryolocalsavegames.h> +#include <gamebryosavegameinfo.h> +#include <pluginsetting.h> +#include <utility.h> + +#include <QCoreApplication> +#include <QDir> +#include <QDirIterator> +#include <QFileInfo> +#include <QJsonDocument> +#include <QList> +#include <QObject> +#include <QString> +#include <QStringList> + +#include "scopeguard.h" +#include <memory> + +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<QString, QString> 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<SkyrimSEScriptExtender>(this)); + registerFeature(std::make_shared<SkyrimSEDataArchives>(this)); + registerFeature(std::make_shared<GamebryoLocalSavegames>(this, "Skyrimcustom.ini")); + registerFeature(std::make_shared<SkyrimSEModDataChecker>(this)); + registerFeature( + std::make_shared<SkyrimSEModDataContent>(m_Organizer->gameFeatures())); + registerFeature(std::make_shared<GamebryoSaveGameInfo>(this)); + registerFeature(std::make_shared<CreationGamePlugins>(moInfo)); + registerFeature(std::make_shared<SkyrimSEUnmangedMods>(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<ExecutableInfo> GameSkyrimSE::executables() const +{ + return QList<ExecutableInfo>() + << ExecutableInfo("SKSE", + findInGameFolder(m_Organizer->gameFeatures() + ->gameFeature<MOBase::ScriptExtender>() + ->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<ExecutableForcedLoadSetting> GameSkyrimSE::executableForcedLoads() const +{ + return QList<ExecutableForcedLoadSetting>(); +} + +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<PluginSetting> 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<const GamebryoSaveGame> +GameSkyrimSE::makeSaveGame(QString filePath) const +{ + return std::make_shared<const SkyrimSESaveGame>(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<QString> 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/gameskyrimse.h b/libs/game_bethesda/src/games/skyrimse/gameskyrimse.h new file mode 100644 index 0000000..5296464 --- /dev/null +++ b/libs/game_bethesda/src/games/skyrimse/gameskyrimse.h @@ -0,0 +1,72 @@ +#ifndef _GAMESKYRIMSE_H +#define _GAMESKYRIMSE_H + +#include "gamegamebryo.h" + +#include <QObject> +#include <QtGlobal> + +class GameSkyrimSE : public GameGamebryo +{ + Q_OBJECT + + Q_PLUGIN_METADATA(IID "com.soundcontactstudio.GameSkyrimSE") + +public: + GameSkyrimSE(); + + virtual bool init(MOBase::IOrganizer* moInfo) override; + +public: // IPluginGame interface + virtual void detectGame() override; + virtual QString gameName() const override; + + virtual QList<MOBase::ExecutableInfo> executables() const override; + virtual QList<MOBase::ExecutableForcedLoadSetting> + executableForcedLoads() const override; + virtual void initializeProfile(const QDir& path, + ProfileSettings settings) const override; + virtual QString steamAPPId() const override; + virtual QStringList primaryPlugins() const override; + virtual QStringList gameVariants() const override; + virtual QString gameShortName() const override; + virtual QString gameNexusName() const override; + virtual QStringList validShortNames() const override; + virtual QStringList iniFiles() const override; + virtual QStringList DLCPlugins() const override; + virtual QStringList CCPlugins() const override; + virtual LoadOrderMechanism loadOrderMechanism() const override; + virtual int nexusModOrganizerID() const override; + virtual int nexusGameID() const override; + + virtual bool isInstalled() const override; + virtual void setGamePath(const QString& path) override; + virtual QDir gameDirectory() const override; + +public: // IPlugin interface + virtual QString name() const override; + virtual QString localizedName() const override; + virtual QString author() const override; + virtual QString description() const override; + virtual MOBase::VersionInfo version() const override; + virtual QList<MOBase::PluginSetting> settings() const override; + virtual MappingType mappings() const override; + +protected: + std::shared_ptr<const GamebryoSaveGame> makeSaveGame(QString filePath) const override; + QString savegameExtension() const override; + QString savegameSEExtension() const override; + + QString gameDirectoryName() const; + QDir documentsDirectory() const; + QDir savesDirectory() const; + QFileInfo findInGameFolder(const QString& relativePath) const; + QString myGamesPath() const; + + void checkVariants(); + void setVariant(QString variant); + + virtual QString identifyGamePath() const override; +}; + +#endif // _GAMESKYRIMSE_H 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 <utility.h> + +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/skyrimsedataarchives.h b/libs/game_bethesda/src/games/skyrimse/skyrimsedataarchives.h new file mode 100644 index 0000000..b3d70c1 --- /dev/null +++ b/libs/game_bethesda/src/games/skyrimse/skyrimsedataarchives.h @@ -0,0 +1,25 @@ +#ifndef _SKYRIMSEDATAARCHIVES_H +#define _SKYRIMSEDATAARCHIVES_H + +#include "gamebryodataarchives.h" +#include <QDir> +#include <QStringList> + +namespace MOBase +{ +class IProfile; +} + +class SkyrimSEDataArchives : public GamebryoDataArchives +{ + using GamebryoDataArchives::GamebryoDataArchives; + + virtual QStringList vanillaArchives() const override; + virtual QStringList archives(const MOBase::IProfile* profile) const override; + +private: + virtual void writeArchiveList(MOBase::IProfile* profile, + const QStringList& before) override; +}; + +#endif // _SKYRIMSEDATAARCHIVES_H diff --git a/libs/game_bethesda/src/games/skyrimse/skyrimsemoddatachecker.h b/libs/game_bethesda/src/games/skyrimse/skyrimsemoddatachecker.h new file mode 100644 index 0000000..ebcca0f --- /dev/null +++ b/libs/game_bethesda/src/games/skyrimse/skyrimsemoddatachecker.h @@ -0,0 +1,42 @@ +#ifndef SKYRIMSE_MODATACHECKER_H +#define SKYRIMSE_MODATACHECKER_H + +#include <gamebryomoddatachecker.h> + +class SkyrimSEModDataChecker : public GamebryoModDataChecker +{ +public: + using GamebryoModDataChecker::GamebryoModDataChecker; + +protected: + virtual const FileNameSet& possibleFolderNames() const override + { + static FileNameSet result{ + + "fonts", "interface", + "menus", "meshes", + "music", "scripts", + "shaders", "sound", + "strings", "textures", + "trees", "video", + "facegen", "materials", + "skse", "distantlod", + "asi", "Tools", + "MCM", "distantland", + "mits", "dllplugins", + "CalienteTools", "NetScriptFramework", + "shadersfx", "Nemesis_Engine", + "Platform", "grass", + "LightPlacer", "mainmenuwallpapers", + "MainMenuVideo", "PBRMaterialObjects", + "PBRNifPatcher", "PBRTextureSets"}; + return result; + } + virtual const FileNameSet& possibleFileExtensions() const override + { + static FileNameSet result{"esp", "esm", "esl", "bsa", "modgroups", "ini"}; + return result; + } +}; + +#endif // SKYRIMSE_MODATACHECKER_H diff --git a/libs/game_bethesda/src/games/skyrimse/skyrimsemoddatacontent.h b/libs/game_bethesda/src/games/skyrimse/skyrimsemoddatacontent.h new file mode 100644 index 0000000..b252e94 --- /dev/null +++ b/libs/game_bethesda/src/games/skyrimse/skyrimsemoddatacontent.h @@ -0,0 +1,21 @@ +#ifndef SKYRIMSE_MODDATACONTENT_H +#define SKYRIMSE_MODDATACONTENT_H + +#include <gamebryomoddatacontent.h> +#include <ifiletree.h> + +class SkyrimSEModDataContent : public GamebryoModDataContent +{ +public: + /** + * + */ + SkyrimSEModDataContent(MOBase::IGameFeatures const* gameFeatures) + : GamebryoModDataContent(gameFeatures) + { + // Just need to disable some contents: + m_Enabled[CONTENT_SKYPROC] = false; + } +}; + +#endif // SKYRIMSE_MODDATACONTENT_H 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 <Windows.h> +#else +#include <QDateTime> + +// 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<int64_t>(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<uint16_t>(d.year()); + st->wMonth = static_cast<uint16_t>(d.month()); + st->wDayOfWeek = static_cast<uint16_t>(d.dayOfWeek() % 7); // Qt Mon=1..Sun=7 → Win Sun=0..Sat=6 + st->wDay = static_cast<uint16_t>(d.day()); + st->wHour = static_cast<uint16_t>(t.hour()); + st->wMinute = static_cast<uint16_t>(t.minute()); + st->wSecond = static_cast<uint16_t>(t.second()); + st->wMilliseconds = static_cast<uint16_t>(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<unsigned short>(temp); + file.read(playerLocation); + + QString timeOfDay; + file.read(timeOfDay); + + QString race; + file.read(race); // race name (i.e. BretonRace) + + file.skip<unsigned short>(); // Player gender (0 = male) + file.skip<float>(2); // experience gathered, experience required + + file.read(creationTime); // filetime +} + +std::unique_ptr<GamebryoSaveGame::DataFields> 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<DataFields> fields = std::make_unique<DataFields>(); + + 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/skyrimsesavegame.h b/libs/game_bethesda/src/games/skyrimse/skyrimsesavegame.h new file mode 100644 index 0000000..060b9ee --- /dev/null +++ b/libs/game_bethesda/src/games/skyrimse/skyrimsesavegame.h @@ -0,0 +1,28 @@ +#ifndef _SKYRIMSESAVEGAME_H +#define _SKYRIMSESAVEGAME_H + +#include "gamebryosavegame.h" +#include "gameskyrimse.h" +#include "windows_compat.h" + +namespace MOBase +{ +class IPluginGame; +} + +class SkyrimSESaveGame : public GamebryoSaveGame +{ +public: + SkyrimSESaveGame(QString const& fileName, GameSkyrimSE const* game); + +protected: + // Fetch easy-to-access information. + void fetchInformationFields(FileWrapper& wrapper, uint32_t& version, + QString& playerName, unsigned short& playerLevel, + QString& playerLocation, uint32_t& saveNumber, + FILETIME& creationTime) const; + + std::unique_ptr<DataFields> fetchDataFields() const override; +}; + +#endif // _SKYRIMSESAVEGAME_H 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 <QString> +#include <QStringList> + +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/skyrimsescriptextender.h b/libs/game_bethesda/src/games/skyrimse/skyrimsescriptextender.h new file mode 100644 index 0000000..9b5f78c --- /dev/null +++ b/libs/game_bethesda/src/games/skyrimse/skyrimsescriptextender.h @@ -0,0 +1,17 @@ +#ifndef _SKYRIMSESCRIPTEXTENDER_H +#define _SKYRIMSESCRIPTEXTENDER_H + +#include "gamebryoscriptextender.h" + +class GameGamebryo; + +class SkyrimSEScriptExtender : public GamebryoScriptExtender +{ +public: + SkyrimSEScriptExtender(GameGamebryo const* game); + + virtual QString BinaryName() const override; + virtual QString PluginPath() const override; +}; + +#endif // _SKYRIMSESCRIPTEXTENDER_H 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/skyrimse/skyrimseunmanagedmods.h b/libs/game_bethesda/src/games/skyrimse/skyrimseunmanagedmods.h new file mode 100644 index 0000000..27938ca --- /dev/null +++ b/libs/game_bethesda/src/games/skyrimse/skyrimseunmanagedmods.h @@ -0,0 +1,16 @@ +#ifndef _SKYRIMSEUNMANAGEDMODS_H +#define _SKYRIMSEUNMANAGEDMODS_H + +#include "gamebryounmanagedmods.h" +#include <gamegamebryo.h> + +class SkyrimSEUnmangedMods : public GamebryoUnmangedMods +{ +public: + SkyrimSEUnmangedMods(const GameGamebryo* game); + ~SkyrimSEUnmangedMods(); + + virtual QStringList mods(bool onlyOfficial) const override; +}; + +#endif // _SKYRIMSEUNMANAGEDMODS_H |
