Skip to content

Commit 7acd594

Browse files
rfortiermiredirex
andauthored
Fix/engine fixes v7 support (#816)
* Update to EngineFixes support to handle both v6 and v7 series. TL;DR: completely new config format for EF7 requires 2 configs for 2 config-checkers. Vastly more general approach to initialization; now, all early-stage IAT hooks are preserved. Revisiting this code, I also figured out how to enable skse_plugin_preloader (d3dx11_42.dll proxy) the right way, by enabling this very early-load, early-run-DllMain() to successfully set a hook in the IAT that Skyrim uses but STR doesn't load. That means the preloader runs its native behavior. It will work with ANY mod that uses a preloader, including SSE EngineFixes. Any mod that sets early IAT hooks will now work too (well, more than it did). Prior hack to initialize EngineFixes is deleted. Fixed some comment / clang-format damage. * Changes to repair damage that EngineFixes allocator does to STR hooks (if present) * Merge better hook damage check / repair from Miredirex * Simplify `IsFormAllocateReplacedByEF` and `RehookFormAllocate` * Update DebugService.cpp: will move SWP_NOACTIVATE to a separate pr --------- Co-authored-by: Richard Fortier <[email protected]> Co-authored-by: Daniil Zakharov <[email protected]>
1 parent d7797cd commit 7acd594

File tree

4 files changed

+127
-46
lines changed

4 files changed

+127
-46
lines changed

Code/client/Games/Memory.cpp

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,39 @@ void Memory::Free(void* apData) noexcept
6767
TiltedPhoques::ThisCall(RealFormFree, GameHeap::Get(), apData, false);
6868
}
6969

70+
static bool IsFormAllocateReplacedByEF(TFormAllocate** appOutEngineFixesAlloc) noexcept
71+
{
72+
POINTER_SKYRIMSE(TFormAllocate, s_formAllocate, 68115);
73+
TFormAllocate* pFormAllocate = s_formAllocate.Get();
74+
75+
auto opcodeBytes = reinterpret_cast<uint16_t*>(*pFormAllocate);
76+
uint8_t shift = 0;
77+
78+
if (*opcodeBytes == 0x25FF) // 'jmp' opcode 'FF 25' and the 4-byte displacement bytes come before the virtual address we're after
79+
shift = 6;
80+
else if (*opcodeBytes == 0xB848) // 'mov' opcode '48 B8' comes before the virtual address we're after
81+
shift = 2;
82+
83+
auto possibleEfAllocAddress = *reinterpret_cast<uintptr_t*>(reinterpret_cast<uint8_t*>(*pFormAllocate) + shift);
84+
85+
MEMORY_BASIC_INFORMATION mbi;
86+
if (VirtualQuery((void*)possibleEfAllocAddress, &mbi, sizeof(mbi)) != 0)
87+
{
88+
if (mbi.AllocationBase == GetModuleHandleW(L"EngineFixes.dll"))
89+
{
90+
*appOutEngineFixesAlloc = reinterpret_cast<TFormAllocate*>(possibleEfAllocAddress);
91+
return true;
92+
}
93+
}
94+
return false;
95+
}
96+
97+
static void RehookFormAllocate(TFormAllocate* apEngineFixesAllocate) noexcept
98+
{
99+
RealFormAllocate = apEngineFixesAllocate;
100+
TP_HOOK_IMMEDIATE(&RealFormAllocate, HookFormAllocate);
101+
}
102+
70103
size_t Hook_msize(void* apData)
71104
{
72105
return mi_malloc_size(apData);
@@ -132,4 +165,26 @@ static TiltedPhoques::Initializer s_memoryHooks(
132165
TP_HOOK(&RealFormAllocate, HookFormAllocate);
133166
});
134167

168+
using T_initterm_e = decltype(&_initterm_e);
169+
T_initterm_e Real_initterm_e = nullptr;
170+
171+
// If EngineFixes loaded, and it changed our FormAllocate hook,
172+
// reset it. Our hook works just fine chaining to theirs.
173+
int __cdecl Hook_initterm_e(_PIFV* apFirst, _PIFV* apLast)
174+
{
175+
// We want to run last, so pre-chain.
176+
auto retval = Real_initterm_e(apFirst, apLast);
177+
178+
// Check if EngineFixes messed with STR's modified alloc hook; if it did, treat EF as truth and rehook
179+
TFormAllocate* pEngineFixesAllocate = nullptr;
180+
if (GetModuleHandleW(L"EngineFixes.dll") && IsFormAllocateReplacedByEF(&pEngineFixesAllocate))
181+
RehookFormAllocate(pEngineFixesAllocate);
182+
183+
return retval;
184+
}
185+
186+
void HookFormAllocateSentinelInit()
187+
{
188+
TP_HOOK_IAT(_initterm_e, "api-ms-win-crt-runtime-l1-1-0.dll");
189+
}
135190
#pragma optimize("", on)

