diff --git a/Minecraft.Client/Minecraft.Client.vcxproj b/Minecraft.Client/Minecraft.Client.vcxproj index 8e54bc42c..a8f871a22 100644 --- a/Minecraft.Client/Minecraft.Client.vcxproj +++ b/Minecraft.Client/Minecraft.Client.vcxproj @@ -16302,6 +16302,7 @@ xcopy /q /y /i /s /e $(ProjectDir)Durango\CU $(LayoutDir)Image\Loose\CUtrue true + true true @@ -29201,6 +29202,51 @@ xcopy /q /y /i /s /e $(ProjectDir)Durango\CU $(LayoutDir)Image\Loose\CUtrue true + + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + false + false + false + false + false + false + false + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true + true true diff --git a/Minecraft.Client/Minecraft.Client.vcxproj.filters b/Minecraft.Client/Minecraft.Client.vcxproj.filters index 7731385cb..c54c0c1a7 100644 --- a/Minecraft.Client/Minecraft.Client.vcxproj.filters +++ b/Minecraft.Client/Minecraft.Client.vcxproj.filters @@ -2344,6 +2344,9 @@ Windows64\Source Files + + Windows64\Source Files + Windows64 @@ -4776,6 +4779,9 @@ Windows64\Source Files + + Windows64\Source Files + Windows64 diff --git a/Minecraft.Client/Windows64/Windows64_DInputManager.cpp b/Minecraft.Client/Windows64/Windows64_DInputManager.cpp new file mode 100644 index 000000000..70b26394c --- /dev/null +++ b/Minecraft.Client/Windows64/Windows64_DInputManager.cpp @@ -0,0 +1,338 @@ +#include "stdafx.h" + +#ifdef _WINDOWS64 + +#include "Windows64_DInputManager.h" +#include +#include +#include // for ImageDirectoryEntryToData / manual PE walk +#pragma comment(lib, "winmm.lib") + +// --------------------------------------------------------------------------- +// Internal types & state +// --------------------------------------------------------------------------- + +typedef DWORD (WINAPI* PFN_XInputGetState )(DWORD, XINPUT_STATE*); +typedef DWORD (WINAPI* PFN_XInputGetCapabilities)(DWORD, DWORD, XINPUT_CAPABILITIES*); + +static PFN_XInputGetState s_origGetState = nullptr; +static PFN_XInputGetCapabilities s_origGetCaps = nullptr; + +// WinMM joystick ID assigned to each XInput slot as a DInput fallback. +// -1 means "no DInput fallback for this slot". +static int s_dInputForSlot[XUSER_MAX_COUNT] = { -1, -1, -1, -1 }; + +// Simple frame-rate throttle: only re-scan for new DInput devices every N ticks. +static int s_rescanCountdown = 0; +static const int RESCAN_INTERVAL = 90; // ~3 s at 30 fps + +// --------------------------------------------------------------------------- +// WinMM -> XInput axis conversion helpers +// --------------------------------------------------------------------------- + +// WinMM range: 0-65535, centre ~32767 +// XInput range: -32768..32767 +static SHORT WinMMAxisToXInput(DWORD winmmAxis) +{ + int v = (int)winmmAxis - 32768; + if (v < -32768) v = -32768; + if (v > 32767) v = 32767; + return (SHORT)v; +} + +// WinMM trigger range: 0-65535 (0 = not pressed) +// XInput trigger range: 0-255 +static BYTE WinMMTriggerToXInput(DWORD winmmAxis) +{ + return (BYTE)(winmmAxis >> 8); +} + +// --------------------------------------------------------------------------- +// Build an XINPUT_STATE from a WinMM joyGetPosEx call. +// Returns true if the joystick responded successfully. +// --------------------------------------------------------------------------- +static bool WinMMToXInputState(UINT wmId, XINPUT_STATE* out) +{ + JOYINFOEX ji = {}; + ji.dwSize = sizeof(ji); + ji.dwFlags = JOY_RETURNALL; + + if (joyGetPosEx(wmId, &ji) != JOYERR_NOERROR) + return false; + + ZeroMemory(out, sizeof(*out)); + out->dwPacketNumber = GetTickCount(); + + WORD buttons = 0; + + // ---- Face buttons (DS4 standard Windows HID driver mapping) ---- + if (ji.dwButtons & (1u << 0)) buttons |= XINPUT_GAMEPAD_X; // Square -> X + if (ji.dwButtons & (1u << 1)) buttons |= XINPUT_GAMEPAD_A; // Cross -> A + if (ji.dwButtons & (1u << 2)) buttons |= XINPUT_GAMEPAD_B; // Circle -> B + if (ji.dwButtons & (1u << 3)) buttons |= XINPUT_GAMEPAD_Y; // Triangle -> Y + if (ji.dwButtons & (1u << 4)) buttons |= XINPUT_GAMEPAD_LEFT_SHOULDER; // L1 -> LB + if (ji.dwButtons & (1u << 5)) buttons |= XINPUT_GAMEPAD_RIGHT_SHOULDER; // R1 -> RB + // buttons 6 & 7 are digital L2/R2 – we use the analog axes instead + if (ji.dwButtons & (1u << 8)) buttons |= XINPUT_GAMEPAD_BACK; // Share -> Back + if (ji.dwButtons & (1u << 9)) buttons |= XINPUT_GAMEPAD_START; // Options -> Start + if (ji.dwButtons & (1u << 10)) buttons |= XINPUT_GAMEPAD_LEFT_THUMB; // L3 + if (ji.dwButtons & (1u << 11)) buttons |= XINPUT_GAMEPAD_RIGHT_THUMB; // R3 + + // ---- D-pad via POV hat ---- + // dwPOV is in 100ths of a degree. JOY_POVCENTERED (0xFFFF) = not pressed. + if (ji.dwPOV != JOY_POVCENTERED) + { + DWORD pov = ji.dwPOV; + if (pov <= 4500 || pov >= 31500) buttons |= XINPUT_GAMEPAD_DPAD_UP; + if (pov >= 4500 && pov <= 13500) buttons |= XINPUT_GAMEPAD_DPAD_RIGHT; + if (pov >= 13500 && pov <= 22500) buttons |= XINPUT_GAMEPAD_DPAD_DOWN; + if (pov >= 22500 && pov <= 31500) buttons |= XINPUT_GAMEPAD_DPAD_LEFT; + } + + out->Gamepad.wButtons = buttons; + + // ---- Sticks ---- + // Left stick: X / Y (WinMM Y: 0=top, XInput Y: positive=up -> invert) + out->Gamepad.sThumbLX = WinMMAxisToXInput(ji.dwXpos); + out->Gamepad.sThumbLY = (SHORT)(-WinMMAxisToXInput(ji.dwYpos) - 1); + + // Right stick: Z (RSX) / Rz (RSY) + // DS4 HID axis order: X, Y, Z(RSX), Rz(RSY), Ry(R2), Rx(L2) + // WinMM maps these sequentially: dwZpos=RSX, dwRpos=RSY, dwUpos=R2, dwVpos=L2 + out->Gamepad.sThumbRX = WinMMAxisToXInput(ji.dwZpos); + out->Gamepad.sThumbRY = (SHORT)(-WinMMAxisToXInput(ji.dwRpos) - 1); + + // ---- Triggers ---- + // Ry (dwUpos) = R2, Rx (dwVpos) = L2 + out->Gamepad.bLeftTrigger = WinMMTriggerToXInput(ji.dwVpos); + out->Gamepad.bRightTrigger = WinMMTriggerToXInput(ji.dwUpos); + + return true; +} + +// --------------------------------------------------------------------------- +// Slot assignment: build / refresh the DInput -> XInput slot map. +// +// Heuristic: Windows places XInput controllers in the lowest WinMM joystick +// slots. Joystick slots at index >= (number of connected XInput controllers) +// are treated as DInput devices. +// --------------------------------------------------------------------------- +static void RefreshDInputSlots() +{ + if (!s_origGetState) + return; + + // Count connected XInput slots and record which slots are empty. + bool xinputConnected[XUSER_MAX_COUNT] = {}; + int numXInputConnected = 0; + for (DWORD xi = 0; xi < XUSER_MAX_COUNT; xi++) + { + XINPUT_STATE st; + if (s_origGetState(xi, &st) == ERROR_SUCCESS) + { + xinputConnected[xi] = true; + numXInputConnected++; + } + } + + // Release any DInput slot whose WinMM device is no longer responding, + // or whose XInput slot is now occupied by a real XInput controller. + for (int xi = 0; xi < XUSER_MAX_COUNT; xi++) + { + if (s_dInputForSlot[xi] < 0) + continue; + + UINT wmId = (UINT)s_dInputForSlot[xi]; + JOYINFOEX ji = {}; + ji.dwSize = sizeof(ji); + ji.dwFlags = JOY_RETURNBUTTONS; + + bool devGone = (joyGetPosEx(wmId, &ji) != JOYERR_NOERROR); + bool slotTaken = xinputConnected[xi]; + if (devGone || slotTaken) + s_dInputForSlot[xi] = -1; + } + + // Build a set of WinMM IDs already in use as DInput slots. + bool wmIdInUse[16] = {}; + for (int xi = 0; xi < XUSER_MAX_COUNT; xi++) + if (s_dInputForSlot[xi] >= 0 && s_dInputForSlot[xi] < 16) + wmIdInUse[s_dInputForSlot[xi]] = true; + + // Enumerate WinMM joysticks. Those at index >= numXInputConnected are + // treated as non-XInput (DInput) devices. + UINT numWinMM = joyGetNumDevs(); + for (UINT wmId = (UINT)numXInputConnected; wmId < numWinMM; wmId++) + { + if (wmId >= 16) break; + if (wmIdInUse[wmId]) continue; + + // Test whether this device is actually connected. + JOYINFOEX ji = {}; + ji.dwSize = sizeof(ji); + ji.dwFlags = JOY_RETURNBUTTONS; + if (joyGetPosEx(wmId, &ji) != JOYERR_NOERROR) + continue; + + // Find the first XInput slot without a real XInput controller or a + // DInput assignment, and assign this device to it. + for (int xi = 0; xi < XUSER_MAX_COUNT; xi++) + { + if (!xinputConnected[xi] && s_dInputForSlot[xi] < 0) + { + s_dInputForSlot[xi] = (int)wmId; + wmIdInUse[wmId] = true; + break; + } + } + } +} + +// --------------------------------------------------------------------------- +// Hooked XInputGetState +// --------------------------------------------------------------------------- +static DWORD WINAPI Hooked_XInputGetState(DWORD dwUserIndex, XINPUT_STATE* pState) +{ + // Always try the real XInput first. + DWORD result = s_origGetState + ? s_origGetState(dwUserIndex, pState) + : ERROR_DEVICE_NOT_CONNECTED; + + if (result == ERROR_DEVICE_NOT_CONNECTED + && dwUserIndex < XUSER_MAX_COUNT + && s_dInputForSlot[dwUserIndex] >= 0) + { + if (WinMMToXInputState((UINT)s_dInputForSlot[dwUserIndex], pState)) + return ERROR_SUCCESS; + + // Device disappeared – clear the slot. + s_dInputForSlot[dwUserIndex] = -1; + } + + return result; +} + +// --------------------------------------------------------------------------- +// Hooked XInputGetCapabilities +// --------------------------------------------------------------------------- +static DWORD WINAPI Hooked_XInputGetCapabilities(DWORD dwUserIndex, DWORD dwFlags, + XINPUT_CAPABILITIES* pCapabilities) +{ + DWORD result = s_origGetCaps + ? s_origGetCaps(dwUserIndex, dwFlags, pCapabilities) + : ERROR_DEVICE_NOT_CONNECTED; + + if (result == ERROR_DEVICE_NOT_CONNECTED + && dwUserIndex < XUSER_MAX_COUNT + && s_dInputForSlot[dwUserIndex] >= 0) + { + ZeroMemory(pCapabilities, sizeof(*pCapabilities)); + pCapabilities->Type = XINPUT_DEVTYPE_GAMEPAD; + pCapabilities->SubType = XINPUT_DEVSUBTYPE_GAMEPAD; + pCapabilities->Gamepad.wButtons = 0xF3FF; // all mapped buttons + pCapabilities->Gamepad.bLeftTrigger = 0xFF; + pCapabilities->Gamepad.bRightTrigger = 0xFF; + pCapabilities->Gamepad.sThumbLX = 32767; + pCapabilities->Gamepad.sThumbLY = 32767; + pCapabilities->Gamepad.sThumbRX = 32767; + pCapabilities->Gamepad.sThumbRY = 32767; + return ERROR_SUCCESS; + } + + return result; +} + +// --------------------------------------------------------------------------- +// IAT patching +// +// Walks the PE import table of the current process and replaces the entries +// for XInputGetState and XInputGetCapabilities with our hooks. This affects +// all callers in the process, including the statically-linked 4J_Input.lib. +// --------------------------------------------------------------------------- + +// Overwrite one function pointer in the IAT with page-permission handling. +static void PatchIATEntry(void** entry, void* newFn, void** oldFn) +{ + if (oldFn) + *oldFn = *entry; + + DWORD oldProtect = 0; + VirtualProtect(entry, sizeof(void*), PAGE_READWRITE, &oldProtect); + *entry = newFn; + VirtualProtect(entry, sizeof(void*), oldProtect, &oldProtect); +} + +static void PatchXInputIAT() +{ + HMODULE hMod = GetModuleHandle(NULL); + if (!hMod) return; + + auto* dosHdr = reinterpret_cast(hMod); + auto* ntHdrs = reinterpret_cast( + reinterpret_cast(hMod) + dosHdr->e_lfanew); + + DWORD impRVA = ntHdrs->OptionalHeader + .DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress; + if (!impRVA) return; + + auto* impDesc = reinterpret_cast( + reinterpret_cast(hMod) + impRVA); + + for (; impDesc->Name; impDesc++) + { + const char* dllName = reinterpret_cast( + reinterpret_cast(hMod) + impDesc->Name); + + // Match any XInput DLL version (xinput1_4.dll, xinput1_3.dll, etc.) + if (_strnicmp(dllName, "xinput", 6) != 0) + continue; + + auto* thunk = reinterpret_cast( + reinterpret_cast(hMod) + impDesc->FirstThunk); + auto* origThunk = reinterpret_cast( + reinterpret_cast(hMod) + impDesc->OriginalFirstThunk); + + for (; thunk->u1.AddressOfData; thunk++, origThunk++) + { + if (origThunk->u1.Ordinal & IMAGE_ORDINAL_FLAG) + continue; // ordinal import, skip + + auto* byName = reinterpret_cast( + reinterpret_cast(hMod) + origThunk->u1.AddressOfData); + + if (strcmp(byName->Name, "XInputGetState") == 0) + { + PatchIATEntry(reinterpret_cast(&thunk->u1.Function), + reinterpret_cast(Hooked_XInputGetState), + reinterpret_cast(&s_origGetState)); + } + else if (strcmp(byName->Name, "XInputGetCapabilities") == 0) + { + PatchIATEntry(reinterpret_cast(&thunk->u1.Function), + reinterpret_cast(Hooked_XInputGetCapabilities), + reinterpret_cast(&s_origGetCaps)); + } + } + } +} + +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + +void DInputManager_Init() +{ + PatchXInputIAT(); + RefreshDInputSlots(); +} + +void DInputManager_Tick() +{ + if (--s_rescanCountdown <= 0) + { + s_rescanCountdown = RESCAN_INTERVAL; + RefreshDInputSlots(); + } +} + +#endif // _WINDOWS64 diff --git a/Minecraft.Client/Windows64/Windows64_DInputManager.h b/Minecraft.Client/Windows64/Windows64_DInputManager.h new file mode 100644 index 000000000..12a0c7ed1 --- /dev/null +++ b/Minecraft.Client/Windows64/Windows64_DInputManager.h @@ -0,0 +1,56 @@ +#pragma once + +#ifdef _WINDOWS64 + +// DInput / WinMM controller support for Windows. +// +// The 4J_Input.lib uses XInput internally, which only sees XInput-compatible +// controllers (Xbox controllers, XInput-mode adapters, etc.). Non-XInput +// devices – DualShock 4, generic HID gamepads, most non-Xbox controllers – +// are invisible to XInput and therefore to the game. +// +// This module fixes that by: +// 1. Hooking XInputGetState / XInputGetCapabilities via IAT patching so +// that every call in the process (including from 4J_Input.lib) passes +// through our wrapper. +// 2. Using the WinMM joystick API (joyGetPosEx) to enumerate non-XInput +// devices and feed their state back through the XInput interface as if +// they were standard XInput gamepads. +// +// The DS4 / DualShock 4 button layout used here matches the standard Windows +// HID driver (no DS4Windows required): +// +// WinMM button index -> Xbox equivalent +// 0 Square -> X +// 1 Cross -> A +// 2 Circle -> B +// 3 Triangle -> Y +// 4 L1 -> Left Shoulder +// 5 R1 -> Right Shoulder +// 6 L2 (digital) -> (analog axis used instead) +// 7 R2 (digital) -> (analog axis used instead) +// 8 Share -> Back +// 9 Options -> Start +// 10 L3 -> Left Thumb +// 11 R3 -> Right Thumb +// +// Axis mapping (joyGetPosEx) - DS4 standard Windows HID driver: +// DS4 HID axis order: X, Y, Z(RSX), Rz(RSY), Ry(R2), Rx(L2) +// WinMM maps HID axes sequentially to dwXpos/Ypos/Zpos/Rpos/Upos/Vpos: +// dwXpos -> Left stick X +// dwYpos -> Left stick Y (WinMM: 0=up; inverted for XInput) +// dwZpos -> Right stick X (HID Z) +// dwRpos -> Right stick Y (HID Rz, inverted for XInput) +// dwUpos -> R2 trigger (HID Ry) +// dwVpos -> L2 trigger (HID Rx) +// dwPOV -> D-pad (centidegrees, JOY_POVCENTERED = not pressed) + +// Call once after InputManager.Initialise() and before the first Tick(). +// Patches the IAT so both game code and 4J_Input.lib see the DInput fallback. +void DInputManager_Init(); + +// Call once per frame (alongside InputManager.Tick()) to refresh the list of +// connected DInput controllers. +void DInputManager_Tick(); + +#endif // _WINDOWS64 diff --git a/Minecraft.Client/Windows64/Windows64_Minecraft.cpp b/Minecraft.Client/Windows64/Windows64_Minecraft.cpp index dce883c2a..53f9ea634 100644 --- a/Minecraft.Client/Windows64/Windows64_Minecraft.cpp +++ b/Minecraft.Client/Windows64/Windows64_Minecraft.cpp @@ -38,6 +38,7 @@ #include "..\..\Minecraft.World\OldChunkStorage.h" #include "Xbox/resource.h" +#include "Windows64_DInputManager.h" #ifdef _MSC_VER #pragma comment(lib, "legacy_stdio_definitions.lib") @@ -395,9 +396,6 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) case WM_MOUSEWHEEL: KMInput.OnMouseWheel(GET_WHEEL_DELTA_WPARAM(wParam)); break; - case WM_MOUSEMOVE: - KMInput.OnMouseMove(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)); - break; case WM_ACTIVATE: if (LOWORD(wParam) == WA_INACTIVE) KMInput.SetCapture(false); @@ -408,8 +406,9 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) break; case WM_SETCURSOR: - // Hide the OS cursor when an Iggy/Flash menu is displayed (it has its own Flash cursor) - if (LOWORD(lParam) == HTCLIENT && !KMInput.IsCaptured() && ui.GetMenuDisplayed(0)) + // Hide the OS cursor in inventory/container menus - the game renders its own in-game pointer. + // Main menus show the OS cursor so the player can see exactly where they are clicking. + if (LOWORD(lParam) == HTCLIENT && !KMInput.IsCaptured() && ui.IsContainerMenuDisplayed(0)) { SetCursor(NULL); return TRUE; @@ -856,6 +855,10 @@ int APIENTRY _tWinMain(_In_ HINSTANCE hInstance, // Set the number of possible joypad layouts that the user can switch between, and the number of actions InputManager.Initialise(1,3,MINECRAFT_ACTION_MAX, ACTION_MAX_MENU); + // Hook XInputGetState/XInputGetCapabilities so non-XInput controllers + // (DS4, generic DInput gamepads) are visible to InputManager and the game. + DInputManager_Init(); + // Initialize keyboard/mouse input KMInput.Init(g_hWnd); @@ -1083,6 +1086,7 @@ int APIENTRY _tWinMain(_In_ HINSTANCE hInstance, app.UpdateTime(); PIXBeginNamedEvent(0,"Input manager tick"); + DInputManager_Tick(); // refresh DInput slot assignments InputManager.Tick(); KMInput.Tick(); PIXEndNamedEvent();