diff --git a/source/Enderal DLL/src/Main.cpp b/source/Enderal DLL/src/Main.cpp index cb1832112..259cce1f9 100644 --- a/source/Enderal DLL/src/Main.cpp +++ b/source/Enderal DLL/src/Main.cpp @@ -13,6 +13,7 @@ #include "Patches/HUDMenuPatch.h" #include "Patches/ForceBorderless.h" #include "Patches/AttachLightHitEffectCrash.h" +#include "Patches/PluginsTxtPatch.h" using namespace SKSE; @@ -178,7 +179,6 @@ SKSEPluginLoad(const LoadInterface* skse) { GetLoadInterface(skse); InitializeLogging(); - EnsurePluginsTxt(); auto* plugin = PluginDeclaration::GetSingleton(); auto version = plugin->GetVersion(); @@ -186,6 +186,7 @@ SKSEPluginLoad(const LoadInterface* skse) { Init(skse); InitializeMessaging(); + PluginsTxtPatch::Install(); RE::INISettingCollection::GetSingleton()->GetSetting("sIntroSequence:General")->data.s = nullptr; diff --git a/source/Enderal DLL/src/Patches/PluginsTxtPatch.h b/source/Enderal DLL/src/Patches/PluginsTxtPatch.h new file mode 100644 index 000000000..0af5305ec --- /dev/null +++ b/source/Enderal DLL/src/Patches/PluginsTxtPatch.h @@ -0,0 +1,92 @@ +#pragma once + +namespace PluginsTxtPatch +{ + using PluginArray = RE::BSTArray; + + inline void PrependIfMissing(PluginArray* a_array, const char* a_name) + { + for (const auto& entry : *a_array) { + if (_stricmp(entry.c_str(), a_name) == 0) { + return; + } + } + RE::BSFixedString newEntry(a_name); + a_array->push_back(a_array->empty() ? newEntry : a_array->back()); + for (std::uint32_t i = a_array->size() - 1; i > 0; --i) { + (*a_array)[i] = (*a_array)[i - 1]; + } + (*a_array)[0] = newEntry; + } + + struct LoadPluginsList + { + static std::int64_t thunk(PluginArray* a_array, bool a_enabledOnly) + { + auto result = func(a_array, a_enabledOnly); + if (!a_array) { + return result; + } + + const char* plugins[] = { + "SkyUI_SE.esp", + "Enderal - Forgotten Stories.esm" // Last = highest priority (index 0) + }; + + for (const auto* plugin : plugins) { + if (std::filesystem::exists(std::format("Data\\{}", plugin))) { + PrependIfMissing(a_array, plugin); + } + } + + return result; + } + static inline REL::Relocation func; + }; + + inline void Install() + { + // Hook at function entry to catch all callers + // SE ID 13650: 1.5.97=0x16FEA0 + // AE ID 13758: 1.6.1170=0x1BB410, 1.6.1179=0x1BB240 + // VR: 0x180630 (not in address library, untested) + // Prolog: 7 pushes (12 bytes) + lea rbp,[rsp+disp32] (8 bytes) = 20 bytes + // if (REL::Module::IsVR()) { + // constexpr std::size_t PROLOG_SIZE = 20; + // auto target = REL::Module::get().base() + 0x180630; + // // TODO: copy rest of Install() logic here when VR is tested + // return; + // } + if (REL::Module::IsVR()) { + return; + } + + constexpr std::size_t PROLOG_SIZE = 20; + auto target = REL::RelocationID(13650, 13758).address(); + + // Allocate trampoline near target (within rel32 range) + void* mem = nullptr; + for (std::uintptr_t off = 0x1000; off < 0x7FFF0000 && !mem; off += 0x1000) { + mem = VirtualAlloc(reinterpret_cast(target - off), 64, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); + if (!mem) mem = VirtualAlloc(reinterpret_cast(target + off), 64, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); + } + if (!mem) return; + + // Trampoline: copy prolog + absolute jump back to original + auto t = static_cast(mem); + std::memcpy(t, reinterpret_cast(target), PROLOG_SIZE); + struct JmpAbs : Xbyak::CodeGenerator { + JmpAbs(std::uintptr_t dst, std::uint8_t* buf) : Xbyak::CodeGenerator(14, buf) { mov(rax, dst); jmp(rax); } + }; + JmpAbs(target + PROLOG_SIZE, t + PROLOG_SIZE); + LoadPluginsList::func = reinterpret_cast(t); + + // Patch function entry: jump to thunk via stub + auto stub = t + PROLOG_SIZE + 14; + JmpAbs(reinterpret_cast(&LoadPluginsList::thunk), stub); + REL::safe_write(target, static_cast(0xE9)); + REL::safe_write(target + 1, static_cast(reinterpret_cast(stub) - target - 5)); + + logger::info("Installed PluginsTxt patch"); + } +} diff --git a/source/Enderal DLL/src/Util.h b/source/Enderal DLL/src/Util.h index 13f268995..051396509 100644 --- a/source/Enderal DLL/src/Util.h +++ b/source/Enderal DLL/src/Util.h @@ -332,185 +332,3 @@ 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"); - } -}