Sekiro Shadows Die Twice ModEngine Analysis (2024)

Sekiro: Shadows Die Twice is a souls-like game produced by FromSoftware with additional ACT features and I quite like it, as well as other souls-like games, namely Bloodborne, Elden Ring. Anyway, recently I’m inspired by some Streamers who tried to mod the game in order to add more fun, therefore I’m also in to analyze this ModEngine to unveil its technical details. I may also create or update some mods if I have enough time.

Prelude

Before the release of this ModEngine which exposes several quite useful APIs to help modders creating mods in an easier way, most of them have to use CheatEngine to do the scripting work. Both of them use the same technique, namely memory patch, but obviously the former one is surely more user-friendly.

Environment

I’ve done my source code analysis work on Visual Studio 2019, and the version of the ModEngine is 0.1.16 which is for Sekiro 1.06 version.

Project Structure

├── DS3ModEngine
│ ├── AOBScanner.cpp
│ ├── AOBScanner.h
│ ├── DS3ModEngine.vcxproj
│ ├── DS3ModEngine.vcxproj.filters
│ ├── Game.cpp
│ ├── Game.h
│ ├── GameplayPatcher.cpp
│ ├── GameplayPatcher.h
│ ├── HideThreadFromDebugger.cpp
│ ├── HideThreadFromDebugger.h
│ ├── ImGui
│ │ ├── imconfig.h
│ │ ├── imgui.cpp
│ │ ├── imgui.h
│ │ ├── imgui_demo.cpp
│ │ ├── imgui_draw.cpp
│ │ ├── imgui_internal.h
│ │ ├── stb_rect_pack.h
│ │ ├── stb_textedit.h
│ │ └── stb_truetype.h
│ ├── InputHook.cpp
│ ├── InputHook.h
│ ├── LooseParams.cpp
│ ├── LooseParams.h
│ ├── Menu.cpp
│ ├── Menu.h
│ ├── MinHook
│ │ ├── include
│ │ │ └── MinHook.h
│ │ └── src
│ │ ├── HDE
│ │ │ ├── hde32.c
│ │ │ ├── hde32.h
│ │ │ ├── hde64.c
│ │ │ ├── hde64.h
│ │ │ ├── pstdint.h
│ │ │ ├── table32.h
│ │ │ └── table64.h
│ │ ├── buffer.c
│ │ ├── buffer.h
│ │ ├── hook.c
│ │ ├── trampoline.c
│ │ └── trampoline.h
│ ├── ModLoader.cpp
│ ├── ModLoader.h
│ ├── NetworkBlocker.cpp
│ ├── NetworkBlocker.h
│ ├── StackWalker
│ │ ├── LICENSE
│ │ ├── StackWalker.cpp
│ │ └── StackWalker.h
│ ├── _VirtualToArchivePathHook.asm
│ ├── d3d11hook.cpp
│ ├── d3d11hook.h
│ ├── dinput8
│ │ ├── dinput.h
│ │ ├── dinput8.def
│ │ ├── dinputWrapper.cpp
│ │ └── dinputWrapper.h
│ ├── dllmain.cpp
│ ├── dllmain.h
│ ├── imgui_impl_dx11.cpp
│ ├── imgui_impl_dx11.h
│ ├── stdafx.cpp
│ └── stdafx.h
└── DS3ModEngine.sln

Then let’s dug deeper into each of its components in a sequential order.

AOBScanner

This is simply a helper class commonly seen in every game hacking techniques. Its full name would be “Array Of Bytes Scanner”, meaning it is used to search arrays of bytes inside the memory of the game process, and replace them with modified ones if needed.

Just look at the AOBSCanner.h, this is all we need to know.

class AOBScanner
{
private:
std::list<MEMORY_BASIC_INFORMATION> mMemoryList;
LPVOID mBaseOffset;

static AOBScanner *mSingleton;
public:
AOBScanner();
~AOBScanner();

static AOBScanner* GetSingleton();
void* Scan(unsigned short aob[], int numberOfBytes);
void FindAndReplace(unsigned short aob[], unsigned char replace[], int numberOfBytes);
};