Code/client/ScriptExtender.cpp

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -141,13 +141,6 @@ void LoadScriptExender()
141141
"logs from the "
142142
"Script Extender and its loaded mods.",
143143
skseVersion);
144-
auto hModule = LoadLibraryW((gameDir / L"Data\\SKSE\\Plugins\\EngineFixes.dll").c_str());
145-
if (hModule != NULL)
146-
{
147-
auto* pStartEngineFixes = reinterpret_cast<void (*)()>(GetProcAddress(hModule, "Initialize"));
148-
if (pStartEngineFixes)
149-
(*pStartEngineFixes)();
150-
}
151144
pStartSKSE();
152145
spdlog::info("SKSE is active");
153146
}

Code/immersive_launcher/loader/ExeLoader.cpp

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -302,19 +302,21 @@ bool ExeLoader::Load(const uint8_t* apProgramBuffer)
302302
auto sourceDebugDir = sourceNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_DEBUG];
303303

304304
LoadSections(ntHeader);
305+
306+
// skse64_plugin_preloader (proxy d3dx9_42_dll and others?) may hook
307+
// _initterm_e during LoadImports(), so we have to ensure that IAT entry exists.
308+
// The simplest way to make sure all SkyrimSE IAT entries exist when mods expect
309+
// them to is to switch to those headers earlier than we used to
310+
DWORD oldProtect;
311+
VirtualProtect(sourceNtHeader, 0x1000, PAGE_EXECUTE_READWRITE, &oldProtect);
312+
sourceNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT] = ntHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
313+
305314
LoadImports(ntHeader);
306315
#if defined(_M_AMD64)
307316
LoadExceptionTable(ntHeader);
308317
LoadTLS(ntHeader, sourceNtHeader);
309318
#endif
310319

311-
// copy over the offset to the new imports directory
312-
DWORD oldProtect;
313-
VirtualProtect(sourceNtHeader, 0x1000, PAGE_EXECUTE_READWRITE, &oldProtect);
314-
315-
// re-target the import directory to the target's; ours isn't needed anymore.
316-
sourceNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT] = ntHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
317-
318320
const size_t ntCompleteHeaderSize = sizeof(IMAGE_NT_HEADERS) + (ntHeader->FileHeader.NumberOfSections * (sizeof(IMAGE_SECTION_HEADER)));
319321

320322
// overwrite our headers with the target headers
@@ -325,7 +327,11 @@ bool ExeLoader::Load(const uint8_t* apProgramBuffer)
325327
sourceNtHeader->OptionalHeader.CheckSum = sourceChecksum;
326328
sourceNtHeader->FileHeader.TimeDateStamp = sourceTimestamp;
327329
sourceNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_DEBUG] = sourceDebugDir;
328-
329330
m_pBinary = nullptr;
331+
332+
// Set a hook to check if anything loaded messes with critical hooks.
333+
extern void HookFormAllocateSentinelInit();
334+
HookFormAllocateSentinelInit();
335+
330336
return true;
331337
}

Code/immersive_launcher/stubs/DllBlocklist.cpp

