diff --git a/source/Enderal DLL/src/CheckInvalidForms.h b/source/Enderal DLL/src/CheckInvalidForms.h index 7f8d72e3f..b45bdb6a8 100644 --- a/source/Enderal DLL/src/CheckInvalidForms.h +++ b/source/Enderal DLL/src/CheckInvalidForms.h @@ -6,7 +6,13 @@ static bool bMessageShown = false; static std::unordered_set aModNames; -inline bool DataFileExists(std::string filename, int maxSize = 1000000) +inline bool DataFileExists(std::string filename) +{ + const auto path = std::format("Data\\{}", filename); + return std::filesystem::exists(path); +} + +inline bool DLCExists(std::string filename, int maxSize = 1000000) { const auto path = std::format("Data\\{}", filename); return std::filesystem::exists(path) && std::filesystem::file_size(path) > maxSize; @@ -745,13 +751,13 @@ inline void CheckCCMods() }; for (short i = 0; i < 73; i++) { - if (DataFileExists(filenames[i], 800)) { + if (DLCExists(filenames[i], 800)) { MessageBoxW(NULL, L"Creation Club mods are incompatible with Enderal.", L"Error", MB_OK | MB_ICONERROR); exit(EXIT_FAILURE); } } - if (DataFileExists("ccBGSSSE001-Fish.esm", 1200000)) { + if (DLCExists("ccBGSSSE001-Fish.esm", 1200000)) { MessageBoxW(NULL, L"Fishing CC are incompatible with Enderal without a patch.", L"Error", MB_OK | MB_ICONERROR); exit(EXIT_FAILURE); } diff --git a/source/Enderal DLL/src/Main.cpp b/source/Enderal DLL/src/Main.cpp index a1cdcbbbf..cb1832112 100644 --- a/source/Enderal DLL/src/Main.cpp +++ b/source/Enderal DLL/src/Main.cpp @@ -158,17 +158,17 @@ SKSEPluginLoad(const LoadInterface* skse) { } } - if (DataFileExists("Dawnguard.esm") || DataFileExists("Dragonborn.esm") || DataFileExists("HearthFires.esm") || DataFileExists("Update.esm")) { + if (DLCExists("Dawnguard.esm") || DLCExists("Dragonborn.esm") || DLCExists("HearthFires.esm") || DLCExists("Update.esm")) { MessageBoxW(NULL, L"Skyrim DLCs are incompatible with Enderal.", L"Enderal SE Error", MB_OK | MB_ICONERROR); exit(EXIT_FAILURE); } - if (DataFileExists("Unofficial Skyrim Special Edition Patch.esp")) { + if (DLCExists("Unofficial Skyrim Special Edition Patch.esp")) { MessageBoxW(NULL, L"Unofficial Skyrim Special Edition Patch is incompatible with Enderal.", L"Enderal SE Error", MB_OK | MB_ICONERROR); exit(EXIT_FAILURE); } - if (!DataFileExists("Enderal - Forgotten Stories.esm")) { + if (!DLCExists("Enderal - Forgotten Stories.esm")) { MessageBoxW(NULL, L"Enderal - Forgotten Stories.esm is not loaded!", L"Enderal SE Error", MB_OK | MB_ICONERROR); exit(EXIT_FAILURE); } @@ -178,6 +178,7 @@ SKSEPluginLoad(const LoadInterface* skse) { GetLoadInterface(skse); InitializeLogging(); + EnsurePluginsTxt(); auto* plugin = PluginDeclaration::GetSingleton(); auto version = plugin->GetVersion(); diff --git a/source/Enderal DLL/src/Util.h b/source/Enderal DLL/src/Util.h index b62326e3d..13f268995 100644 --- a/source/Enderal DLL/src/Util.h +++ b/source/Enderal DLL/src/Util.h @@ -331,3 +331,186 @@ inline RE::BSFixedString StringToHex(RE::BSFixedString a_string) return sstream.str(); } + +inline std::filesystem::path GetPluginsTxtPath() +{ + wchar_t* buffer{ nullptr }; + const auto result = ::SHGetKnownFolderPath(::FOLDERID_LocalAppData, ::KNOWN_FOLDER_FLAG::KF_FLAG_DEFAULT, nullptr, std::addressof(buffer)); + std::unique_ptr knownPath(buffer, ::CoTaskMemFree); + + if (!knownPath || result != S_OK) { + return {}; + } + + std::filesystem::path path = knownPath.get(); + + bool hasRedirector = GetLoadInterface()->GetPluginInfo("Skyrim Redirector") != nullptr; + bool isGOG = GetModuleHandle(L"Galaxy64.dll") != NULL; + + if (hasRedirector) { + path /= isGOG ? "Enderal Special Edition GOG"sv : "Enderal Special Edition"sv; + } else { + path /= isGOG ? "Skyrim Special Edition GOG"sv : "Skyrim Special Edition"sv; + } + + path /= "plugins.txt"sv; + return path; +} + +// Reads plugins.txt and returns the lines +inline std::vector ReadPluginsTxt(const std::filesystem::path& pluginsPath) +{ + std::vector lines; + if (std::filesystem::exists(pluginsPath)) { + std::ifstream inFile(pluginsPath); + if (inFile.is_open()) { + std::string line; + while (std::getline(inFile, line)) { + lines.push_back(line); + } + inFile.close(); + } + } + return lines; +} + +// Writes lines to plugins.txt +inline bool WritePluginsTxt(const std::filesystem::path& pluginsPath, const std::vector& lines) +{ + std::filesystem::create_directories(pluginsPath.parent_path()); + std::ofstream outFile(pluginsPath); + if (outFile.is_open()) { + for (const auto& line : lines) { + outFile << line << "\n"; + } + outFile.close(); + return true; + } + return false; +} + +// Gets the plugin name from a line (strips asterisk if present) +inline std::string GetPluginNameFromLine(const std::string& line) +{ + if (!line.empty() && line[0] == '*') { + return line.substr(1); + } + return line; +} + +// Checks if a line is a comment or empty +inline bool IsCommentOrEmpty(const std::string& line) +{ + return line.empty() || line[0] == '#'; +} + +// Ensures a plugin is enabled in plugins.txt (adds asterisk if missing, adds to file if not present) +// Only operates if the plugin file exists in Data folder +// Returns true if plugins.txt was modified +inline bool EnsurePluginEnabled(std::vector& lines, const char* pluginName) +{ + if (!DataFileExists(pluginName)) { + return false; + } + + std::string enabledEntry = std::string("*") + pluginName; + + for (auto& line : lines) { + if (IsCommentOrEmpty(line)) { + continue; + } + + std::string name = GetPluginNameFromLine(line); + if (name == pluginName) { + if (line[0] != '*') { + logger::info("{} found but not enabled, enabling it", pluginName); + line = enabledEntry; + return true; + } + // Already enabled + return false; + } + } + + // Plugin not found in plugins.txt, add it at the end + logger::info("{} not found in plugins.txt, adding it", pluginName); + lines.push_back(enabledEntry); + return true; +} + +// Moves a plugin to the first position (after comments) +// Returns true if plugins.txt was modified +inline bool EnsurePluginFirst(std::vector& lines, const char* pluginName) +{ + int pluginIndex = -1; + int firstPluginIndex = -1; + + for (size_t i = 0; i < lines.size(); i++) { + if (IsCommentOrEmpty(lines[i])) { + continue; + } + + if (firstPluginIndex == -1) { + firstPluginIndex = static_cast(i); + } + + std::string name = GetPluginNameFromLine(lines[i]); + if (name == pluginName) { + pluginIndex = static_cast(i); + break; + } + } + + if (pluginIndex == -1) { + // Plugin not in list + return false; + } + + if (pluginIndex == firstPluginIndex) { + // Already first + return false; + } + + logger::info("{} found but not first plugin (at index {}, first plugin at {}), moving to first position", + pluginName, pluginIndex, firstPluginIndex); + + // Save the plugin line and remove it from current position + std::string pluginLine = lines[pluginIndex]; + lines.erase(lines.begin() + pluginIndex); + + // Insert at first plugin position + lines.insert(lines.begin() + firstPluginIndex, pluginLine); + + return true; +} + +inline void EnsurePluginsTxt() +{ + auto pluginsPath = GetPluginsTxtPath(); + if (pluginsPath.empty()) { + logger::error("Failed to get plugins.txt path"); + return; + } + + logger::info("Checking plugins.txt at: {}", pluginsPath.string()); + + std::vector lines = ReadPluginsTxt(pluginsPath); + bool modified = false; + + // Ensure required plugins are enabled + modified |= EnsurePluginEnabled(lines, "Enderal - Forgotten Stories.esm"); + modified |= EnsurePluginEnabled(lines, "SkyUI_SE.esp"); + + // Ensure Enderal ESM is first + modified |= EnsurePluginFirst(lines, "Enderal - Forgotten Stories.esm"); + + if (modified) { + if (WritePluginsTxt(pluginsPath, lines)) { + logger::info("Successfully updated plugins.txt"); + } else { + logger::error("Failed to write plugins.txt"); + } + } else { + logger::info("plugins.txt is correctly configured"); + } +}