Two interesting methods are Scan and FindAndReplace. Namely speaking, we can use these two methods to scan bytes in memory and replace them.

This is a quite simple one, but actually I have no idea about what this thing is doing. In Game.h, we have:

typedef enum
{
GAME_DARKSOULS_REMASTERED,
GAME_DARKSOULS_2_SOTFS,
GAME_DARKSOULS_3,
GAME_SEKIRO,
} DSGame;

DSGame GetGameType();

then in Game.c:

#include "Game.h"

DSGame GetGameType()
{
return GAME_DARKSOULS_2_SOTFS;
}

The weird thing is, this is Sekiro, not Darksouls2, but this function returns an enum referring to the latter anyway. I predict that this isn’t quite important, so returning anything would be okay.

GameplayPatcher

tips: skip “unimportant” functions.

In this class, we have several tool functions that are related to modifying the game features. Look at the file GameplayPatcher.h:

BOOL ApplyGameplayPatches();

BOOL ApplyMiscPatches();

BOOL ApplyShadowMapResolutionPatches(int dirSize, int atlasSize, int pointSize, int spotSize);

BOOL ApplyFModHooks();

BOOL ApplyDS3SekiroAllocatorLimitPatch();

BOOL ApplyAllocatorLimitPatchVA();

In this header file, we only spot 6 functions. However, in the source file, we found other functions that are not exposed, like ApplyBonfileSacrificePatch. I presume that this ModEngine is migrated from the one for the Dark Souls 2, and these functions weren’t removed. But I’m going to analyze all of them in any case.

ApplyBonfireSacrificePatch(postponed)

BOOL ApplyBonfireSacrificePatch()
{
BYTE sacrificePatch[6] = {0xC6, 0x41, 0x10, 0x01, 0x90, 0x90};

DWORD oldProtect;

// Supposed to be at 0x1409AD0DE
unsigned short scanBytes[14] = { 0x66, 0xC7, 0x41, 0x10, 0x00, 0x00, 0x48, 0x8B, 0x88, 0xC8, 0x11, 0x00, 0x00, 0x48 };
LPVOID addr = AOBScanner::GetSingleton()->Scan(scanBytes, 14);
if (!VirtualProtect(addr, 6, PAGE_EXECUTE_READWRITE, &oldProtect))
return false;
memcpy((LPVOID)0x1409AD0DE, &sacrificePatch[0], 6);
VirtualProtect(addr, 6, oldProtect, &oldProtect);
return true;
}

Just a simple function that replaces an array of bytes into another self-defined one using VirtualProtect, AOBSCanner::Scan and memcpy.

According to static analysis through IDA, I couldn’t find the scanBytes array in the whole program. Therefore I guess this function is truly for DS2. So lets postpone its analysis.

ApplyGameplayPatches(postponed)

BOOL ApplyGameplayPatches()
{
bool sacrificePatch = (GetPrivateProfileIntW(L"gameplay", L"restoreBonfireSacrifice", 0, L".\\modengine.ini") == 1);

if (sacrificePatch)
{
wprintf(L"[ModEngine] Patching in bonfire sacrifice mechanic\r\n");
if (!ApplyBonfireSacrificePatch())
{
return false;
}
}
return true;
}

This function simply reads the user profile and then invokes the previous ApplyBonfireSacrificePatch. However, I failed to find restoreBonfireSacrifice in that profile.

Just like the statements I’ve made above, let’s postpone its analysis.

ApplyNoLogoPatch

This is the byte sequence to be modified: “7430488d542430488bcde810010010010090bb1000895c242044fb64e4”

The first two bytes are “7430”, meaning je 0x32 in x64 ISA. The function firstly will find the array’s address, and then increase the first byte with 1, making it “7530”, meaning jne 0x32. The following sequence can’t be disassembled correctly, but I presume its just a pattern and doesn’t have an actual meaning.

Anyway, it’s just a patch on the jump instruction.

