Initial commit: Complete open-source Decal rebuild

All 5 phases of the open-source Decal rebuild:

Phase 1: 14 decompiled .NET projects (Interop.*, Adapter, FileService, DecalUtil)
Phase 2: 10 native DLLs rewritten as C# COM servers with matching GUIDs
  - DecalDat, DHS, SpellFilter, DecalInput, DecalNet, DecalFilters
  - Decal.Core, DecalControls, DecalRender, D3DService
Phase 3: C++ shims for Inject.DLL (D3D9 hooking) and LauncherHook.DLL
Phase 4: DenAgent WinForms tray application
Phase 5: WiX installer and build script

25 C# projects building with 0 errors.
Native C++ projects require VS 2022 + Windows SDK (x86).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
erik 2026-02-08 18:27:56 +01:00
commit d1442e3747
1382 changed files with 170725 additions and 0 deletions

View file

@ -0,0 +1,327 @@
// LauncherHook.cpp - Modern implementation of LauncherHook.DLL for Decal
//
// This DLL is loaded into the Asheron's Call launcher/lobby process.
// It intercepts CreateProcessW to detect when client.exe is launched,
// then injects Inject.DLL into the suspended game process before resuming it.
//
// Exported functions:
// LauncherHookEnable - Install the CBT hook for injection into launcher
// LauncherHookDisable - Remove the CBT hook
// LauncherHookLauncherStartup - Called when launcher starts (no-op placeholder)
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <tchar.h>
#include <string>
// ---------------------------------------------------------------------------
// Shared data section - persists across all processes that load this DLL
// ---------------------------------------------------------------------------
#pragma data_seg(".LHookDll")
static HHOOK g_hHook = NULL;
static HINSTANCE g_hInstance = NULL;
#pragma data_seg()
#pragma comment(linker, "/SECTION:.LHookDll,RWS")
// ---------------------------------------------------------------------------
// Import Address Table hooking infrastructure
// (Simplified from ApiHook.h for this single-purpose DLL)
// ---------------------------------------------------------------------------
#define MakePtr(cast, ptr, addValue) (cast)((DWORD_PTR)(ptr) + (DWORD_PTR)(addValue))
typedef BOOL(WINAPI* PFN_CreateProcessW)(
LPCWSTR, LPWSTR, LPSECURITY_ATTRIBUTES, LPSECURITY_ATTRIBUTES,
BOOL, DWORD, LPVOID, LPCWSTR, LPSTARTUPINFOW, LPPROCESS_INFORMATION);
static PFN_CreateProcessW g_pfnOrigCreateProcessW = NULL;
// ---------------------------------------------------------------------------
// DLL injection via CreateRemoteThread + LoadLibraryW
// ---------------------------------------------------------------------------
static BOOL InjectDLL(HANDLE hProcess, const WCHAR* szDllPath)
{
SIZE_T cbPath = (wcslen(szDllPath) + 1) * sizeof(WCHAR);
// Allocate memory in target process for the DLL path string
LPVOID pRemotePath = VirtualAllocEx(hProcess, NULL, cbPath, MEM_COMMIT, PAGE_READWRITE);
if (!pRemotePath)
return FALSE;
// Write the DLL path into the target process
if (!WriteProcessMemory(hProcess, pRemotePath, szDllPath, cbPath, NULL))
{
VirtualFreeEx(hProcess, pRemotePath, 0, MEM_RELEASE);
return FALSE;
}
// Get LoadLibraryW address (same in all processes due to ASLR kernel32 sharing)
LPTHREAD_START_ROUTINE pfnLoadLibrary =
(LPTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "LoadLibraryW");
if (!pfnLoadLibrary)
{
VirtualFreeEx(hProcess, pRemotePath, 0, MEM_RELEASE);
return FALSE;
}
// Create remote thread that calls LoadLibraryW(dllPath)
HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, pfnLoadLibrary, pRemotePath, 0, NULL);
if (!hThread)
{
VirtualFreeEx(hProcess, pRemotePath, 0, MEM_RELEASE);
return FALSE;
}
// Wait for the remote LoadLibrary to complete
WaitForSingleObject(hThread, 10000);
CloseHandle(hThread);
VirtualFreeEx(hProcess, pRemotePath, 0, MEM_RELEASE);
return TRUE;
}
// ---------------------------------------------------------------------------
// Hooked CreateProcessW - intercepts game client launch
// ---------------------------------------------------------------------------
static BOOL WINAPI Hooked_CreateProcessW(
LPCWSTR lpApplicationName,
LPWSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCWSTR lpCurrentDirectory,
LPSTARTUPINFOW lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation)
{
static LONG s_recursion = 0;
// Check if this is launching client.exe / acclient.exe
bool bIsClient = false;
if (lpCommandLine)
{
std::wstring cmd(lpCommandLine);
// Case-insensitive search for client.exe in command line
for (auto& c : cmd) c = towlower(c);
bIsClient = (cmd.find(L"client.exe") != std::wstring::npos);
}
if (!bIsClient && lpApplicationName)
{
std::wstring app(lpApplicationName);
for (auto& c : app) c = towlower(c);
bIsClient = (app.find(L"client.exe") != std::wstring::npos);
}
if (InterlockedIncrement(&s_recursion) == 1 && bIsClient)
{
// Read the Inject.DLL path from registry
WCHAR szDllPath[MAX_PATH] = {};
HKEY hKey = NULL;
if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"Software\\Decal\\Agent",
0, KEY_READ, &hKey) == ERROR_SUCCESS)
{
DWORD dwSize = (MAX_PATH - 20) * sizeof(WCHAR);
if (RegQueryValueExW(hKey, L"AgentPath", NULL, NULL,
(LPBYTE)szDllPath, &dwSize) == ERROR_SUCCESS)
{
// Ensure trailing backslash
size_t len = wcslen(szDllPath);
if (len > 0 && szDllPath[len - 1] != L'\\')
wcscat_s(szDllPath, L"\\");
wcscat_s(szDllPath, L"Inject.dll");
}
RegCloseKey(hKey);
}
// Create the process in SUSPENDED state
PROCESS_INFORMATION localPI = {};
LPPROCESS_INFORMATION pPI = lpProcessInformation ? lpProcessInformation : &localPI;
BOOL bResult = g_pfnOrigCreateProcessW(
lpApplicationName, lpCommandLine,
lpProcessAttributes, lpThreadAttributes,
bInheritHandles,
dwCreationFlags | CREATE_SUSPENDED,
lpEnvironment, lpCurrentDirectory,
lpStartupInfo, pPI);
if (bResult)
{
// Only inject if Decal Agent window is running
if (FindWindowW(NULL, L"Decal Agent") != NULL && szDllPath[0] != L'\0')
{
InjectDLL(pPI->hProcess, szDllPath);
}
// Resume the process (unless caller originally wanted it suspended)
if (!(dwCreationFlags & CREATE_SUSPENDED))
{
ResumeThread(pPI->hThread);
}
}
InterlockedDecrement(&s_recursion);
return bResult;
}
else
{
// Not client.exe or recursive call - pass through
BOOL bResult = g_pfnOrigCreateProcessW(
lpApplicationName, lpCommandLine,
lpProcessAttributes, lpThreadAttributes,
bInheritHandles, dwCreationFlags,
lpEnvironment, lpCurrentDirectory,
lpStartupInfo, lpProcessInformation);
InterlockedDecrement(&s_recursion);
return bResult;
}
}
// ---------------------------------------------------------------------------
// IAT patching - hook CreateProcessW in the current process
// ---------------------------------------------------------------------------
static PIMAGE_IMPORT_DESCRIPTOR FindImportDescriptor(HMODULE hModule, LPCSTR szImportMod)
{
PIMAGE_DOS_HEADER pDOS = (PIMAGE_DOS_HEADER)hModule;
PIMAGE_NT_HEADERS pNT = MakePtr(PIMAGE_NT_HEADERS, pDOS, pDOS->e_lfanew);
if (pNT->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress == 0)
return NULL;
PIMAGE_IMPORT_DESCRIPTOR pImport = MakePtr(PIMAGE_IMPORT_DESCRIPTOR, pDOS,
pNT->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
while (pImport->Name)
{
LPCSTR szName = MakePtr(LPCSTR, pDOS, pImport->Name);
if (_stricmp(szName, szImportMod) == 0)
return pImport;
pImport++;
}
return NULL;
}
static bool PatchIAT(HMODULE hTarget, LPCSTR szModule, LPCSTR szFunction,
void* pNewFunc, void** ppOrigFunc, bool bInstall)
{
PIMAGE_IMPORT_DESCRIPTOR pImport = FindImportDescriptor(hTarget, szModule);
if (!pImport)
return false;
PIMAGE_THUNK_DATA pOrigThunk = MakePtr(PIMAGE_THUNK_DATA, hTarget, pImport->OriginalFirstThunk);
PIMAGE_THUNK_DATA pRealThunk = MakePtr(PIMAGE_THUNK_DATA, hTarget, pImport->FirstThunk);
for (; pOrigThunk->u1.Function; pOrigThunk++, pRealThunk++)
{
if (pOrigThunk->u1.Ordinal & IMAGE_ORDINAL_FLAG)
continue;
PIMAGE_IMPORT_BY_NAME pByName = MakePtr(PIMAGE_IMPORT_BY_NAME, hTarget, pOrigThunk->u1.AddressOfData);
if (pByName->Name[0] == '\0')
continue;
if (_stricmp((const char*)pByName->Name, szFunction) != 0)
continue;
// Found it - patch
MEMORY_BASIC_INFORMATION mbi;
VirtualQuery(pRealThunk, &mbi, sizeof(mbi));
VirtualProtect(mbi.BaseAddress, mbi.RegionSize, PAGE_READWRITE, &mbi.Protect);
if (bInstall)
{
if (ppOrigFunc)
*ppOrigFunc = (void*)pRealThunk->u1.Function;
pRealThunk->u1.Function = (DWORD_PTR)pNewFunc;
}
else if (ppOrigFunc && *ppOrigFunc)
{
pRealThunk->u1.Function = (DWORD_PTR)*ppOrigFunc;
}
DWORD dwOld;
VirtualProtect(mbi.BaseAddress, mbi.RegionSize, mbi.Protect, &dwOld);
return true;
}
return false;
}
static void InstallHooks(bool bInstall)
{
// Hook CreateProcessW in the main executable
HMODULE hExe = GetModuleHandle(NULL);
PatchIAT(hExe, "kernel32.dll", "CreateProcessW",
(void*)Hooked_CreateProcessW, (void**)&g_pfnOrigCreateProcessW, bInstall);
// Also try hooking in AsheronsCall.dll if loaded (legacy launcher)
HMODULE hAC = GetModuleHandleA("AsheronsCall.dll");
if (hAC)
{
PatchIAT(hAC, "kernel32.dll", "CreateProcessW",
(void*)Hooked_CreateProcessW, (void**)&g_pfnOrigCreateProcessW, bInstall);
}
}
// ---------------------------------------------------------------------------
// CBT hook procedure - gets us loaded into the launcher process
// ---------------------------------------------------------------------------
static LRESULT CALLBACK CBTHookProc(int nCode, WPARAM wParam, LPARAM lParam)
{
return CallNextHookEx(g_hHook, nCode, wParam, lParam);
}
// ---------------------------------------------------------------------------
// Exported functions
// ---------------------------------------------------------------------------
extern "C" __declspec(dllexport) void __stdcall LauncherHookEnable()
{
if (!g_hHook)
{
g_hHook = SetWindowsHookExW(WH_CBT, CBTHookProc, g_hInstance, 0);
}
}
extern "C" __declspec(dllexport) void __stdcall LauncherHookDisable()
{
if (g_hHook)
{
UnhookWindowsHookEx(g_hHook);
g_hHook = NULL;
}
}
extern "C" __declspec(dllexport) void __stdcall LauncherHookLauncherStartup()
{
// Placeholder - called by DenAgent when launcher starts
// The actual hooking is done in DllMain when we detect the launcher process
}
// ---------------------------------------------------------------------------
// DllMain
// ---------------------------------------------------------------------------
BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved)
{
switch (dwReason)
{
case DLL_PROCESS_ATTACH:
g_hInstance = hInstance;
DisableThreadLibraryCalls(hInstance);
// Install IAT hooks to intercept CreateProcessW
InstallHooks(true);
break;
case DLL_PROCESS_DETACH:
// Remove IAT hooks
InstallHooks(false);
break;
}
return TRUE;
}