Add Enderal plugins to plugins.txt array in runtime

This commit is contained in:
Eddoursul 2026-01-18 22:05:45 +01:00
parent 6917fd712c
commit 4379f11539
3 changed files with 94 additions and 183 deletions

View File

@ -13,6 +13,7 @@
#include "Patches/HUDMenuPatch.h" #include "Patches/HUDMenuPatch.h"
#include "Patches/ForceBorderless.h" #include "Patches/ForceBorderless.h"
#include "Patches/AttachLightHitEffectCrash.h" #include "Patches/AttachLightHitEffectCrash.h"
#include "Patches/PluginsTxtPatch.h"
using namespace SKSE; using namespace SKSE;
@ -178,7 +179,6 @@ SKSEPluginLoad(const LoadInterface* skse) {
GetLoadInterface(skse); GetLoadInterface(skse);
InitializeLogging(); InitializeLogging();
EnsurePluginsTxt();
auto* plugin = PluginDeclaration::GetSingleton(); auto* plugin = PluginDeclaration::GetSingleton();
auto version = plugin->GetVersion(); auto version = plugin->GetVersion();
@ -186,6 +186,7 @@ SKSEPluginLoad(const LoadInterface* skse) {
Init(skse); Init(skse);
InitializeMessaging(); InitializeMessaging();
PluginsTxtPatch::Install();
RE::INISettingCollection::GetSingleton()->GetSetting("sIntroSequence:General")->data.s = nullptr; RE::INISettingCollection::GetSingleton()->GetSetting("sIntroSequence:General")->data.s = nullptr;

View File

@ -0,0 +1,92 @@
#pragma once
namespace PluginsTxtPatch
{
using PluginArray = RE::BSTArray<RE::BSFixedString>;
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<decltype(thunk)> 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<void*>(target - off), 64, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (!mem) mem = VirtualAlloc(reinterpret_cast<void*>(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<std::uint8_t*>(mem);
std::memcpy(t, reinterpret_cast<void*>(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<std::uintptr_t>(t);
// Patch function entry: jump to thunk via stub
auto stub = t + PROLOG_SIZE + 14;
JmpAbs(reinterpret_cast<std::uintptr_t>(&LoadPluginsList::thunk), stub);
REL::safe_write(target, static_cast<std::uint8_t>(0xE9));
REL::safe_write(target + 1, static_cast<std::int32_t>(reinterpret_cast<std::uintptr_t>(stub) - target - 5));
logger::info("Installed PluginsTxt patch");
}
}

View File

@ -332,185 +332,3 @@ inline RE::BSFixedString StringToHex(RE::BSFixedString a_string)
return sstream.str(); 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<wchar_t[], decltype(&::CoTaskMemFree)> 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<std::string> ReadPluginsTxt(const std::filesystem::path& pluginsPath)
{
std::vector<std::string> 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<std::string>& 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<std::string>& 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<std::string>& 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<int>(i);
}
std::string name = GetPluginNameFromLine(lines[i]);
if (name == pluginName) {
pluginIndex = static_cast<int>(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<std::string> 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");
}
}