BOOL ApplyNoLogoPatch()
{
DWORD oldProtect;
if (GetGameType() == GAME_SEKIRO)
{
unsigned short aob[30] = {0x74, 0x30, 0x48, 0x8d, 0x54, 0x24, 0x30, 0x48, 0x8b, 0xcd,
0xe8, 0x100,0x100,0x100,0x100,0x90, 0xbb, 0x01, 0x00, 0x00,
0x00, 0x89, 0x5c, 0x24, 0x20, 0x44, 0x0f, 0xb6, 0x4e, 0x04};
LPVOID address = AOBScanner::GetSingleton()->Scan(aob, 30);
if (address != NULL)
{
wprintf(L"[ModEngine] Applying No logo patch to %#p\r\n", address);
if (!VirtualProtect((LPVOID)address, 1, PAGE_READWRITE, &oldProtect))
return false;
*(char*)address += 1;
VirtualProtect((LPVOID)address, 1, oldProtect, &oldProtect);
}
else
{
wprintf(L"[ModEngine] AOB scan failed to find no logo patch\r\n");
}
}
return true;
}

tFXRConstructor(unimportant)

typedef void* (*FXRCONSTRUCTOR)(LPVOID, LPVOID, wchar_t*, LPVOID, UINT64);
FXRCONSTRUCTOR fpFXRConstructor = NULL;
void* tFXRConstructor(LPVOID p1, LPVOID p2, wchar_t* fxrname, LPVOID p4, UINT64 p5)
{
wprintf(L"[FXR] Engine loading FXR %s (object %#p)\r\n", fxrname, p1);
return fpFXRConstructor(p1, p2, fxrname, p4, p5);
}

I have no idea about what this function does. The only place where it is invoked has been commented.

tFXR1(unimportant)

typedef void* (*FXR1)(LPVOID, LPVOID);
FXR1 fpFXR1 = NULL;
void* tFXR1(LPVOID p1, LPVOID p2)
{
wprintf(L"[FXR] Loaded to object %#p\r\n", p2);
return fpFXR1(p1, p2);
}

tMSBHitConstructor(unimportant)

typedef void* (*MSBHITCONSTRUCTOR)(LPVOID, LPVOID, LPVOID, LPVOID, char);
MSBHITCONSTRUCTOR fpMSBHitConstructor = NULL;
void* tMSBHitConstructor(LPVOID p1, LPVOID p2, LPVOID p3, LPVOID p4, char p5)
{
wprintf(L"[HIT] Engine loading MSB Hit (object %#p)\r\n", p1);
LPVOID hit = fpMSBHitConstructor(p1, p2, p3, p4, p5);
wprintf(L"Disp groups: %#d %#d %#d %#d %#d %#d %#d %#d\r\n", *((char*)hit + 0x6C), *((char*)hit + 0x70), *((char*)hit + 0x74), *((char*)hit + 0x78), *((char*)hit + 0x7C), *((char*)hit + 0x80), *((char*)hit + 0x84), *((char*)hit + 0x88));
return hit;
}

Called when the game is DS3. pass.

tMemoryAllocate(unimportant)

typedef void* (*MEMORYALLOCATE)(UINT32, UINT32, LPVOID);
MEMORYALLOCATE fpMemoryAllocate = NULL;
void* tMemoryAllocate(UINT32 size, UINT32 unk, LPVOID allocator)
{
LPVOID ret = fpMemoryAllocate(size, unk, allocator);
if (ret == NULL)
{
wprintf(L"[Allocator] Allocation of size %d with allocator at %#p\r\n", size, allocator);
wprintf(L"[Allocator] Allocation failed\r\n");
}
return ret;
}

tVirtualAlloc

BOOL gPatchedAllocatorLimits = false;

