Add Enderal plugins to plugins.txt array in runtime
This commit is contained in:
parent
6917fd712c
commit
4379f11539
@ -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;
|
||||||
|
|
||||||
|
|||||||
92
source/Enderal DLL/src/Patches/PluginsTxtPatch.h
Normal file
92
source/Enderal DLL/src/Patches/PluginsTxtPatch.h
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user