Lines changed: 58 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ struct DllGreyEntry
3434
{
3535
const wchar_t* m_dllName; // The name of the DLL to check.
3636
const wchar_t* m_configLocation; // The relative path of the config file (from the game dir)
37+
const char* m_configValidator; // Regex to check if config is sane. Helps distinguish multiple releases of same dll.
3738
const char* m_sigRegex; // If this pattern is found in the config, comments were already added.
3839
const wchar_t* m_prompt; // The MessageBox prompt asking for permission to fix.
3940
const char* m_sigToInsert; // This is added at the top of the rewritten config. It can/should say more than just the pattern match signature.
@@ -46,36 +47,46 @@ struct DllGreyEntry
4647
const DllGreyEntry kDllGreyList[] =
4748
{
4849
{
49-
L"EngineFixes.dll",
50+
L"EngineFixes.dll",
5051
L"Data\\SKSE\\Plugins\\EngineFixes.toml",
51-
"# SKYRIM TOGETHER REBORN marker for EngineFixes required compatibility settings v2, DO NOT CHANGE THIS LINE",
52-
52+
"^VerboseLogging\\s*=", // Matches EngineFixes Release 6.x series.
53+
"# SKYRIM TOGETHER REBORN marker for EngineFixes required compatibility settings v2, DO NOT CHANGE THIS LINE",
54+
5355
L"For EngineFixes to work with Skyrim Together Reborn, some settings are required:\n"
54-
"\tMemoryManager = false\n"
55-
"\tScaleformAllocator = false\n"
56-
"\tMaxStdio = 8192\n\n"
57-
58-
"OK:\tMakes the changes for you\n"
59-
"Cancel:\tEngineFixes will not load\n\n"
60-
61-
"If later you get the (harmless) SrtCrashFix64 popup, manually make this EngineFixes configuration change to suppress it:\n"
62-
"\tAnimationLoadSignedCrash = false",
63-
64-
"# SKYRIM TOGETHER REBORN marker for EngineFixes required compatibility settings v2, DO NOT CHANGE THIS LINE\n"
65-
"# For EngineFixes to work with Skyrim Together Reborn, some settings are required:\n"
66-
"# MemoryManager = false\n"
67-
"# ScaleformAllocator = false\n"
68-
"# MaxStdio = 8192\n"
69-
"#\n"
70-
71-
"# If you get a SrtCrashFix64 popup, it is because you've loaded a mod like Animation Limit Crash Fixe SSE\n"
72-
"# that is doing the same thing as EngineFixes. Manually set\n"
73-
"# AnimationLoadSignedCrash = false\n"
74-
"# to eliminate the annoying popup.\n\n",
75-
76-
"(^\\s*MemoryManager\\s*=\\s*)(true ?|false)\n$1false\n"
77-
"(^\\s*ScaleformAllocator\\s*=\\s*)(true ?|false)\n$1false\n"
78-
"(^\\s*MaxStdio\\s*=\\s*)([0-9]+)\n$018192\n" // Only huge builds need this many files, but make EF match what STR sets.
56+
"\tMemoryManager = false\n"
57+
"\tScaleformAllocator = false\n"
58+
"\tMaxStdio = 8192\n\n"
59+
60+
"OK:\tMakes the changes for you\n"
61+
"Cancel:\tEngineFixes will not load\n\n"
62+
63+
"If later you get the (harmless) SrtCrashFix64 popup, manually make this EngineFixes configuration change to suppress it:\n"
64+
"\tAnimationLoadSignedCrash = false",
65+
66+
"# SKYRIM TOGETHER REBORN marker for EngineFixes required compatibility settings v2, DO NOT CHANGE THIS LINE\n"
67+
"# For EngineFixes to work with Skyrim Together Reborn, some settings are required:\n"
68+
"# MemoryManager = false\n"
69+
"# ScaleformAllocator = false\n"
70+
"# MaxStdio = 8192\n"
71+
"#\n"
72+
73+
"# If you get a SrtCrashFix64 popup, it is because you've loaded a mod like Animation Limit Crash Fixe SSE\n"
74+
"# that is doing the same thing as EngineFixes. Manually set\n"
75+
"# AnimationLoadSignedCrash = false\n"
76+
"# to eliminate the annoying popup.\n\n",
77+
78+
"(^\\s*MemoryManager\\s*=\\s*)(true ?|false)\n$1false\n"
79+
"(^\\s*ScaleformAllocator\\s*=\\s*)(true ?|false)\n$1false\n"
80+
"(^\\s*MaxStdio\\s*=\\s*)([0-9]+)\n$018192\n" // Only huge builds need this many files, but make EF match what STR sets.
81+
},
82+
{
83+
L"EngineFixes.dll",
84+
L"Data\\SKSE\\Plugins\\EngineFixes.toml",
85+
"^bVerboseLogging\\s*=", // Matches EngineFixes Release 7.x series.
86+
"", // No sig regex
87+
L"", // No prompt
88+
"", // No sig to insert
89+
nullptr // We need to check for EF7 vs. EF6, but don't need any changes for EF7.
7990
}
8091
};
8192

@@ -111,20 +122,32 @@ GreyListDisposition IsConfigOK(const std::filesystem::path& aPath, const DllGrey
111122
Die(msg.c_str(), true);
112123
return kGreyListAbort;
113124
}
125+
114126
// Rewind config stream and prepare to generate newConfig;
115127
configStream.clear(); // Clear any EOF flags
116128
configStream.seekg(0, std::ios::beg);
117129
std::string newConfig;
118130

119-
// std::regex throws exceeptions
120131
try
121-
{
132+
{ // std::regex constructors can throw exceptions, so must be used inside try/catch
133+
// subblock to throw away the temps
134+
{
135+
const std::regex validatorRegex(aEntry.m_configValidator, std::regex_constants::icase);
136+
const std::string configStr = configStream.str();
137+
std::smatch vmatch;
138+
if (!std::regex_search(configStr.begin(), configStr.end(), vmatch, validatorRegex))
139+
return kGreyListAbort;
140+
}
141+
122142
// This has to be done inside the try/catch, but will be used outside at the end.
123143
signatureRegex = std::regex(aEntry.m_sigRegex, std::regex_constants::icase);
124144

125145
// Iterate over each line of the config file
126146
// Iterate over each replacer, possibly changing the line
127147
// Output upddated line to new config
148+
if (!aEntry.m_replacers)
149+
return kGreyListAccept; // Must be good if nothing to change.
150+
128151
std::string line;
129152
std::stringstream replacers(aEntry.m_replacers);
130153

@@ -207,7 +230,11 @@ enum GreyListDisposition IsDllGreyListBlocked(const std::wstring_view aDllName)
207230
if (std::wcscmp(aDllName.data(), greyListEntry.m_dllName) == 0)
208231
{
209232
// DLL name matches, read in entire config file.
210-
return IsConfigOK(gamePath, greyListEntry);
233+
// Might be multiple configs for differing versions,
234+
// so only exit iteration early if config accepted.
235+
retval = IsConfigOK(gamePath, greyListEntry);
236+
if (retval == kGreyListAccept)
237+
break;
211238
}
212239
}
213240
return retval;

0 commit comments

Comments
 (0)