typedef HANDLE(WINAPI* VIRTUALALLOC)(LPVOID, SIZE_T, DWORD, DWORD);
VIRTUALALLOC fpVirtualAlloc = NULL;
LPVOID tVirtualAlloc(
LPVOID lpAddress,
SIZE_T dwSize,
DWORD flAllocationType,
DWORD flProtect
)
{
if (!gPatchedAllocatorLimits)
{
ApplyDS3SekiroAllocatorLimitPatch();
//onSteamInit();
gPatchedAllocatorLimits = true;
}
return fpVirtualAlloc(lpAddress, dwSize, flAllocationType, flProtect);
}

A VirtualAlloc hook API. Will call ApplyDS3SekiroAllocatorLimitPatch once.

ApplyAllocationTracer(unimportant)

BOOL ApplyAllocationTracer()
{
if (GetGameType() == GAME_DARKSOULS_2_SOTFS)
{
if (MH_CreateHook((LPVOID)0x14082bdc0, &tMemoryAllocate, reinterpret_cast<LPVOID*>(&fpMemoryAllocate)) != MH_OK)
return false;

if (MH_EnableHook((LPVOID)0x14082bdc0) != MH_OK)
return false;
}
}

Another heritage from the ModEngine for Dark Souls 2. Not invoked.

ApplyMiscPatches

A function for miscellaneous patches.

BOOL ApplyMiscPatches()
{
bool noLogo = (GetPrivateProfileIntW(L"misc", L"skipLogos", 1, L".\\modengine.ini") == 1);

if (noLogo)
{
if (!ApplyNoLogoPatch())
{
return false;
}
}

if (GetGameType() == GAME_DARKSOULS_3)
//if (0)
{
/*if (MH_CreateHook((LPVOID)0x140e60320, &tFXRConstructor, reinterpret_cast<LPVOID*>(&fpFXRConstructor)) != MH_OK)
return false;

if (MH_EnableHook((LPVOID)0x140e60320) != MH_OK)
return false;

if (MH_CreateHook((LPVOID)0x141ac32f0, &tFXR1, reinterpret_cast<LPVOID*>(&fpFXR1)) != MH_OK)
return false;

if (MH_EnableHook((LPVOID)0x141ac32f0) != MH_OK)
return false;*/
if (MH_CreateHook((LPVOID)0x14084ea60, &tMSBHitConstructor, reinterpret_cast<LPVOID*>(&fpMSBHitConstructor)) != MH_OK)
return false;

if (MH_EnableHook((LPVOID)0x14084ea60) != MH_OK)
return false;
}

//ApplyAllocationTracer();
//ApplyFModHooks();
ApplyDS3SekiroAllocatorLimitPatch();

return true;
}

Firstly it calls ApplyNoLogoPatch to skip the prelude, CG and logos.

Then there comes an “if” statement, but I think it’s assertion can’t be satisfied as the GetGameType function will return theGAME_DARKSOULS_2_SOTFS enumeration, which isn’t equal to GAME_DARKSOULS_3. But let’s just mark this place for further review.

The “if” statement is unlikely to be met.

Finally it invokes ApplyDS3SekiroAllocatorLimitPatch. Details of this function will be discussed later.

ApplyShadowMapResolutionPatches(unimportant)

According to the function’s name, may be it’s used to change the in-game shadow or map resolutions. But it’s not invoked. Let’s review this later.

This function will be invoked in dllmain.cpp if and only if the game is Dark Souls 2.

tFmodMemoryAllocate(unimportant)

typedef void* (*FMODMEMORYALLOCATE)(UINT32, UINT32, LPVOID);
FMODMEMORYALLOCATE fpFmodMemoryAllocate = NULL;
void* tFmodMemoryAllocate(UINT32 size, UINT32 unk, LPVOID allocator)
{
LPVOID ret = fpFmodMemoryAllocate(size, unk, allocator);
wprintf(L"[Allocator] FMOD Allocation of size %d with typet %d\r\n", size, unk);
if (ret == NULL)
{
wprintf(L"[Allocator] FMOD Allocation failed\r\n");
while (1) {}
}
return ret;
}

Used in DS3. pass.

ApplyFModHooks(unimportant)

Not invoked anyway.

