// 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: \BSO\BSO_Loader.txt (schreibt auch die Mod ueber HostApi.Log). #define WIN32_LEAN_AND_MEAN #define NETHOST_USE_AS_STATIC #include #include #include #include #include #include #include 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 /// Was die Mod vom Loader bekommt (Gegenstueck: BSO.HostApi). 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); /// Originalfunktionen je vtable (A- und W-Schnittstellen haben eigene vtables). 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; } /// Ersetzt Eintraege einer vtable (liegt schreibgeschuetzt in dinput8.dll) und merkt sich die Originale. 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; }