diff --git a/dist/wcdx/src/key_jacker/KJ_Joystick.h b/dist/wcdx/src/key_jacker/KJ_Joystick.h new file mode 100644 index 0000000..9fde1c7 --- /dev/null +++ b/dist/wcdx/src/key_jacker/KJ_Joystick.h @@ -0,0 +1,493 @@ +//*****************************************KJ_Joystick.h****************************************** +// Low-Level Joystick Hook and Input Injection System. +// Install via KJ_Main +// +// Usage: +// KJ_VJoy_SetAxis(axis, value) - Sets a virtual joystick axis position +// KJ_VJoy_SetButton(buttonIndex, down) - Sets a virtual joystick button state +// KJ_VJoy_SetPOV(povValue) - Sets a virtual joystick POV hat switch value +// +//************************************************************************************************* + +#pragma once + +#include +#include +#include +#include "wcdx.h" +#include "se_/SE_Controls.h" + +//============================================================================= +// Virtual Joystick State +//============================================================================= + +static CRITICAL_SECTION s_joyLock; +static bool s_joyInitialized = false; + +struct VirtualJoyState { + DWORD dwX = 32767, dwY = 32767, dwZ = 32767; + DWORD dwR = 32767, dwU = 32767, dwV = 32767; + DWORD dwButtons = 0; + DWORD dwPOV = JOY_POVCENTERED; + VirtualJoyState() {} +}; +static VirtualJoyState s_virtualJoy; + +// Real WinMM function pointers +static HMODULE s_hWinMM = nullptr; +static bool s_winmmOriginalsInit = false; +typedef UINT (WINAPI *joyGetNumDevs_t)(void); +typedef MMRESULT (WINAPI *joyGetPos_t)(UINT, LPJOYINFO); +typedef MMRESULT (WINAPI *joyGetPosEx_t)(UINT, LPJOYINFOEX); +typedef MMRESULT (WINAPI *joyGetDevCapsA_t)(UINT, LPJOYCAPSA, UINT); +typedef MMRESULT (WINAPI *joyGetDevCapsW_t)(UINT, LPJOYCAPSW, UINT); +static joyGetNumDevs_t orig_joyGetNumDevs = nullptr; +static joyGetPos_t orig_joyGetPos = nullptr; +static joyGetPosEx_t orig_joyGetPosEx = nullptr; +static joyGetDevCapsA_t orig_joyGetDevCapsA = nullptr; +static joyGetDevCapsW_t orig_joyGetDevCapsW = nullptr; + +enum class KJ_VJoyAxis : int { X, Y, Z, R, U, V }; + +//============================================================================= +// Helper Functions +//============================================================================= + +static inline DWORD KJ_GetVirtualJoyCenter() { return 32767u; } +static inline DWORD KJ_GetVirtualJoyRange() { return 32767u; } + +// Returns true if any gamepad buttons/axes are mapped in SE_Controls +static inline bool KJ_HasAnyGamepadMappings() { + for (int code = -10; code >= -33; --code) + if (SE_Controls_IsMapped(code)) return true; + return false; +} + +// Compute backoff interval for joystick probing +static inline DWORD KJ_GetProbeInterval(int failures) { + if (failures >= 6) return 2000; + if (failures >= 4) return 1000; + if (failures >= 2) return 500; + return 250; +} + +static void KJ_Ensure_WinMM_Originals() { + if (s_winmmOriginalsInit) return; + s_winmmOriginalsInit = true; + + s_hWinMM = LoadLibraryA("winmm.dll"); + if (!s_hWinMM) return; + + orig_joyGetNumDevs = (joyGetNumDevs_t)GetProcAddress(s_hWinMM, "joyGetNumDevs"); + orig_joyGetPos = (joyGetPos_t)GetProcAddress(s_hWinMM, "joyGetPos"); + orig_joyGetPosEx = (joyGetPosEx_t)GetProcAddress(s_hWinMM, "joyGetPosEx"); + orig_joyGetDevCapsA = (joyGetDevCapsA_t)GetProcAddress(s_hWinMM, "joyGetDevCapsA"); + orig_joyGetDevCapsW = (joyGetDevCapsW_t)GetProcAddress(s_hWinMM, "joyGetDevCapsW"); +} + +// Returns true if a real joystick appears to be present (with adaptive probing) +static inline bool KJ_IsRealJoystickActive(UINT uJoyID) { + if (uJoyID >= 4) return false; + + KJ_Ensure_WinMM_Originals(); + if (!orig_joyGetPosEx) return false; + + static DWORD s_lastProbeTick[4] = {0}; + static DWORD s_probeIntervalMs[4] = {0}; + static int s_consecutiveFailures[4] = {0}; + static bool s_cachedPresent[4] = {false}; + static bool s_cacheValid[4] = {false}; + + DWORD now = GetTickCount(); + UINT idx = uJoyID; + + // Check if we should probe + bool shouldProbe = !s_cacheValid[idx] || + s_probeIntervalMs[idx] == 0 || + (now - s_lastProbeTick[idx] >= s_probeIntervalMs[idx]); + + if (!shouldProbe) return s_cachedPresent[idx]; + + s_lastProbeTick[idx] = now; + + // Try to get device capabilities first (faster) + MMRESULT capsResult = MMSYSERR_NOERROR; + if (orig_joyGetDevCapsA) { + JOYCAPSA caps = {}; + capsResult = orig_joyGetDevCapsA(uJoyID, &caps, sizeof(caps)); + } else if (orig_joyGetDevCapsW) { + JOYCAPSW caps = {}; + capsResult = orig_joyGetDevCapsW(uJoyID, &caps, sizeof(caps)); + } + + if (capsResult != MMSYSERR_NOERROR) { + s_cacheValid[idx] = true; + s_cachedPresent[idx] = false; + s_probeIntervalMs[idx] = KJ_GetProbeInterval(++s_consecutiveFailures[idx]); + return false; + } + + // Verify with actual position query + JOYINFOEX ji = {}; + ji.dwSize = sizeof(ji); + ji.dwFlags = JOY_RETURNALL; + bool present = (orig_joyGetPosEx(uJoyID, &ji) == MMSYSERR_NOERROR); + + s_cacheValid[idx] = true; + s_cachedPresent[idx] = present; + + if (present) { + s_consecutiveFailures[idx] = 0; + s_probeIntervalMs[idx] = 0; + } else { + s_probeIntervalMs[idx] = KJ_GetProbeInterval(++s_consecutiveFailures[idx]); + } + + return present; +} + +//============================================================================= +// IAT Patching +//============================================================================= + +// WinMM IAT Patching Helper +static void KJ_PatchIATForModule(HMODULE hModule, const char* targetDll, + const char* funcName, FARPROC newAddr) { + if (!hModule || !targetDll || !funcName || !newAddr) return; + + unsigned char* base = reinterpret_cast(hModule); + auto dos = reinterpret_cast(base); + if (dos->e_magic != IMAGE_DOS_SIGNATURE) return; + + auto nt = reinterpret_cast(base + dos->e_lfanew); + if (nt->Signature != IMAGE_NT_SIGNATURE) return; + + auto& dir = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]; + if (dir.VirtualAddress == 0 || dir.Size == 0) return; + + auto imports = reinterpret_cast(base + dir.VirtualAddress); + if (!imports) return; + + for (; imports->Name != 0; ++imports) { + const char* dllName = reinterpret_cast(base + imports->Name); + if (_stricmp(dllName, targetDll) != 0) continue; + + PIMAGE_THUNK_DATA orig = reinterpret_cast(base + imports->OriginalFirstThunk); + PIMAGE_THUNK_DATA thunk = reinterpret_cast(base + imports->FirstThunk); + + for (; orig->u1.AddressOfData != 0; ++orig, ++thunk) { + if (orig->u1.Ordinal & IMAGE_ORDINAL_FLAG) continue; + + auto importByName = reinterpret_cast(base + orig->u1.AddressOfData); + const char* name = reinterpret_cast(importByName->Name); + + if (_stricmp(name, funcName) == 0) { + DWORD oldProtect; + VirtualProtect(&thunk->u1.Function, sizeof(void*), PAGE_READWRITE, &oldProtect); + thunk->u1.Function = reinterpret_cast(newAddr); + VirtualProtect(&thunk->u1.Function, sizeof(void*), oldProtect, &oldProtect); + } + } + } +} + + + +// Forward declarations +static MMRESULT WINAPI KJ_joyGetNumDevs(); +static MMRESULT WINAPI KJ_joyGetPos(UINT uJoyID, LPJOYINFO pji); +static MMRESULT WINAPI KJ_joyGetPosEx(UINT uJoyID, LPJOYINFOEX pji); +static MMRESULT WINAPI KJ_joyGetDevCapsA(UINT uJoyID, LPJOYCAPSA pjc, UINT cbjc); +static MMRESULT WINAPI KJ_joyGetDevCapsW(UINT uJoyID, LPJOYCAPSW pjc, UINT cbjc); + +static void KJ_Install_WinMM_IAT_Hooks() { + HMODULE hMain = GetModuleHandle(nullptr); + + // Patch main module + KJ_PatchIATForModule(hMain, "winmm.dll", "joyGetNumDevs", (FARPROC)&KJ_joyGetNumDevs); + KJ_PatchIATForModule(hMain, "winmm.dll", "joyGetPos", (FARPROC)&KJ_joyGetPos); + KJ_PatchIATForModule(hMain, "winmm.dll", "joyGetPosEx", (FARPROC)&KJ_joyGetPosEx); + KJ_PatchIATForModule(hMain, "winmm.dll", "joyGetDevCapsA", (FARPROC)&KJ_joyGetDevCapsA); + KJ_PatchIATForModule(hMain, "winmm.dll", "joyGetDevCapsW", (FARPROC)&KJ_joyGetDevCapsW); + + // Patch all loaded modules + HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, GetCurrentProcessId()); + if (snap == INVALID_HANDLE_VALUE) return; + + MODULEENTRY32 me; + me.dwSize = sizeof(me); + if (Module32First(snap, &me)) { + do { + KJ_PatchIATForModule(me.hModule, "winmm.dll", "joyGetNumDevs", (FARPROC)&KJ_joyGetNumDevs); + KJ_PatchIATForModule(me.hModule, "winmm.dll", "joyGetPos", (FARPROC)&KJ_joyGetPos); + KJ_PatchIATForModule(me.hModule, "winmm.dll", "joyGetPosEx", (FARPROC)&KJ_joyGetPosEx); + KJ_PatchIATForModule(me.hModule, "winmm.dll", "joyGetDevCapsA", (FARPROC)&KJ_joyGetDevCapsA); + KJ_PatchIATForModule(me.hModule, "winmm.dll", "joyGetDevCapsW", (FARPROC)&KJ_joyGetDevCapsW); + } while (Module32Next(snap, &me)); + } + CloseHandle(snap); +} + +//============================================================================= +// Initialization & Shutdown +//============================================================================= + +static void KJ_Input_Install_Joystick() { + if (s_joyInitialized) return; + + InitializeCriticalSection(&s_joyLock); + s_virtualJoy = VirtualJoyState(); + s_joyInitialized = true; + + KJ_Ensure_WinMM_Originals(); + KJ_Install_WinMM_IAT_Hooks(); +} + +// Shutdown joystick system and cleanup resources +static void KJ_Joystick_Shutdown() { + if (!s_joyInitialized) return; + + EnterCriticalSection(&s_joyLock); + + // Reset virtual joystick state + DWORD c = KJ_GetVirtualJoyCenter(); + s_virtualJoy.dwX = c; + s_virtualJoy.dwY = c; + s_virtualJoy.dwZ = c; + s_virtualJoy.dwR = c; + s_virtualJoy.dwU = c; + s_virtualJoy.dwV = c; + s_virtualJoy.dwButtons = 0; + s_virtualJoy.dwPOV = JOY_POVCENTERED; + + LeaveCriticalSection(&s_joyLock); + + DeleteCriticalSection(&s_joyLock); + s_joyInitialized = false; + + // Cleanup loaded library + if (s_hWinMM) { + FreeLibrary(s_hWinMM); + s_hWinMM = nullptr; + } + + // Reset function pointers + orig_joyGetNumDevs = nullptr; + orig_joyGetPos = nullptr; + orig_joyGetPosEx = nullptr; + orig_joyGetDevCapsA = nullptr; + orig_joyGetDevCapsW = nullptr; + s_winmmOriginalsInit = false; +} + +//============================================================================= +// Virtual Joystick Control +//============================================================================= + +static inline void KJ_VJoy_SetAxis(KJ_VJoyAxis axis, DWORD v) { + if (!s_joyInitialized) KJ_Input_Install_Joystick(); + + EnterCriticalSection(&s_joyLock); + switch (axis) { + case KJ_VJoyAxis::X: s_virtualJoy.dwX = v; break; + case KJ_VJoyAxis::Y: s_virtualJoy.dwY = v; break; + case KJ_VJoyAxis::Z: s_virtualJoy.dwZ = v; break; + case KJ_VJoyAxis::R: s_virtualJoy.dwR = v; break; + case KJ_VJoyAxis::U: s_virtualJoy.dwU = v; break; + case KJ_VJoyAxis::V: s_virtualJoy.dwV = v; break; + } + LeaveCriticalSection(&s_joyLock); +} + +static inline void KJ_VJoy_SetButton(int buttonIndex, bool down) { + if (!s_joyInitialized) KJ_Input_Install_Joystick(); + + EnterCriticalSection(&s_joyLock); + if (down) s_virtualJoy.dwButtons |= (1u << buttonIndex); + else s_virtualJoy.dwButtons &= ~(1u << buttonIndex); + LeaveCriticalSection(&s_joyLock); +} + +static inline void KJ_VJoy_SetPOV(DWORD pov) { + if (!s_joyInitialized) KJ_Input_Install_Joystick(); + + EnterCriticalSection(&s_joyLock); + s_virtualJoy.dwPOV = pov; + LeaveCriticalSection(&s_joyLock); +} + +// Calibration helper: force all virtual joystick axes back to center +static void SE_Joystick_ResetAxesToCenter() { + if (!s_joyInitialized) return; + + EnterCriticalSection(&s_joyLock); + DWORD c = KJ_GetVirtualJoyCenter(); + s_virtualJoy.dwX = c; s_virtualJoy.dwY = c; s_virtualJoy.dwZ = c; + s_virtualJoy.dwR = c; s_virtualJoy.dwU = c; s_virtualJoy.dwV = c; + LeaveCriticalSection(&s_joyLock); +} + +// Reset all virtual joystick state (axes, buttons, POV) +static void KJ_ResetVirtualJoystick() { + if (!s_joyInitialized) return; + + EnterCriticalSection(&s_joyLock); + DWORD c = KJ_GetVirtualJoyCenter(); + s_virtualJoy.dwX = c; + s_virtualJoy.dwY = c; + s_virtualJoy.dwZ = c; + s_virtualJoy.dwR = c; + s_virtualJoy.dwU = c; + s_virtualJoy.dwV = c; + s_virtualJoy.dwButtons = 0; + s_virtualJoy.dwPOV = JOY_POVCENTERED; + LeaveCriticalSection(&s_joyLock); +} + +//============================================================================= +// WinMM Joystick API Hooks +//============================================================================= + +// WinMM Joystick API Hooks +static MMRESULT WINAPI KJ_joyGetNumDevs() { + // KJ_Ensure_WinMM_Originals(); + // if (orig_joyGetNumDevs) + // { + // UINT real = orig_joyGetNumDevs(); + // if (real > 0) + // return static_cast(real); + // } + return 1; +} + +// Blend helper for combining real and virtual joystick values +template +static inline T KJ_BlendJoyValue(T realVal, DWORD virtVal) { + int real = static_cast(realVal); + int virt = static_cast(virtVal); + int offset = virt - 32767; + int combined = real + offset; + if (combined < 0) combined = 0; + if (combined > 65535) combined = 65535; + return static_cast(combined); +} + +static MMRESULT WINAPI KJ_joyGetPos(UINT uJoyID, LPJOYINFO pji) { + if (!pji) return MMSYSERR_INVALPARAM; + + KJ_Ensure_WinMM_Originals(); + bool haveReal = (orig_joyGetPos != nullptr) && KJ_IsRealJoystickActive(uJoyID); + + if (haveReal) { + MMRESULT res = orig_joyGetPos(uJoyID, pji); + if (res == MMSYSERR_NOERROR && s_joyInitialized) { + EnterCriticalSection(&s_joyLock); + + pji->wXpos = KJ_BlendJoyValue(pji->wXpos, s_virtualJoy.dwX); + pji->wYpos = KJ_BlendJoyValue(pji->wYpos, s_virtualJoy.dwY); + pji->wZpos = KJ_BlendJoyValue(pji->wZpos, s_virtualJoy.dwZ); + + if (KJ_HasAnyGamepadMappings()) pji->wButtons = 0; + pji->wButtons = static_cast(pji->wButtons | (s_virtualJoy.dwButtons & 0xFFFF)); + + LeaveCriticalSection(&s_joyLock); + } + return res; + } + + // No real joystick, return virtual state + if (!s_joyInitialized) KJ_Input_Install_Joystick(); + + EnterCriticalSection(&s_joyLock); + pji->wXpos = static_cast(s_virtualJoy.dwX & 0xFFFF); + pji->wYpos = static_cast(s_virtualJoy.dwY & 0xFFFF); + pji->wZpos = static_cast(s_virtualJoy.dwZ & 0xFFFF); + pji->wButtons = static_cast(s_virtualJoy.dwButtons & 0xFFFF); + LeaveCriticalSection(&s_joyLock); + return MMSYSERR_NOERROR; +} + +static MMRESULT WINAPI KJ_joyGetPosEx(UINT uJoyID, LPJOYINFOEX pji) { + if (!pji || pji->dwSize < sizeof(JOYINFOEX)) return MMSYSERR_INVALPARAM; + + KJ_Ensure_WinMM_Originals(); + bool haveReal = (orig_joyGetPosEx != nullptr) && KJ_IsRealJoystickActive(uJoyID); + + if (haveReal) { + MMRESULT res = orig_joyGetPosEx(uJoyID, pji); + if (res == MMSYSERR_NOERROR && s_joyInitialized) { + EnterCriticalSection(&s_joyLock); + + pji->dwXpos = KJ_BlendJoyValue(pji->dwXpos, s_virtualJoy.dwX); + pji->dwYpos = KJ_BlendJoyValue(pji->dwYpos, s_virtualJoy.dwY); + pji->dwZpos = KJ_BlendJoyValue(pji->dwZpos, s_virtualJoy.dwZ); + pji->dwRpos = KJ_BlendJoyValue(pji->dwRpos, s_virtualJoy.dwR); + pji->dwUpos = KJ_BlendJoyValue(pji->dwUpos, s_virtualJoy.dwU); + pji->dwVpos = KJ_BlendJoyValue(pji->dwVpos, s_virtualJoy.dwV); + + if (KJ_HasAnyGamepadMappings()) { + pji->dwButtons = 0; + pji->dwPOV = JOY_POVCENTERED; + } + pji->dwButtons |= s_virtualJoy.dwButtons; + if (s_virtualJoy.dwPOV != JOY_POVCENTERED) + pji->dwPOV = s_virtualJoy.dwPOV; + + LeaveCriticalSection(&s_joyLock); + } + return res; + } + + // No real joystick, return virtual state + if (!s_joyInitialized) KJ_Input_Install_Joystick(); + + EnterCriticalSection(&s_joyLock); + pji->dwXpos = s_virtualJoy.dwX; + pji->dwYpos = s_virtualJoy.dwY; + pji->dwZpos = s_virtualJoy.dwZ; + pji->dwRpos = s_virtualJoy.dwR; + pji->dwUpos = s_virtualJoy.dwU; + pji->dwVpos = s_virtualJoy.dwV; + pji->dwButtons = s_virtualJoy.dwButtons; + pji->dwPOV = s_virtualJoy.dwPOV; + LeaveCriticalSection(&s_joyLock); + return MMSYSERR_NOERROR; +} + +static MMRESULT WINAPI KJ_joyGetDevCapsA(UINT uJoyID, LPJOYCAPSA pjc, UINT cbjc) { + if (!pjc || cbjc < sizeof(JOYCAPSA)) return MMSYSERR_INVALPARAM; + + KJ_Ensure_WinMM_Originals(); + if (orig_joyGetDevCapsA) { + MMRESULT res = orig_joyGetDevCapsA(uJoyID, pjc, cbjc); + if (res == MMSYSERR_NOERROR) return res; + } + + ZeroMemory(pjc, cbjc); + pjc->wMid = 0; pjc->wPid = 0; pjc->szPname[0] = '\0'; + pjc->wXmin = 0; pjc->wXmax = 65535; + pjc->wYmin = 0; pjc->wYmax = 65535; + pjc->wZmin = 0; pjc->wZmax = 65535; + pjc->wNumAxes = 3; pjc->wMaxAxes = 6; pjc->wNumButtons = 32; + pjc->wPeriodMin = 0; pjc->wPeriodMax = 0; + return MMSYSERR_NOERROR; +} + +static MMRESULT WINAPI KJ_joyGetDevCapsW(UINT uJoyID, LPJOYCAPSW pjc, UINT cbjc) { + if (!pjc || cbjc < sizeof(JOYCAPSW)) return MMSYSERR_INVALPARAM; + + KJ_Ensure_WinMM_Originals(); + if (orig_joyGetDevCapsW) { + MMRESULT res = orig_joyGetDevCapsW(uJoyID, pjc, cbjc); + if (res == MMSYSERR_NOERROR) return res; + } + + ZeroMemory(pjc, cbjc); + pjc->wMid = 0; pjc->wPid = 0; pjc->szPname[0] = L'\0'; + pjc->wXmin = 0; pjc->wXmax = 65535; + pjc->wYmin = 0; pjc->wYmax = 65535; + pjc->wZmin = 0; pjc->wZmax = 65535; + pjc->wNumAxes = 3; pjc->wMaxAxes = 6; pjc->wNumButtons = 32; + pjc->wPeriodMin = 0; pjc->wPeriodMax = 0; + return MMSYSERR_NOERROR; +} \ No newline at end of file diff --git a/dist/wcdx/src/key_jacker/KJ_Keyboard.h b/dist/wcdx/src/key_jacker/KJ_Keyboard.h new file mode 100644 index 0000000..652c962 --- /dev/null +++ b/dist/wcdx/src/key_jacker/KJ_Keyboard.h @@ -0,0 +1,269 @@ +//*****************************************KJ_Keyboard.h****************************************** +// Low-Level Keyboard Hook and Input Injection System. +// Install via KJ_Main +// +// Usage: +// KJ_Input_InjectKey(keyCode, down) - Injects a key press/release +// KJ_Input_InjectKeyFastRepeat(keyCode) - Injects a key press with auto-repeat until released +// +//************************************************************************************************* + +#pragma once + +#include +#include +#include +#include +#include + +#include "wcdx.h" + +#include "se_/SE_Controls.h" + +static HHOOK s_keyboardHook = nullptr; + +// Input Injection State +static CRITICAL_SECTION s_injectLock; +static std::queue s_injectQueue; +static HANDLE s_injectEvent = nullptr; +static HANDLE s_injectThread = nullptr; +static bool s_injectThreadInit = false; +static volatile LONG s_injectStop = 0; + +struct InjectedKeyInfo { + DWORD initialTick = 0; + DWORD lastRepeatTick = 0; + bool initialConsumed = false; +}; + +static std::unordered_map s_injectedKeys; +static DWORD s_initialDelayMs = 500; +static DWORD s_repeatIntervalMs = 50; + +// Low-Level Keyboard Hook Callback +LRESULT CALLBACK LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) { + if (nCode < 0) return ::CallNextHookEx(s_keyboardHook, nCode, wParam, lParam); + + auto kb = reinterpret_cast(lParam); + if (!kb) return ::CallNextHookEx(s_keyboardHook, nCode, wParam, lParam); + + // Ignore injected input and our own tagged events + if ((kb->flags & LLKHF_INJECTED) || kb->dwExtraInfo == WCDX_INJECT_TAG) + return ::CallNextHookEx(s_keyboardHook, nCode, wParam, lParam); + + // Swallow mapped keys, notify controls system + if (SE_Controls_IsMapped(static_cast(kb->vkCode))) { + bool down = (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN); + SE_Controls_NotifyPhysicalKey(static_cast(kb->vkCode), down); + return 1; // Block event from reaching app + } + + return ::CallNextHookEx(s_keyboardHook, nCode, wParam, lParam); +} + +// Input Injection Thread (used by keyboard injection) +static DWORD WINAPI KJ_InjectThreadProc(LPVOID) { + for (;;) { + // Check if we have keys being held down + EnterCriticalSection(&s_injectLock); + bool hasInjectedKeys = !s_injectedKeys.empty(); + LeaveCriticalSection(&s_injectLock); + + // Wait with timeout if we have keys to repeat, otherwise wait indefinitely + DWORD waitTime = hasInjectedKeys ? (s_repeatIntervalMs / 4) : INFINITE; + if (waitTime == 0) waitTime = 1; + + DWORD waitResult = WaitForSingleObject(s_injectEvent, waitTime); + if (waitResult != WAIT_OBJECT_0 && waitResult != WAIT_TIMEOUT) break; + if (InterlockedCompareExchange(&s_injectStop, 0, 0) != 0) break; + + // Drain input queue + while (true) { + EnterCriticalSection(&s_injectLock); + if (s_injectQueue.empty()) { + ResetEvent(s_injectEvent); + LeaveCriticalSection(&s_injectLock); + break; + } + INPUT inp = s_injectQueue.front(); + s_injectQueue.pop(); + LeaveCriticalSection(&s_injectLock); + ::SendInput(1, &inp, sizeof(INPUT)); + } + + // Auto-repeat for injected keys + if (InterlockedCompareExchange(&s_injectStop, 0, 0) != 0) break; + + EnterCriticalSection(&s_injectLock); + if (!s_injectedKeys.empty()) { + DWORD now = GetTickCount(); + + for (auto& kv : s_injectedKeys) { + UINT vk = kv.first; + InjectedKeyInfo& info = kv.second; + + INPUT inp = {}; + inp.type = INPUT_KEYBOARD; + inp.ki.wScan = static_cast(MapVirtualKey(vk, MAPVK_VK_TO_VSC)); + inp.ki.dwFlags = KEYEVENTF_SCANCODE; + inp.ki.dwExtraInfo = WCDX_INJECT_TAG; + + if (!info.initialConsumed && now - info.initialTick >= s_initialDelayMs) { + ::SendInput(1, &inp, sizeof(INPUT)); + info.initialConsumed = true; + info.lastRepeatTick = now; + } else if (info.initialConsumed && now - info.lastRepeatTick >= s_repeatIntervalMs) { + ::SendInput(1, &inp, sizeof(INPUT)); + info.lastRepeatTick = now; + } + } + } + LeaveCriticalSection(&s_injectLock); + } + return 0; +} + +static void EnsureInjectThread() { + if (s_injectThreadInit) return; + + InterlockedExchange(&s_injectStop, 0); + InitializeCriticalSection(&s_injectLock); + + UINT delayVal = 0, speedVal = 0; + SystemParametersInfo(SPI_GETKEYBOARDDELAY, 0, &delayVal, 0); + SystemParametersInfo(SPI_GETKEYBOARDSPEED, 0, &speedVal, 0); + + s_initialDelayMs = (delayVal + 1) * 250; + double charsPerSec = 2.0 + (static_cast(speedVal) * (28.0 / 31.0)); + s_repeatIntervalMs = static_cast(1000.0 / (charsPerSec < 1.0 ? 1.0 : charsPerSec)); + + s_injectEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr); + s_injectThread = CreateThread(nullptr, 0, KJ_InjectThreadProc, nullptr, 0, nullptr); + s_injectThreadInit = true; +} + +static inline bool KJ_Input_InstallKeyboardHook(HINSTANCE hInstance) { + if (s_keyboardHook) return true; + s_keyboardHook = ::SetWindowsHookEx(WH_KEYBOARD_LL, LowLevelKeyboardProc, hInstance, 0); + return s_keyboardHook != nullptr; +} + +static inline void KJ_Input_UninstallKeyboardHook() { + if (s_keyboardHook) { + ::UnhookWindowsHookEx(s_keyboardHook); + s_keyboardHook = nullptr; + } +} + +// Release all currently injected keys +static void KJ_Keyboard_ReleaseAllKeys() { + if (!s_injectThreadInit) return; + + EnterCriticalSection(&s_injectLock); + for (const auto& pair : s_injectedKeys) { + UINT vk = pair.first; + INPUT input = {}; + input.type = INPUT_KEYBOARD; + input.ki.wScan = static_cast(MapVirtualKey(vk, MAPVK_VK_TO_VSC)); + input.ki.wVk = static_cast(vk); + input.ki.dwFlags = KEYEVENTF_SCANCODE | KEYEVENTF_KEYUP; + input.ki.dwExtraInfo = WCDX_INJECT_TAG; + ::SendInput(1, &input, sizeof(input)); + // Also send VK-based keyup for extra safety + input.ki.dwFlags = KEYEVENTF_KEYUP; + ::SendInput(1, &input, sizeof(input)); + } + s_injectedKeys.clear(); + LeaveCriticalSection(&s_injectLock); +} + +// Shutdown injection thread and clear all state +static void KJ_Keyboard_Shutdown() { + // Release all keys before stopping thread + KJ_Keyboard_ReleaseAllKeys(); + + InterlockedExchange(&s_injectStop, 1); + if (s_injectEvent) SetEvent(s_injectEvent); + + if (s_injectThread) { + WaitForSingleObject(s_injectThread, 2000); + CloseHandle(s_injectThread); + s_injectThread = nullptr; + } + if (s_injectEvent) { + CloseHandle(s_injectEvent); + s_injectEvent = nullptr; + } + + if (s_injectThreadInit) { + EnterCriticalSection(&s_injectLock); + while (!s_injectQueue.empty()) s_injectQueue.pop(); + s_injectedKeys.clear(); + LeaveCriticalSection(&s_injectLock); + DeleteCriticalSection(&s_injectLock); + } + s_injectThreadInit = false; +} + +// Inject a low-level keyboard event +static void KJ_Input_InjectKey(UINT vk, bool down) { + EnsureInjectThread(); + + EnterCriticalSection(&s_injectLock); + + if (down) { + if (s_injectedKeys.count(vk)) { + LeaveCriticalSection(&s_injectLock); + return; + } + InjectedKeyInfo info; + info.initialTick = GetTickCount(); + info.initialConsumed = false; + info.lastRepeatTick = info.initialTick; + s_injectedKeys[vk] = info; + } else { + s_injectedKeys.erase(vk); + } + + INPUT input = {}; + input.type = INPUT_KEYBOARD; + input.ki.wScan = static_cast(MapVirtualKey(vk, MAPVK_VK_TO_VSC)); + input.ki.dwFlags = KEYEVENTF_SCANCODE | (down ? 0 : KEYEVENTF_KEYUP); + input.ki.dwExtraInfo = WCDX_INJECT_TAG; + s_injectQueue.push(input); + + LeaveCriticalSection(&s_injectLock); + SetEvent(s_injectEvent); +} + +// Inject a key-down and start auto-repeat immediately +static void KJ_Input_InjectKeyFastRepeat(UINT vk) { + EnsureInjectThread(); + const DWORD now = GetTickCount(); + + EnterCriticalSection(&s_injectLock); + + auto it = s_injectedKeys.find(vk); + if (it == s_injectedKeys.end()) { + InjectedKeyInfo info; + info.initialTick = now; + info.initialConsumed = true; + info.lastRepeatTick = (s_repeatIntervalMs > 0 && now >= s_repeatIntervalMs) + ? (now - s_repeatIntervalMs) : 0; + s_injectedKeys[vk] = info; + + INPUT input = {}; + input.type = INPUT_KEYBOARD; + input.ki.wScan = static_cast(MapVirtualKey(vk, MAPVK_VK_TO_VSC)); + input.ki.dwFlags = KEYEVENTF_SCANCODE; + input.ki.dwExtraInfo = WCDX_INJECT_TAG; + s_injectQueue.push(input); + } else { + it->second.initialConsumed = true; + it->second.lastRepeatTick = (s_repeatIntervalMs > 0 && now >= s_repeatIntervalMs) + ? (now - s_repeatIntervalMs) : 0; + } + + LeaveCriticalSection(&s_injectLock); + SetEvent(s_injectEvent); +} diff --git a/dist/wcdx/src/key_jacker/KJ_Main.h b/dist/wcdx/src/key_jacker/KJ_Main.h new file mode 100644 index 0000000..92fcfdd --- /dev/null +++ b/dist/wcdx/src/key_jacker/KJ_Main.h @@ -0,0 +1,110 @@ +//*******************************************KJ_Main.h******************************************** +// Main header for Key_Jacker input injection and hooking system. +// Hooks keyboard, mouse, and joystick input to allow for remapping and virtual device injection. +// Added XInput hooking to support Xbox controller remapping. +// +// Install Function: +// #include "key_jacker/KJ_Main.h" +// +// KJ_Input_Install(HINSTANCE hInstance) +// +// Uninstall Function: +// KJ_Input_Uninstall() +//************************************************************************************************* + +#pragma once + +#include +#include "wcdx.h" +#include "se_/SE_Controls.h" +#include "key_jacker/KJ_Keyboard.h" +#include "key_jacker/KJ_Mouse.h" +#include "key_jacker/KJ_Joystick.h" +#include "key_jacker/KJ_Xinput.h" + +// Compatibility for missing SDK flags +#ifndef LLKHF_INJECTED +#define LLKHF_INJECTED 0x00000010 +#endif +#ifndef LLMHF_INJECTED +#define LLMHF_INJECTED 0x00000001 +#endif + +//============================================================================= +// Input System Management +//============================================================================= + +// Release all currently mapped physical keys to prevent stuck keys +static void KJ_Input_ReleaseAllPhysicalKeys() { + auto heldKeys = SE_Controls_GetCurrentlyHeldKeys(); + for (int keyCode : heldKeys) { + // Keyboard keys + if (keyCode > 0 && keyCode < 256) { + INPUT input = {}; + input.type = INPUT_KEYBOARD; + input.ki.wVk = static_cast(keyCode); + input.ki.dwFlags = KEYEVENTF_KEYUP; + input.ki.dwExtraInfo = WCDX_INJECT_TAG; + ::SendInput(1, &input, sizeof(input)); + // Also send scan code keyup for safety + input.ki.wScan = static_cast(MapVirtualKey(keyCode, MAPVK_VK_TO_VSC)); + input.ki.dwFlags = KEYEVENTF_SCANCODE | KEYEVENTF_KEYUP; + ::SendInput(1, &input, sizeof(input)); + Sleep(1); + } + // Mouse buttons (negative codes -1 to -5) + else if (keyCode >= -5 && keyCode < 0) { + INPUT input = {}; + input.type = INPUT_MOUSE; + input.mi.dwExtraInfo = WCDX_INJECT_TAG; + switch (keyCode) { + case -1: input.mi.dwFlags = MOUSEEVENTF_LEFTUP; break; + case -2: input.mi.dwFlags = MOUSEEVENTF_MIDDLEUP; break; + case -3: input.mi.dwFlags = MOUSEEVENTF_RIGHTUP; break; + case -4: // XBUTTON1 + case -5: // XBUTTON2 + input.mi.dwFlags = MOUSEEVENTF_XUP; + input.mi.mouseData = (keyCode == -4) ? XBUTTON1 : XBUTTON2; + break; + } + ::SendInput(1, &input, sizeof(input)); + Sleep(1); + } + } +} + +// Install all input hooks and start input handling +static inline void KJ_Input_Install(HINSTANCE hInstance) { + + // Clear any stuck keys/buttons from previous run + KJ_Input_ReleaseAllPhysicalKeys(); + KJ_Keyboard_ReleaseAllKeys(); + KJ_Mouse_ReleaseAllButtons(); + KJ_ResetVirtualJoystick(); + + // Install hooks + KJ_Input_InstallKeyboardHook(hInstance); + KJ_Input_InstallMouseHook(hInstance); + KJ_Input_Install_Joystick(); + InstallXInputIATHooks(); + InstallGetProcAddressIATHook(); +} + +// Uninstall all hooks and clear all input states +static inline void KJ_Input_Uninstall() { + // Uninstall hooks first + KJ_Input_UninstallKeyboardHook(); + KJ_Input_UninstallMouseHook(); + + // Clear all input states + KJ_Input_ReleaseAllPhysicalKeys(); + KJ_Keyboard_ReleaseAllKeys(); + KJ_Mouse_ReleaseAllButtons(); + KJ_ResetVirtualJoystick(); + + // Shutdown subsystems + KJ_Keyboard_Shutdown(); + KJ_Joystick_Shutdown(); + KJ_XInput_Shutdown(); +} + diff --git a/dist/wcdx/src/key_jacker/KJ_Mouse.h b/dist/wcdx/src/key_jacker/KJ_Mouse.h new file mode 100644 index 0000000..e0655b1 --- /dev/null +++ b/dist/wcdx/src/key_jacker/KJ_Mouse.h @@ -0,0 +1,157 @@ +//**********************************99*******KJ_Mouse.h*******99*********************************** +// Low-Level Mouse Hook and Input Injection System. +// Install via KJ_Main +// +// Usage: +// KJ_Input_InjectMouseButton(button, down) - Injects a mouse button press/release +// KJ_Input_InjectMouseMove(x, y, absolute) - Injects mouse movement (absolute or relative) +// KJ_Input_InjectMouseSignal() - Injects a mouse move signal to clear mouse button states +// +//************************************************************************************************* + +#pragma once + +#include +#include +#include +#include +#include + +#include "wcdx.h" + +#include "se_/SE_Controls.h" + +static HHOOK s_mouseHook = nullptr; + +// Low-Level Mouse Hook Callback +LRESULT CALLBACK LowLevelMouseProc(int nCode, WPARAM wParam, LPARAM lParam) { + if (nCode < 0) return ::CallNextHookEx(s_mouseHook, nCode, wParam, lParam); + + auto ms = reinterpret_cast(lParam); + if (!ms) return ::CallNextHookEx(s_mouseHook, nCode, wParam, lParam); + + if ((ms->flags & LLMHF_INJECTED) || ms->dwExtraInfo == WCDX_INJECT_TAG) + return ::CallNextHookEx(s_mouseHook, nCode, wParam, lParam); + + int keyCode = 0; + bool isDown = false; + + switch (wParam) { + case WM_LBUTTONDOWN: keyCode = -1; isDown = true; break; + case WM_LBUTTONUP: keyCode = -1; isDown = false; break; + case WM_MBUTTONDOWN: keyCode = -2; isDown = true; break; + case WM_MBUTTONUP: keyCode = -2; isDown = false; break; + case WM_RBUTTONDOWN: keyCode = -3; isDown = true; break; + case WM_RBUTTONUP: keyCode = -3; isDown = false; break; + case WM_XBUTTONDOWN: + case WM_XBUTTONUP: { + int which = HIWORD(ms->mouseData); + keyCode = (which == XBUTTON1) ? -4 : -5; + isDown = (wParam == WM_XBUTTONDOWN); + break; + } + default: return ::CallNextHookEx(s_mouseHook, nCode, wParam, lParam); + } + + // Mouse move intentionally not mapped here + if (keyCode != 0 && SE_Controls_IsMapped(keyCode)) { + SE_Controls_NotifyPhysicalKey(keyCode, isDown); + return 1; + } + + return ::CallNextHookEx(s_mouseHook, nCode, wParam, lParam); +} + +static inline bool KJ_Input_InstallMouseHook(HINSTANCE hInstance) { + if (s_mouseHook) return true; + s_mouseHook = ::SetWindowsHookEx(WH_MOUSE_LL, LowLevelMouseProc, hInstance, 0); + return s_mouseHook != nullptr; +} + +static inline void KJ_Input_UninstallMouseHook() { + if (s_mouseHook) { + ::UnhookWindowsHookEx(s_mouseHook); + s_mouseHook = nullptr; + } +} + +// Release all currently held mouse buttons +static void KJ_Mouse_ReleaseAllButtons() { + // Get all currently held mouse button codes from controls system + auto heldKeys = SE_Controls_GetCurrentlyHeldKeys(); + for (int keyCode : heldKeys) { + // Mouse buttons have negative codes + if (keyCode < 0 && keyCode >= -5) { + INPUT input = {}; + input.type = INPUT_MOUSE; + input.mi.dwExtraInfo = WCDX_INJECT_TAG; + switch (keyCode) { + case -1: input.mi.dwFlags = MOUSEEVENTF_LEFTUP; break; + case -2: input.mi.dwFlags = MOUSEEVENTF_MIDDLEUP; break; + case -3: input.mi.dwFlags = MOUSEEVENTF_RIGHTUP; break; + case -4: // XBUTTON1 + case -5: // XBUTTON2 + input.mi.dwFlags = MOUSEEVENTF_XUP; + input.mi.mouseData = (keyCode == -4) ? XBUTTON1 : XBUTTON2; + break; + default: continue; + } + ::SendInput(1, &input, sizeof(input)); + } + } +} + +// Inject a mouse button event (button: 0=left, 1=right, 2=middle) +inline void KJ_Input_InjectMouseButton(int button, bool down) { + INPUT input = {}; + input.type = INPUT_MOUSE; + input.mi.dwExtraInfo = WCDX_INJECT_TAG; + + switch (button) { + case 0: input.mi.dwFlags = down ? MOUSEEVENTF_LEFTDOWN : MOUSEEVENTF_LEFTUP; break; + case 1: input.mi.dwFlags = down ? MOUSEEVENTF_RIGHTDOWN : MOUSEEVENTF_RIGHTUP; break; + case 2: input.mi.dwFlags = down ? MOUSEEVENTF_MIDDLEDOWN : MOUSEEVENTF_MIDDLEUP; break; + default: return; + } + + ::SendInput(1, &input, sizeof(input)); +} + +// Inject absolute or relative mouse movement +inline void KJ_Input_InjectMouseMove(int x, int y, bool absolute = false) { + INPUT input = {}; + input.type = INPUT_MOUSE; + input.mi.dwExtraInfo = WCDX_INJECT_TAG; + + if (absolute) { + int sx = GetSystemMetrics(SM_CXSCREEN); + int sy = GetSystemMetrics(SM_CYSCREEN); + input.mi.dx = (sx > 0) ? (LONG)((x * 65535) / sx) : 0; + input.mi.dy = (sy > 0) ? (LONG)((y * 65535) / sy) : 0; + input.mi.dwFlags = MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE; + } else { + input.mi.dx = x; + input.mi.dy = y; + input.mi.dwFlags = MOUSEEVENTF_MOVE; + } + + ::SendInput(1, &input, sizeof(input)); +} + +// Inject a small synthetic mouse "signal" (for games that poll mouse movement) +inline void KJ_Input_InjectMouseSignal() { + INPUT inputs[2] = {}; + inputs[0].type = INPUT_MOUSE; + inputs[0].mi.dwFlags = MOUSEEVENTF_MOVE; + inputs[0].mi.dwExtraInfo = WCDX_INJECT_TAG; + inputs[1].type = INPUT_MOUSE; + inputs[1].mi.dwFlags = MOUSEEVENTF_MOVE; + inputs[1].mi.dwExtraInfo = WCDX_INJECT_TAG; + + for (int i = 0; i < 6; ++i) { + inputs[0].mi.dx = 2; + inputs[1].mi.dx = -2; + ::SendInput(2, inputs, sizeof(INPUT)); + Sleep(2); + } +} diff --git a/dist/wcdx/src/key_jacker/KJ_Xinput.h b/dist/wcdx/src/key_jacker/KJ_Xinput.h new file mode 100644 index 0000000..2b34ef4 --- /dev/null +++ b/dist/wcdx/src/key_jacker/KJ_Xinput.h @@ -0,0 +1,305 @@ +//******************************************KJ_Xinput.h******************************************* +// Low-Level XInput Hook and Input Injection System. +// Install via KJ_Main +// +// Usage: +// Currently this only suppresses XInput state for mapped buttons to allow for remapping to +// keyboard/mouse/joystick inputs. +// +//************************************************************************************************* + +#pragma once + +#include +#include +#include "wcdx.h" +#include "se_/SE_Controls.h" + +//============================================================================= +// XInput State Structures +//============================================================================= + +// Minimal XInput structs (avoid linking XInput.lib) + struct KJ_XINPUT_GAMEPAD { + WORD wButtons; + BYTE bLeftTrigger; + BYTE bRightTrigger; + SHORT sThumbLX; + SHORT sThumbLY; + SHORT sThumbRX; + SHORT sThumbRY; + }; + struct KJ_XINPUT_STATE { + DWORD dwPacketNumber; + KJ_XINPUT_GAMEPAD Gamepad; + }; + +typedef DWORD(WINAPI* KJ_XInputGetState_t)(DWORD, KJ_XINPUT_STATE*); +static HMODULE s_hXInput = nullptr; +static bool s_xinputOriginalsInit = false; +static KJ_XInputGetState_t orig_XInputGetState = nullptr; + +//============================================================================= +// XInput Initialization +//============================================================================= + +static void EnsureXInputOriginals() { + if (s_xinputOriginalsInit) return; + s_xinputOriginalsInit = true; + + const wchar_t* xinputDlls[] = { + L"xinput1_4.dll", L"xinput1_3.dll", L"xinput9_1_0.dll", + L"xinput1_2.dll", L"xinput1_1.dll" + }; + + for (const wchar_t* dll : xinputDlls) { + s_hXInput = LoadLibraryW(dll); + if (s_hXInput) break; + } + + if (s_hXInput) + orig_XInputGetState = (KJ_XInputGetState_t)GetProcAddress(s_hXInput, "XInputGetState"); +} + +//============================================================================= +// Helper Functions +//============================================================================= + +static inline bool KJ_IsXInputDllName(const char* dllName) { + if (!dllName) return false; + return (_stricmp(dllName, "xinput1_4.dll") == 0) || + (_stricmp(dllName, "xinput1_3.dll") == 0) || + (_stricmp(dllName, "xinput9_1_0.dll") == 0) || + (_stricmp(dllName, "xinput1_2.dll") == 0) || + (_stricmp(dllName, "xinput1_1.dll") == 0); +} + +static inline bool KJ_IsXInputModuleHandle(HMODULE h) { + if (!h) return false; + + wchar_t path[MAX_PATH] = {0}; + if (GetModuleFileNameW(h, path, MAX_PATH) == 0) return false; + + const wchar_t* file = path; + for (const wchar_t* p = path; *p; ++p) + if (*p == L'\\' || *p == L'/') file = p + 1; + + return (_wcsicmp(file, L"xinput1_4.dll") == 0) || + (_wcsicmp(file, L"xinput1_3.dll") == 0) || + (_wcsicmp(file, L"xinput9_1_0.dll") == 0) || + (_wcsicmp(file, L"xinput1_2.dll") == 0) || + (_wcsicmp(file, L"xinput1_1.dll") == 0); +} + +//============================================================================= +// XInput State Suppression +//============================================================================= + +static inline void KJ_ApplyXInputSuppression(KJ_XINPUT_STATE* pState) { + if (!pState) return; + + // Build button clear mask (optimized: checks are inlined) + WORD clearButtons = 0; + if (SE_Controls_IsMapped(-10)) clearButtons |= 0x1000; // A + if (SE_Controls_IsMapped(-11)) clearButtons |= 0x2000; // B + if (SE_Controls_IsMapped(-12)) clearButtons |= 0x4000; // X + if (SE_Controls_IsMapped(-13)) clearButtons |= 0x8000; // Y + if (SE_Controls_IsMapped(-14)) clearButtons |= 0x0100; // LB + if (SE_Controls_IsMapped(-15)) clearButtons |= 0x0200; // RB + if (SE_Controls_IsMapped(-16)) clearButtons |= 0x0040; // LS Click + if (SE_Controls_IsMapped(-17)) clearButtons |= 0x0080; // RS Click + if (SE_Controls_IsMapped(-18)) clearButtons |= 0x0020; // Back + if (SE_Controls_IsMapped(-19)) clearButtons |= 0x0010; // Start + if (SE_Controls_IsMapped(-20)) clearButtons |= 0x0001; // DPad Up + if (SE_Controls_IsMapped(-21)) clearButtons |= 0x0002; // DPad Down + if (SE_Controls_IsMapped(-22)) clearButtons |= 0x0004; // DPad Left + if (SE_Controls_IsMapped(-23)) clearButtons |= 0x0008; // DPad Right + + pState->Gamepad.wButtons &= ~clearButtons; + + // Clear triggers if mapped + if (SE_Controls_IsMapped(-24)) pState->Gamepad.bLeftTrigger = 0; + if (SE_Controls_IsMapped(-25)) pState->Gamepad.bRightTrigger = 0; + + // Clear thumbsticks if mapped + if (SE_Controls_IsMapped(-28) || SE_Controls_IsMapped(-29)) pState->Gamepad.sThumbLX = 0; + if (SE_Controls_IsMapped(-26) || SE_Controls_IsMapped(-27)) pState->Gamepad.sThumbLY = 0; + if (SE_Controls_IsMapped(-32) || SE_Controls_IsMapped(-33)) pState->Gamepad.sThumbRX = 0; + if (SE_Controls_IsMapped(-30) || SE_Controls_IsMapped(-31)) pState->Gamepad.sThumbRY = 0; +} + +//============================================================================= +// XInput Hook Functions +//============================================================================= + +static DWORD WINAPI KJ_XInputGetState(DWORD dwUserIndex, KJ_XINPUT_STATE* pState) { + EnsureXInputOriginals(); + if (!orig_XInputGetState) return ERROR_DEVICE_NOT_CONNECTED; + + DWORD res = orig_XInputGetState(dwUserIndex, pState); + if (res == ERROR_SUCCESS && pState) + KJ_ApplyXInputSuppression(pState); + + return res; +} + +//============================================================================= +// GetProcAddress Hook (intercept dynamic XInput loads) +//============================================================================= + +typedef FARPROC(WINAPI* KJ_GetProcAddress_t)(HMODULE, LPCSTR); +static bool s_getProcAddressOriginalInit = false; +static KJ_GetProcAddress_t orig_GetProcAddress = nullptr; + +static void EnsureGetProcAddressOriginal() { + if (s_getProcAddressOriginalInit) return; + s_getProcAddressOriginalInit = true; + + HMODULE hKernel32 = GetModuleHandleW(L"kernel32.dll"); + if (hKernel32) + orig_GetProcAddress = (KJ_GetProcAddress_t)::GetProcAddress(hKernel32, "GetProcAddress"); +} + +static FARPROC WINAPI KJ_GetProcAddress(HMODULE hModule, LPCSTR lpProcName) { + EnsureGetProcAddressOriginal(); + + // Check if requesting XInputGetState from an XInput DLL + if (lpProcName && (reinterpret_cast(lpProcName) > 0xFFFF)) { + if (_stricmp(lpProcName, "XInputGetState") == 0 && KJ_IsXInputModuleHandle(hModule)) + return (FARPROC)&KJ_XInputGetState; + } + + return orig_GetProcAddress ? orig_GetProcAddress(hModule, lpProcName) : nullptr; +} + +//============================================================================= +// IAT Patching +//============================================================================= + +static void InstallXInputIATHooks() { + auto patchModule = [](HMODULE hModule) { + if (!hModule) return; + + unsigned char* base = reinterpret_cast(hModule); + auto dos = reinterpret_cast(base); + if (dos->e_magic != IMAGE_DOS_SIGNATURE) return; + + auto nt = reinterpret_cast(base + dos->e_lfanew); + if (nt->Signature != IMAGE_NT_SIGNATURE) return; + + auto& dir = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]; + if (dir.VirtualAddress == 0 || dir.Size == 0) return; + + auto imports = reinterpret_cast(base + dir.VirtualAddress); + if (!imports) return; + + for (; imports->Name != 0; ++imports) { + const char* dllName = reinterpret_cast(base + imports->Name); + if (!KJ_IsXInputDllName(dllName)) continue; + + PIMAGE_THUNK_DATA orig = reinterpret_cast(base + imports->OriginalFirstThunk); + PIMAGE_THUNK_DATA thunk = reinterpret_cast(base + imports->FirstThunk); + + for (; orig->u1.AddressOfData != 0; ++orig, ++thunk) { + if (orig->u1.Ordinal & IMAGE_ORDINAL_FLAG) continue; + + auto importByName = reinterpret_cast(base + orig->u1.AddressOfData); + const char* name = reinterpret_cast(importByName->Name); + + if (_stricmp(name, "XInputGetState") == 0) { + DWORD oldProtect; + VirtualProtect(&thunk->u1.Function, sizeof(void*), PAGE_READWRITE, &oldProtect); + thunk->u1.Function = reinterpret_cast(&KJ_XInputGetState); + VirtualProtect(&thunk->u1.Function, sizeof(void*), oldProtect, &oldProtect); + } + } + } + }; + + patchModule(GetModuleHandle(nullptr)); + + HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, GetCurrentProcessId()); + if (snap == INVALID_HANDLE_VALUE) return; + + MODULEENTRY32 me; + me.dwSize = sizeof(me); + if (Module32First(snap, &me)) { + do { patchModule(me.hModule); } while (Module32Next(snap, &me)); + } + CloseHandle(snap); +} + +static void InstallGetProcAddressIATHook() { + EnsureGetProcAddressOriginal(); + if (!orig_GetProcAddress) return; + + auto patchModule = [](HMODULE hModule) { + if (!hModule) return; + + unsigned char* base = reinterpret_cast(hModule); + auto dos = reinterpret_cast(base); + if (dos->e_magic != IMAGE_DOS_SIGNATURE) return; + + auto nt = reinterpret_cast(base + dos->e_lfanew); + if (nt->Signature != IMAGE_NT_SIGNATURE) return; + + auto& dir = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]; + if (dir.VirtualAddress == 0 || dir.Size == 0) return; + + auto imports = reinterpret_cast(base + dir.VirtualAddress); + if (!imports) return; + + for (; imports->Name != 0; ++imports) { + const char* dllName = reinterpret_cast(base + imports->Name); + const bool isKernel = (_stricmp(dllName, "kernel32.dll") == 0) || + (_stricmp(dllName, "KernelBase.dll") == 0); + if (!isKernel) continue; + + PIMAGE_THUNK_DATA orig = reinterpret_cast(base + imports->OriginalFirstThunk); + PIMAGE_THUNK_DATA thunk = reinterpret_cast(base + imports->FirstThunk); + + for (; orig->u1.AddressOfData != 0; ++orig, ++thunk) { + if (orig->u1.Ordinal & IMAGE_ORDINAL_FLAG) continue; + + auto importByName = reinterpret_cast(base + orig->u1.AddressOfData); + const char* name = reinterpret_cast(importByName->Name); + + if (_stricmp(name, "GetProcAddress") == 0) { + DWORD oldProtect; + VirtualProtect(&thunk->u1.Function, sizeof(void*), PAGE_READWRITE, &oldProtect); + thunk->u1.Function = reinterpret_cast(&KJ_GetProcAddress); + VirtualProtect(&thunk->u1.Function, sizeof(void*), oldProtect, &oldProtect); + } + } + } + }; + + patchModule(GetModuleHandle(nullptr)); + + HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, GetCurrentProcessId()); + if (snap == INVALID_HANDLE_VALUE) return; + + MODULEENTRY32 me; + me.dwSize = sizeof(me); + if (Module32First(snap, &me)) { + do { patchModule(me.hModule); } while (Module32Next(snap, &me)); + } + CloseHandle(snap); +} + +//============================================================================= +// Shutdown +//============================================================================= + +// Shutdown XInput system and cleanup resources +static void KJ_XInput_Shutdown() { + // Cleanup loaded library + if (s_hXInput) { + FreeLibrary(s_hXInput); + s_hXInput = nullptr; + } + + // Reset function pointers and flags + orig_XInputGetState = nullptr; + s_xinputOriginalsInit = false; +} \ No newline at end of file diff --git a/dist/wcdx/src/main.cpp b/dist/wcdx/src/main.cpp index 1a909f1..2f5a008 100644 --- a/dist/wcdx/src/main.cpp +++ b/dist/wcdx/src/main.cpp @@ -1,4 +1,6 @@ #include "platform.h" +#include "se_/SE_Log.h" +#include "wcdx_ini.h" HINSTANCE DllInstance; @@ -7,9 +9,25 @@ BOOL WINAPI DllMain(HINSTANCE hInstDLL, DWORD fdwReason, [[maybe_unused]] LPVOID switch (fdwReason) { case DLL_PROCESS_ATTACH: + DllInstance = hInstDLL; - break; + + if (!wcdx::Load("wcdx.ini")) { /* handle error */ } + + #ifdef SE_Log + Initialize_SE_Log(); + #endif + break; + + case DLL_PROCESS_DETACH: + + wcdx::Save("wcdx.ini"); + + #ifdef SE_Log + Shutdown_SE_Log(); + #endif + break; } return TRUE; -} +} \ No newline at end of file diff --git a/dist/wcdx/src/se_/SE_Controls.cpp b/dist/wcdx/src/se_/SE_Controls.cpp new file mode 100644 index 0000000..a550131 --- /dev/null +++ b/dist/wcdx/src/se_/SE_Controls.cpp @@ -0,0 +1,568 @@ + +// SE_Controls - Simple input mapping system for keyboard, mouse, and gamepad +// Direct Windows API usage for input handling (no SDL dependency) +// +// Usage: +// SE_Controls_Reset(); - Clear existing mappings +// SE_Controls_Map("key", onPressFunc, onConstantFunc, onReleaseFunc); +// SE_Controls_Read(); - Call this each frame to process key states and invoke callbacks + +#include "../se_/SE_Controls.h" +#include + +// Mouse button codes +namespace MouseCode { + constexpr int LEFT = -1; + constexpr int MIDDLE = -2; + constexpr int RIGHT = -3; + constexpr int X1 = -4; + constexpr int X2 = -5; + constexpr int MOVE = -8; +} + +// Xbox gamepad button codes +namespace XboxCode { + constexpr int A = -10; + constexpr int B = -11; + constexpr int X = -12; + constexpr int Y = -13; + constexpr int LB = -14; + constexpr int RB = -15; + constexpr int LS_CLICK = -16; + constexpr int RS_CLICK = -17; + constexpr int BACK = -18; + constexpr int START = -19; + constexpr int DPAD_UP = -20; + constexpr int DPAD_DOWN = -21; + constexpr int DPAD_LEFT = -22; + constexpr int DPAD_RIGHT = -23; + constexpr int LT = -24; + constexpr int RT = -25; + constexpr int LS_UP = -26; + constexpr int LS_DOWN = -27; + constexpr int LS_LEFT = -28; + constexpr int LS_RIGHT = -29; + constexpr int RS_UP = -30; + constexpr int RS_DOWN = -31; + constexpr int RS_LEFT = -32; + constexpr int RS_RIGHT = -33; +} + +// XInput constants +namespace XInputThreshold { + constexpr BYTE TRIGGER_PRESS = 30; + constexpr BYTE TRIGGER_RELEASE = 20; + constexpr SHORT LEFT_THUMB_PRESS = 7849; + constexpr SHORT LEFT_THUMB_RELEASE = 5887; // 3/4 of press + constexpr SHORT RIGHT_THUMB_PRESS = 8689; + constexpr SHORT RIGHT_THUMB_RELEASE = 6517; // 3/4 of press +} + +// Returns an integer key code. Uses Windows virtual-key codes where applicable. +static int getKeyCode(const std::string& key) { + if (key.size() == 1) + { + char c = key[0]; + if (c >= 'a' && c <= 'z') c = char(c - 'a' + 'A'); + return static_cast(c); + } + + if (key == "esc") return VK_ESCAPE; + if (key == "space") return VK_SPACE; + if (key == "enter") return VK_RETURN; + if (key == "tab") return VK_TAB; + if (key == "backspace") return VK_BACK; + if (key == "shift") return VK_LSHIFT; + if (key == "ctrl") return VK_LCONTROL; + if (key == "alt") return VK_LMENU; + if (key == "capslock") return VK_CAPITAL; + if (key == "insert") return VK_INSERT; + if (key == "delete") return VK_DELETE; + if (key == "home") return VK_HOME; + if (key == "end") return VK_END; + if (key == "pageup") return VK_PRIOR; + if (key == "pagedown") return VK_NEXT; + + if (key == "up") return VK_UP; + if (key == "down") return VK_DOWN; + if (key == "left") return VK_LEFT; + if (key == "right") return VK_RIGHT; + + // Number keys (main keyboard) + if (key == "0") return 0x30; + if (key == "1") return 0x31; + if (key == "2") return 0x32; + if (key == "3") return 0x33; + if (key == "4") return 0x34; + if (key == "5") return 0x35; + if (key == "6") return 0x36; + if (key == "7") return 0x37; + if (key == "8") return 0x38; + if (key == "9") return 0x39; + + if (key == "f1") return VK_F1; + if (key == "f2") return VK_F2; + if (key == "f3") return VK_F3; + if (key == "f4") return VK_F4; + if (key == "f5") return VK_F5; + if (key == "f6") return VK_F6; + if (key == "f7") return VK_F7; + if (key == "f8") return VK_F8; + if (key == "f9") return VK_F9; + if (key == "f10") return VK_F10; + if (key == "f11") return VK_F11; + if (key == "f12") return VK_F12; + + if (key == "printscreen") return VK_SNAPSHOT; + if (key == "scrolllock") return VK_SCROLL; + if (key == "pause") return VK_PAUSE; + + // Numpad + if (key == "numlock") return VK_NUMLOCK; + if (key == "kp_divide") return VK_DIVIDE; + if (key == "kp_multiply") return VK_MULTIPLY; + if (key == "kp_minus") return VK_SUBTRACT; + if (key == "kp_plus") return VK_ADD; + if (key == "kp_enter") return VK_RETURN; + + // Simple punctuation + if (key == "comma") return VK_OEM_COMMA; + if (key == "period") return VK_OEM_PERIOD; + if (key == "slash") return VK_OEM_2; + if (key == "minus") return VK_OEM_MINUS; + if (key == "equals") return VK_OEM_PLUS; + + // Mouse buttons + if (key == "mouse_left") return MouseCode::LEFT; + if (key == "mouse_middle") return MouseCode::MIDDLE; + if (key == "mouse_right") return MouseCode::RIGHT; + if (key == "mouse_x1") return MouseCode::X1; + if (key == "mouse_x2") return MouseCode::X2; + if (key == "mouse_move") return MouseCode::MOVE; + + // Xbox gamepad buttons + if (key == "XboxA") return XboxCode::A; + if (key == "XboxB") return XboxCode::B; + if (key == "XboxX") return XboxCode::X; + if (key == "XboxY") return XboxCode::Y; + if (key == "XboxLB") return XboxCode::LB; + if (key == "XboxRB") return XboxCode::RB; + if (key == "XboxLSClick") return XboxCode::LS_CLICK; + if (key == "XboxRSClick") return XboxCode::RS_CLICK; + if (key == "XboxBack") return XboxCode::BACK; + if (key == "XboxStart") return XboxCode::START; + if (key == "XboxDPadUp") return XboxCode::DPAD_UP; + if (key == "XboxDPadDown") return XboxCode::DPAD_DOWN; + if (key == "XboxDPadLeft") return XboxCode::DPAD_LEFT; + if (key == "XboxDPadRight") return XboxCode::DPAD_RIGHT; + if (key == "XboxLT") return XboxCode::LT; + if (key == "XboxRT") return XboxCode::RT; + if (key == "XboxLSUp") return XboxCode::LS_UP; + if (key == "XboxLSDown") return XboxCode::LS_DOWN; + if (key == "XboxLSLeft") return XboxCode::LS_LEFT; + if (key == "XboxLSRight") return XboxCode::LS_RIGHT; + if (key == "XboxRSUp") return XboxCode::RS_UP; + if (key == "XboxRSDown") return XboxCode::RS_DOWN; + if (key == "XboxRSLeft") return XboxCode::RS_LEFT; + if (key == "XboxRSRight") return XboxCode::RS_RIGHT; + + return 0; // Unknown +} + +// Mouse move sensitivity / deadzone +static float s_mouseMoveScale = 0.20f; +static int s_mouseMoveDeadzone = 0; + +// Key-state storage +static std::unordered_map keyStates; + +static bool isGamepadCode(int code) +{ + return code <= -10 && code >= -99; +} + +bool SE_Controls_IsMapped(int key) { + return keyStates.find(key) != keyStates.end(); +} + +void SE_Controls_Read() +{ + // Poll keyboard state for registered keys (uses GetAsyncKeyState so no event system dependency) + for (auto& kv : keyStates) + { + int code = kv.first; + KeyState& state = kv.second; + + if (code > 0) + { + state.isDown = state.overrideActive ? state.overrideValue : (GetAsyncKeyState(code) & 0x8000) != 0; + } + else + { + state.isDown = false; + + if (code == MouseCode::MOVE) + { + static POINT lastPos = { 0, 0 }; + POINT cur; + GetCursorPos(&cur); + int dx = cur.x - lastPos.x; + int dy = cur.y - lastPos.y; + if (dx != 0 || dy != 0) + { + state.isDown = true; + // Apply scaling and deadzone before invoking callback + extern float s_mouseMoveScale; + extern int s_mouseMoveDeadzone; + float fdx = dx * s_mouseMoveScale; + float fdy = dy * s_mouseMoveScale; + int sdx = (int)roundf(fdx); + int sdy = (int)roundf(fdy); + // Ensure small movements still register when scale > 0 + if (sdx == 0 && dx != 0 && s_mouseMoveScale > 0.00f) sdx = (dx > 0) ? 1 : -1; + if (sdy == 0 && dy != 0 && s_mouseMoveScale > 0.00f) sdy = (dy > 0) ? 1 : -1; + if (abs(sdx) > s_mouseMoveDeadzone || abs(sdy) > s_mouseMoveDeadzone) + { + if (state.onMove) + state.onMove(sdx, sdy); + } + lastPos = cur; + } + else + state.isDown = false; + } + else if (state.overrideActive) + { + state.isDown = state.overrideValue; + } + else + { + switch (code) + { + case MouseCode::LEFT: + state.isDown = (GetAsyncKeyState(VK_LBUTTON) & 0x8000) != 0; + break; + case MouseCode::MIDDLE: + state.isDown = (GetAsyncKeyState(VK_MBUTTON) & 0x8000) != 0; + break; + case MouseCode::RIGHT: + state.isDown = (GetAsyncKeyState(VK_RBUTTON) & 0x8000) != 0; + break; + case MouseCode::X1: + state.isDown = (GetAsyncKeyState(VK_XBUTTON1) & 0x8000) != 0; + break; + case MouseCode::X2: + state.isDown = (GetAsyncKeyState(VK_XBUTTON2) & 0x8000) != 0; + break; + default: + break; + } + } + } + } + + // XInput structures (minimal to avoid linking XInput.lib) + struct XINPUT_GAMEPAD { + WORD wButtons; + BYTE bLeftTrigger; + BYTE bRightTrigger; + SHORT sThumbLX; + SHORT sThumbLY; + SHORT sThumbRX; + SHORT sThumbRY; + }; + struct XINPUT_STATE { + DWORD dwPacketNumber; + XINPUT_GAMEPAD Gamepad; + }; + + // XInput state + static bool xinputLoaded = false; + static HMODULE xinputModule = nullptr; + using XInputGetState_t = DWORD(WINAPI*)(DWORD, void*); + static XInputGetState_t pXInputGetState = nullptr; + static DWORD s_lastXInputProbeTick = 0; + static DWORD s_xinputProbeIntervalMs = 0; + static int s_xinputConsecutiveFailures = 0; + + // Check if we need XInput polling + bool needXInput = false; + for (const auto& kv : keyStates) + { + if (isGamepadCode(kv.first)) + { + needXInput = true; + break; + } + } + + XINPUT_STATE xiState = {}; + bool haveXInputState = false; + + if (needXInput) + { + if (!xinputLoaded) + { + xinputModule = LoadLibraryW(L"xinput1_4.dll"); + if (xinputModule == nullptr) + xinputModule = LoadLibraryW(L"xinput1_3.dll"); + if (xinputModule == nullptr) + xinputModule = LoadLibraryW(L"xinput9_1_0.dll"); + + if (xinputModule != nullptr) + pXInputGetState = reinterpret_cast(GetProcAddress(xinputModule, "XInputGetState")); + + xinputLoaded = true; + } + + if (pXInputGetState != nullptr) + { + const DWORD now = GetTickCount(); + const bool shouldProbe = (s_xinputProbeIntervalMs == 0) || + (now - s_lastXInputProbeTick >= s_xinputProbeIntervalMs); + + if (shouldProbe) + { + s_lastXInputProbeTick = now; + + // Try all 4 possible controllers + for (DWORD userIndex = 0; userIndex < 4; ++userIndex) + { + DWORD res = pXInputGetState(userIndex, &xiState); + if (res == 0) // ERROR_SUCCESS + { + haveXInputState = true; + break; + } + } + + // Adjust probe interval based on connection state + if (haveXInputState) + { + s_xinputConsecutiveFailures = 0; + s_xinputProbeIntervalMs = 0; // Poll every frame when connected + } + else + { + ++s_xinputConsecutiveFailures; + // Exponential backoff: 250ms, 500ms, 1s, 2s + if (s_xinputConsecutiveFailures >= 6) + s_xinputProbeIntervalMs = 2000; + else if (s_xinputConsecutiveFailures >= 4) + s_xinputProbeIntervalMs = 1000; + else if (s_xinputConsecutiveFailures >= 2) + s_xinputProbeIntervalMs = 500; + else + s_xinputProbeIntervalMs = 250; + } + } + } + } + + + // Helper lambdas for analog input hysteresis + auto isTriggerPressed = [](BYTE value, bool wasDown) -> bool + { + const BYTE threshold = wasDown ? XInputThreshold::TRIGGER_RELEASE : XInputThreshold::TRIGGER_PRESS; + return value >= threshold; + }; + + auto isAxisPositive = [](SHORT value, SHORT press, SHORT release, bool wasDown) -> bool + { + return value > (wasDown ? release : press); + }; + + auto isAxisNegative = [](SHORT value, SHORT press, SHORT release, bool wasDown) -> bool + { + return value < -(wasDown ? release : press); + }; + + // Process mapped callbacks + for (auto& [k, state] : keyStates) + { + // Gamepad mapping: update state.isDown based on XInput, or force-release if not available. + if (isGamepadCode(k)) + { + if (haveXInputState) + { + const WORD btn = xiState.Gamepad.wButtons; + const BYTE lt = xiState.Gamepad.bLeftTrigger; + const BYTE rt = xiState.Gamepad.bRightTrigger; + const SHORT lx = xiState.Gamepad.sThumbLX; + const SHORT ly = xiState.Gamepad.sThumbLY; + const SHORT rx = xiState.Gamepad.sThumbRX; + const SHORT ry = xiState.Gamepad.sThumbRY; + + switch (k) + { + case XboxCode::A: state.isDown = (btn & 0x1000) != 0; break; + case XboxCode::B: state.isDown = (btn & 0x2000) != 0; break; + case XboxCode::X: state.isDown = (btn & 0x4000) != 0; break; + case XboxCode::Y: state.isDown = (btn & 0x8000) != 0; break; + case XboxCode::LB: state.isDown = (btn & 0x0100) != 0; break; + case XboxCode::RB: state.isDown = (btn & 0x0200) != 0; break; + case XboxCode::LS_CLICK: state.isDown = (btn & 0x0040) != 0; break; + case XboxCode::RS_CLICK: state.isDown = (btn & 0x0080) != 0; break; + case XboxCode::BACK: state.isDown = (btn & 0x0020) != 0; break; + case XboxCode::START: state.isDown = (btn & 0x0010) != 0; break; + case XboxCode::DPAD_UP: state.isDown = (btn & 0x0001) != 0; break; + case XboxCode::DPAD_DOWN: state.isDown = (btn & 0x0002) != 0; break; + case XboxCode::DPAD_LEFT: state.isDown = (btn & 0x0004) != 0; break; + case XboxCode::DPAD_RIGHT: state.isDown = (btn & 0x0008) != 0; break; + + case XboxCode::LT: state.isDown = isTriggerPressed(lt, state.wasDown); break; + case XboxCode::RT: state.isDown = isTriggerPressed(rt, state.wasDown); break; + + case XboxCode::LS_UP: + state.isDown = isAxisPositive(ly, XInputThreshold::LEFT_THUMB_PRESS, + XInputThreshold::LEFT_THUMB_RELEASE, state.wasDown); + break; + case XboxCode::LS_DOWN: + state.isDown = isAxisNegative(ly, XInputThreshold::LEFT_THUMB_PRESS, + XInputThreshold::LEFT_THUMB_RELEASE, state.wasDown); + break; + case XboxCode::LS_LEFT: + state.isDown = isAxisNegative(lx, XInputThreshold::LEFT_THUMB_PRESS, + XInputThreshold::LEFT_THUMB_RELEASE, state.wasDown); + break; + case XboxCode::LS_RIGHT: + state.isDown = isAxisPositive(lx, XInputThreshold::LEFT_THUMB_PRESS, + XInputThreshold::LEFT_THUMB_RELEASE, state.wasDown); + break; + + case XboxCode::RS_UP: + state.isDown = isAxisPositive(ry, XInputThreshold::RIGHT_THUMB_PRESS, + XInputThreshold::RIGHT_THUMB_RELEASE, state.wasDown); + break; + case XboxCode::RS_DOWN: + state.isDown = isAxisNegative(ry, XInputThreshold::RIGHT_THUMB_PRESS, + XInputThreshold::RIGHT_THUMB_RELEASE, state.wasDown); + break; + case XboxCode::RS_LEFT: + state.isDown = isAxisNegative(rx, XInputThreshold::RIGHT_THUMB_PRESS, + XInputThreshold::RIGHT_THUMB_RELEASE, state.wasDown); + break; + case XboxCode::RS_RIGHT: + state.isDown = isAxisPositive(rx, XInputThreshold::RIGHT_THUMB_PRESS, + XInputThreshold::RIGHT_THUMB_RELEASE, state.wasDown); + break; + + default: + state.isDown = false; + break; + } + } + else + { + // Controller disconnected: force-release + state.isDown = false; + } + } + + // Invoke callbacks based on state transitions + if (state.isDown && !state.wasDown && state.onPress) + state.onPress(); + + if (state.isDown && state.onConstant) + state.onConstant(); + + if (!state.isDown && state.wasDown && state.onRelease) + state.onRelease(); + + state.wasDown = state.isDown; + } +} + +void SE_Controls_NotifyPhysicalKey(int vk, bool down) +{ + auto it = keyStates.find(vk); + if (it != keyStates.end()) + { + it->second.overrideActive = true; + it->second.overrideValue = down; + } +} + +void SE_Controls_ClearOverrides() +{ + for (auto& [key, state] : keyStates) + { + state.overrideActive = false; + state.overrideValue = false; + } +} + +std::vector SE_Controls_GetCurrentlyHeldKeys() +{ + std::vector heldKeys; + heldKeys.reserve(keyStates.size()); + + for (const auto& [key, state] : keyStates) + { + if (state.isDown || state.wasDown) + heldKeys.push_back(key); + } + return heldKeys; +} + +void SE_Controls_Reset() +{ + // Fire onRelease for any held keys + for (auto& [key, state] : keyStates) + { + if ((state.isDown || state.wasDown) && state.onRelease) + state.onRelease(); + } + + // Clear all mappings + keyStates.clear(); +} + +void SE_Controls_Map(const std::string& key, std::function onPress, + std::function onConstant, std::function onRelease) +{ + int code = getKeyCode(key); + KeyState& state = keyStates[code]; + + state.onPress = onPress; + state.onConstant = onConstant; + state.onRelease = onRelease; + state.isDown = false; + state.wasDown = false; + state.overrideActive = false; + state.overrideValue = false; +} + +void SE_Controls_Map(const std::string& key, std::function onMove) +{ + int code = getKeyCode(key); + KeyState& state = keyStates[code]; + + state.onMove = onMove; + state.isDown = false; + state.wasDown = false; + state.overrideActive = false; + state.overrideValue = false; +} + +void SE_Controls_SetMouseMoveScale(float scale) +{ + if (scale < 0.0f) scale = 0.0f; + s_mouseMoveScale = scale; +} + +void SE_Controls_SetMouseMoveDeadzone(int deadzone) +{ + if (deadzone < 0) deadzone = 0; + s_mouseMoveDeadzone = deadzone; +} + +float SE_Controls_GetMouseMoveScale() +{ + return s_mouseMoveScale; +} + +int SE_Controls_GetMouseMoveDeadzone() +{ + return s_mouseMoveDeadzone; +} diff --git a/dist/wcdx/src/se_/SE_Controls.h b/dist/wcdx/src/se_/SE_Controls.h new file mode 100644 index 0000000..a62056d --- /dev/null +++ b/dist/wcdx/src/se_/SE_Controls.h @@ -0,0 +1,53 @@ +// SE_Controls - Simple input mapping system for keyboard, mouse, and gamepad +// Removed SDL dependency for direct Windows API usage. +// Angelscript bindings also not included here. + +#pragma once + +#include +#include +#include +#include +#include + +struct KeyState { + bool isDown = false; + bool wasDown = false; + + bool overrideActive = false; + bool overrideValue = false; + + std::function onPress; // Fires once on press + std::function onRelease; // Fires once on release + std::function onConstant; // Fires every frame while held + std::function onMove; // Fires when mouse moves: (dx, dy) +}; + +void SE_Controls_Read(); // Process per-frame key state updates + +void SE_Controls_ClearOverrides(); + +void SE_Controls_Reset(); + +std::vector SE_Controls_GetCurrentlyHeldKeys(); + +void SE_Controls_Map(const std::string& key, std::function onPress, std::function onConstant, std::function onRelease); +// Overload for mouse-move mappings: callback receives delta x,y in screen coords +void SE_Controls_Map(const std::string& key, std::function onMove); + +// Adjust mouse movement sensitivity (multiplies reported dx/dy). Default is 0.2. +void SE_Controls_SetMouseMoveScale(float scale); +// Set deadzone (ignore movements with absolute delta <= deadzone) +void SE_Controls_SetMouseMoveDeadzone(int deadzone); + +// Read current mouse move scale +float SE_Controls_GetMouseMoveScale(); +int SE_Controls_GetMouseMoveDeadzone(); + +// Returns true if the given virtual-key (or mapped code) has a mapping registered. +bool SE_Controls_IsMapped(int key); + +// Notify the control system of a physical key event consumed by an input hook. +// When called, the key's state will be reported to `SE_Controls_Read` from +// the override value until the override is cleared by a matching call. +void SE_Controls_NotifyPhysicalKey(int vk, bool down); \ No newline at end of file diff --git a/dist/wcdx/src/se_/SE_Log.h b/dist/wcdx/src/se_/SE_Log.h new file mode 100644 index 0000000..103e821 --- /dev/null +++ b/dist/wcdx/src/se_/SE_Log.h @@ -0,0 +1,52 @@ +//******************************************SE_Log.h******************************************* +// Enables logging to wcdx.log for debugging purposes +// Captures all std::cout, std::cerr, std::clog output etc.. +// Used to help find sleep_hooks that the game uses. +// This can create large log files quickly, so only enable when needed and be quick. +//********************************************************************************************* + +#pragma once + +//#define SE_Log + +#ifdef SE_Log + #include + #include + #include + static std::ofstream* g_se_log_stream = nullptr; + static std::streambuf* g_orig_cout_buf = nullptr; + static std::streambuf* g_orig_cerr_buf = nullptr; + static std::streambuf* g_orig_clog_buf = nullptr; + + static void Initialize_SE_Log(){ + g_se_log_stream = new std::ofstream("wcdx.log", std::ios::out); + if (g_se_log_stream && g_se_log_stream->is_open()) + { + g_orig_cout_buf = std::cout.rdbuf(); + g_orig_cerr_buf = std::cerr.rdbuf(); + g_orig_clog_buf = std::clog.rdbuf(); + std::cout.rdbuf(g_se_log_stream->rdbuf()); + std::cerr.rdbuf(g_se_log_stream->rdbuf()); + std::clog.rdbuf(g_se_log_stream->rdbuf()); + } + else + { + delete g_se_log_stream; + g_se_log_stream = nullptr; + } + } + + static void Shutdown_SE_Log(){ + if (g_se_log_stream) + { + // restore original buffers + if (g_orig_cout_buf) std::cout.rdbuf(g_orig_cout_buf); + if (g_orig_cerr_buf) std::cerr.rdbuf(g_orig_cerr_buf); + if (g_orig_clog_buf) std::clog.rdbuf(g_orig_clog_buf); + g_se_log_stream->close(); + delete g_se_log_stream; + g_se_log_stream = nullptr; + g_orig_cout_buf = g_orig_cerr_buf = g_orig_clog_buf = nullptr; + } + } +#endif \ No newline at end of file diff --git a/dist/wcdx/src/se_/SE_Peek.cpp b/dist/wcdx/src/se_/SE_Peek.cpp new file mode 100644 index 0000000..e84e794 --- /dev/null +++ b/dist/wcdx/src/se_/SE_Peek.cpp @@ -0,0 +1,102 @@ +// SE_PEEK.cpp +#include "../se_/SE_PEEK.h" +#include +#include + +SE_PEEK::SE_PEEK(HANDLE processHandle) + : process_(processHandle) + , ownHandle_(false) +{ + if (process_ != GetCurrentProcess()) { + // If caller supplied a real process handle, assume they own it and don't close. + // To open by PID, use CreateFromPid. + } +} + +SE_PEEK::~SE_PEEK() { + if (ownHandle_ && process_) { + CloseHandle(process_); + } +} + +SE_PEEK SE_PEEK::CreateFromPid(DWORD pid) { + HANDLE h = OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, FALSE, pid); + if (!h) return SE_PEEK(nullptr); + SE_PEEK p(h); + p.ownHandle_ = true; + return p; +} + +bool SE_PEEK::ReadAnsiString(uintptr_t address, std::string &out, size_t maxLen) const noexcept { + out.clear(); + if (!process_ || maxLen == 0) return false; + + std::vector buffer; + buffer.reserve(std::min(256, maxLen)); + + size_t chunk = 256; + size_t readTotal = 0; + while (readTotal < maxLen) { + size_t toRead = std::min(chunk, maxLen - readTotal); + buffer.resize(readTotal + toRead); + SIZE_T actuallyRead = 0; + BOOL ok = ReadProcessMemory(process_, reinterpret_cast(address + readTotal), buffer.data() + readTotal, toRead, &actuallyRead); + if (!ok || actuallyRead == 0) break; + + // search for null terminator + for (SIZE_T i = 0; i < actuallyRead; ++i) { + if (buffer[readTotal + i] == '\0') { + out.assign(buffer.data(), readTotal + i); + return true; + } + } + + readTotal += actuallyRead; + if (actuallyRead < toRead) break; + } + + // No null terminator found; return what we have if any + if (readTotal > 0) { + out.assign(buffer.data(), readTotal); + return true; + } + return false; +} + +bool SE_PEEK::ReadWideString(uintptr_t address, std::wstring &out, size_t maxChars) const noexcept { + out.clear(); + if (!process_ || maxChars == 0) return false; + + std::vector buffer; + buffer.reserve(std::min(256, maxChars)); + + size_t chunk = 256; + size_t readTotalChars = 0; + while (readTotalChars < maxChars) { + size_t toReadChars = std::min(chunk, maxChars - readTotalChars); + buffer.resize(readTotalChars + toReadChars); + SIZE_T actuallyRead = 0; + BOOL ok = ReadProcessMemory(process_, reinterpret_cast(address + readTotalChars * sizeof(wchar_t)), buffer.data() + readTotalChars, toReadChars * sizeof(wchar_t), &actuallyRead); + if (!ok || actuallyRead == 0) break; + + SIZE_T actuallyReadChars = actuallyRead / sizeof(wchar_t); + + for (SIZE_T i = 0; i < actuallyReadChars; ++i) { + if (buffer[readTotalChars + i] == L'\0') { + out.assign(buffer.data(), readTotalChars + i); + return true; + } + } + + readTotalChars += actuallyReadChars; + if (actuallyReadChars < toReadChars) break; + } + + if (readTotalChars > 0) { + out.assign(buffer.data(), readTotalChars); + return true; + } + return false; +} + +//std::unordered_map SE_Peek_Processes; \ No newline at end of file diff --git a/dist/wcdx/src/se_/SE_Peek.h b/dist/wcdx/src/se_/SE_Peek.h new file mode 100644 index 0000000..7a5d1eb --- /dev/null +++ b/dist/wcdx/src/se_/SE_Peek.h @@ -0,0 +1,50 @@ +//********************************************SE_PEEK.h******************************************** +// Inital setup for reading another process memory safely. +// Place holder for voice peeking functionality needed later. +//************************************************************************************************* + +#pragma once + +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include +#include +//#include + +class SE_PEEK { +public: + // Construct from an existing process HANDLE. If you pass GetCurrentProcess(), + // the class won't CloseHandle() it on destruction. + explicit SE_PEEK(HANDLE processHandle = GetCurrentProcess()); + ~SE_PEEK(); + + // Open a process by PID. Returns a SE_PEEK with isValid()==false on failure. + static SE_PEEK CreateFromPid(DWORD pid); + + // Check validity + bool isValid() const noexcept { return process_ != nullptr; } + + // Read a trivially-copyable type from the target process memory. + // Returns true on success and fills 'out'. + template + bool Read(uintptr_t address, T &out) const noexcept { + SIZE_T read = 0; + if (!process_) return false; + BOOL ok = ReadProcessMemory(process_, reinterpret_cast(address), &out, sizeof(T), &read); + return (ok != 0) && (read == sizeof(T)); + } + + // Read an ANSI (byte) null-terminated string from the target. Stops at maxLen bytes. + bool ReadAnsiString(uintptr_t address, std::string &out, size_t maxLen = 4096) const noexcept; + + // Read a UTF-16 (wide) null-terminated string from the target. Stops at maxChars characters. + bool ReadWideString(uintptr_t address, std::wstring &out, size_t maxChars = 4096) const noexcept; + +private: + HANDLE process_ = nullptr; + bool ownHandle_ = false; +}; + +//extern std::unordered_map SE_Peek_Processes; \ No newline at end of file diff --git a/dist/wcdx/src/se_/defines.h b/dist/wcdx/src/se_/defines.h new file mode 100644 index 0000000..5f667f3 --- /dev/null +++ b/dist/wcdx/src/se_/defines.h @@ -0,0 +1,110 @@ +//********************************************defines.h******************************************** +// Currently used for helper functions and other values I have yet to place. +//************************************************************************************************* + +#pragma once + +#include + +// Minimum joystick fraction that will be reported when a movement key is held. +// Values below this may not register in some games. Default 0.1 (10%). +inline float MOVEMENT_MIN_EFFECTIVE = 0.1f; +// Max movement speed for keyboad controls. +// Can be lower that 1.0 but not higher. Default 1.0 (100%). +inline float MOVEMENT_MAX_SPEED = 1.0f; +// Ramp time in milliseconds to reach max movement speed. +inline int MOVEMENT_RAMP_TIME_MS = 1000; +// Time in milliseconds of inactivity before hiding cursor. Use 64-bit so it +// can be compared directly with GetTickCount64() without implicit narrowing. +inline unsigned long long HIDE_MOUSE_CURSOR_IN_GAME_TIME = 3000ULL; + +// Global inline variable controlling whether the in-game cursor is hidden or not. +// This flag is managed by the main rendering loop in wcdx.cpp. +inline bool HIDE_MOUSE_CURSOR_IN_GAME = false; + +// Helper for Process_Has_Title +struct Window_EnumData { const wchar_t* expected; DWORD pid; bool found; }; + +inline BOOL CALLBACK Push_Byte_EnumProc(HWND hwnd, LPARAM lparam) +{ + Window_EnumData* d = reinterpret_cast(lparam); + DWORD wpid = 0; + GetWindowThreadProcessId(hwnd, &wpid); + if (wpid != d->pid) return TRUE; + if (!IsWindowVisible(hwnd)) return TRUE; + + wchar_t buf[512] = {}; + int len = GetWindowTextW(hwnd, buf, (int)std::size(buf)); + if (len > 0) { + if (wcsstr(buf, d->expected) != nullptr) { + d->found = true; + return FALSE; + } + } + return TRUE; +} + +// Retrun a true or false if the current process has a window with the given title. +inline bool Process_Has_Title(const wchar_t* expectedSubstring) +{ + if (!expectedSubstring) return true; + Window_EnumData data; + data.expected = expectedSubstring; + data.pid = GetCurrentProcessId(); + data.found = false; + EnumWindows(Push_Byte_EnumProc, (LPARAM)&data); + return data.found; +} + +// Find the center point of the game window. +// static POINT GetGameWindowCenter() +// { +// POINT pt; +// pt.x = GetSystemMetrics(SM_CXSCREEN) / 2; +// pt.y = GetSystemMetrics(SM_CYSCREEN) / 2; + +// HWND fg = ::GetForegroundWindow(); +// DWORD fgPid = 0; +// if (fg != nullptr) +// ::GetWindowThreadProcessId(fg, &fgPid); + +// DWORD myPid = ::GetCurrentProcessId(); + +// // Prefer foreground window if it belongs to this process +// if (fg != nullptr && fgPid == myPid) +// { +// RECT r; +// if (::GetWindowRect(fg, &r)) +// { +// pt.x = (r.left + r.right) / 2; +// pt.y = (r.top + r.bottom) / 2; +// return pt; +// } +// } + +// // Otherwise enumerate top-level windows to find a visible window for this pid +// struct EnumData { DWORD pid; HWND found; } data{ myPid, nullptr }; + +// ::EnumWindows([](HWND hwnd, LPARAM lparam)->BOOL { +// EnumData* d = reinterpret_cast(lparam); +// DWORD pid = 0; +// ::GetWindowThreadProcessId(hwnd, &pid); +// if (pid == d->pid && ::IsWindowVisible(hwnd)) +// { +// d->found = hwnd; +// return FALSE; // stop enumeration +// } +// return TRUE; // continue +// }, reinterpret_cast(&data)); + +// if (data.found != nullptr) +// { +// RECT r; +// if (::GetWindowRect(data.found, &r)) +// { +// pt.x = (r.left + r.right) / 2; +// pt.y = (r.top + r.bottom) / 2; +// } +// } +// return pt; +// } \ No newline at end of file diff --git a/dist/wcdx/src/se_/destro_thread.h b/dist/wcdx/src/se_/destro_thread.h new file mode 100644 index 0000000..2ba6e66 --- /dev/null +++ b/dist/wcdx/src/se_/destro_thread.h @@ -0,0 +1,747 @@ +//*****************************************destro_thread.h***************************************** +// Custom thread for SE_ code to run on that does not interfere with main thread. +// Used for input remapping and other features that need to run independently of wcdx. +// Still in testing phase. +// Once controls are completed this thread will be tidied up. +//************************************************************************************************* + +#pragma once + +// Ensure Win32 APIs introduced in Vista (GetTickCount64) are declared. +#ifndef _WIN32_WINNT +#define _WIN32_WINNT 0x0600 +#endif + +#include +#if !defined(GetTickCount64) + // For older SDKs that may not declare GetTickCount64, declare it manually. + #if defined(__cplusplus) + extern "C" { + #endif + typedef unsigned __int64 ULONGLONG; + typedef ULONGLONG(WINAPI* GetTickCount64_t)(void); + static ULONGLONG WINAPI GetTickCount64(void) + { + // Fallback implementation for pre-Vista systems + static DWORD lastTick = 0; + static ULONGLONG tick64 = 0; + DWORD now = GetTickCount(); + if (now < lastTick) + tick64 += (1ULL << 32); // handle wrap + lastTick = now; + return (tick64 & 0xFFFFFFFF00000000ULL) | now; + } + #if defined(__cplusplus) + } + #endif +#endif + +#include "wcdx.h" +#include "SE_Controls.h" +#include "key_jacker/KJ_Main.h" +#include "frame_limiter.h" + +#include "se_/defines.h" +#include "se_/push_byte.h" +#include "se_/widescreen.h" +static HANDLE DESTRO_Thread; +static volatile LONG DESTRO_Thread_ShouldStop = 0; + +inline bool DESTRO_Thread_ShouldTerminate() { + return InterlockedCompareExchange(&DESTRO_Thread_ShouldStop, 0, 0) != 0; +} + +inline void DESTRO_Thread_RequestStop() { + InterlockedExchange(&DESTRO_Thread_ShouldStop, 1); +} + +// File-scope ramp start so other functions can reset it. +static unsigned long long g_movementRampStart = 0; +static unsigned long long g_rollRampStart = 0; +// Per-axis joystick ramp timers to keep X/Y ramps independent +static unsigned long long g_joystickRampStartX = 0; +static unsigned long long g_joystickRampStartY = 0; + +// Used for the Mouse Ramping +static bool Up_Down_WasPressed = false; +static bool Up_Key = false; +static bool Down_Key = false; +static bool Left_Right_WasPressed = false; +static bool Left_Key = false; +static bool Right_Key = false; +static bool Comma_Period_WasPressed = false; +static bool Comma_key = false; +static bool Period_key = false; + +//DEBUG +static int MAX_ROLL_SPEED = 8; + +// value 0x00 causes broken movement so we use 0x01 for positive and 0xFF for negitive +// Value needs to be 0xFF for negitive modifer and 0x01 for positive modifier +uint8_t Modifier(uint8_t value, int modifier) { + // Treat the stored byte as a signed 8-bit value when applying a + // positive or negative modifier, then return the resulting raw byte. + int signedVal = static_cast(static_cast(value)); + signedVal += modifier; + // Wrap to 8 bits (preserve two's complement representation) + return static_cast(signedVal & 0xFF); +} + +static inline int GetRampedRollSpeed() +{ + // If no movement keys are down, reset and return 0 + if (!(Comma_key || Period_key)) { + g_rollRampStart = 0; + return 0; + } + + unsigned long long now = GetTickCount64(); + if (g_rollRampStart == 0) g_rollRampStart = now; + + unsigned long long elapsed = (now > g_rollRampStart) ? (now - g_rollRampStart) : 0; + + // Ensure sensible behavior for small or non-positive configured max + if (MAX_ROLL_SPEED <= 1) return MAX_ROLL_SPEED > 0 ? MAX_ROLL_SPEED : 1; + + if (elapsed >= MOVEMENT_RAMP_TIME_MS) return MAX_ROLL_SPEED; + + double frac = (double)elapsed / (double)MOVEMENT_RAMP_TIME_MS; + int val = 1 + static_cast((MAX_ROLL_SPEED - 1) * frac + 0.5); + if (val < 1) val = 1; + return val; +} + +// Returns a fractional ramp 0.0 .. 1.0 for joystick axes based on how long +// any directional key has been held. Resets to 0.0 on release. +// Get ramp 0..1 for Y axis (Up/Down) +static inline float GetRampedJoystickSpeedY() +{ + if (!(Up_Key || Down_Key)) { + g_joystickRampStartY = 0; + return 0.0f; + } + + unsigned long long now = GetTickCount64(); + if (g_joystickRampStartY == 0) g_joystickRampStartY = now; + + unsigned long long elapsed = (now > g_joystickRampStartY) ? (now - g_joystickRampStartY) : 0; + + double minR = static_cast(MOVEMENT_MIN_EFFECTIVE); + double maxR = static_cast(MOVEMENT_MAX_SPEED); + + // Keep values in sensible 0..1 range for joystick fractions + if (maxR > 1.0) maxR = 1.0; + if (maxR < 0.0) maxR = 0.0; + if (minR < 0.0) minR = 0.0; + if (minR > 1.0) minR = 1.0; + if (maxR < minR) maxR = minR; + + // If ramp time is non-positive, immediately return the maximum value + double rampTime = static_cast(MOVEMENT_RAMP_TIME_MS); + if (rampTime <= 0.0) return static_cast(maxR); + + double frac = static_cast(elapsed) / rampTime; + if (frac < 0.0) frac = 0.0; + if (frac > 1.0) frac = 1.0; + + double value = minR + frac * (maxR - minR); + return static_cast(value); +} + +// Get ramp 0..1 for X axis (Left/Right) +static inline float GetRampedJoystickSpeedX() +{ + if (!(Left_Key || Right_Key)) { + g_joystickRampStartX = 0; + return 0.0f; + } + + unsigned long long now = GetTickCount64(); + if (g_joystickRampStartX == 0) g_joystickRampStartX = now; + + unsigned long long elapsed = (now > g_joystickRampStartX) ? (now - g_joystickRampStartX) : 0; + + double minR = static_cast(MOVEMENT_MIN_EFFECTIVE); + double maxR = static_cast(MOVEMENT_MAX_SPEED); + + if (maxR > 1.0) maxR = 1.0; + if (maxR < 0.0) maxR = 0.0; + if (minR < 0.0) minR = 0.0; + if (minR > 1.0) minR = 1.0; + if (maxR < minR) maxR = minR; + + double rampTime = static_cast(MOVEMENT_RAMP_TIME_MS); + if (rampTime <= 0.0) return static_cast(maxR); + + double frac = static_cast(elapsed) / rampTime; + if (frac < 0.0) frac = 0.0; + if (frac > 1.0) frac = 1.0; + + double value = minR + frac * (maxR - minR); + return static_cast(value); +} + +static inline void Reset_RampedSpeedIfNeeded() +{ + if (!(Up_Key || Down_Key)) { + g_joystickRampStartY = 0; + } + + if (!(Left_Key || Right_Key)) { + g_joystickRampStartX = 0; + } + + + if (!(Comma_key || Period_key)) + g_rollRampStart = 0; + +} + +int Roll_Override() { + + SIZE_T GameAddress1; + if (Process_Has_Title(L"Wing Commander II: Vengeance of the Kilrathi")) { + GameAddress1 = 0x931AC; + } + else { + return 0; // Not Wing Commander 2, skip + } + + // Comma and Period movement + if (Comma_key && Period_key) { + Comma_Period_WasPressed = true; + Push_Byte(2, GameAddress1, { 0x00, 0x00 }); + } + else if (Comma_key) { + Comma_Period_WasPressed = true; + Push_Byte(2, GameAddress1, {(Modifier(0x01, GetRampedRollSpeed())), 0x00}); // Left strafe + } + else if (Period_key) { + Comma_Period_WasPressed = true; + Push_Byte(2, GameAddress1, {(Modifier(0xFF, -GetRampedRollSpeed())), 0xFF}); // Right strafe + } + else { + if (Comma_Period_WasPressed) { + // Reset to neutral only if we previously sent movement + Push_Byte(2, GameAddress1, { 0x00, 0x00 }); + Comma_Period_WasPressed = false; + } + } + return 0; +} + + +bool Key_Shift = false; + +// Main Thread loop for adding in SE_ code +static DWORD WINAPI DestroThread(LPVOID param) { + + // Initialize termination flag + InterlockedExchange(&DESTRO_Thread_ShouldStop, 0); + + // Reset any mapped controls to null state. + SE_Controls_Reset(); + // Ensure joystick axes start centered and ramp timers cleared to avoid + // spurious initial movement. Use dynamic center depending on real + // joystick presence. + + SE_Joystick_ResetAxesToCenter(); + g_joystickRampStartX = 0; + g_joystickRampStartY = 0; + +// Fire Remapped to Joystick Button 1 +// Spacebar on release causes issues with with controls studdering. + SE_Controls_Map("space", + []{ + }, + []{ + KJ_VJoy_SetButton(0, true); // Joystick Button 1 down + }, + []{ + KJ_VJoy_SetButton(0, false); // Joystick Button 1 up + }); + + +// Test as mouse movements still cause issues with C and other keys such as 1,2,3,4,etc. + SE_Controls_Map("c", + []{ + KJ_Input_InjectKey(0x43, true); // Joystick Button 1 down + }, + []{ + nullptr; + }, + []{ + KJ_Input_InjectKey(0x43, false); // Joystick Button 1 up + }); + + SE_Controls_Map("1", + []{ + KJ_Input_InjectKey(0x31, true); // Joystick Button 1 down + }, + []{ + nullptr; + }, + []{ + KJ_Input_InjectKey(0x31, false); // Joystick Button 1 up + }); + + SE_Controls_Map("2", + []{ + KJ_Input_InjectKey(0x32, true); // Joystick Button 1 down + }, + []{ + nullptr; + }, + []{ + KJ_Input_InjectKey(0x32, false); // Joystick Button 1 up + }); + + + SE_Controls_Map("3", + []{ + KJ_Input_InjectKey(0x33, true); // Joystick Button 1 down + }, + []{ + nullptr; + }, + []{ + KJ_Input_InjectKey(0x33, false); // Joystick Button 1 up + }); + + SE_Controls_Map("4", + []{ + KJ_Input_InjectKey(0x34, true); // Joystick Button 1 down + }, + []{ + nullptr; + }, + []{ + KJ_Input_InjectKey(0x34, false); // Joystick Button 1 up + }); + + + SE_Controls_Map("5", + []{ + KJ_Input_InjectKey(0x35, true); // Joystick Button 1 down + }, + []{ + nullptr; + }, + []{ + KJ_Input_InjectKey(0x35, false); // Joystick Button 1 up + }); + + + SE_Controls_Map("6", + []{ + KJ_Input_InjectKey(0x36, true); // Joystick Button 1 down + }, + []{ + nullptr; + }, + []{ + KJ_Input_InjectKey(0x36, false); // Joystick Button 1 up + }); + + + + + + + + SE_Controls_Map("XboxRT", + []{ + if (Key_Shift) KJ_Input_InjectKey(0x47, true); // G Pressed + }, + []{ + if (!Key_Shift) KJ_VJoy_SetButton(0, true); // Button 1 down (held) + }, + []{ + KJ_VJoy_SetButton(0, false); // Button 1 up + KJ_Input_InjectKey(0x47, false); // G Released + }); + +// Speed Up + SE_Controls_Map("minus", + []{ + KJ_Input_InjectKeyFastRepeat(VK_OEM_MINUS); + }, + nullptr, + []{ + KJ_Input_InjectKey(VK_OEM_MINUS, false); + }); + + SE_Controls_Map("XboxRSDown", + []{ + KJ_Input_InjectKeyFastRepeat(VK_OEM_MINUS); + }, + nullptr, + []{ + KJ_Input_InjectKey(VK_OEM_MINUS, false); + }); + +// Speed Down + SE_Controls_Map("equals", + []{ + KJ_Input_InjectKeyFastRepeat(VK_OEM_PLUS); + }, + nullptr, + []{ + KJ_Input_InjectKey(VK_OEM_PLUS, false); + }); + SE_Controls_Map("XboxRSUp", + []{ + KJ_Input_InjectKeyFastRepeat(VK_OEM_PLUS); + }, + nullptr, + []{ + KJ_Input_InjectKey(VK_OEM_PLUS, false); + }); + + +// Roll remappings with ramping. + SE_Controls_Map("comma", + []{ + Comma_key = true; + }, + []{ + Comma_key = true; + }, + []{ + Comma_key = false; + }); + + SE_Controls_Map("XboxRSLeft", + []{ + Comma_key = true; + }, + []{ + Comma_key = true; + }, + []{ + Comma_key = false; + }); + +// Roll remappings with ramping. + SE_Controls_Map("period", + []{ + Period_key = true; + }, + []{ + Period_key = true; + }, + []{ + Period_key = false; + }); + + SE_Controls_Map("XboxRSRight", + []{ + Period_key = true; + }, + []{ + Period_key = true; + }, + []{ + Period_key = false; + }); + +// Afterburner and stearing! + SE_Controls_Map("tab", + []{ + KJ_Input_InjectKeyFastRepeat(VK_TAB); + }, + nullptr, + []{ + KJ_Input_InjectKey(VK_TAB, false); + }); + + SE_Controls_Map("XboxLT", + []{ + KJ_Input_InjectKeyFastRepeat(VK_TAB); + }, + nullptr, + []{ + KJ_Input_InjectKey(VK_TAB, false); + }); + +// Arrow keys + // Map arrow keys directly to the virtual joystick axes so games that + // poll WinMM joystick APIs observe joystick movement when arrow keys + // are pressed. Pressed => extreme, release => centered. + SE_Controls_Map("up", + []{ + Up_Key = true; + KJ_VJoy_SetAxis(KJ_VJoyAxis::Y, static_cast(KJ_GetVirtualJoyCenter() - (KJ_GetVirtualJoyCenter() * GetRampedJoystickSpeedY()))); + }, + []{ + Up_Key = true; + KJ_VJoy_SetAxis(KJ_VJoyAxis::Y, static_cast(KJ_GetVirtualJoyCenter() - (KJ_GetVirtualJoyCenter() * GetRampedJoystickSpeedY()))); + }, + []{ + Up_Key = false; + KJ_VJoy_SetAxis(KJ_VJoyAxis::Y, static_cast(KJ_GetVirtualJoyCenter())); + }); + + SE_Controls_Map("down", + []{ + Down_Key = true; + KJ_VJoy_SetAxis(KJ_VJoyAxis::Y, static_cast(KJ_GetVirtualJoyCenter() + (KJ_GetVirtualJoyCenter() * GetRampedJoystickSpeedY()))); + }, + []{ + Down_Key = true; + KJ_VJoy_SetAxis(KJ_VJoyAxis::Y, static_cast(KJ_GetVirtualJoyCenter() + (KJ_GetVirtualJoyCenter() * GetRampedJoystickSpeedY()))); + }, + []{ + Down_Key = false; + KJ_VJoy_SetAxis(KJ_VJoyAxis::Y, static_cast(KJ_GetVirtualJoyCenter())); + }); + + SE_Controls_Map("left", + []{ + Left_Key = true; + KJ_VJoy_SetAxis(KJ_VJoyAxis::X, static_cast(KJ_GetVirtualJoyCenter() - (KJ_GetVirtualJoyCenter() * GetRampedJoystickSpeedX()))); + }, + []{ + Left_Key = true; + KJ_VJoy_SetAxis(KJ_VJoyAxis::X, static_cast(KJ_GetVirtualJoyCenter() - (KJ_GetVirtualJoyCenter() * GetRampedJoystickSpeedX()))); + }, + []{ + Left_Key = false; + KJ_VJoy_SetAxis(KJ_VJoyAxis::X, static_cast(KJ_GetVirtualJoyCenter())); + }); + + SE_Controls_Map("right", + []{ + Right_Key = true; + KJ_VJoy_SetAxis(KJ_VJoyAxis::X, static_cast(KJ_GetVirtualJoyCenter() + (KJ_GetVirtualJoyCenter() * GetRampedJoystickSpeedX()))); + }, + []{ + Right_Key = true; + KJ_VJoy_SetAxis(KJ_VJoyAxis::X, static_cast(KJ_GetVirtualJoyCenter() + (KJ_GetVirtualJoyCenter() * GetRampedJoystickSpeedX()))); + }, + []{ + Right_Key = false; + KJ_VJoy_SetAxis(KJ_VJoyAxis::X, static_cast(KJ_GetVirtualJoyCenter())); + }); + + + + + SE_Controls_Map("XboxStart", + []{ + KJ_Input_InjectKey(VK_ESCAPE, true); + }, + nullptr, + []{ + KJ_Input_InjectKey(VK_ESCAPE, false); + }); + +// Shift Modifer + SE_Controls_Map("XboxBack", + []{ + Key_Shift = true; + }, + []{ + Key_Shift = true; + }, + []{ + Key_Shift = false; + }); + + SE_Controls_Map("XboxA", + []{ + if (Key_Shift) KJ_Input_InjectKey(0x41, true); + }, + nullptr, + []{ + KJ_Input_InjectKey(0x41, false); + }); + + + + SE_Controls_Map("XboxB", + []{ + KJ_Input_InjectKey(VK_RETURN, true); + }, + nullptr, + []{ + KJ_Input_InjectKey(VK_RETURN, false); + }); + + SE_Controls_Map("XboxX", + []{ + KJ_Input_InjectKey(0x43, true); + }, + nullptr, + []{ + KJ_Input_InjectKey(0x43, false); + }); + + SE_Controls_Map("XboxY", + []{ + KJ_Input_InjectKey(0x4E, true); + }, + nullptr, + []{ + KJ_Input_InjectKey(0x4E, false); + }); + + + + + + + + SE_Controls_Map("XboxRB", + []{ + if (Key_Shift) KJ_Input_InjectKey(0x57, true); // W (Change Missels) + else KJ_Input_InjectKey(VK_RETURN, true); // Fire Missels + }, + nullptr, + []{ + KJ_Input_InjectKey(VK_RETURN, false); // Enter Release + KJ_Input_InjectKey(0x57, false); // W Release + }); + + SE_Controls_Map("XboxLB", + []{ + KJ_Input_InjectKey(0x54, true); // T (Cycle Target) + }, + nullptr, + []{ + KJ_Input_InjectKey(0x54, false); // T Release + }); + + SE_Controls_Map("XboxRSClick", + []{ + KJ_Input_InjectKey(0x4C, true); // L (Lock Target) + }, + nullptr, + []{ + KJ_Input_InjectKey(0x4C, false); // L Release + }); + + SE_Controls_Map("XboxDPadUp", + []{ + KJ_Input_InjectKey(0x31, true); // 1 (Coms Option 1) + }, + nullptr, + []{ + KJ_Input_InjectKey(0x31, false); // 1 Release + }); + + SE_Controls_Map("XboxDPadRight", + []{ + KJ_Input_InjectKey(0x32, true); // 2 (Coms Option 2) + }, + nullptr, + []{ + KJ_Input_InjectKey(0x32, false); // 2 Release + }); + + SE_Controls_Map("XboxDPadDown", + []{ + KJ_Input_InjectKey(0x33, true); // 3 (Coms Option 3) + }, + nullptr, + []{ + KJ_Input_InjectKey(0x33, false); // 3 Release + }); + + SE_Controls_Map("XboxDPadLeft", + []{ + KJ_Input_InjectKey(0x34, true); // 4 (Coms Option 4) + }, + nullptr, + []{ + KJ_Input_InjectKey(0x34, false); // 4 Release + }); + + + + +// Mouse movement mapping — inject relative mouse movement into the game + // SE_Controls_Map("mouse_move", + // [](int dx, int dy){ + // KJ_Input_InjectMouseMove(dx, dy); + // }); + +// Mouse Click mappings — use dedicated mouse injection to avoid keyboard/mouse conflicts + // SE_Controls_Map("mouse_left", + // []{ + // Inject_Mouse_Button(0, true); // left down (send once on press) + // }, + // []{ + // nullptr; + // // No repeated DOWN while held — avoid sending multiple + // // LEFTDOWN events which can confuse the OS/game and + // // lead to stuck/unresponsive mouse state. + // }, + // []{ + // Inject_Mouse_Button(0, false); // left up + // }); + + // SE_Controls_Map("mouse_right", + // []{ + // Inject_Mouse_Button(1, true); // right down (send once on press) + // }, + // []{ + // nullptr; + // // No repeated RIGHTDOWN while held for same reasons + // // as left button. + // }, + // []{ + // Inject_Mouse_Button(1, false); // right up + // }); + + + + +// Cycle Space Frame rates + SE_Controls_Map("f11", + []{ + Frame_Limiter_Space_Cycle(); + }, + []{ + nullptr; + }, + []{ + nullptr; + }); + + +// Aspect toggle + SE_Controls_Map("f12", + []{ + AspectRatioSwitch(); + }, + []{ + nullptr; + }, + []{ + nullptr; + }); + + + + +// Initial delay to allow game to start up properly before injecting inputs. +// Otherwise the keypesses get stuck down. +Sleep(10); + + +// Main Thread Loop + for (;;) + { + // Check if we should terminate + if (DESTRO_Thread_ShouldTerminate()) + break; + + SE_Controls_Read(); // Read the controls for all the keys pressed this loop and process the functions mapped. + Reset_RampedSpeedIfNeeded(); + Roll_Override(); // Apply roll override based on current key states. + } + + SE_Controls_Reset(); //Cleanup control mappings on thread exit + return 0; +} diff --git a/dist/wcdx/src/se_/frame_limiter.h b/dist/wcdx/src/se_/frame_limiter.h new file mode 100644 index 0000000..467dc90 --- /dev/null +++ b/dist/wcdx/src/se_/frame_limiter.h @@ -0,0 +1,135 @@ +//*****************************************frame_limiter.h***************************************** +// Frame Limiter that can be used to limit the framerate of the main thread. +// Also a push byte to the game to modify the space framerate for Wing Commander 2. +// Games math (# * 1000) / 60; +//************************************************************************************************* + +#pragma once +#include +#include +#include + +#include +#include +#include +#include + +#include "se_/push_byte.h" + +#pragma comment(lib, "winmm.lib") + +// Defauly value for frame limiter target fps. +inline int FRAME_MAX = 30; +inline int SPACE_MAX = 3; + +//============================================ Frame Limiter ============================================ + +struct FrameLimiter +{ + std::atomic targetFps{ FRAME_MAX }; + LARGE_INTEGER freq{}; + LONGLONG lastTick{ 0 }; + + FrameLimiter() + { + timeBeginPeriod(1); + QueryPerformanceFrequency(&freq); + QueryPerformanceCounter(reinterpret_cast(&lastTick)); + } + + ~FrameLimiter() + { + timeEndPeriod(1); + } + + void onFrame(std::function sleeper) + { + if (targetFps.load() <= 0) + { + QueryPerformanceCounter(reinterpret_cast(&lastTick)); + return; + } + + LARGE_INTEGER nowLI; + QueryPerformanceCounter(&nowLI); + double elapsedMs = double(nowLI.QuadPart - lastTick) * 1000.0 / double(freq.QuadPart); + double targetMs = 1000.0 / double(targetFps.load()); + if (elapsedMs < targetMs) + { + double remaining = targetMs - elapsedMs; + + if (remaining > 2.0) + { + DWORD sleepMs = static_cast(std::floor(remaining)) - 1; + if (sleepMs > 0) + sleeper(sleepMs); + } + + do + { + QueryPerformanceCounter(&nowLI); + elapsedMs = double(nowLI.QuadPart - lastTick) * 1000.0 / double(freq.QuadPart); + } while (elapsedMs < targetMs); + } + + lastTick = nowLI.QuadPart; + } + }; + +inline FrameLimiter g_frameLimiter; + +inline void Frame_Limiter_Set_Target(int fps) { g_frameLimiter.targetFps.store(fps); } + +inline void Frame_Limiter(){ + g_frameLimiter.onFrame([](DWORD ms){ ::Sleep(ms); }); +} + +//============================================ Space Limiter ============================================ + +inline void Frame_Limiter_Space(int modifer) +{ + if (modifer >= 0 && modifer <= 4) + { + if (Process_Has_Title(L"Wing Commander II: Vengeance of the Kilrathi")) { + if (modifer == 0) Push_Byte(1, 0x006944F, {0x00}); // Frame Rate Unlimited + else if (modifer == 1) Push_Byte(1, 0x006944F, {0x01}); // Frame Rate 60 + else if (modifer == 2) Push_Byte(1, 0x006944F, {0x02}); // Frame Rate 30 + else if (modifer == 3) Push_Byte(1, 0x006944F, {0x03}); // Frame Rate 20 + else if (modifer == 4) Push_Byte(1, 0x006944F, {0x04}); // Frame Rate 15 + } + else if (Process_Has_Title(L"Wing Commander II: Special Operations II")) { + + if (modifer == 0) Push_Byte(1, 0x002EEDE, {0x00}); // Frame Rate Unlimited + else if (modifer == 1) Push_Byte(1, 0x002EEDE, {0x01}); // Frame Rate 60 + else if (modifer == 2) Push_Byte(1, 0x002EEDE, {0x02}); // Frame Rate 30 + else if (modifer == 3) Push_Byte(1, 0x002EEDE, {0x03}); // Frame Rate 20 + else if (modifer == 4) Push_Byte(1, 0x002EEDE, {0x04}); // Frame Rate 15 + } + else if (Process_Has_Title(L"Wing Commander II: Special Operations I")) { + if (modifer == 0) Push_Byte(1, 0x003C86E, {0x00}); // Frame Rate Unlimited + else if (modifer == 1) Push_Byte(1, 0x003C86E, {0x01}); // Frame Rate 60 + else if (modifer == 2) Push_Byte(1, 0x003C86E, {0x02}); // Frame Rate 30 + else if (modifer == 3) Push_Byte(1, 0x003C86E, {0x03}); // Frame Rate 20 + else if (modifer == 4) Push_Byte(1, 0x003C86E, {0x04}); // Frame Rate 15 + } + SPACE_MAX = modifer; + } + else { + // Invalid modifier, reset to 0 + if (Process_Has_Title(L"Wing Commander II: Vengeance of the Kilrathi")) { + Push_Byte(1, 0x006944F, {0x00}); // Frame Rate Unlimited + } + else if (Process_Has_Title(L"Wing Commander II: Special Operations II")) { + Push_Byte(1, 0x002EEDE, {0x00}); // Frame Rate Unlimited + } + else if (Process_Has_Title(L"Wing Commander II: Special Operations I")) { + Push_Byte(1, 0x003C86E, {0x00}); // Frame Rate Unlimited + } + SPACE_MAX = 0; + } +} + +inline void Frame_Limiter_Space_Cycle() +{ + Frame_Limiter_Space(SPACE_MAX + 1); +} \ No newline at end of file diff --git a/dist/wcdx/src/se_/push_byte.h b/dist/wcdx/src/se_/push_byte.h new file mode 100644 index 0000000..67cb01a --- /dev/null +++ b/dist/wcdx/src/se_/push_byte.h @@ -0,0 +1,87 @@ +//******************************************push_byte.h******************************************* +// Enables the ability to patch bytes into the target process at runtime. +// Used to hide mouse cursor in game and enable frame rate hacks. +// Things got a bit messy with multiple overloads, but this should make it easier to use. +// +// Usage: +// Push_Byte(PatchSize, Address, {0x90,0x90,...}); +// +//************************************************************************************************* +#pragma once + +#include +#include +#include +#include +#include +#include "se_/defines.h" //temporary, for Process_Has_Title + +// Overload to accept initializer lists like: Push_Byte(6, 0x12345, {0x90,0x90,...}); +// Accept any initializer_list of integral values and convert to uint8_t. +template +inline bool Push_Byte(const SIZE_T PatchSize, const SIZE_T PatchRva, std::initializer_list patchList) +{ + if (patchList.size() < PatchSize) return false; + std::vector tmp; + tmp.reserve(PatchSize); + for (auto v : patchList) + tmp.push_back(static_cast(v)); + return Push_Byte(PatchSize, PatchRva, tmp.data()); +} + +// Accept raw pointers of other element types (e.g., const char*) and forward +// them to the uint8_t* overload. +inline bool Push_Byte(const SIZE_T PatchSize, const SIZE_T PatchRva, const void* patch) +{ + return Push_Byte(PatchSize, PatchRva, reinterpret_cast(patch)); +} + +template +inline bool Push_Byte(const SIZE_T PatchSize, const SIZE_T PatchRva, const T* patch) +{ + return Push_Byte(PatchSize, PatchRva, reinterpret_cast(patch)); +} + +// Overload for C-style arrays: Push_Byte(2, 0x931AA, {0x00,0x02}) or +// uint8_t arr[] = {0x00,0x02}; Push_Byte(2, 0x931AA, arr); +template +inline bool Push_Byte(const SIZE_T PatchSize, const SIZE_T PatchRva, const uint8_t (&arr)[N]) +{ + if (N < PatchSize) return false; + return Push_Byte(PatchSize, PatchRva, arr); +} + +// Overload for std::vector +inline bool Push_Byte(const SIZE_T PatchSize, const SIZE_T PatchRva, const std::vector& vec) +{ + if (vec.size() < PatchSize) return false; + return Push_Byte(PatchSize, PatchRva, vec.data()); +} + +inline bool Push_Byte(const SIZE_T PatchSize, const SIZE_T PatchRva, const uint8_t* patch = nullptr) +{ + HMODULE exe = ::GetModuleHandleW(nullptr); + if (exe == nullptr) + return false; + + auto base = reinterpret_cast(exe); + uint8_t* addr = base + PatchRva; + + DWORD oldProtect = 0; + if (!::VirtualProtect(addr, PatchSize, PAGE_EXECUTE_READWRITE, &oldProtect)) + return false; + + if (patch != nullptr) + { + ::memcpy(addr, patch, PatchSize); + } + else + { + for (SIZE_T i = 0; i < PatchSize; ++i) + addr[i] = 0x90; // NOP + } + + // restore protection + ::VirtualProtect(addr, PatchSize, oldProtect, &oldProtect); + return true; +} diff --git a/dist/wcdx/src/se_/widescreen.h b/dist/wcdx/src/se_/widescreen.h new file mode 100644 index 0000000..9d3ede9 --- /dev/null +++ b/dist/wcdx/src/se_/widescreen.h @@ -0,0 +1,45 @@ +//*******************************************widescreen.h****************************************** +// Simple management of widescreen aspect ratio settings for Wcdx. +//************************************************************************************************* + +#pragma once + +#include +#include "wcdx.h" + +// Default values +inline bool FULLSCREEN = true; +inline int RatioX = 4; +inline int RatioY = 3; + +// When there is a windows chage that could affect aspect ratio, call this to recompute the window frame rect. +// This is because when aspect ratio changes, the client area size changes and the window frame needs to be adjusted to match. +// Or else the mouse will not be confined properly and the black bars will not clear correctly. +inline void RecomputeWindowFrameRect(){ + HWND hwnd = ::FindWindowW(L"Wcdx Frame Window", nullptr); + if (hwnd != nullptr) + { + RECT rect; + if (::GetWindowRect(hwnd, &rect)) + { + ::SendMessage(hwnd, WM_APP_ASPECT_CHANGED, 0, reinterpret_cast(&rect)); + } + } +} + +// Switch between common aspect ratios: 4:3, 16:9, 16:10 +inline void AspectRatioSwitch(){ + if (RatioX == 4 && RatioY == 3){ + RatioX = 16; + RatioY = 9; + } + else if (RatioX == 16 && RatioY == 9){ + RatioX = 16; + RatioY = 10; + } + else{ + RatioX = 4; + RatioY = 3; + } + RecomputeWindowFrameRect(); +} \ No newline at end of file diff --git a/dist/wcdx/src/wcdx.cpp b/dist/wcdx/src/wcdx.cpp index cf1884d..ce08815 100644 --- a/dist/wcdx/src/wcdx.cpp +++ b/dist/wcdx/src/wcdx.cpp @@ -25,28 +25,23 @@ #pragma warning(pop) #include +#include "se_/defines.h" -namespace -{ - enum - { - WM_APP_RENDER = WM_APP - }; - - POINT ConvertTo(POINT point, RECT rect); - POINT ConvertFrom(POINT point, RECT rect); - HRESULT GetSavedGamePath(LPCWSTR subdir, LPWSTR path); - HRESULT GetLocalAppDataPath(LPCWSTR subdir, LPWSTR path); +#include "se_/SE_Controls.h" +#include "se_/destro_thread.h" +#include "se_/frame_limiter.h" +#include "key_jacker/KJ_Main.h" - bool CreateDirectoryRecursive(LPWSTR pathName); - - std::independent_bits_engine RandomBit(std::random_device{}()); -} +#include "se_/push_byte.h" +#include "se_/widescreen.h" WCDXAPI IWcdx* WcdxCreate(LPCWSTR windowTitle, WNDPROC windowProc, BOOL _fullScreen) { try { + _fullScreen = FULLSCREEN; // Use saved get wcdx.ini setting on startup + // Destro: Create DESTRO Thread to run custom code on + DESTRO_Thread = CreateThread(NULL, 0, DestroThread, NULL, 0, NULL); return new Wcdx(windowTitle, windowProc, _fullScreen != FALSE); } catch (const _com_error&) @@ -57,7 +52,7 @@ WCDXAPI IWcdx* WcdxCreate(LPCWSTR windowTitle, WNDPROC windowProc, BOOL _fullScr Wcdx::Wcdx(LPCWSTR title, WNDPROC windowProc, bool _fullScreen) : _refCount(1), _monitor(nullptr), _clientWindowProc(windowProc), _frameStyle(WS_OVERLAPPEDWINDOW), _frameExStyle(WS_EX_OVERLAPPEDWINDOW) - , _fullScreen(false), _dirty(false), _sizeChanged(false) + , _fullScreen(false), _dirty(false), _sizeChanged(false), _lastMouseMoveTick(::GetTickCount64()) #if DEBUG_SCREENSHOTS , _screenshotFrameCounter(0), _screenshotIndex(0), _screenshotFileIndex(0) #endif @@ -94,6 +89,9 @@ Wcdx::Wcdx(LPCWSTR title, WNDPROC windowProc, bool _fullScreen) SetFullScreen(IsDebuggerPresent() ? false : _fullScreen); + // Initialize input system and install hooks via Key_Jacker + ::KJ_Input_Install(DllInstance); + #if DEBUG_SCREENSHOTS auto res = ::FindResource(DllInstance, MAKEINTRESOURCE(RESOURCE_ID_WC1PAL), RT_RCDATA); if (res == nullptr) @@ -104,7 +102,20 @@ Wcdx::Wcdx(LPCWSTR title, WNDPROC windowProc, bool _fullScreen) #endif } -Wcdx::~Wcdx() = default; +Wcdx::~Wcdx() +{ + // Signal thread to stop and wait for graceful shutdown + if (DESTRO_Thread) { + ::DESTRO_Thread_RequestStop(); + ::WaitForSingleObject(DESTRO_Thread, 500); + ::CloseHandle(DESTRO_Thread); + DESTRO_Thread = nullptr; + } + + // Shutdown Key_Jacker input system + ::KJ_Input_Uninstall(); + FULLSCREEN = _fullScreen; +} HRESULT STDMETHODCALLTYPE Wcdx::QueryInterface(REFIID riid, void** ppvObject) { @@ -194,6 +205,13 @@ HRESULT STDMETHODCALLTYPE Wcdx::UpdateFrame(INT x, INT y, UINT width, UINT heigh HRESULT STDMETHODCALLTYPE Wcdx::Present() { HRESULT hr; + // Set the mouse to hide if there's been no mouse movement for 3 seconds. + auto now = ::GetTickCount64(); + if (!HIDE_MOUSE_CURSOR_IN_GAME && (now - this->_lastMouseMoveTick) >= HIDE_MOUSE_CURSOR_IN_GAME_TIME) + { + HIDE_MOUSE_CURSOR_IN_GAME = true; + } + if (FAILED(hr = _device->TestCooperativeLevel())) { if (hr != D3DERR_DEVICENOTRESET) @@ -295,6 +313,48 @@ HRESULT STDMETHODCALLTYPE Wcdx::Present() if (FAILED(hr = _device->Present(&clientRect, nullptr, nullptr, nullptr))) return hr; +// Hide/show mouse cursor based on inactivity setting + if (HIDE_MOUSE_CURSOR_IN_GAME) { + // Move mouse cursor to the crosshairs when mouse hides // Resets the counter unfortunately. + // POINT center = GetGameWindowCenter(); + // ::Inject_Mouse_Move((int)(center.x), + // (int)(center.y / 1.5), + // true); + if (Process_Has_Title(L"Wing Commander II: Vengeance of the Kilrathi")) { + Push_Byte(7, 0x003D787, {0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90}); // Hide cursor + } + else if (Process_Has_Title(L"Wing Commander II: Special Operations II")) { + Push_Byte(7, 0x0028bd7, {0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90}); // Hide cursor + } + else if (Process_Has_Title(L"Wing Commander II: Special Operations I")) { + Push_Byte(7, 0x00154a7, {0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90}); // Hide cursor + } + + // Haven't found the right place to hide the cursor in WC1 yet + // This hides the coursor but then it displays the wrong cursor image + // else if (Process_Has_Title(L"Wing Commander")) { + // Push_Byte(2, 0x00AE618, {0x90, 0x90}); // Hide cursor + //} + } + else { + // Show mouse cursor + if (Process_Has_Title(L"Wing Commander II: Vengeance of the Kilrathi")) { + Push_Byte(7, 0x003D787, {0x0F, 0xBF, 0x05, 0xAC, 0xD7, 0x49, 0x00}); // Show cursor + } + else if (Process_Has_Title(L"Wing Commander II: Special Operations II")) { + Push_Byte(7, 0x0028bd7, {0x0F, 0xBF, 0x05, 0xE4, 0xC6, 0x49, 0x00}); // Show cursor + } + else if (Process_Has_Title(L"Wing Commander II: Special Operations I")) { + Push_Byte(7, 0x00154a7, {0x0F, 0xBF, 0x05, 0xC4, 0xF9, 0x49, 0x00}); // Show cursor + } + // Haven't found the right place to hide the cursor in WC1 yet + // else if (Process_Has_Title(L"Wing Commander")) { + // Push_Byte(2, 0x00AE618, {0x01, 0x00}); // Show cursor + //} + } + + // Destro: Frame-rate limiter + ::Frame_Limiter(); return S_OK; } @@ -632,6 +692,10 @@ LRESULT CALLBACK Wcdx::FrameWindowProc(HWND hwnd, UINT message, WPARAM wParam, L case WM_ACTIVATE: wcdx->OnActivate(LOWORD(wParam), HIWORD(wParam), reinterpret_cast(lParam)); + + // Set initial Space frame rate + Frame_Limiter_Space(SPACE_MAX); + return 0; case WM_ERASEBKGND: @@ -646,15 +710,40 @@ LRESULT CALLBACK Wcdx::FrameWindowProc(HWND hwnd, UINT message, WPARAM wParam, L return 0; case WM_NCLBUTTONDBLCLK: - if (wcdx->OnNCLButtonDblClk(int(wParam), *reinterpret_cast(&lParam))) - return 0; - break; + // if (wcdx->OnNCLButtonDblClk(int(wParam), *reinterpret_cast(&lParam))) + // return 0; + // break; - case WM_SYSCHAR: + case WM_SYSCHAR: // While Alt is pressed down. if (wcdx->OnSysChar(DWORD(wParam), LOWORD(lParam), HIWORD(lParam))) return 0; break; + case WM_KEYDOWN: + return ::CallWindowProc(wcdx->_clientWindowProc, hwnd, message, wParam, lParam); + case WM_KEYUP: + return ::CallWindowProc(wcdx->_clientWindowProc, hwnd, message, wParam, lParam); + // case WM_CHAR: + + // case WM_LBUTTONDOWN: + // return ::CallWindowProc(wcdx->_clientWindowProc, hwnd, message, wParam, lParam); + + case WM_LBUTTONUP: + // Swallow mouse up. + // Wing Commander 2 registers the Mouse Up as an action, causing issues with double skips in cutscenes. + // Swallowing the event here causes issues with the mouse button gettings stuck down. + // The game does register a mouse move as a clear state. + // Therefore, we inject a mouse signal to ensure the state is correct. + ::KJ_Input_InjectMouseSignal(); + return 0; + break; + //return ::CallWindowProc(wcdx->_clientWindowProc, hwnd, message, wParam, lParam); + + case WM_MOUSEMOVE: + // Record mouse movement time and forward to client + wcdx->_lastMouseMoveTick = ::GetTickCount64(); + HIDE_MOUSE_CURSOR_IN_GAME = false; + return ::CallWindowProc(wcdx->_clientWindowProc, hwnd, message, wParam, lParam); case WM_SYSCOMMAND: if (wcdx->OnSysCommand(WORD(wParam), LOWORD(lParam), HIWORD(lParam))) return 0; @@ -667,6 +756,15 @@ LRESULT CALLBACK Wcdx::FrameWindowProc(HWND hwnd, UINT message, WPARAM wParam, L case WM_APP_RENDER: wcdx->OnRender(); break; + + case WM_APP_ASPECT_CHANGED: + // When aspect toggles (windowed or fullscreen), force a size-change + // so Present will recompute active rect / bars and redraw immediately. + wcdx->_sizeChanged = true; + wcdx->ConfineCursor(); + ::PostMessage(hwnd, WM_APP_RENDER, 0, 0); + wcdx->OnSizing(DWORD(wParam), reinterpret_cast(lParam)); + return 0; } return ::CallWindowProc(wcdx->_clientWindowProc, hwnd, message, wParam, lParam); } @@ -763,7 +861,7 @@ void Wcdx::OnSizing(DWORD windowEdge, RECT* dragRect) { case WMSZ_LEFT: case WMSZ_RIGHT: - adjustWidth = false; + adjustWidth = true; break; case WMSZ_TOP: @@ -772,13 +870,13 @@ void Wcdx::OnSizing(DWORD windowEdge, RECT* dragRect) break; default: - adjustWidth = height > (3 * width) / 4; + adjustWidth = (height * RatioX) > (RatioY * width); break; } if (adjustWidth) { - width = (4 * height) / 3; + width = (RatioX * height) / RatioY; auto delta = width - (client.right - client.left); switch (windowEdge) { @@ -801,7 +899,7 @@ void Wcdx::OnSizing(DWORD windowEdge, RECT* dragRect) } else { - height = (3 * width) / 4; + height = (RatioY * width) / RatioX; auto delta = height - (client.bottom - client.top); switch (windowEdge) { @@ -953,16 +1051,33 @@ void Wcdx::SetFullScreen(bool enabled) RECT Wcdx::GetContentRect(RECT clientRect) { - auto width = (4 * clientRect.bottom) / 3; - auto height = (3 * clientRect.right) / 4; - if (width < clientRect.right) + auto width = 0, height = 0; + + // Compute client width/height from the rect returned by GetClientRect + const int clientW = clientRect.right - clientRect.left; + const int clientH = clientRect.bottom - clientRect.top; + + const int widthForHeight = (RatioX * clientH) / RatioY; // fit widescreen ratio by height + const int heightForWidth = (RatioY * clientW) / RatioX; // fit widescreen ratio by width + if (widthForHeight <= clientW) + { + width = widthForHeight; + height = clientH; + } + else + { + width = clientW; + height = heightForWidth; + } + + if (width < clientW) { - clientRect.left = (clientRect.right - width) / 2; + clientRect.left = clientRect.left + (clientW - width) / 2; clientRect.right = clientRect.left + width; } else { - clientRect.top = (clientRect.bottom - height) / 2; + clientRect.top = clientRect.top + (clientH - height) / 2; clientRect.bottom = clientRect.top + height; } diff --git a/dist/wcdx/src/wcdx.h b/dist/wcdx/src/wcdx.h index bb1c668..356206c 100644 --- a/dist/wcdx/src/wcdx.h +++ b/dist/wcdx/src/wcdx.h @@ -12,10 +12,29 @@ #include #include - +#include #define DEBUG_SCREENSHOTS 0 +// Moved namespace contents from wcdx.cpp here so widescreen.h can include wcdx.h without circular dependency. +namespace +{ + enum + { + WM_APP_RENDER = WM_APP, + WM_APP_ASPECT_CHANGED = WM_APP + 1 + }; + + POINT ConvertTo(POINT point, RECT rect); + POINT ConvertFrom(POINT point, RECT rect); + HRESULT GetSavedGamePath(LPCWSTR subdir, LPWSTR path); + HRESULT GetLocalAppDataPath(LPCWSTR subdir, LPWSTR path); + + bool CreateDirectoryRecursive(LPWSTR pathName); + + std::independent_bits_engine RandomBit(std::random_device{}()); +} + class Wcdx : public IWcdx { public: @@ -108,7 +127,8 @@ class Wcdx : public IWcdx bool _fullScreen; bool _dirty; bool _sizeChanged; - +// Store time of last mouse movement for cursor hiding + unsigned long long _lastMouseMoveTick; #if DEBUG_SCREENSHOTS int _screenshotFrameCounter; std::byte _screenshotBuffers[10][ContentWidth * ContentHeight]; @@ -119,4 +139,7 @@ class Wcdx : public IWcdx #endif }; +// Need the inject tag in wcdx.h for use in KJ_Mouse.h and KJ_Keyboard.h +static constexpr ULONG_PTR WCDX_INJECT_TAG = 0x57434458; // 'WCDX' + #endif diff --git a/dist/wcdx/src/wcdx_ini.h b/dist/wcdx/src/wcdx_ini.h new file mode 100644 index 0000000..5656c13 --- /dev/null +++ b/dist/wcdx/src/wcdx_ini.h @@ -0,0 +1,130 @@ +//********************************************wcdx_ini.h******************************************* +// Simple INI loader/saver for custom user settings. +//************************************************************************************************* +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "wcdx.h" +#include "se_\frame_limiter.h" + +#include "se_\SE_Controls.h" +#include "se_\defines.h" +#include "se_\widescreen.h" + +namespace wcdx { + +inline std::string trim(const std::string &s) { + const char *ws = " \t\n\r"; + auto start = s.find_first_not_of(ws); + if (start == std::string::npos) return std::string(); + auto end = s.find_last_not_of(ws); + return s.substr(start, end - start + 1); +} + +// Save settings to `path`. Returns true on success. +inline bool Save(const std::string &path = "wcdx.ini") { + std::ofstream ofs(path, std::ofstream::trunc); + if (!ofs) return false; + ofs << "; =============================================================================================\n"; + ofs << "; wcdx settings\n"; + ofs << "; =============================================================================================\n"; + ofs << "; Set the fullscreen mode or windowed.\n"; + ofs << "FULLSCREEN=" << FULLSCREEN << "\n"; + ofs << "; Set widescreen ratio X and Y values (e.g., 16:9 or 16:10 SteamDeck)\n"; + ofs << "RATIO=" << RatioX << ":" << RatioY << "\n"; + ofs << "; Frame rate max is a maximum overall frame rate for cutscenes and animations.\n"; + ofs << "FRAME_MAX=" << FRAME_MAX << "\n"; + ofs << "; Cycle Space Frame rates 0=Unlimited, 1=60fps, 2=30fps, 3=20fps (default), 4=15fps(original)\n"; + ofs << "; Note that FRAME_MAX will override any SPACE_MAX setting if it is set to a value less than SPACE_MAX.\n"; + ofs << "SPACE_MAX=" << SPACE_MAX << "\n"; + ofs << "; Movement ramp speed adjusts the responsiveness of movement controls.\n"; + ofs << "MOVEMENT_RAMP_TIME_MS=" << MOVEMENT_RAMP_TIME_MS << "\n"; + ofs << "; Minimum movement speed to ensure keypresses register in-game.\n"; + ofs << "MOVEMENT_MIN_EFFECTIVE=" << MOVEMENT_MIN_EFFECTIVE << "\n"; + ofs << "; Set max movement speed, game requires at least 1 so setting MOVEMENT_MAX_SPEED to 0 is actually 1.\n"; + ofs << "MOVEMENT_MAX_SPEED=" << MOVEMENT_MAX_SPEED << "\n"; + ofs << "; Hide mouse cursor in game after inactivity\n"; + ofs << "HIDE_MOUSE_CURSOR_IN_GAME_TIME=" << HIDE_MOUSE_CURSOR_IN_GAME_TIME << "\n"; + return ofs.good(); +} + +// Load settings from `path`. If file does not exist a default file is created and saved. +inline bool Load(const std::string &path = "wcdx.ini") { + namespace fs = std::filesystem; + if (!fs::exists(path)) { + return Save(path); + } + + std::ifstream ifs(path); + if (!ifs) return false; + + std::string line; + while (std::getline(ifs, line)) { + // strip comments + auto comment_pos = line.find_first_of(";#"); + if (comment_pos != std::string::npos) line.erase(comment_pos); + auto eq = line.find('='); + if (eq == std::string::npos) continue; + auto key = trim(line.substr(0, eq)); + auto val = trim(line.substr(eq + 1)); + if (key.empty() || val.empty()) continue; + try { + if (key == "FULLSCREEN") FULLSCREEN = std::stoi(val); + else if (key == "RATIO") { + auto colon = val.find(':'); + if (colon != std::string::npos) { + RatioX = std::stoi(trim(val.substr(0, colon))); + RatioY = std::stoi(trim(val.substr(colon + 1))); + } + } + else if (key == "FRAME_MAX") { + FRAME_MAX = std::stoi(val); + Frame_Limiter_Set_Target(FRAME_MAX); + } + + else if (key == "SPACE_MAX") { + SPACE_MAX = std::stoi(val); + } + + else if (key == "MOVEMENT_MIN_EFFECTIVE") { + float v = std::stof(val); + if (v >= 0.0f && v < 1.0f) { + MOVEMENT_MIN_EFFECTIVE = v; + } + } + + else if (key == "MOVEMENT_MAX_SPEED") { + float v = std::stof(val); + if (v >= 0.0f && v < 1.0f) { + MOVEMENT_MAX_SPEED = v; + } + } + else if (key == "MOVEMENT_RAMP_TIME_MS") { + int v = std::stoi(val); + if (v >= 0 && v <= 10000) { + MOVEMENT_RAMP_TIME_MS = v; + } + } + else if (key == "HIDE_MOUSE_CURSOR_IN_GAME_TIME") { + try { + unsigned long long t = std::stoull(val); + if (t > 600000ULL) t = 600000ULL; + HIDE_MOUSE_CURSOR_IN_GAME_TIME = t; + } catch (...) { + } + } + + } catch (...) { + } + } + + return true; +} + +} // namespace wcdx \ No newline at end of file diff --git a/dist/wcimg/src/main.cpp b/dist/wcimg/src/main.cpp index f271af9..9a285f1 100644 --- a/dist/wcimg/src/main.cpp +++ b/dist/wcimg/src/main.cpp @@ -262,7 +262,7 @@ namespace } return value; - }) >> stdext::to_utf16() >> stdext::make_consumer(std::back_inserter(raw_args)); + }) >> stdext::to_utf16(), stdext::make_consumer(std::back_inserter(raw_args)); raw_args.push_back(L'\0'); int count; diff --git a/dist/wcpatch/src/main.cpp b/dist/wcpatch/src/main.cpp index 09d96ae..4785572 100644 --- a/dist/wcpatch/src/main.cpp +++ b/dist/wcpatch/src/main.cpp @@ -9,6 +9,7 @@ #include #include +#include #include #include #include @@ -401,7 +402,16 @@ bool apply_dif(stdext::multi_ref file_data, ui seekable.seek(stdext::seek_from::begin, offset); auto value = stream.read(); if (value != original_value) + { + unsigned file_b = std::to_integer(value); + unsigned expect_b = std::to_integer(original_value); + unsigned repl_b = std::to_integer(replacement_value); + std::cerr << "apply_dif: mismatch at offset 0x" << std::hex << offset + << ": file=0x" << std::setw(2) << std::setfill('0') << file_b + << " expected=0x" << std::setw(2) << expect_b + << " replace=0x" << std::setw(2) << repl_b << std::dec << "\n"; return false; + } seekable.seek(stdext::seek_from::current, -1); stream.write(replacement_value); } diff --git a/dist/wcres/src/main.cpp b/dist/wcres/src/main.cpp index c5ef26a..89bb1f6 100644 --- a/dist/wcres/src/main.cpp +++ b/dist/wcres/src/main.cpp @@ -206,7 +206,7 @@ namespace _src_byte = _stream->read(); _src_bit_position = 0; } - size_t bits_used = stdext::min(bit_width, size_t(CHAR_BIT - _src_bit_position)); + size_t bits_used = (bit_width < size_t(CHAR_BIT - _src_bit_position)) ? bit_width : size_t(CHAR_BIT - _src_bit_position); auto byte = (_src_byte >> _src_bit_position) & std::byte((1 << bits_used) - 1); _src_bit_position += bits_used; bit_width -= bits_used; diff --git a/external/stdext b/external/stdext index 3609ba6..be224e3 160000 --- a/external/stdext +++ b/external/stdext @@ -1 +1 @@ -Subproject commit 3609ba67f4fd2718350f254931b2d04f89abd945 +Subproject commit be224e3cccf95c86960ec115b0d2821f124d20ba