BOOL ApplyFModHooks()
{
if (GetGameType() == GAME_DARKSOULS_3)
{
if (MH_CreateHook((LPVOID)0x141a4bfd0, &tFmodMemoryAllocate, reinterpret_cast<LPVOID*>(&fpFmodMemoryAllocate)) != MH_OK)
return false;

if (MH_EnableHook((LPVOID)0x141a4bfd0) != MH_OK)
return false;
}
}

Used in DS3. Pass.

ApplyDS3SekiroAllocatorLimitPatch

BOOL ApplyDS3SekiroAllocatorLimitPatch()
{
DWORD oldProtect;
if (GetGameType() == GAME_DARKSOULS_3 || GetGameType() == GAME_SEKIRO)
{
// Find limit table
unsigned short table[56] = { 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00 };
uint64_t* tablePtr = (uint64_t*)AOBScanner::GetSingleton()->Scan(table, 56);
if (tablePtr != NULL)
{
wprintf(L"[ModEngine] Patching memory limit table at %#p\r\n", tablePtr);
if (!VirtualProtect((LPVOID)tablePtr, 0x100, PAGE_READWRITE, &oldProtect))
return false;
// FMod allocator limit
tablePtr[9] = tablePtr[9] * 3;
tablePtr[10] = tablePtr[10] * 3;
VirtualProtect((LPVOID)tablePtr, 0x100, oldProtect, &oldProtect);
}
else
{
wprintf(L"[ModEngine] Could not find allocation limit table\r\n");
return false;
}
return true;
}
}

Search a byte sequence called “table” with a length of 56 and then multiply the 9th and 10th QWORD by 3.

This function is called from ApplyMiscPatches function, and is also used to hook VirtualAlloc API as well.

I’ve failed to find this table in IDA. But according to the function’s name, It’s just used to unlock memory limit which isn’t quite important.

ApplyAllocatorLimitPatchVA

BOOL ApplyAllocatorLimitPatchVA()
{
wprintf(L"[ModEngine] Hooking VirtualAlloc\r\n");
if (MH_CreateHookApi(L"kernel32", "VirtualAlloc", &tVirtualAlloc, reinterpret_cast<LPVOID*>(&fpVirtualAlloc)) != MH_OK)
return false;

if (MH_EnableHook((LPVOID)GetProcAddress(GetModuleHandleW(L"kernel32"), "VirtualAlloc")) != MH_OK)
return false;

return true;
}

Used to hook kernel32.VirtualAlloc API with self-defined one to unlock memory limit. Not quite important.

Minhook

I have to mention this helper class first as it’s a very important one.

It’s a very large class so I guess I can explain this in detail in another blog. But now I will talk about it’s functionality in brief.

According to New Bing:

MinHook is a minimalistic x86/x64 API hooking library for Windows. It provides the basic part of Microsoft Detours functionality for both x64/x86 environments. It can be used to intercept calls to exported functions.

TsudaKageyu/minhook: The Minimalistic x86/x64 API Hooking Library for Windows (github.com)

To be honest, let’s just look at the MinHook.h. It has all the information we need.

MH_CreateHook

MH_STATUS WINAPI MH_CreateHook(LPVOID pTarget, LPVOID pDetour, LPVOID *ppOriginal);

Creates a Hook for the specified API function, in disabled state. Therefore we need to invoke MH_EnableHook as well to enable the hook.

MH_CreatHookApi

MH_STATUS WINAPI MH_CreateHookApi(
LPCWSTR pszModule, LPCSTR pszProcName, LPVOID pDetour, LPVOID *ppOriginal);

Creates a Hook for the specified API function, in disabled state. Therefore we need to invoke MH_EnableHook as well to enable the hook.

These two functions also have a extended one respectively with an “Ex” postfix.

MH_EnableHook

MH_STATUS WINAPI MH_EnableHook(LPVOID pTarget);

Just enable the hook.

HideThreadFromDebugger

The only function used is BypassHideThreadFromDebugger.

