Files
BSO-Mod/Loader/dinput8.cpp
T
MrMahoandClaude Opus 5.5 35f2d1330d Erste Version
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 17:26:23 +01:00

306 lines
11 KiB
C++

// BSO Loader - dinput8.dll-Proxy (wie Unity Doorstop bei MSCO, nur fuer die Vision Engine).
//
// Game.vPlugin importiert DINPUT8.dll, Windows sucht sie zuerst im Spielordner. Diese DLL reicht alles an die echte
// dinput8.dll aus System32 weiter und
// 1. startet beim ersten DirectInput8Create .NET (hostfxr) und ruft BSO.Bootstrap.Start in BSO\BSO.dll auf,
// 2. haengt sich in GetDeviceState/GetDeviceData der Tastatur: das Spiel fragt die Tastatur einmal pro Bild im
// Hauptthread ab - dort wird BSO.Bootstrap.OnFrame aufgerufen. Kein Spielcode wird gepatcht.
// Protokoll: <Spielordner>\BSO\BSO_Loader.txt (schreibt auch die Mod ueber HostApi.Log).
#define WIN32_LEAN_AND_MEAN
#define NETHOST_USE_AS_STATIC
#include <windows.h>
#include <stdio.h>
#include <stdarg.h>
#include <string>
#include <nethost.h>
#include <hostfxr.h>
#include <coreclr_delegates.h>
static HMODULE self, real;
static FILE* logFile;
static CRITICAL_SECTION logLock;
static std::wstring bsoDir;
static void LogLine(const char* text)
{
EnterCriticalSection(&logLock);
if (!logFile)
{
CreateDirectoryW(bsoDir.c_str(), nullptr);
logFile = _wfsopen((bsoDir + L"\\BSO_Loader.txt").c_str(), L"w", _SH_DENYWR);
}
if (logFile)
{
SYSTEMTIME t;
GetLocalTime(&t);
fprintf(logFile, "%02d:%02d:%02d %s\n", t.wHour, t.wMinute, t.wSecond, text);
fflush(logFile);
}
LeaveCriticalSection(&logLock);
}
static void Log(const char* fmt, ...)
{
char buf[1024];
va_list args;
va_start(args, fmt);
vsnprintf(buf, sizeof(buf), fmt, args);
va_end(args);
LogLine(buf);
}
// ---------------------------------------------------------------------------------------------------------------
// .NET starten
/// <summary>Was die Mod vom Loader bekommt (Gegenstueck: BSO.HostApi).</summary>
struct HostApi
{
void (*log)(const char* utf8);
const wchar_t* gameDir;
const wchar_t* bsoDir;
};
typedef void (CORECLR_DELEGATE_CALLTYPE* StartFn)(HostApi*);
typedef void (CORECLR_DELEGATE_CALLTYPE* FrameFn)();
static HostApi hostApi;
static std::wstring gameDir;
static FrameFn onFrame;
static bool managedTried;
static void StartManaged()
{
if (managedTried) return;
managedTried = true;
std::wstring assembly = bsoDir + L"\\BSO.dll";
std::wstring config = bsoDir + L"\\BSO.runtimeconfig.json";
if (GetFileAttributesW(assembly.c_str()) == INVALID_FILE_ATTRIBUTES)
{
Log("BSO\\BSO.dll fehlt - nur Proxy, keine Mod");
return;
}
char_t fxrPath[MAX_PATH];
size_t fxrSize = MAX_PATH;
int rc = get_hostfxr_path(fxrPath, &fxrSize, nullptr);
if (rc != 0) { Log(".NET nicht gefunden (get_hostfxr_path 0x%08X) - .NET 10 Runtime installieren", rc); return; }
HMODULE fxr = LoadLibraryW(fxrPath);
auto init = (hostfxr_initialize_for_runtime_config_fn)GetProcAddress(fxr, "hostfxr_initialize_for_runtime_config");
auto getDelegate = (hostfxr_get_runtime_delegate_fn)GetProcAddress(fxr, "hostfxr_get_runtime_delegate");
auto close = (hostfxr_close_fn)GetProcAddress(fxr, "hostfxr_close");
if (!init || !getDelegate || !close) { Log("hostfxr unvollstaendig"); return; }
hostfxr_handle ctx = nullptr;
rc = init(config.c_str(), nullptr, &ctx);
if (rc < 0 || !ctx) { Log("hostfxr_initialize_for_runtime_config 0x%08X", rc); if (ctx) close(ctx); return; }
load_assembly_and_get_function_pointer_fn load = nullptr;
rc = getDelegate(ctx, hdt_load_assembly_and_get_function_pointer, (void**)&load);
close(ctx);
if (rc != 0 || !load) { Log("hostfxr_get_runtime_delegate 0x%08X", rc); return; }
StartFn start = nullptr;
rc = load(assembly.c_str(), L"BSO.Bootstrap, BSO", L"Start", UNMANAGEDCALLERSONLY_METHOD, nullptr, (void**)&start);
if (rc != 0 || !start) { Log("BSO.Bootstrap.Start nicht gefunden (0x%08X)", rc); return; }
rc = load(assembly.c_str(), L"BSO.Bootstrap, BSO", L"OnFrame", UNMANAGEDCALLERSONLY_METHOD, nullptr, (void**)&onFrame);
if (rc != 0) { Log("BSO.Bootstrap.OnFrame nicht gefunden (0x%08X)", rc); onFrame = nullptr; }
hostApi.log = LogLine;
hostApi.gameDir = gameDir.c_str();
hostApi.bsoDir = bsoDir.c_str();
start(&hostApi);
}
// ---------------------------------------------------------------------------------------------------------------
// Bild-Takt ueber die Tastatur-Abfrage
static const GUID GUID_SysKeyboard_ = { 0x6F1D2B61, 0xD5A0, 0x11CF, { 0xBF, 0xC7, 0x44, 0x45, 0x53, 0x54, 0x00, 0x00 } };
// Feste Plaetze in den COM-vtables (IDirectInput8 / IDirectInputDevice8, A und W gleich)
enum { SlotCreateDevice = 3, SlotGetDeviceState = 9, SlotGetDeviceData = 10 };
typedef HRESULT (WINAPI* CreateDeviceFn)(void* self, REFGUID guid, void** device, void* outer);
typedef HRESULT (WINAPI* GetDeviceStateFn)(void* self, DWORD size, void* data);
typedef HRESULT (WINAPI* GetDeviceDataFn)(void* self, DWORD size, void* data, DWORD* count, DWORD flags);
/// <summary>Originalfunktionen je vtable (A- und W-Schnittstellen haben eigene vtables).</summary>
struct Patched { void** vtable; void* original[3]; };
static Patched patched[16];
static int patchedCount;
static Patched* Find(void* object)
{
void** vtable = *(void***)object;
for (int i = 0; i < patchedCount; i++) if (patched[i].vtable == vtable) return &patched[i];
return nullptr;
}
/// <summary>Ersetzt Eintraege einer vtable (liegt schreibgeschuetzt in dinput8.dll) und merkt sich die Originale.</summary>
static Patched* Patch(void* object, const int* slots, void* const* hooks, int n)
{
if (Patched* p = Find(object)) return p;
if (patchedCount == 16) return nullptr;
Patched* p = &patched[patchedCount++];
p->vtable = *(void***)object;
for (int i = 0; i < n; i++)
{
void** entry = &p->vtable[slots[i]];
p->original[i] = *entry;
DWORD old;
VirtualProtect(entry, sizeof(void*), PAGE_READWRITE, &old);
*entry = hooks[i];
VirtualProtect(entry, sizeof(void*), old, &old);
}
return p;
}
static void* keyboard;
static DWORD mainThread; // Thread, der Game.vPlugin (und damit uns) geladen hat
static int frameSlot; // 0 = noch offen, sonst SlotGetDeviceState/SlotGetDeviceData - nur einer zaehlt
static DWORD statStart, statMain, statOther;
static bool statDone;
// Das Spiel fragt die Tastatur mehrmals pro Bild ab (1. Lauf: ~7400/s). Die Mod erkennt ein neues Bild selbst an der
// Engine-Uhr - hier wird nur sichergestellt, dass sie ausschliesslich im Hauptthread laeuft.
static void Frame(void* device, int slot)
{
if (device != keyboard) return;
if (!frameSlot)
{
frameSlot = slot;
Log("Takt ueber Tastatur-%s", slot == SlotGetDeviceState ? "GetDeviceState" : "GetDeviceData");
statStart = GetTickCount();
}
if (slot != frameSlot) return;
bool main = GetCurrentThreadId() == mainThread;
if (!statDone)
{
if (main) statMain++; else statOther++;
if (GetTickCount() - statStart >= 5000)
{
statDone = true;
Log("Takt: in 5 s %lu Aufrufe im Hauptthread, %lu in anderen Threads", statMain, statOther);
}
}
if (main && onFrame) onFrame();
}
static HRESULT WINAPI HookGetDeviceState(void* self, DWORD size, void* data)
{
HRESULT hr = ((GetDeviceStateFn)Find(self)->original[0])(self, size, data);
Frame(self, SlotGetDeviceState);
return hr;
}
static HRESULT WINAPI HookGetDeviceData(void* self, DWORD size, void* data, DWORD* count, DWORD flags)
{
HRESULT hr = ((GetDeviceDataFn)Find(self)->original[1])(self, size, data, count, flags);
Frame(self, SlotGetDeviceData);
return hr;
}
static HRESULT WINAPI HookCreateDevice(void* self, REFGUID guid, void** device, void* outer)
{
HRESULT hr = ((CreateDeviceFn)Find(self)->original[0])(self, guid, device, outer);
if (SUCCEEDED(hr) && device && *device && IsEqualGUID(guid, GUID_SysKeyboard_))
{
keyboard = *device;
static const int slots[] = { SlotGetDeviceState, SlotGetDeviceData };
static void* const hooks[] = { (void*)HookGetDeviceState, (void*)HookGetDeviceData };
Patch(*device, slots, hooks, 2);
Log("Tastatur-Geraet erstellt");
}
return hr;
}
// ---------------------------------------------------------------------------------------------------------------
// Exporte der echten dinput8.dll
static FARPROC Real(const char* name)
{
if (!real)
{
wchar_t path[MAX_PATH];
GetSystemDirectoryW(path, MAX_PATH);
wcscat_s(path, L"\\dinput8.dll");
real = LoadLibraryW(path);
if (!real) Log("Echte dinput8.dll nicht ladbar (%lu)", GetLastError());
}
return real ? GetProcAddress(real, name) : nullptr;
}
extern "C" HRESULT WINAPI Proxy_DirectInput8Create(HINSTANCE inst, DWORD version, REFIID iid, void** out, void* outer)
{
typedef HRESULT (WINAPI* Fn)(HINSTANCE, DWORD, REFIID, void**, void*);
Fn fn = (Fn)Real("DirectInput8Create");
if (!fn) return E_FAIL;
HRESULT hr = fn(inst, version, iid, out, outer);
StartManaged();
if (SUCCEEDED(hr) && out && *out)
{
static const int slots[] = { SlotCreateDevice };
static void* const hooks[] = { (void*)HookCreateDevice };
Patch(*out, slots, hooks, 1);
}
return hr;
}
extern "C" HRESULT WINAPI Proxy_DllCanUnloadNow()
{
typedef HRESULT (WINAPI* Fn)();
Fn fn = (Fn)Real("DllCanUnloadNow");
return fn ? fn() : S_FALSE;
}
extern "C" HRESULT WINAPI Proxy_DllGetClassObject(REFCLSID clsid, REFIID iid, void** out)
{
typedef HRESULT (WINAPI* Fn)(REFCLSID, REFIID, void**);
Fn fn = (Fn)Real("DllGetClassObject");
return fn ? fn(clsid, iid, out) : CLASS_E_CLASSNOTAVAILABLE;
}
extern "C" HRESULT WINAPI Proxy_DllRegisterServer()
{
typedef HRESULT (WINAPI* Fn)();
Fn fn = (Fn)Real("DllRegisterServer");
return fn ? fn() : E_FAIL;
}
extern "C" HRESULT WINAPI Proxy_DllUnregisterServer()
{
typedef HRESULT (WINAPI* Fn)();
Fn fn = (Fn)Real("DllUnregisterServer");
return fn ? fn() : E_FAIL;
}
extern "C" const void* WINAPI Proxy_GetdfDIJoystick()
{
typedef const void* (WINAPI* Fn)();
Fn fn = (Fn)Real("GetdfDIJoystick");
return fn ? fn() : nullptr;
}
BOOL WINAPI DllMain(HINSTANCE module, DWORD reason, void*)
{
if (reason == DLL_PROCESS_ATTACH)
{
// Unter dem Loader-Lock nur Pfade merken - .NET startet erst im ersten DirectInput8Create
self = module;
mainThread = GetCurrentThreadId();
DisableThreadLibraryCalls(module);
InitializeCriticalSection(&logLock);
wchar_t path[MAX_PATH];
GetModuleFileNameW(module, path, MAX_PATH);
gameDir = path;
gameDir = gameDir.substr(0, gameDir.find_last_of(L'\\'));
bsoDir = gameDir + L"\\BSO";
Log("BSO Loader geladen aus %ls", path);
}
return TRUE;
}