BOOL BypassHideThreadFromDebugger()
{

if (MH_CreateHookApi(L"ntdll", "NtSetInformationThread", &tZwSetInformationThread, reinterpret_cast<LPVOID*>(&fpZwSetInformationThread)) != MH_OK)
return false;

if (MH_EnableHook((LPVOID)GetProcAddress(GetModuleHandleW(L"ntdll"), "NtSetInformationThread")) != MH_OK)
return false;

return true;
}

This function hooks the Windows API NtSetInformationThread with a self-defined one.

// Detour function
INT WINAPI tZwSetInformationThread(HANDLE threadHandle, THREAD_INFORMATION_CLASS threadInfoClass, PVOID threadInfo, ULONG threadInfoLength)
{
if (threadInfoClass == 0x11) // ThreadHideFromDebugger
{
return 0x1; // return STATUS_SUCCESS as if we set the ThreadHideFromDebugger flag
}

return fpZwSetInformationThread(threadHandle, threadInfoClass, threadInfo, threadInfoLength); // return the original function if any other info class
}

The function checks the second argument “threadInfoClass” and avoids invoking the original API if the argument is 0x11, meaning the ThreadHideFromDebugger enumeration.

If we don’t hook this API, the NtSetInformaitonThread with ThreadHideFromDebugger will make the whole thread undetectable to a debugger, which is needed for a mod.

ImGui

Let’s just skip this class because it’s just a GUI library. If interested, check this web: ocornut/imgui: Dear ImGui: Bloat-free Graphical User interface for C++ with minimal dependencies (github.com)

ImGuiHook

This class includes the following files(both source files and header files):

  • d3d11hook
  • imgui_impl_dx11
  • InputHook
  • stdafx

I guess it’s used for the in-game menu.

d3d11hook

What the ModEngine is using is an old one. The author actually has integrated them into the following:

Rebzzel/kiero: Universal graphical hook for a D3D9-D3D12, OpenGL and Vulkan based games. (github.com)

But we can also refer to the following demo projects:

lxfly2000/D3D11Hook: Direct3D 11 Hook (github.com)

xo1337/D3D11Hook: A simple D3D11 Hook for x64 and x86 games. This project is ready to compile (x64 or x86). (github.com)

imgui_impl_dx11

Actually it’s a subset of d3d11hook.

InputHook

Used to get the keyboard inputs. Skip.

In-game menu’s specification. Skip.

LooseParams

LooseParamsPatch(unimportant)

This function still has nothing to do with Sekiro. pass.

ModLoader

The whole point of this class is to hook File related functions(Windows API or in-game functions etc.) to load modders’ self-defined functionalities. (I guess)

HookModLoader

BOOL HookModLoader(bool loadUXMFiles, bool useModOverride, bool cachePaths, wchar_t *modOverrideDirectory);

Firstly it gets the ArchiveFunction‘s address:

LPVOID hookAddress = GetArchiveFunctionAddress();

If found:

if (GetGameType() == GAME_SEKIRO)
{
if (MH_CreateHook(hookAddress, &tf*ckSekiro, reinterpret_cast<LPVOID*>(&fpf*ckSekiro)) != MH_OK)
return false;

if (MH_EnableHook(hookAddress) != MH_OK)
return false;
}

Details of tf*ckSekiro will be covered later.

Then, It hooks Windows File Create/Open API:

else
{
wprintf(L"[ModEngine] Hooking CreateFileW\r\n");
if (MH_CreateHookApi(L"kernel32", "CreateFileW", &tCreateFileW, reinterpret_cast<LPVOID*>(&fpCreateFileW)) != MH_OK)
return false;

if (MH_EnableHook((LPVOID)GetProcAddress(GetModuleHandleW(L"kernel32"), "CreateFileW")) != MH_OK)
return false;
}

Details of tCreateFileW will be covered later.

GetArchiveFunctionAddress(postponed)

else if (game == GAME_SEKIRO)
{
//return (LPVOID)0x1401c5d80;
//return (LPVOID)0x1401c5d80;
unsigned short scanBytes[14] = {0x40, 0x55, 0x56, 0x41, 0x54, 0x41, 0x55, 0x48, 0x83, 0xec, 0x28, 0x4d, 0x8b, 0xe0};
return AOBScanner::GetSingleton()->Scan(scanBytes, 14);
}

Sadly I’ve failed to search this array in static analysis. But it’s actually not that important.

tf*ckSekiro

void* tf*ckSekiro(SekiroString *path, UINT64 p2, UINT64 p3, DLString *p4, UINT64 p5, UINT64 p6)
{
void *res = fpf*ckSekiro(path, p2, p3, p4, p5, p6);
if (res != NULL)
{
return ((char*)ReplaceFileLoadPath(&((SekiroString*)res)->string)) - 8;
}
return res;
}

Invokes ReplaceFileLoadPath.

ReplaceFileLoadPath

Details of this function isn’t that important XD. Just like its name.

NetworkBlocker

This class is used to block network access to prevent from being banned.

INT __stdcall tWSAStartup(WORD wVersionRequested, void* lpWSAData)
{
//wprintf(L"[ModEngine] WSAStartup called\r\n");
return 10091L; // WSASYSNOTREADY
}

BOOL BlockNetworkConnection()
{
wprintf(L"[ModEngine] Blocking winsocket startup\r\n");
if (MH_CreateHookApi(L"ws2_32", "WSAStartup", &tWSAStartup, reinterpret_cast<LPVOID*>(&fpWsaStartup)) != MH_OK)
return false;

if (MH_EnableHook((LPVOID)GetProcAddress(GetModuleHandleW(L"ws2_32"), "WSAStartup")) != MH_OK)
return false;

return true;
}

It’s quite an easy one, just hook the WSAStartup function and refrain the network library from initializing itself.

StackWalker

JochenKalmbach/StackWalker: Walking the callstack in windows applications (github.com)

Walk a callstack.

dllmain⭐

DllMain⭐

if (GetPrivateProfileIntW(L"debug", L"showDebugLog", 0, L".\\modengine.ini") == 1)
{
AllocConsole();
FILE *stream;
freopen_s(&stream, "CONOUT$", "w", stdout);
freopen_s(&stream, "CONIN$", "r", stdin);
gDebugLog = true;
}

Firstly it checks your profile about whether you need a debug log. If so, it will call AllocConsole. I guess It will open a new terminal for logging.

Then it will call InitInstance function while the DLL is attaching to the game process:

InitInstance(hModule);

Next, it will call ApplyAllocatorLimitPatchVA which has already been discussed.

Finally it may create a new thread:

if (GetGameType() == GAME_DARKSOULS_3)
{
// Experimental threaded patcher
CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)MainThread, hModule, NULL, NULL);
}

To be honest, the GetGameType function is really wrecked. I thought this “if” statement should be invoked because it’s for DS3, but there truly are some functionalities related to Sekiro, so… anyway.

InitInstance

Firstly, it checks chainDInput8DLLPath to load other DLLs that are also used to hook dinput8.dll.

GetPrivateProfileStringW(L"misc", L"chainDInput8DLLPath", L"", chainPath, MAX_PATH, L".\\modengine.ini");

if (lstrlenW(chainPath) > 0)
{
GetCurrentDirectoryW(MAX_PATH, dllPath);
lstrcatW(dllPath, chainPath);
wprintf(L"[Mod Engine] Attempting to chain load DLL %s\r\n", dllPath);
hMod = LoadLibraryW(dllPath);
if (hMod != NULL)
{
oDirectInput8Create = (tDirectInput8Create)GetProcAddress(hMod, "DirectInput8Create");
wprintf(L"[Mod Engine] Chain load successful\r\n");
}
else
{
wprintf(L"[Mod Engine] Chain load failed. Falling back to system dinput8.dll\r\n");
}
}
if (hMod == NULL)
{
GetSystemDirectoryW(dllPath, MAX_PATH);
lstrcatW(dllPath, L"\\dinput8.dll");
hMod = LoadLibraryW(dllPath);
oDirectInput8Create = (tDirectInput8Create)GetProcAddress(hMod, "DirectInput8Create");
}

Then it initialize MinHook:

// Initialize MinHook
if (MH_Initialize() != MH_OK)
{
// error throw ...
}

Then it blocks network access:

// Do early hook of WSA stuff
bool blockNetworkAccess = (GetPrivateProfileIntW(L"online", L"blockNetworkAccess", 1, L".\\modengine.ini") == 1);
if (GetGameType() != GAME_SEKIRO && blockNetworkAccess)
{
if (!BlockNetworkConnection())
{
// error throw...
}
}

Sekiro may not need this because it’s truly a single player game.

Then it hooks the entry of steam_api64.dll if the game is on Steam:

if (GetGameType() == GAME_SEKIRO || GetGameType() == GAME_DARKSOULS_3 || GetGameType() == GAME_DARKSOULS_REMASTERED || GetGameType() == GAME_DARKSOULS_2_SOTFS)
{
auto steamApiHwnd = GetModuleHandleW(L"steam_api64.dll");
auto initAddr = GetProcAddress(steamApiHwnd, "SteamAPI_Init");
MH_CreateHook(initAddr, &onSteamInit, reinterpret_cast<LPVOID*>(&fpSteamInit));
MH_EnableHook(initAddr);
}

The hook function:

// SteamAPI hook
typedef DWORD64(__cdecl *STEAMINIT)();
STEAMINIT fpSteamInit = NULL;
DWORD64 __cdecl onSteamInit()
{
ApplyPostUnpackHooks();
return fpSteamInit();
}

It calls ApplyPostUnpackHooks which will be discussed later.

Finally it does something to the DS2’s shadow rendering but lets skip this.

MainThread

Firstly, it unlocks the game’s memory limit:

ApplyDS3SekiroAllocatorLimitPatch();

Then it tries to find the game’s window and title:

if (GetGameType() == GAME_SEKIRO)
{
HWND window = NULL;
char title[255];
while (window == NULL)
{
window = FindWindowW(0, AppWindowTitleSekiro);
RECT rect;
GetWindowRect(window, &rect);
if (!IsIconic(window))
{
window = NULL;
}
BOOL vis = IsWindowEnabled(window);
printf("Not found window? vis %d\n", vis);
}

GetWindowTextA(window, title, 255);

printf("Found window %s?\n", title);
}

But I actually don’t think it does anything useful.

Finally, it calls ApplyPostUnpackHooks as well.

ApplyPostUnpackHooks

Firstly it checks Sekiro’s version:

// Check Sekiro version
if ((GetGameType() == GAME_SEKIRO) && !CheckSekiroVersion())
{
// error msg
}

Then there are some statements related to acquiring the profile’s options, but lets just skip that. And we also skip anything unrelated to Sekiro.

Next:

if (!ApplyMiscPatches())
throw(0xDEAD0004);

Finally, there should be a LoadPlugins function called but somehow I found this function is commented:

void LoadPlugins()
{
//LoadLibraryW(L".\\plugins\\SekiroTutorialRemover.dll");
}

Weird enough.

Conclusion

To be honest, this mod engine actually doesn’t do a lot of things, just some simple patches, nor does it succeeds in wrapping and exposing helpful APIs to modders. I suppose I have to read other’s mod for further study.

Sekiro Shadows Die Twice ModEngine Analysis (2024)
Top Articles
Latest Posts
Article information

Author: Margart Wisoky

Last Updated:

Views: 6442

Rating: 4.8 / 5 (78 voted)

Reviews: 85% of readers found this page helpful

Author information

Name: Margart Wisoky

Birthday: 1993-05-13

Address: 2113 Abernathy Knoll, New Tamerafurt, CT 66893-2169

Phone: +25815234346805

Job: Central Developer

Hobby: Machining, Pottery, Rafting, Cosplaying, Jogging, Taekwondo, Scouting

Introduction: My name is Margart Wisoky, I am a gorgeous, shiny, successful, beautiful, adventurous, excited, pleasant person who loves writing and wants to share my knowledge and understanding with you.